Skip to content

Migrate money request time step to dynamic routes (#83850 part 2 - #97762

Open
suneox wants to merge 3 commits into
Expensify:mainfrom
suneox:83850-migrate-money-request-part-2-v2-suneox
Open

Migrate money request time step to dynamic routes (#83850 part 2#97762
suneox wants to merge 3 commits into
Expensify:mainfrom
suneox:83850-migrate-money-request-part-2-v2-suneox

Conversation

@suneox

@suneox suneox commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

Fixed Issues

$ #83850
$ #97558
PROPOSAL:

Tests

DYNAMIC_ROUTES.MONEY_REQUEST_STEP_TIMEwizard time step (non-edit)

Case 1.1 — Destination → Time forward (multi-policy, through the workspace selector)

Target: src/pages/iou/request/step/DynamicIOURequestStepDestination.tsx:183 · src/pages/iou/request/step/IOURequestStepPerDiemWorkspace.tsx:101

Precondition: at least 2 workspaces with per diem enabled and rates, and the currently selected workspace without per diem, so the per diem tab shows the workspace selector.

Steps:

  1. Open the app and sign in with the test account.
  2. Tap the FAB in the LHN (global create — not the FAB inside a report).
  3. Tap Create expense.
  4. Open the Per diem tab.
  5. Pick a workspace from the selector.
  6. Pick a destination.

Expected: the Time step opens at create/create/start/1/<workspace expense chat reportID>/per-diem-destination/per-diem-time. The reportID in the URL is the picked workspace's expense chat, not the report the flow was started from.


Case 1.2 ⭐ — Time Back → Destination (the deploy blocker case, #97558)

Target: src/pages/iou/request/step/DynamicIOURequestStepTime.tsx:90

Precondition: Case 1.1 completed — the Time step is open in the multi-policy flow.

Steps:

  1. Tap Back on the Time step.

Expected: the Destination step opens with a single per-diem-destination suffix in the URL — no duplicated per-diem-destination/per-diem-destination and no "Oops, this page cannot be found".


Case 1.3 — Time Save → Subrate (forward; the subrate step is still a static route)

Precondition: the Time step is open. start = end = 12:00 AM is invalid.

Steps:

  1. Open the End time picker.
  2. Switch to PM and save the picker.
  3. Tap Save on the step.

Expected: the Subrate step opens.


Case 1.4 — Subrate Back → Time (cross-file goBack)

Target: src/pages/iou/request/step/IOURequestStepSubrate.tsx:131

Precondition: Case 1.3 completed — the Subrate step is open.

Steps:

  1. Tap Back on the Subrate step.

Expected: the Time step opens again at …/per-diem-destination/per-diem-time, with no duplicated suffix and no "Oops, this page cannot be found".


Case 1.5 — Refresh on the Time URL (multi-policy) → Back

Precondition: the Time step is open in the multi-policy flow (Case 1.1).

Steps:

  1. Refresh the page (F5) while on the Time URL.
  2. Wait for the app to restore.
  3. Tap Back.

Expected: the Time step is restored with its policy resolved (the URL carries the workspace expense chat reportID), and Back returns to the Destination step.


Case 1.6 — Single per diem policy: Time Back → start page

Target: src/pages/iou/request/step/DynamicIOURequestStepTime.tsx:90

Precondition: exactly 1 workspace with per diem rates, so the destination step renders inline on the start page.

Steps:

  1. Tap the FAB in the LHN → Create expense.
  2. Open the Per diem tab (the destination list renders inline).
  3. Pick a destination — the Time step opens at …/start/1/<reportID>/per-diem-time, without a per-diem-destination suffix.
  4. Tap Back.

Expected: the start page's Per diem tab opens on the first tap.


Case 1.7 — Report-bound flow: Time Back → start page Per diem tab

Target: src/pages/iou/request/step/DynamicIOURequestStepTime.tsx:90

Precondition: an expense chat of a workspace with per diem enabled and rates.

Steps:

  1. Open that expense chat and tap the FAB inside the reportCreate expense.
  2. Open the Per diem tab and pick a destination.
  3. Tap Back on the Time step.

Expected: the start page's Per diem tab opens (…/start/1/<reportID>/per-diem). Not the Destination RHP and not "Oops, this page cannot be found".


Case 1.8 — Old URL redirect (non-edit)

Target: src/libs/Navigation/linkingConfig/OldRoutes.ts:14

Precondition: none — the redirect is evaluated while building the state from the URL.

Steps:

  1. Open the old URL create/submit/time/<transactionID>/<reportID> in the address bar.

Expected: it redirects to create/submit/destination/<transactionID>/<reportID>/per-diem-time and the Time step opens. ⚠️ A ?backTo= query on the old URL is not preserved (the trailing wildcard swallows it) — acceptable because no call site produces the old path with a query anymore.



DYNAMIC_ROUTES.MONEY_REQUEST_STEP_TIME_EDITediting time from the confirmation step

Case 2.1 — Confirmation → edit Time (forward)

Target: src/components/MoneyRequestConfirmationList/sections/PerDiemFields.tsx:129

Precondition: a per diem expense driven all the way to the Confirmation step (destination → time → subrate with a quantity → Save).

Steps:

  1. On the Confirmation step, tap the Time field.

Expected: the Time step opens in edit mode (header "Time") at …/confirmation/…/per-diem-time-edit.


Case 2.2 — Edit Back → Confirmation (value unchanged)

Target: src/pages/iou/request/step/DynamicIOURequestStepTime.tsx:86

Precondition: Case 2.1 completed — the edit page is open.

Steps:

  1. Tap Back without changing anything.

Expected: the Confirmation step opens again with the time unchanged and no "Oops, this page cannot be found".


Case 2.3 — Edit, change the time and Save → Confirmation (updated)

Precondition: Case 2.1 completed — the edit page is open.

Steps:

  1. Change the end time.
  2. Tap Save.

Expected: the Confirmation step opens again and the Time field shows the new value.


Case 2.4 — Refresh on the per-diem-time-edit URL → Back

Precondition: Case 2.1 completed — the edit page is open.

Steps:

  1. Refresh the page (F5).
  2. Wait for the app to restore, then tap Back.

Expected: the edit page is restored on top of the Confirmation base, and Back returns to Confirmation.


Case 2.5 — Old URL redirect (edit)

Target: src/libs/Navigation/linkingConfig/OldRoutes.ts:13

Precondition: none.

Steps:

  1. Open the old URL create/submit/time/<transactionID>/<reportID>/edit in the address bar.

Expected: it redirects to create/submit/confirmation/<transactionID>/<reportID>/per-diem-time-edit and the edit page opens. ⚠️ Known gap across the whole migration (MoneyRequestInitializer re-initialises the flow when the transaction draft no longer exists) — if it lands on start/1/<reportID>/per-diem instead, that is the known gap, not a regression of this PR.


  • Verify that no errors appear in the JS console

Offline tests

  • Same as tests — navigation refactor, offline behavior unchanged.

QA Steps

Test case 1: Per diem time step opens and goes back correctly when a workspace is picked first

  1. Sign in with an account that has at least 2 workspaces with per diem rates, while the currently selected workspace does not have per diem enabled.
  2. Tap the + button in the left-hand navigation (not the one inside a chat) and tap Create expense.
  3. Open the Per diem tab and pick a workspace.
  4. Pick a destination.
  5. Verify the Time step opens and the browser URL ends with /per-diem-destination/per-diem-time.
  6. Tap the back arrow.
  7. Verify the Destination step opens again, the URL ends with a single /per-diem-destination, and no "Oops, this page cannot be found" is shown.

Test case 2: Time can be edited from the confirmation step

  1. Continue the per diem expense from Test case 1: set the end time to a PM value, save the step, add a subrate quantity and save until the Confirmation step is shown.
  2. Tap the Time field on the Confirmation step.
  3. Verify the time page opens with the header Time and the browser URL ends with /per-diem-time-edit.
  4. Change the end time and tap Save.
  5. Verify the Confirmation step is shown again and the Time field displays the new value.
  6. Tap the Time field again, then tap the back arrow.
  7. Verify the Confirmation step is shown again with the time unchanged.
  • Verify that no errors appear in the JS console

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
CleanShot.2026-08-04.at.14.04.23.1.mp4
Case 1.1 — Destination → Time forward (multi-policy, through the workspace selector) 1 1-1-before-destination-list 1 1-2-entry-time-step-ws-reportID
Case 1.2 — Time Back → Destination (the deploy blocker case, #97558) 1 2-3-after-back-destination-one-suffix
Case 1.3 — Time Save → Subrate 1 3-1-before-time-step-before-save 1 3-2-entry-subrate-static
Case 1.4 — Subrate Back → Time 1 4-1-before-subrate 1 4-3-after-back-time-restored
Case 1.5 — Refresh on the Time URL → Back 1 5-1-before-time-before-refresh 1 5-2-entry-time-after-refresh 1 5-3-after-back-destination
Case 1.6 — Single per diem policy: Time Back → start page 1 6-1-before-start-perdiem-dest-inline 1 6-2-entry-time-single-policy 1 6-3-after-back-start-perdiem
Case 1.7 — Report-bound flow: Time Back → start page Per diem tab 1 7-1-before-start-perdiem-dest-inline 1 7-2-entry-time-inline-dest 1 7-3-after-back-start-perdiem-tab
Case 1.8 — Old URL redirect (non-edit) 1 8-1-before-time-oldurl 1 8-2-entry-time-redirected
Case 2.1 — Confirmation → edit Time 2 1-1-before-confirmation 2 1-2-entry-time-edit
Case 2.2 — Edit Back → Confirmation 2 2-1-before-time-edit 2 2-3-after-back-confirmation
Case 2.3 — Edit, change the time and Save → Confirmation 2 3-1-before-confirmation-time-12pm 2 3-2-entry-time-edit 2 3-3-after-save-confirmation-time-updated
Case 2.4 — Refresh on the per-diem-time-edit URL → Back 2 4-1-before-time-edit 2 4-2-entry-time-edit-restored 2 4-3-after-back-confirmation
Case 2.5 — Old URL redirect (edit) 2 5-1-before-time-edit-oldurl 2 5-2-entry-time-edit-redirected

…rt 2, re-land)

Re-lands PR Expensify#96963 (reverted by Expensify#97582 as deploy blocker Expensify#97558) together with
the fix for the regression that caused the revert.

Migration: MONEY_REQUEST_STEP_TIME / _TIME_EDIT become the dynamic suffixes
`per-diem-time` / `per-diem-time-edit`, the page is renamed to
DynamicIOURequestStepTime, `backTo` is dropped from both screen params and the
old static paths redirect through OldRoutes.

Regression fix (Expensify#97558): the per diem workspace selector retargets the report
when it opens the destination step, but the start route beneath it kept the
report the flow was started from. Because the dynamic suffixes carry no
`:reportID`, the URL - and therefore any back path derived from it - described
the stale report, so `goUp` could not match the destination route in the stack
and replaced the time step instead of popping to it, duplicating the suffix and
landing on a not found page. The selector now retargets its own route params so
the URL stays truthful, and the wizard back target is rebuilt from the time
route's own params instead of being derived from the URL.

Adds tests/navigation/PerDiemTimeGoBackTests.tsx, which locks in the live
(non-refresh) back path for both the matching and the stale-reportID case.
@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

@suneox
suneox marked this pull request as ready for review August 4, 2026 07:14
@suneox
suneox requested review from a team as code owners August 4, 2026 07:14
@melvin-bot
melvin-bot Bot requested review from JmillsExpensify and huult and removed request for a team August 4, 2026 07:14
@melvin-bot

melvin-bot Bot commented Aug 4, 2026

Copy link
Copy Markdown

@huult 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 August 4, 2026 07:14
`retarget` is not in the cspell dictionary, so the comments and the test helper
name use plain wording instead, and each explanation is trimmed to three lines.

@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: 2e7a9a8ea4

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

// makes the URL describe a report that has no per diem rates: refreshing or sharing the destination/time
// URL then resolves an empty policy and renders Not Found, and the start page treats its own draft as
// stale (`isStaleTransactionDraft`) because the draft's reportID no longer matches the route's.
navigation.setParams({reportID: targetReport.reportID});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retarget the base iouType along with the report

When the workspace selector retargets a global-create per diem flow to the self-DM/unreported case, getInitialPerDiemTargetReport() can change targetIouType to TRACK, but this only updates the base route's reportID. Because the new destination/time steps are dynamic suffixes and their browser URL is rebuilt from the underlying start route, refreshing or sharing the time URL in that case rehydrates with the original iouType instead of TRACK, even though the transaction was retargeted. Please set the start route's iouType to targetIouType at the same time as the reportID.

Useful? React with 👍 / 👎.

// the wizard variant to the destination-based suffix. Query is not preserved (trailing wildcard would swallow it) -
// acceptable because callers no longer append `?backTo=`.
'/*/*/time/*/*/edit': '/$1/$2/confirmation/$3/$4/per-diem-time-edit',
'/*/*/time/*/*': '/$1/$2/destination/$3/$4/per-diem-time',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve legacy time URLs with backToReport

The removed static time route accepted an optional backToReport path segment, so existing links like /create/submit/time/<transactionID>/<reportID>/<backToReport> no longer match this redirect because the pattern ends after the reportID. Those legacy URLs now fall through after MONEY_REQUEST_STEP_TIME was removed from the linking config instead of opening the migrated dynamic time step; add a more specific redirect that carries the extra segment into the new start/destination/time path.

Useful? React with 👍 / 👎.

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.

@suneox Could you check the Codex comments? 😉

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

/*/*/time/*/* does not end at the reportID. In getMatchingNewRoute a trailing * compiles to (.*) (multi-segment) while non-trailing wildcards compile to ([^/]+) (exactly one segment). So the last wildcard swallows <reportID>/<backToReport> and $4 carries both segments into the new path.

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