Migrate Money Request (Part 1) routes to dynamic URL navigation - #96900
mjasikowski merged 3 commits into
Conversation
Migrate the 3 migratable routes from Expensify#83849 to dynamic routes, removing their backTo params: - NEW_TASK_SHARE_DESTINATION -> task-share-destination (entry: DYNAMIC_ROOT) - MONEY_REQUEST_STEP_SEND_FROM -> send-from (entry: STEP_CONFIRMATION) - MONEY_REQUEST_STEP_COMPANY_INFO -> company-info (entry: STEP_CONFIRMATION) Swap SCREENS keys to DYNAMIC_*, rename pages to Dynamic*, wire linkingConfig + ModalStack + types, replace navigate call sites with createDynamicRoute, and use useDynamicBackPath for back navigation. OldRoutes redirects include the dynamic suffix so a legacy deep-link restores the actual substep (not just the confirmation entry). Query preservation is not guaranteed for these suffixed redirects due to the trailing-wildcard matcher; no call site produces these legacy paths with a query anymore. CONFIRMATION and AMOUNT are not migratable (multiple path params) and are left as static routes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fbb9c20 to
608f613
Compare
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.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 608f613b0a
ℹ️ 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".
| '/create/*/from/*/*': '/create/$1/confirmation/$2/$3/send-from', | ||
| '/create/*/company-info/*/*': '/create/$1/confirmation/$2/$3/company-info', |
There was a problem hiding this comment.
Preserve queries when rewriting legacy invoice substeps
These mappings append the new dynamic suffix after $3, but getMatchingNewRoute compiles a * at the end of a pattern as (.*), so $3 also captures any existing query string. The legacy route builders used to add ?backTo=..., so refreshing or opening an old URL like /create/invoice/from/123/456?backTo=/home redirects to /create/invoice/confirmation/123/456?backTo=/home/send-from; the dynamic suffix is then inside the query, and the app parses the confirmation page instead of the Send from/Company info substep. Please split/preserve the query before appending the suffix, or adjust the matcher, for these invoice substep redirects.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
This PR removes the legacy route builders that were the only source of a ?backTo=, this pattern is a common for migrate route
|
Reviewing... |
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppAndroid: mWeb ChromeiOS: HybridAppiOS: mWeb SafariMacOS: Chrome / SafariScreen.Recording.2026-07-25.at.14.58.24.mov |
|
🚧 mjasikowski has triggered a test Expensify/App build. You can view the workflow run here. |
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
🚀 Deployed to staging by https://github.com/mjasikowski in version: 9.4.45-0 🚀
|
|
🤖 I reviewed the changes in this PR against the help site content under No help site changes are required, so I did not create a draft PR. Why: This PR is a purely internal navigation refactor. It migrates three RHP routes ( There are no new features, renamed UI labels, tabs, settings, or workflow changes that the help articles document. A search of If a follow-up part of this migration introduces a user-visible change (e.g. a new page or a changed label), ping me again and I'll draft the docs update then. |
|
@suneox I think we just need to update the precondition to match the correct steps. Could you take a look? |
|
I’ll take a look at this one after an hour. It appears that the condition test case 1 has been updated in the latest main. |
@mitarachim Thanks for flagging this. Based on the current code, TC 1 only reproduces the "Share row is interactive" path when the task draft has no parentReportID, which happens only when isValidReport === false. So we can ignore this test case — I've updated the QA Steps section accordingly |
|
🚀 Deployed to production by https://github.com/marcaaron in version: 9.4.45-14 🚀
Bundle Size Analysis (Sentry): |

Explanation of Change
Migrate 3 RHP routes in the new-task / money-request flow to dynamic URL navigation (feature #73825), removing their
backToparams:NEW_TASK_SHARE_DESTINATION→task-share-destination(entry screen:NEW_TASK.DYNAMIC_ROOT)MONEY_REQUEST_STEP_SEND_FROM→send-from(entry screen:MONEY_REQUEST.STEP_CONFIRMATION)MONEY_REQUEST_STEP_COMPANY_INFO→company-info(entry screen:MONEY_REQUEST.STEP_CONFIRMATION)Screens are renamed to
Dynamic*and wired throughDYNAMIC_ROUTES+linkingConfig+ModalStackNavigators+types. Call sites now usecreateDynamicRoute(...)and back navigation usesuseDynamicBackPath(...).OldRoutesredirects map the legacy paths to the new nested routes — the invoicesend-from/company-inforedirects include the dynamic suffix so a legacy deep-link restores the actual substep.MONEY_REQUEST_STEP_CONFIRMATIONandMONEY_REQUEST_STEP_AMOUNTare not migratable (multiple required path params) and remain static.Fixed Issues
$ #83849
PROPOSAL:
Tests
DYNAMIC_ROUTES.NEW_TASK_SHARE_DESTINATION— Share Destination from Confirm TaskCase 1.1 — Entry: Confirm task → tap Share → opens Share Destination (dynamic)
Target:
DynamicNewTaskPage.tsx—Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.NEW_TASK_SHARE_DESTINATION.path)).Precondition: logged in; workspace chat
<R>; clean task draft; on Confirm task (task-confirm) withoutparentReportID(Share is interactive).Steps:
<R>.task-confirm(e.g./r/<R>/task-confirmor/task-details/task-confirm)./task-share-destination(e.g.…/task-confirm/task-share-destination) — not absolute/new/task/share-destination.task-share-destination.Expected: Dynamic entry opens the correct page; new URL shape is correct; Back strips the suffix to confirm. No console errors.
Case 1.2 — Select destination → goBack to confirm (selection kept)
Target:
DynamicTaskShareDestinationSelectorModal.tsx—selectReportHandler→Navigation.goBack(backPath).Precondition: on Share Destination (Case 1.1).
Steps:
task-share-destination.Expected: Selection uses
goBack(backPath)(not baregoBack()); confirm context is preserved.Case 1.3 — Refresh / deeplink on new URL
…/task-share-destinationPrecondition: on Share Destination; copy the full URL.
Steps:
Expected: Refresh + deeplink restore the correct stack (confirm underneath + share on top).
Case 1.4 — Old URL
/new/task/share-destination(backward-compat)Target:
OldRoutes.ts—'/new/task/share-destination': '/task-details/task-confirm/task-share-destination'.Precondition: logged in.
Steps:
https://dev.new.expensify.com:8082/new/task/share-destination→ Enter./task-details/task-confirm/task-share-destination(Share Destination on confirm stack).Expected: Old bookmark resolves to the full new URL including suffix.
DYNAMIC_ROUTES.MONEY_REQUEST_STEP_SEND_FROM— Send from (invoice confirmation)Case 2.1 — Entry: Invoice confirmation → tap Send from → opens Send from (dynamic)
Target:
InvoiceSenderField.tsx—Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.MONEY_REQUEST_STEP_SEND_FROM.path)).Precondition: admin workspace
<W>withareInvoicesEnabled; invoice created from FAB / global create (isFromGlobalCreate); participant is not an invoice room; user has ≥1 admin workspace that can send invoices →canUpdateSenderWorkspace === true(Send from chevron visible).Steps:
/create/invoice/confirmation/<T>/<R>/send-from(confirmation + suffix) — not/create/invoice/from/<T>/<R>./send-from.Expected: Dynamic entry is correct; new URL shape is correct; Back strips the suffix. No console errors.
Case 2.2 — Select workspace → return to confirmation
Target:
DynamicIOURequestStepSendFrom.tsx—navigateBack/selectWorkspace.Precondition: on Send from (Case 2.1); ≥2 workspaces that can send invoices (so a change is possible).
Steps:
/send-from.Expected: Selection + back via
useDynamicBackPath; confirmation context preserved.Case 2.3 — Refresh / deeplink on new URL
…/confirmation/…/send-fromPrecondition: on Send from; copy the URL.
Steps:
Expected: Refresh + deeplink restore correctly.
Case 2.4 — Old URL
/create/invoice/from/<T>/<R>(backward-compat)Target:
OldRoutes.ts—'/create/*/from/*/*': '/create/$1/confirmation/$2/$3/send-from'(includes suffix → restores substep).Precondition: logged in; valid
<T>/<R>from Case 2.1 (or any path-shaped txn/report IDs).Steps:
/create/invoice/from/<T>/<R>(no query) → Enter./create/invoice/confirmation/<T>/<R>/send-from(opens Send from on the confirmation stack).Expected: old deeplink restores the Send from substep. (Query variant
?backTo=…is not supported for suffixed redirects; not tested.)DYNAMIC_ROUTES.MONEY_REQUEST_STEP_COMPANY_INFO— Company info (invoice confirm short-circuit)Case 3.1 — Entry: Confirm invoice (no company info yet) → opens Company info (dynamic)
Target:
confirmAction.ts—Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.MONEY_REQUEST_STEP_COMPANY_INFO.path)). Guard:iouType === INVOICE && !hasInvoicingDetails(policy) && transactionID && !routeError.Precondition: sender workspace has no
invoice.companyName+invoice.companyWebsite; on invoice confirmation (FAB → Send Invoice → …).Steps:
/create/invoice/confirmation/<T>/<R>/company-info./company-info.Expected: Short-circuit navigate uses the dynamic suffix; Back strips correctly. No console errors.
Case 3.2 — Refresh / deeplink on new URL
…/company-infoPrecondition: on Company info; copy the URL.
Steps:
Expected: Refresh + deeplink restore correctly.
Case 3.3 — Old URL
/create/invoice/company-info/<T>/<R>(backward-compat)Target:
OldRoutes.ts—'/create/*/company-info/*/*': '/create/$1/confirmation/$2/$3/company-info'(includes suffix → restores substep).Precondition: logged in.
Steps:
/create/invoice/confirmation/<T>/<R>/company-info(opens Company info).Expected: old deeplink restores Company info. (Query variant not supported for suffixed redirects; not tested.)
Offline tests
QA Steps
#### Test case 1: New Task — Share destination opens as a nested route (Only happen from FAB with isValidReport=false base on current code)/task-share-destination(e.g.…/task-confirm/task-share-destination) — not the old/new/task/share-destination.task-share-destination).Test case 2: Invoice — Send from opens as a nested route
/create/invoice/confirmation/<T>/<R>/send-from— not the old/create/invoice/from/<T>/<R>./send-from).Test case 3: Invoice without company info — Company info opens as a nested route
/create/invoice/confirmation/<T>/<R>/company-info./company-info).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 — Entry: Confirm task → tap Share → opens Share Destination (dynamic)
Case 1.2 — Select destination → goBack to confirm (selection kept)
Case 1.3 — Refresh / deeplink on new URL …/task-share-destination
Case 1.4 — Old URL /new/task/share-destination (backward-compat)
Case 2.1 — Entry: Invoice confirmation → tap Send from → opens Send from (dynamic)
Case 2.2 — Select workspace → return to confirmation
Case 2.3 — Refresh / deeplink on new URL …/confirmation/…/send-from
Case 2.4 — Old URL /create/invoice/from/<T>/<R> (backward-compat)
Case 3.1 — Entry: Confirm invoice (no company info yet) → opens Company info (dynamic)
Case 3.2 — Refresh / deeplink on new URL …/company-info
Case 3.3 — Old URL /create/invoice/company-info/<T>/<R> (backward-compat)