Skip to content

Split the confirmation footer by variants pt2 - #100041

Merged
mountiny merged 15 commits into
Expensify:mainfrom
callstack-internal:VickyStash/refactor/99449-confirmation-footers-pt2
Sep 8, 2026
Merged

mountiny merged 15 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 any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • 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 any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • 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

Manual
manual.mp4
Scan
scan.mp4
Distance -> map
distance_map.mp4
Distance -> manual
distance_manual.mp4
Distance -> odometr
distance_odometr.mp4
Invoice
invoice.mp4
Submit from self dm
submit.mp4

@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,61 — ManualFooterProps 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

@VickyStash

Copy link
Copy Markdown
Contributor Author

@MelvinBot review

@MelvinBot

Copy link
Copy Markdown
Contributor

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

Both round-2 asks are addressed, and the new router test is the right shape — I mutation-tested it and it genuinely catches a misroute. npm run typecheck passes; all 27 tests across the 5 relevant suites pass.

One gap left in the new test, and it's the one branch that matters most.


🟡 The new test doesn't lock in the invoice-first ordering

MoneyRequestConfirmationListFooter.test.tsx:108-116 renders the invoice case with an empty mode, so every request-type flag is false and the invoice branch is the only one that can match. That asserts invoice routing works — but not that invoice is checked first, which is the one ordering your own comment at index.tsx:26-27 calls out as load-bearing.

I confirmed this by moving the invoice branch from the top of the router to just above the ManualFooter return. All 10 tests still passed.

One-line fix — give the invoice case a mode that would otherwise route elsewhere:

renderFooter({isScanRequest: true}, CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.INVOICE);
expect(jest.mocked(InvoiceFooter)).toHaveBeenCalled();
expect(jest.mocked(ScanFooter)).not.toHaveBeenCalled();

That is also a more faithful regression test: the risk you documented isn't "invoice doesn't route", it's "invoice silently loses the map, Distance, Rate, or compact mode if it ever gains a request type".

While you're there, the seven it.each cases assert only toHaveBeenCalled() on the expected variant. Since exactly one variant renders per case that's sufficient today, but adding a .not.toHaveBeenCalled() on the sibling would state the intent — same reason the two action-guard tests at L118-138 already do it, and those two read the best in the file.

🟢 Still open from round 2 (both optional)

  • index.tsx:44-52 — the distance sub-ordering is the one load-bearing order still uncommented. Less pressing now that the test covers it, but 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 still lives nowhere.
  • types.ts:59,61 — ManualFooterProps and InvoiceFooterProps are still character-identical.
Verified fixed since round 2

isTypeInvoice → iouType === CONST.IOU.TYPE.INVOICE (index.tsx:28). The footer shape no longer hinges on an optional = false boolean that could disagree with iouType. I checked that isTypeInvoice isn't now dead — it's still read by ConfirmationFieldList, useFooterDerivedFlags, useConfirmationSections, and useConfirmationCtaText, so leaving it on the context is correct.

Stale comment fixed — // --- InvoiceSenderField --- in selectors.ts:230.

Router test coverage. All eight branches are now exercised, up from one. I mutation-tested it rather than taking it on faith: reordering the distance branches so bare isDistanceRequest is checked before the manual and odometer sub-branches failed exactly the two expected cases ({isDistanceRequest, isManualDistanceRequest} and {isDistanceRequest, isOdometerDistanceRequest}), so the invariant that replaced the deleted DistanceMapSection guard is genuinely protected now.

Mocking each variant to null and asserting which was called is the right call here — it tests the router's actual job without coupling to what any variant draws. The %s in the it.each title also pretty-prints the mode object ({ isDistanceRequest: true, isManualDistanceRequest: true }), so failures are readable.

Gates. npm run typecheck → passed, zero errors. Tests: 27 passed across MoneyRequestConfirmationListFooter.test.tsx, DistanceField.test.tsx, DistanceRequestController.test.tsx, selectorsTest.ts, and tests/ui/MoneyRequestReportFooter.tsx.

