Skip to content

Split the confirmation footer by variants pt2 - #100041

Draft
VickyStash wants to merge 10 commits into
Expensify:mainfrom
callstack-internal:VickyStash/refactor/99449-confirmation-footers-pt2
Draft

Split the confirmation footer by variants pt2#100041
VickyStash wants to merge 10 commits into
Expensify:mainfrom
callstack-internal:VickyStash/refactor/99449-confirmation-footers-pt2

Conversation

@VickyStash

@VickyStash VickyStash commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

Refactor with no behavior change.
Follows #99578, which turned MoneyRequestConfirmationListFooter into a dispatcher with per-type variants and migrated per-diem and time footers. This one migrates the rest and deletes the fallback.

Six new variants:

  • DistanceMapFooter
  • DistanceManualFooter
  • DistanceOdometerFooter
  • ScanFooter
  • ManualFooter
  • InvoiceFooter

Each renders only the sections its expense type reaches.

Fixed Issues

$ #99449
PROPOSAL: N/A

Tests

  • Verify that no errors appear in the JS console

This is a refactor with no intended behavior change, so testing is regression-focused: every expense type must render and submit exactly as before.

Please, verify the confirmation page looks and works same way as before in the next scenariosu:

  1. Manual expense creation.
  2. Scan expense creation. Verify the compact layout with the receipt thumbnail, that "Show more" expands the remaining fields, and that submitting works.
  3. Distance expense via the map creation.
  4. Distance expense via manual entry.
  5. Distance expense via odometer.
  6. Create an invoice. Verify the sender row renders above the fields.
  7. Submitting untracked expense from self DM.

Offline tests

Same, as in Tests section

QA Steps

// TODO: These must be filled out, or the issue title must include "[No QA]."

  • Verify that no errors appear in the JS console

Same, as in Tests section

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

@codecov

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

@VickyStash

Copy link
Copy Markdown
Contributor Author

@codex review

@VickyStash

Copy link
Copy Markdown
Contributor Author

@MelvinBot review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bf73a117e2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@MelvinBot

Copy link
Copy Markdown
Contributor

🤖 Code review: split the confirmation footer by variants pt2

One thing I'd fix before merge — the amount field's autofocus is silently dropped. Everything else is robustness/style, and I confirmed several things that look risky in the diff are actually fine.


🔴 autoFocus is no longer passed to AmountField

The deleted TransactionDetailsFields computed shouldAutoFocusAmountField = !canUseTouchScreen() and passed autoFocus={shouldAutoFocusAmountField} to AmountField. Neither ManualDetailsFields.tsx:36-45 nor DistanceDetailsFields passes it, so it falls back to its autoFocus = false default. After this PR nothing in the footer tree passes it:

$ grep -rn "autoFocus\|canUseTouchScreen" src/components/MoneyRequestConfirmationListFooter/
(no matches)

That disables the focus effect at AmountField.tsx:117-130, so in the new manual expense flow the amount input no longer regains focus when the participant picker closes on non-touch devices.

The giveaway that this was accidental: ManualDetailsFields still threads isParticipantPickerVisible into AmountField (L43), but that prop exists only to gate the autoFocus effect — so it is now inert too.

Fix: pass autoFocus={!canUseTouchScreen()} in ManualDetailsFields. Distance and time don't need it (isAmountFieldDisabled already covers both). If dropping it everywhere was intentional, then delete the autoFocus prop and the isParticipantPickerVisible threading rather than leaving them half-wired.

🟡 The router's branch order is load-bearing but undocumented

index.tsx:19-49 checks isTypeInvoice before the distance and scan branches. An invoice that was also a distance or scan request would silently lose the map, the Distance field, the Rate field, and compact mode.

I traced it and neither combination is reachable today — the Distance tab is SPLIT-only (IOURequestStartPage.tsx:318) and shouldUseTab excludes invoice (L71), so iouRequestType never becomes DISTANCE*/SCAN for an invoice. So it's not a bug — but this file's correctness now rests on an invariant three files away, and the PR deletes the JSDoc that used to explain footer selection. Please add a comment saying why invoice wins.

🟡 New files have no header description

Seven new files, none with a header comment. The two files this PR deletes (TransactionDetailsFields/index.tsx, InvoiceSenderSection.tsx) and DefaultFooter all had one explaining their role, and our standards ask non-trivial new files to start with a description. The refactor reads as a net loss of "why" documentation.

🟢 Checked and fine

  • {...errorState} is equivalent to the four explicit props — ErrorState is exactly {shouldDisplayFieldError, formError, clearFormErrors, setFormError}.
  • receiptStitchError kept only in DistanceOdometerFooter — correct, it originates from useOdometerReceiptStitcher.
  • Dropping isCompactMode/compactControls from the non-scan variants — correct, isCompactMode = !showMoreFields && isScanRequest && !isInLandscapeMode.
  • Dropping the fieldVisibility.distance / .rate guards inside DistanceDetailsFields — safe, both equal isDistanceRequest.
  • distanceRateCurrency dropped from ManualDetailsFields' AmountField — no effect, only read when isDistanceRequest.
  • TimeFields still rendered, via the pre-existing TimeDetailsFields.
  • shouldShowDistanceMap is not orphaned — still used by DistanceMapSection and ReceiptSection.

