Migrate money request hold reason and receipt view routes to dynamic … - #98751
Draft
suneox wants to merge 2 commits into
Draft
Migrate money request hold reason and receipt view routes to dynamic …#98751suneox wants to merge 2 commits into
suneox wants to merge 2 commits into
Conversation
melvin-bot
Bot
requested review from
JmillsExpensify and
huult
and removed request for
a team
August 17, 2026 07:47
suneox
marked this pull request as draft
August 17, 2026 07:48
Codecov Report❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.
|
…routes Part 1 of Expensify#83853. Replaces the backTo-based static routes MONEY_REQUEST_HOLD_REASON and MONEY_REQUEST_RECEIPT_VIEW with dynamic routes (suffix hold-reason / receipt-view + query params), following the MONEY_REQUEST_STEP_TAX_RATE pattern: - Add DYNAMIC_ROUTES entries with entryScreens (REPORT + search RHP screens for hold-reason; CREATE + STEP_SCAN for receipt-view) - Rename pages to DynamicHoldReasonPage / DynamicReceiptView using useDynamicBackPath instead of route.params.backTo - Swap screen keys (DYNAMIC_HOLD_REASON / DYNAMIC_RECEIPT_VIEW) in SCREENS, linkingConfig, ModalStackNavigators and types - Update navigation call sites in ReportUtils.changeMoneyRequestHoldStatus and ReceiptPreviews to createDynamicRoute - Update ReportNavigateAwayHandler screen-name check and unit tests Legacy URLs carried the base screen inside ?backTo= (query), so no OldRoutes redirect is technically possible; both routes had no existing redirect on main either.
suneox
force-pushed
the
83853-migrate-money-request-part-6-part-1-suneox
branch
from
August 17, 2026 07:57
111b739 to
f1c9c09
Compare
JmillsExpensify
left a comment
Contributor
There was a problem hiding this comment.
No product review required.
JmillsExpensify
self-requested a review
August 17, 2026 08:39
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Explanation of Change
Part 1 of #83853 (BT-125). Migrates the last
backTo-based money request routesMONEY_REQUEST_HOLD_REASONandMONEY_REQUEST_RECEIPT_VIEWto dynamic routes, following the establishedMONEY_REQUEST_STEP_TAX_RATEpattern:DYNAMIC_ROUTES.MONEY_REQUEST_HOLD_REASON(suffixhold-reason, query paramstransactionID/reportID, entry screens:REPORT+ the 3 search RHP screens) andDYNAMIC_ROUTES.MONEY_REQUEST_RECEIPT_VIEW(suffixreceipt-view, query paramtransactionID, entry screens:CREATE+STEP_SCAN).DynamicHoldReasonPage/DynamicReceiptViewand replacedroute.params.backTowithuseDynamicBackPath, so the back target is derived from the current URL instead of an encoded query param.DYNAMIC_HOLD_REASON/DYNAMIC_RECEIPT_VIEW) inSCREENS, the linking config,MoneyRequestModalStackNavigatorand the param list types; updated the navigation call sites inReportUtils.changeMoneyRequestHoldStatusandReceiptPreviewstocreateDynamicRoute.OldRoutesredirect was added: the legacy URLs carried the base screen inside the?backTo=query (andreportIDwas hand-appended to the query as well), which the redirect engine cannot lift into the path. Neither route had any existing redirect onmain, so no previously-working link changes behavior — legacy URLs now land on the NotFound page gracefully.Fixed Issues
$ #83853
PROPOSAL:
Tests
DYNAMIC_ROUTES.MONEY_REQUEST_HOLD_REASON — single-expense hold reason page (context menu / secondary actions)
+++ Case 1.1 — Open hold reason page from expense context menu and verify the dynamic URL
Target: src/libs/ReportUtils.ts:5555
Precondition: Test account has at least one expense (not on hold) in a 1:1 chat or workspace report; report not approved/paid/closed; expense not being SmartScanned.
Steps:
/r/<reportID>).Expected: The hold reason page opens: header "Hold expense", description "Explain why you're holding this expense.", a Reason field and a "Hold expense" submit button. The URL is the source screen's URL plus the suffix:
/r/<reportID>/hold-reason?transactionID=<transactionID>&reportID=<childReportID>— no?backTo=parameter remains.+++
+++ Case 1.2 — Back (X) returns to the source screen with the suffix stripped from the URL
Target: src/pages/iou/HoldReasonFormView.tsx:52
Precondition: Same as Case 1.1 (currently on the hold reason page, URL
/r/<reportID>/hold-reason?transactionID=...&reportID=...).Steps:
Expected: You return to the same chat report
/r/<reportID>. The URL no longer has thehold-reasonsuffix nor thetransactionID/reportIDquery params. The expense is NOT held (no "held this expense" message).+++
+++ Case 1.3 — Submit the reason: expense is held and the app returns to the source screen
Target: src/pages/iou/DynamicHoldReasonPage.tsx:80
Precondition: Same as Case 1.1.
Steps:
Expected: Step 2 shows a required-field error on Reason. Step 4 submits successfully:
putOnHoldis called and the app goes back to the source chat report — URL returns to/r/<reportID>(suffix stripped). A system message like "held this expense" appears in the chat and the expense shows the on-hold state (context-menu label changes to "Remove hold"/"Unhold").+++
+++ Case 1.4 — Refresh (F5) on the dynamic URL restores the full stack
Target: src/libs/Navigation/linkingConfig/config.ts:1794
Precondition: Same as Case 1.1; currently on the hold reason page at URL
/r/<reportID>/hold-reason?transactionID=...&reportID=....Steps:
Expected: After reload the hold reason page is fully restored: "Hold expense" title, Reason field, submit button — no white screen, no NotFound. Back in step 3 returns to the correct chat report
/r/<reportID>(the base screen + dynamic page stack is restored from the URL).+++
+++ Case 1.5 — Legacy URL (
:type/edit/reason/...?backTo=) shows the NotFound page (accepted regression)Target: src/ROUTES.ts:369
Precondition: Signed in to the app in a browser.
Steps:
https://<domain>/personal/edit/reason/1234567890.1234567?backTo=%2Fr%2F9876543210(or replacepersonalwithteam/corporate).Expected: The NotFound page is shown ("Oops, this page cannot be found") with a back button — no crash, no white screen, no redirect loop.⚠️ This is an accepted regression: the old static route was removed and the legacy base lived inside the
?backTo=query, which cannot be lifted into the path for a redirect. The case only confirms the failure is graceful.+++
DYNAMIC_ROUTES.MONEY_REQUEST_RECEIPT_VIEW — receipt carousel view from multi-scan thumbnail previews
+++ Case 2.1 — Open receipt view from a thumbnail preview on the scan tab and verify the dynamic URL
Target: src/pages/iou/request/step/IOURequestStepScan/components/ReceiptPreviews/index.tsx:105
Precondition: Enable mobile emulation / mobile UA in DevTools (thumbnail previews only render in the mobile camera variant; desktop shows drag-and-drop). Allow camera access.
Steps:
/create/expense/start/<transactionID>/<reportID>/scan).Expected: The receipt view opens fullscreen: header "Receipt", back (X) and delete (trash) buttons, the receipt shown in a carousel. The URL is the current scan URL plus the suffix:
/create/expense/start/<transactionID>/<reportID>/scan/receipt-view?transactionID=<draftTransactionID>— no?backTo=, and thetransactionIDquery matches the tapped receipt (the second image is displayed).+++
+++ Case 2.2 — Back (header X and swipe down) returns to the scan tab with the suffix stripped
Target: src/pages/iou/request/step/IOURequestStepScan/DynamicReceiptView/index.tsx:83
Precondition: Same as Case 2.1; currently on the receipt view.
Steps:
Expected: Both actions return to the scan tab of the create-expense flow (camera + thumbnail strip as before); the URL returns to
/create/expense/start/<transactionID>/<reportID>/scan— thereceipt-viewsuffix andtransactionIDquery are stripped. The receipt remains in the preview strip (not deleted). If the swipe-down gesture can't be performed with a mouse, use touch emulation and just verify the header X.+++
+++ Case 2.3 — Refresh (F5) on the dynamic URL restores the full scan + receipt view stack
Target: src/libs/Navigation/linkingConfig/config.ts:1800
Precondition: Same as Case 2.1; currently on the receipt view at URL
/create/expense/start/<transactionID>/<reportID>/scan/receipt-view?transactionID=....Steps:
Expected: After reload the receipt view is restored: "Receipt" header + the receipt matching the
transactionIDquery still displays (the draft transaction persists in Onyx). Back in step 3 returns to the scan tab with the base URL (/create/expense/start/<transactionID>/<reportID>/scan) — no NotFound, no draft data loss.+++
+++ Case 2.4 — Legacy URL (
receipt-view/:transactionID?backTo=) shows the NotFound page (accepted regression)Target: src/ROUTES.ts:375
Precondition: Signed in to the app in a browser.
Steps:
https://<domain>/receipt-view/1234567890.1234567?backTo=%2Fcreate%2Fexpense%2Fstart%2F....Expected: The NotFound page is shown ("Oops, this page cannot be found") with a back button — no crash, no white screen.⚠️ Accepted regression (old static route removed;
backTolived in the query so a redirect is impossible). The case only confirms graceful failure.+++
Offline tests
QA Steps
Test case 1: Hold an expense and verify the new URL + back navigation
/hold-reason(e.g./r/<reportID>/hold-reason?...) with nobackToin the URL.hold-reason, and the expense shows as held.Test case 2: Open a captured receipt from the multi-scan previews (mobile web)
/receipt-view(nobackToin the URL).receipt-view.PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectionAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari
+++ Case 1.1 — Open hold reason page from expense context menu and verify the dynamic URL
+++
+++ Case 1.2 — Back (X) returns to the source screen with the suffix stripped from the URL
+++
+++ Case 1.3 — Submit the reason: expense is held and the app returns to the source screen
+++
+++ Case 1.4 — Refresh (F5) on the dynamic URL restores the full stack
+++
+++ Case 1.5 — Legacy URL (
:type/edit/reason/...?backTo=) shows the NotFound page (accepted regression)+++
+++ Case 2.1 — Open receipt view from a thumbnail preview on the scan tab and verify the dynamic URL
+++
+++ Case 2.2 — Back (header X and swipe down) returns to the scan tab with the suffix stripped
+++
+++ Case 2.3 — Refresh (F5) on the dynamic URL restores the full scan + receipt view stack
+++
+++ Case 2.4 — Legacy URL (
receipt-view/:transactionID?backTo=) shows the NotFound page (accepted regression)+++