The round-1 and round-2 behavior analysis still stands: no regression found across all eight branches and the props each variant threads.


🤖 Web test steps verification results: ⚠️ partial

4 of the 5 expense-type confirm screens verified correctly on dev NewDot web after this refactor. Scan could not be fully verified because attaching a receipt needs a native file dialog, which the web test harness does not support.

Odometer-based distance confirm page with Distance and Rate

Step Status
Manual expense: confirmation page renders Amount, Merchant, Description and submitsConfirm screen showed Amount=€25, Merchant='Coffee Shop', Description='Team meeting coffee'; clicking Create expense posted the expense into the personal space chat (Coffee Shop, €25.00, Cash).
Manual expense confirm page with Amount, Merchant, Description
Manual expense successfully submitted, visible in chat
✅
Scan expense: compact layout with receipt thumbnail; Show more expands remaining fields; submitting worksScan tab rendered its upload UI (Upload receipts / Choose files / forward-to-email / text-to-47777), but attaching an actual receipt requires a native OS file dialog, which agent-device web does not support. No receipt could be attached, so the app never reached the Scan-type confirm screen and the compact layout, Show more, and submit could not be exercised. This is a test-harness limitation, not a confirmed defect in the PR.
Scan tab upload UI, unable to attach a receipt
❌
Distance expense via Map tab: route map, Distance field, Rate field all renderConfirm details page rendered the Mapbox route between the two waypoints plus Distance='51.01 kilometers' and Rate='€0.3 / kilometer' (confirmed via snapshot read-back).
Distance (Map) confirm page showing route map and Amount
Distance (Map) confirm page showing Distance and Rate fields
✅
Invoice: confirmation page renders the invoice sender row ABOVE the other fieldsAfter enabling Invoices on a test workspace, the Send invoice confirm page rendered 'To' (recipient) then the sender row ('…'s Workspace, Workspace') immediately above Amount/Merchant/Description/Category/Date.
Invoice confirm page with sender row above other fields
✅
Distance expense via Odometer: odometer reading fields plus Distance and Rate renderOdometer entry screen rendered 'Start reading' and 'End reading' fields (computing Total distance: 50 km); the resulting confirm details page rendered Distance='50.00 kilometers' and Rate='€0.3 / kilometer'.
Odometer entry screen with Start/End reading fields
Odometer-based distance confirm page with Distance and Rate
✅

Since Scan is the only variant that reaches compact mode, that path remains unverified end to end — worth a manual pass before merge.


view run

@VickyStash
VickyStash marked this pull request as ready for review September 2, 2026 11:03
@VickyStash
VickyStash requested review from a team as code owners September 2, 2026 11:03
@melvin-bot
melvin-bot Bot requested review from heyjennahay and situchan September 2, 2026 11:03
@melvin-bot

melvin-bot Bot commented Sep 2, 2026

Copy link
Copy Markdown

@situchan Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot
melvin-bot Bot removed the request for review from a team September 2, 2026 11:03
@situchan

situchan commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible 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 checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified proper code patterns were followed (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick).
    • 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 verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately and each prop has a /** comment above it */
    • The file is named correctly
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • For Class Components, any internal methods passed to components event handlers are bound to this properly so there are no scoping issues (i.e. for onClick={this.submit} the method this.submit should be bound to this in the constructor)
    • Any internal methods bound to this are necessary to be bound (i.e. avoid this.submit = this.submit.bind(this); if this.submit is never passed to a component event handler like onClick)
    • All JSX used for rendering exists in the render method
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • 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 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.
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this 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.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp
Android: mWeb Chrome
iOS: HybridApp
ios.mov
iOS: mWeb Safari
MacOS: Chrome / Safari
web1.mov
web2.mov

@situchan

situchan commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

typescript failing

@VickyStash

Copy link
Copy Markdown
Contributor Author

typescript failing

@situchan It's from main, not from this PR #100311

@VickyStash

Copy link
Copy Markdown
Contributor Author

@situchan I'll be OOO next week, is it possible to do the review today?

@situchan

situchan commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

yes

@situchan situchan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

:shipit:

@melvin-bot
melvin-bot Bot requested a review from mountiny September 4, 2026 10:18
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: 35bba5f908

ℹ️ 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

🤖 Regression review — no confirmed user-reachable regression

I reconstructed what the old DefaultFooter + TransactionDetailsFields rendered for each expense type and compared it against every new variant, prop by prop. Every dropped gate and hardcoded prop is provably equivalent given the dispatcher's routing. Full no-op proofs are in the collapsed section.

What I would still change before merging are three robustness gaps, not behavior breaks:

1. The invoice branch is checked first, and nothing enforces its invariant

MoneyRequestConfirmationListFooter/index.tsx:28 returns InvoiceFooter before the scan and distance checks. InvoiceFooter passes no compactState and no distanceData, so if an invoice were ever a scan or distance request the page would render broken — not degraded:

  • isCompactMode is computed purely from the transaction (MoneyRequestConfirmationList.tsx:525), so the wrapper and ReceiptSection would both go compact while ConfirmationFieldList renders every field expanded.
  • The Show more button is the only caller of setShowMoreFields(true), and it needs compactState.isCompactMode. Without it, showMoreFields can never flip — the user is stuck.

I could not find a reachable path today: shouldUseTab is false for invoice so there are no Scan/Distance tabs (IOURequestStartPage.tsx:72), every invoice entry point clears the draft first, and shouldShowReceiptEmptyState excludes invoice so no receipt can be attached from the confirmation page. The old code degraded gracefully here because InvoiceSenderSection self-gated inside DefaultFooter; the new code does not.

Suggested: make the guard self-enforcing — iouType === CONST.IOU.TYPE.INVOICE && !isScanRequest && !isDistanceRequest, or move the invoice check after the scan/distance checks.

2. hasBelowShowMore no longer matches what the compact variant renders

fieldVisibility.ts:149-163 still counts flags.rate and flags.time, but ScanFooter is now the only compact-capable variant and it renders ManualDetailsFields, which has neither. No live impact — scan, distance and time are mutually exclusive iouRequestType values, so both flags are always false under ScanFooter. But the file's comment ("the show-more button cannot drift from the fields actually rendered below it") is now false, and a future variant would get a button that reveals nothing with no test catching it.

3. AmountField autofocus is now opt-out-by-accident

Removing the autoFocus prop for an internal !canUseTouchScreen() (AmountField.tsx:67) changes TimeDetailsFields from false to !canUseTouchScreen(). That is a no-op today only because isAmountFieldDisabled includes shouldShowTimeRequestFields and short-circuits the effect. Any future caller that isn't disabled silently grabs focus on desktop.

Minor: DetailsFieldsContext still types fieldVisibility as Pick<..., 'amount' | 'distance' | 'rate' | 'merchant' | 'time'> and documents isCompactMode as suppressing "Amount, Rate, Merchant, Time" — no consumer reads distance, rate or time any more.

Test coverage gap

tests/unit/components/MoneyRequestConfirmationListFooter.test.tsx mocks all eight variants, so it asserts the routing table and nothing about rendered output. None of the above would be caught by it.

Verified no-ops (things that look like regressions but aren't)
  • DistanceField / RateField now unconditional in DistanceDetailsFields (was fieldVisibility.distance and !isCompactMode && fieldVisibility.rate). fieldVisibility.distance === fieldVisibility.rate === isDistanceRequest, and all three distance variants are gated on isDistanceRequest from the same context. isCompactMode requires isScanRequest, which excludes distance.
  • AmountField isDistanceRequest hardcoded true in DistanceDetailsFields / false in ManualDetailsFields — matches fieldVisibility.distance on every reachable path, and correctly keeps the field disabled and rate-currency-based for distance.
  • shouldShowTimeRequestFields={false} in both new groups. shouldShowTimeRequestFields = isTimeRequest && action === CREATE (useFooterDerivedFlags.ts:102) — that exact predicate routes to TimeFooter first.
  • distanceRateCurrency dropped in ManualDetailsFields — AmountField reads it only behind isDistanceRequest, which is false there.
  • {...errorState} spread — ErrorState is exactly the four props AmountField needs; nothing extra, nothing shadowed.
  • receiptStitchError only in DistanceOdometerFooter — useOdometerReceiptStitcher's error can only leave null via a fail dispatched from the stitching state, and beginStitch requires isOdometerDistanceRequest; flipping the flag off aborts and resets. No other variant can hide a message.
  • DistanceMapSection's two-level guard removed — the deleted guard was isDistanceRequest && !isManualDistanceRequest && !isOdometerDistanceRequest, which is now exactly the DistanceMapFooter dispatch condition. The Onyx-subscription saving is preserved (better: the component isn't mounted at all). Same for InvoiceSenderSection → InvoiceSenderField.
  • compactState / showMoreFields omitted in non-scan variants — the ConfirmationFieldList default {isCompactMode: false, setShowMoreFields: noop} equals what DefaultFooter passed, because isCompactMode is scan-only.
  • toggleHandlers = {} vs toggleHandlers ?? {} — the prop is optional and never null; equivalent.
  • Removed inner <View> that wrapped InvoiceSenderSection + DistanceMapSection — both self-returned null on every other flow, so it was a zero-height box with default flexShrink: 0.
  • Ordering preserved — InvoiceSenderField and DistanceMapSection both still render before ReceiptSection.
  • Merchant omitted from DistanceDetailsFields — shouldShowMerchant already had && !isDistanceRequest.
  • Prop-to-context migration in DistanceField / RateField / MerchantField / InvoiceSenderField — every moved prop is sourced from the same useConfirmationFields() value the old parent read, and there are no consumers outside the provider.
  • isParticipantPickerVisible dropped in DistanceDetailsFields — no-op, isAmountFieldDisabled is true for distance so the picker-close effect never runs. Same latent-coupling caveat as item 3.
  • Component-type swap on tab change now remounts the footer subtree instead of reconciling in place. showMoreFields is safe (it lives in the parent and already resets on transactionID change); the lost state is useCompactReceiptDimensions' cached aspect ratio and container width. Only reachable via a SPLIT Manual→Distance tab switch on a hidden tab, so not user-visible.
  • No stale imports to TransactionDetailsFields, InvoiceSenderSection or DefaultFooter remain.

CI is green on this branch apart from Check independent approval, including typecheck, all 8 test jobs, ESLint and React Compiler Compliance.

Caveat: I could not drive NewDot in a browser for this run, so none of the above was confirmed at runtime — this is static analysis only. The seven scenarios in your Tests section still need a manual pass, particularly Scan (compact layout + Show more) and Invoice.


view run

@MelvinBot

Copy link
Copy Markdown
Contributor

🤖 Code quality review

The direction is right, and a few things here are genuinely good: pushing action / iouType / isReadOnly / didConfirm out of the field prop lists into useConfirmationFields(), replacing AmountField's autoFocus prop with an internal !canUseTouchScreen(), deleting the two-level guard wrappers now that the dispatcher owns the gate, and adding a dispatcher test at all.

Four things I'd fix before merge, then some smaller stuff.

1. Two dispatcher lines look symmetric and aren't

index.tsx:44 and :48 both read isDistanceRequest && isXDistanceRequest, but only one conjunct is doing work:

Drop the redundant conjunct on :44 and add a comment on :48 saying why its check is real. Otherwise a future "simplification" deletes the wrong one and silently misroutes.

2. isTypeInvoice and iouType === INVOICE are now two sources of truth

The dispatcher branches on iouType === CONST.IOU.TYPE.INVOICE (index.tsx:28) while its direct child reads isTypeInvoice for the same question (ConfirmationFieldList.tsx:82). isTypeInvoice is literally iouType === CONST.IOU.TYPE.INVOICE at MoneyRequestConfirmationList.tsx:275.

The new test already shows the hazard: MoneyRequestConfirmationListFooter.test.tsx:112 renders with iouType={INVOICE} and never sets isTypeInvoice, which defaults to false (Provider.tsx:99). That render says both "this is an invoice" and "this is not." It only passes because the variants are mocked. Derive it in ConfirmationFieldList and drop it from the context.

3. The variants repeat ~72 lines of pure pass-through

DistanceManualFooter is DistanceMapFooter minus one JSX line. InvoiceFooter is ManualFooter plus one. The 8-prop <ConfirmationFieldList> block is written out verbatim in all eight variants, and toggleHandlers = {} appears eight times.

Splitting by expense type is the right call and I would not collapse the variants — but re-threading eight identical props isn't explicitness, it buries the two or three lines that actually differ. Two cheap fixes that keep the structure:

  • Give toggleHandlers a default in ConfirmationFieldList.tsx:60, exactly as compactState already has at :76. Removes eight lines, no behavior change.
  • Collect the shared block into one rest object so each variant destructures only what it uses. That takes the variants from ~394 lines to ~240 and makes the difference the majority of each file.

Worth avoiding aboveReceipt / belowReceipt ReactNode slot props as the fix — named slot props are configuration in disguise, and you'd trade one coupling for another.

4. types.ts has three identical aliases, and the Omit enforces nothing

TimeFooterProps (:49), ManualFooterProps (:59) and InvoiceFooterProps (:61) are character-for-character the same Omit<...>. Seven names, five shapes.

And the subtraction doesn't enforce what a reader will assume. TypeScript's excess-property check only fires on fresh object literals, so <InvoiceFooter {...props} /> with the full props object compiles fine — that's already true on main, where PerDiemFooterProps omits receiptOptions and the dispatcher spreads everything anyway. The Omit only stops the body from destructuring the name. Additive Pick composition would say what each variant needs and collapse the duplicates.

Smaller items

DetailsFieldsContext is stale. :7 still picks 'amount' | 'distance' | 'rate' | 'merchant' | 'time', but only amount and merchant are read now — DistanceDetailsFields renders Distance and Rate unconditionally and TimeDetailsFields renders TimeFields unconditionally. :9's comment ("suppresses ... Amount, Rate, Merchant, Time") is now wrong: only Amount and Merchant, only in ManualDetailsFields.

Sibling contracts disagree. DistanceDetailsFields and TimeDetailsFields both take isDescriptionRequired: boolean, while ManualDetailsFields:21 takes the whole requiredFlags bag — including isCategoryRequired, which it never reads. Two scalars would match its siblings. Also TimeDetailsFields is the only one of the three with no description comment, and TimeFooter / PerDiemFooter are the only two variants without one now that six documented ones sit beside them.

A comment points at a function this PR deleted. DistanceMapSection.tsx:19-20 says "the outer gate has already guaranteed..." — that gate was the sibling function in this file, and it now lives three files away in the dispatcher. Reword to name DistanceMapFooter. The three literal true / false arguments below it are also dead configuration now that shouldShowDistanceMap short-circuits on exactly those.

Two dispatcher comments describe the branch not taken. index.tsx:32-33 sits above the PerDiemFooter return but explains what happens when the condition is false; :38-39 does the same for TimeFooter. Reframe each as an explanation of the guard. Separately, the "residual, not a fallback" prose is now written three times — the dispatcher docblock, ManualFooter's header, and two test names. Keep the long version on ManualFooter.

Test gaps, each one line. Mocking all eight variants is right for a routing test. What's missing is the ordering the comments defend: renderFooter({isScanRequest: true}, CREATE, INVOICE) → InvoiceFooter pins invoice-first, and renderFooter({isOdometerDistanceRequest: true}) → ManualFooter pins the load-bearing conjunct from item 1. Adding expect.objectContaining(footerProps) to the existing it.each assertion would also cover the dispatcher's other job — forwarding props — for free. Minor: the {} case at :90 renders as an unhelpful title, and no case names the component it expects.

Folder nesting. fieldGroups/ holds ClassificationFields.tsx and SettingsFields.tsx flat, but the three details groups went into fieldGroups/detailsFields/. Five field groups doesn't earn a subfolder — I'd flatten, and now is the cheap moment since the import paths are already churning. Replacing fieldGroups/TransactionDetailsFields/index.tsx with named files is a clear improvement either way.

I couldn't run the suite locally, so the test observations are from reading. CI is green on this branch apart from Check independent approval.


view run

@VickyStash

Copy link
Copy Markdown
Contributor Author

Melvin's feedback overview:

  1. The invoice branch is checked first, and nothing enforces its invariant
    Suggested: make the guard self-enforcing — iouType === CONST.IOU.TYPE.INVOICE && !isScanRequest && !isDistanceRequest, or move the invoice check after the scan/distance checks.

iouType === CONST.IOU.TYPE.INVOICE can't be scan/distance request, so I think the condition is okay the way it's rigth now.

  1. hasBelowShowMore no longer matches what the compact variant renders
    No live impact — scan, distance and time are mutually exclusive iouRequestType values, so both flags are always false under ScanFooter.

I think it's a small code cleanup we can do in the following PRs.

  1. AmountField autofocus is now opt-out-by-accident
    ... Any future caller that isn't disabled silently grabs focus on desktop.

Since it currently works as expected, I wouldn't consider it's a problem for future callers.


  1. Two dispatcher lines look symmetric and aren't

I think it's okay to keep it as is, considering it's more readable

  1. isTypeInvoice and iouType === INVOICE are now two sources of truth

I think it's a cleanup that can be made in the following PRs.

  1. The variants repeat ~72 lines of pure pass-through
  2. types.ts has three identical aliases, and the Omit enforces nothing

THIS is actually valid, and it will be resolved when we split MoneyRequestConfirmationList.tsx, so every confirmation list will use its footer, and the necessary parameters for this footer will be passed.

@VickyStash

Copy link
Copy Markdown
Contributor Author

I'll be OOO next week, back on September 14 🌴
If any action is needed before that, please reach to @TMisiukiewicz so he can help to find someone

@situchan

situchan commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Agree automated reviews are not blockers

@mountiny mountiny left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@mountiny
mountiny merged commit 378f5f8 into Expensify:main Sep 8, 2026
34 of 35 checks passed
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

🚧 mountiny has triggered a test Expensify/App build. You can view the workflow run here.

@OSBotify

OSBotify commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@OSBotify OSBotify mentioned this pull request Sep 8, 2026
69 tasks done
@OSBotify

OSBotify commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/mountiny in version: 9.4.74-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

@MelvinBot

MelvinBot commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

No help site changes are required, so I did not create a draft PR.

This PR is an internal refactor with no behavior change. It splits MoneyRequestConfirmationListFooter into six per-type variants (DistanceMapFooter, DistanceManualFooter, DistanceOdometerFooter, ScanFooter, ManualFooter, InvoiceFooter) and deletes the shared fallback. Nothing a member sees on the confirmation page changes.

How I checked

Since there is no help site PR, the review and Ready for review steps in your request don't apply here. If a follow-up PR in this series does change what a member sees, tag me on it and I'll draft the docs update then.


view run · view recording

@OSBotify

OSBotify commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/mountiny in version: 9.4.74-2 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

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.

6 participants