Tests

Only DistanceField.test.tsx was updated, and the Provider-wrapping change there is a clean improvement. But the new router in index.tsx is the whole point of the PR and has no test — a mis-ordered or missing branch drops fields with no type error and no test failure. A small table-driven test asserting each flag combination selects the expected variant would lock in the behavior this PR is asserting.

The PR body is also still the empty template: no explanation of change, no Tests or QA steps, no screenshots, checklist unchecked.

Duplication

The five new variants are largely identical ConfirmationFieldList prop threading. I read that as the deliberate tradeoff of splitting by variant (explicit over DRY) so I'm not asking you to change it — just flagging that a reviewer may push back on it.


🤖 Web smoke test: ⚠️ partial

I drove the PR branch on dev NewDot to confirm the two main variants still render. Manual and distance confirmation pages both render their expected fields; I could not check the JS console because the web driver doesn't expose it.

Distance expense confirmation page with route map, Amount, Description

Step Status
Manual expense confirmation page shows Amount, Merchant, and Description fieldsis visible passed for label="Amount, €5.00", label="Merchant", label="Description" on the Confirm details page reached via + > Create expense > Manual.
Manual expense confirmation page with Amount, Merchant, Description
Distance expense confirmation page shows route map, Distance field, and Rate fieldis visible passed for role=region label="Map", label="Distance, 0.51 kilometers", label="Rate, €0.3 / kilometer" on the Confirm details page reached via Distance > Map tab with start/stop addresses set.
Distance expense confirmation page with route map, Amount, Description
JS console errors/warnings during both flowsNot checkable: the web driver has no console inspection command. No error banners, toasts, or broken screens appeared in either flow, but that is not equivalent to a console check.

Note the smoke test only covers the manual and distance-map variants. Scan, odometer, invoice, and the compact-mode path are untested, and the autofocus regression above would not show up in either flow I ran.


view run

…rs-pt2

# Conflicts:
#	src/components/MoneyRequestConfirmationList/sections/InvoiceSenderField.tsx
@VickyStash

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: 0ff12e019d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@VickyStash

Copy link
Copy Markdown
Contributor Author

@MelvinBot review

@MelvinBot

Copy link
Copy Markdown
Contributor

🤖 Code review round 2: split the confirmation footer by variants pt2

No behavior regression found. I traced all eight router branches and every prop each variant threads, and the refactor is equivalent to the old DefaultFooter + TransactionDetailsFields pair for every reachable flag combination. The autoFocus fix from round 1 is genuinely correct, and the added comments closed most of the documentation gap.

npm run test and npm run typecheck both pass locally (38 tests across the 6 relevant suites, zero type errors).

Two things worth addressing before merge, both about the new structure rather than the behavior it produces.


🟡 The router is the point of this PR and has no test coverage

tests/ui/MoneyRequestReportFooter.tsx is the only test that renders the router, and its providerProps omit every mode flag — isTypeInvoice, isPerDiemRequest, isTimeRequest, isDistanceRequest, isManualDistanceRequest, isOdometerDistanceRequest, isScanRequest. They all take their false default, so all five tests fall through to the residual return <ManualFooter {...props} /> at index.tsx:60. Seven of eight branches never render.

And all five assertions target the same shared element:

const reportItem = screen.getByTestId('menu-item-Report');

menu-item-Report comes from SettingsFields inside ConfirmationFieldList, which every variant renders — so even a deliberately mis-wired router would still pass.

This matters more than usual because this PR deleted a guard rather than moving it. DistanceMapSection used to self-guard with if (!isDistanceRequest || isManualDistanceRequest || isOdometerDistanceRequest) return null;. That invariant now lives only in the fall-through order at index.tsx:44-54, where reordering three lines silently drops the map with no type error and no test failure.

Ask: the harness in that file is already 90% of the work. Parameterize providerProps over the eight flag combinations and assert one variant-unique marker each — the map section, the invoice sender row, the odometer stitch error, the compact flex1 wrapper.

🟡 The footer shape now hinges on optional = false booleans

Every mode flag in Provider.tsx:95-102 is optional with a false default. Before this PR, the invoice guard read iouType !== CONST.IOU.TYPE.INVOICE off the required iouType field. Now isTypeInvoice — a separate optional boolean that can legally be false while iouType === INVOICE — gates the entire InvoiceFooter including the sender row.

Not reachable today (MoneyRequestConfirmationList passes both from the same expression), but the invariant went from enforced-by-type to enforced-by-convention. Deriving isTypeInvoice from the already-required iouType inside the router, as the deleted InvoiceSenderSection did, removes one way to get a silently wrong footer for free.

Related: the Omit<> variant prop types read like a safety net but aren't one. InvoiceFooterProps omits distanceData/isCompactMode, but the router spreads the full props object, so if invoice ever gains a scan or distance tab the footer drops those fields with no compile error.

🟢 Nits

  • index.tsx:44-54 — the distance sub-ordering is the one load-bearing order left uncommented. The deleted DistanceMapSection guard also carried a performance rationale (its two-level structure avoided Onyx subscriptions on flows where the map can never render) that now lives nowhere.
  • selectors.ts:230 — stale // --- InvoiceSenderSection --- header pointing at the file this PR deletes. invoiceSenderSliceSelector is now consumed by InvoiceSenderField.
  • types.ts:59,61ManualFooterProps and InvoiceFooterProps are character-identical. type InvoiceFooterProps = ManualFooterProps; keeps them from drifting.
  • The test steps skip numbers 6 and 7 (1,2,3,4,5,8,9), and the author checklist and Screenshots sections are still empty.
Things I checked that turned out fine

autoFocus (round 1's 🔴) — genuinely fixed and equivalent. aef4d61b31 moved !canUseTouchScreen() into the component as shouldAutoFocusOnMount (AmountField.tsx:67) and deleted the prop. Old semantics weren't uniform — TimeDetailsFields never passed it and got the false default — so the new always-on value technically diverges for the time path. It's inert: isAmountFieldDisabled = didConfirm || isReadOnly || shouldShowTimeRequestFields || isDistanceRequest is true there, and the effect returns before autofocusing. Worth knowing that the policy is now the component's rather than the caller's, so future call sites opt in silently.

isParticipantPickerVisible dropped from the distance path. DistanceDetailsFields.tsx:38-47 no longer passes it. The state is reachable (a distance expense from global create can open the picker), and it's harmless only because isDistanceRequest already forces isAmountFieldDisabled true. Cheap to pass through if you want it robust.

Section-by-section equivalence. Order is preserved in every variant. Old was Amount → Merchant → Description → Distance → Rate → Time; ManualDetailsFields keeps Amount → Merchant → Description, DistanceDetailsFields keeps Amount → Description → Distance → Rate. The removed wrapper <View> around the invoice/map sections carried no style, so removing it is layout-neutral in a column container.

Deleted fieldVisibility guards are provably dead in their new homes. fieldVisibility.distance and .rate are both literally isDistanceRequest. shouldShowMerchant contains && !isDistanceRequest. shouldShowTimeRequestFields = isTimeRequest && action === CREATE, which is exactly the TimeFooter branch condition.

Compact-mode plumbing scoped to ScanFooter. Safe because isCompactMode = !showMoreFields && isScanRequest && !isInLandscapeMode (MoneyRequestConfirmationList.tsx:525), and the mode flags are mutually exclusive since they all key off the single transaction.iouRequestType scalar. The defaults are unreachable rather than merely benign — the show-more button only renders behind compactState.isCompactMode.

receiptStitchError scoped to DistanceOdometerFooter. useOdometerReceiptStitcher resets to idle whenever !isOdometerDistanceRequest, so the error is non-null only for odometer.

distanceRateCurrency dropped from ManualDetailsFields. Only read under isDistanceRequest ? ..., which that variant passes as false.

Invoice-first branch order is safe. An invoice is always iouRequestType: 'manual'IOURequestStartPage.tsx:72 sets shouldUseTab = false for INVOICE and the request type then returns MANUAL. No escape hatch via the receipt empty state either.

Context migration. useConfirmationFields throws if used outside the Provider, so a missing Provider would fail loudly rather than default silently. There is exactly one Provider render site and it wraps the whole footer. Nothing outside the footer tree imports the changed or deleted components.

DistanceField.test.tsx is a clean adaptation. Both assertions are byte-identical to main; the ten deleted defaultProps all land on the same values via Provider defaults. It's marginally stronger on typing. One caveat: it now depends on those defaults rather than stating the flags, so if isReadOnly or isManualDistanceRequest ever defaults to true the test silently exercises a different branch instead of failing.

Standards. All seven new files have header descriptions, none uses useMemo/useCallback/React.memo (correct under React Compiler), no inline styles, no unused props or imports, and npm run lint-changed is clean.

Duplication across the five variants. They're largely identical ConfirmationFieldList prop threading. I read that as the deliberate explicit-over-DRY tradeoff of splitting by variant, so I'm not asking you to change it.

Couldn't verify: web smoke test and React Compiler check

Web smoke test — blocked by infra, twice. The browser tab in this run's session was blank (0 DOM nodes, no network activity, pure white screenshots) from before my first command through the end of the run. Two testers tried independently; both confirmed the dev server and bundles were healthy (curl returns 200, rsbuild reported ready built in 20.6s with no compile errors on your branch) and that the page never painted even the static splash HTML. The session's Chrome process had restarted underneath it without a corresponding navigation. Reload is forbidden in this harness because it drops auth, so no in-session recovery was possible. None of the seven expense-type flows were exercised — that is an infra failure, not a signal about your code.

react-compiler-compliance-check aborted on an empty base-ref env var in this sandbox (fatal: ambiguous argument 'origin/'), so I can't claim compliance either way for the new components. CI is the real gate.


view run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants