From 18e37099105a6eb6d433d0e08b463ae323ab993f Mon Sep 17 00:00:00 2001 From: Cong Pham Date: Wed, 29 Jul 2026 17:39:34 +0700 Subject: [PATCH 1/3] Migrate money request time step to dynamic routes (#83850 part 2) Migrate MONEY_REQUEST_STEP_TIME (path per-diem-time, entry STEP_DESTINATION + MONEY_REQUEST.CREATE) and MONEY_REQUEST_STEP_TIME_EDIT (path per-diem-time-edit, entry STEP_CONFIRMATION) to dynamic routes, removing their static routes and the backTo param. - Rename IOURequestStepTime -> DynamicIOURequestStepTime; edit mode from route.name, back path from useDynamicBackPath (replaces the reconstructed conditional back logic). - Destination forward builds Time on the start base when opened inline on the start page (single per-diem policy) and on the dynamic destination base otherwise; subrate goBack and confirmation edit use createDynamicRoute. - Add OldRoutes redirects + getMatchingNewRoute unit tests. Co-Authored-By: Claude --- src/ROUTES.ts | 18 +++--- src/SCREENS.ts | 4 +- .../sections/PerDiemFields.tsx | 2 +- .../ModalStackNavigators/index.tsx | 4 +- .../Navigation/linkingConfig/OldRoutes.ts | 5 ++ src/libs/Navigation/linkingConfig/config.ts | 4 +- src/libs/Navigation/types.ts | 8 +-- .../step/DynamicIOURequestStepDestination.tsx | 11 +++- ...Time.tsx => DynamicIOURequestStepTime.tsx} | 62 ++++++------------- .../request/step/IOURequestStepSubrate.tsx | 10 ++- .../step/withFullTransactionOrNotFound.tsx | 4 +- .../step/withWritableReportOrNotFound.tsx | 4 +- tests/navigation/getMatchingNewRouteTest.ts | 18 ++++++ 13 files changed, 80 insertions(+), 74 deletions(-) rename src/pages/iou/request/step/{IOURequestStepTime.tsx => DynamicIOURequestStepTime.tsx} (72%) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index ac8319cb9aa7..98849d6af43d 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -247,6 +247,14 @@ const DYNAMIC_ROUTES = { path: 'per-diem-destination-edit', entryScreens: [SCREENS.MONEY_REQUEST.STEP_CONFIRMATION], }, + MONEY_REQUEST_STEP_TIME: { + path: 'per-diem-time', + entryScreens: [SCREENS.MONEY_REQUEST.DYNAMIC_STEP_DESTINATION, SCREENS.MONEY_REQUEST.CREATE], + }, + MONEY_REQUEST_STEP_TIME_EDIT: { + path: 'per-diem-time-edit', + entryScreens: [SCREENS.MONEY_REQUEST.STEP_CONFIRMATION], + }, PROFILE: { path: 'a/:accountID', entryScreens: ['*'], @@ -2022,21 +2030,11 @@ const ROUTES = { return getUrlWithBackToParam(`${action as string}/${iouType as string}/vendor/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo); }, }, - MONEY_REQUEST_STEP_TIME: { - route: ':action/:iouType/time/:transactionID/:reportID/:backToReport?', - getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backToReport?: string, backTo = '') => - getUrlWithBackToParam(`${action as string}/${iouType as string}/time/${transactionID}/${reportID}${backToReport ? `/${backToReport}` : ''}`, backTo), - }, MONEY_REQUEST_STEP_SUBRATE: { route: ':action/:iouType/subrate/:transactionID/:reportID/:backToReport?/:pageIndex', getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backToReport?: string, backTo = '') => getUrlWithBackToParam(`${action as string}/${iouType as string}/subrate/${transactionID}/${reportID}${backToReport ? `/${backToReport}` : ''}/0`, backTo), }, - MONEY_REQUEST_STEP_TIME_EDIT: { - route: ':action/:iouType/time/:transactionID/:reportID/edit', - getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`${action as string}/${iouType as string}/time/${transactionID}/${reportID}/edit`, backTo), - }, MONEY_REQUEST_STEP_SUBRATE_EDIT: { route: ':action/:iouType/subrate/:transactionID/:reportID/edit/:pageIndex', getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, pageIndex = 0, backTo = '') => diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 7aec49c2c919..5cdb5378c832 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -357,10 +357,10 @@ const SCREENS = { DYNAMIC_STEP_ATTENDEES: 'Dynamic_Money_Request_Attendee', STEP_ACCOUNTANT: 'Money_Request_Accountant', DYNAMIC_STEP_DESTINATION: 'Dynamic_Money_Request_Destination', - STEP_TIME: 'Money_Request_Time', + DYNAMIC_STEP_TIME: 'Dynamic_Money_Request_Time', STEP_SUBRATE: 'Money_Request_SubRate', DYNAMIC_STEP_DESTINATION_EDIT: 'Dynamic_Money_Request_Destination_Edit', - STEP_TIME_EDIT: 'Money_Request_Time_Edit', + DYNAMIC_STEP_TIME_EDIT: 'Dynamic_Money_Request_Time_Edit', STEP_SUBRATE_EDIT: 'Money_Request_SubRate_Edit', STEP_REPORT: 'Money_Request_Report', EDIT_REPORT: 'Money_Request_Edit_Report', diff --git a/src/components/MoneyRequestConfirmationList/sections/PerDiemFields.tsx b/src/components/MoneyRequestConfirmationList/sections/PerDiemFields.tsx index d662387a3c24..e001a9e63c5f 100644 --- a/src/components/MoneyRequestConfirmationList/sections/PerDiemFields.tsx +++ b/src/components/MoneyRequestConfirmationList/sections/PerDiemFields.tsx @@ -126,7 +126,7 @@ function PerDiemFields({perDiemCustomUnit, transaction, isReadOnly, didConfirm, if (!transactionID) { return; } - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TIME_EDIT.getRoute(action, iouType, transactionID, reportID)); + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.MONEY_REQUEST_STEP_TIME_EDIT.path)); }} disabled={didConfirm} interactive={!isReadOnly} diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 268cf5e4c082..4b4b6b1244bb 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -208,10 +208,10 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../../pages/iou/request/step/IOURequestStepAccountant').default, [SCREENS.MONEY_REQUEST.STEP_UPGRADE]: () => require('../../../../pages/iou/request/step/IOURequestStepUpgrade').default, [SCREENS.MONEY_REQUEST.DYNAMIC_STEP_DESTINATION]: () => require('../../../../pages/iou/request/step/DynamicIOURequestStepDestination').default, - [SCREENS.MONEY_REQUEST.STEP_TIME]: () => require('../../../../pages/iou/request/step/IOURequestStepTime').default, + [SCREENS.MONEY_REQUEST.DYNAMIC_STEP_TIME]: () => require('../../../../pages/iou/request/step/DynamicIOURequestStepTime').default, [SCREENS.MONEY_REQUEST.STEP_SUBRATE]: () => require('../../../../pages/iou/request/step/IOURequestStepSubrate').default, [SCREENS.MONEY_REQUEST.DYNAMIC_STEP_DESTINATION_EDIT]: () => require('../../../../pages/iou/request/step/DynamicIOURequestStepDestination').default, - [SCREENS.MONEY_REQUEST.STEP_TIME_EDIT]: () => require('../../../../pages/iou/request/step/IOURequestStepTime').default, + [SCREENS.MONEY_REQUEST.DYNAMIC_STEP_TIME_EDIT]: () => require('../../../../pages/iou/request/step/DynamicIOURequestStepTime').default, [SCREENS.MONEY_REQUEST.STEP_SUBRATE_EDIT]: () => require('../../../../pages/iou/request/step/IOURequestStepSubrate').default, [SCREENS.MONEY_REQUEST.RECEIPT_VIEW]: () => require('../../../../pages/iou/request/step/IOURequestStepScan/ReceiptView').default, [SCREENS.MONEY_REQUEST.SPLIT_EXPENSE]: () => require('../../../../pages/iou/SplitExpensePage').default, diff --git a/src/libs/Navigation/linkingConfig/OldRoutes.ts b/src/libs/Navigation/linkingConfig/OldRoutes.ts index c65ccacd5b37..21606ab39654 100644 --- a/src/libs/Navigation/linkingConfig/OldRoutes.ts +++ b/src/libs/Navigation/linkingConfig/OldRoutes.ts @@ -7,6 +7,11 @@ const oldRoutes: Record = { // would swallow it) - acceptable because callers no longer append `?backTo=`. '/*/*/destination/*/*/edit': '/$1/$2/confirmation/$3/$4/per-diem-destination-edit', '/*/*/destination/*/*': '/$1/$2/start/$3/$4/per-diem-destination', + // Per diem time step migrated to dynamic routes (#83850). Edit variant redirects to the confirmation-based suffix; + // 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', '/settings/*/category/*/edit': '/settings/$1/categories/category-settings/$2/category-edit', '/settings/*/category/*': '/settings/$1/categories/category-settings/$2', '/settings/*/tags/*/edit': '/settings/$1/tags/settings-tags-edit/$2', diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index fbd6da661583..28ed3120dbdf 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1791,10 +1791,10 @@ const config: LinkingOptions['config'] = { [SCREENS.MONEY_REQUEST.STEP_ACCOUNTANT]: ROUTES.MONEY_REQUEST_ACCOUNTANT.route, [SCREENS.MONEY_REQUEST.STEP_UPGRADE]: ROUTES.MONEY_REQUEST_UPGRADE.route, [SCREENS.MONEY_REQUEST.DYNAMIC_STEP_DESTINATION]: DYNAMIC_ROUTES.MONEY_REQUEST_STEP_DESTINATION.path, - [SCREENS.MONEY_REQUEST.STEP_TIME]: ROUTES.MONEY_REQUEST_STEP_TIME.route, + [SCREENS.MONEY_REQUEST.DYNAMIC_STEP_TIME]: DYNAMIC_ROUTES.MONEY_REQUEST_STEP_TIME.path, [SCREENS.MONEY_REQUEST.STEP_SUBRATE]: ROUTES.MONEY_REQUEST_STEP_SUBRATE.route, [SCREENS.MONEY_REQUEST.DYNAMIC_STEP_DESTINATION_EDIT]: DYNAMIC_ROUTES.MONEY_REQUEST_STEP_DESTINATION_EDIT.path, - [SCREENS.MONEY_REQUEST.STEP_TIME_EDIT]: ROUTES.MONEY_REQUEST_STEP_TIME_EDIT.route, + [SCREENS.MONEY_REQUEST.DYNAMIC_STEP_TIME_EDIT]: DYNAMIC_ROUTES.MONEY_REQUEST_STEP_TIME_EDIT.path, [SCREENS.MONEY_REQUEST.STEP_SUBRATE_EDIT]: ROUTES.MONEY_REQUEST_STEP_SUBRATE_EDIT.route, [SCREENS.MONEY_REQUEST.STEP_TIME_RATE]: ROUTES.MONEY_REQUEST_STEP_TIME_RATE.route, [SCREENS.MONEY_REQUEST.STEP_HOURS]: ROUTES.MONEY_REQUEST_STEP_HOURS.route, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 2f3c5dfbbde6..cb9e5bc047f6 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -2277,14 +2277,12 @@ type MoneyRequestNavigatorParamList = { reportID: string; backToReport?: string; }; - [SCREENS.MONEY_REQUEST.STEP_TIME]: { + [SCREENS.MONEY_REQUEST.DYNAMIC_STEP_TIME]: { action: IOUAction; iouType: Exclude; transactionID: string; reportID: string; backToReport?: string; - // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md - backTo: Routes | undefined; }; [SCREENS.MONEY_REQUEST.STEP_SUBRATE]: { iouType: Exclude; @@ -2303,14 +2301,12 @@ type MoneyRequestNavigatorParamList = { reportID: string; backToReport?: string; }; - [SCREENS.MONEY_REQUEST.STEP_TIME_EDIT]: { + [SCREENS.MONEY_REQUEST.DYNAMIC_STEP_TIME_EDIT]: { action: IOUAction; iouType: Exclude; transactionID: string; reportID: string; backToReport?: string; - // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md - backTo: Routes | undefined; }; [SCREENS.MONEY_REQUEST.STEP_SUBRATE_EDIT]: { iouType: Exclude; diff --git a/src/pages/iou/request/step/DynamicIOURequestStepDestination.tsx b/src/pages/iou/request/step/DynamicIOURequestStepDestination.tsx index 39931a56b2b8..4770b3cbfef6 100644 --- a/src/pages/iou/request/step/DynamicIOURequestStepDestination.tsx +++ b/src/pages/iou/request/step/DynamicIOURequestStepDestination.tsx @@ -21,6 +21,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {fetchPerDiemRates} from '@libs/actions/Policy/PerDiem'; import {setTransactionReport} from '@libs/actions/Transaction'; import {getInitialPerDiemTargetReport} from '@libs/IOUUtils'; +import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import {getPerDiemCustomUnit, getPolicyByCustomUnitID, isPolicyAdmin} from '@libs/PolicyUtils'; import {findSelfDMReportID, getPolicyExpenseChat} from '@libs/ReportUtils'; @@ -166,7 +167,15 @@ function DynamicIOURequestStepDestination({ if (isEditPage) { navigateBack(); } else { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TIME.getRoute(action, targetIouType, transactionID, targetReport?.reportID ?? reportID, backToReport)); + // Time is a dynamic route: build it on the start base when the destination is shown inline on the start page + // (single per-diem policy) and on the destination base otherwise, so Time's back returns to the right step. + const timeBase = openedFromStartPage + ? ROUTES.MONEY_REQUEST_CREATE.getRoute(action, targetIouType, transactionID, targetReport?.reportID ?? reportID, backToReport) + : createDynamicRoute( + DYNAMIC_ROUTES.MONEY_REQUEST_STEP_DESTINATION.path, + ROUTES.MONEY_REQUEST_CREATE.getRoute(action, targetIouType, transactionID, targetReport?.reportID ?? reportID, backToReport), + ); + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.MONEY_REQUEST_STEP_TIME.path, timeBase)); } }; diff --git a/src/pages/iou/request/step/IOURequestStepTime.tsx b/src/pages/iou/request/step/DynamicIOURequestStepTime.tsx similarity index 72% rename from src/pages/iou/request/step/IOURequestStepTime.tsx rename to src/pages/iou/request/step/DynamicIOURequestStepTime.tsx index a3961c75311f..cf07f657e9b9 100644 --- a/src/pages/iou/request/step/IOURequestStepTime.tsx +++ b/src/pages/iou/request/step/DynamicIOURequestStepTime.tsx @@ -5,18 +5,15 @@ import type {FormOnyxValues} from '@components/Form/types'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import TimeModalPicker from '@components/TimeModalPicker'; -import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useDynamicBackPath from '@hooks/useDynamicBackPath'; import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; import usePolicyForTransaction from '@hooks/usePolicyForTransaction'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; import {addErrorMessage} from '@libs/ErrorUtils'; import {isValidMoneyRequestType} from '@libs/IOUUtils'; -import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; -import {getActivePoliciesWithExpenseChatAndPerDiemEnabledAndHasRates} from '@libs/PolicyUtils'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import {getIOURequestPolicyID, setMoneyRequestDateAttribute} from '@userActions/IOU/MoneyRequest'; @@ -31,7 +28,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {OnyxEntry} from 'react-native-onyx'; -import React, {useMemo} from 'react'; +import React from 'react'; import {View} from 'react-native'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; @@ -40,7 +37,7 @@ import StepScreenWrapper from './StepScreenWrapper'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; -type IOURequestStepTimeProps = WithWritableReportOrNotFoundProps & { +type DynamicIOURequestStepTimeProps = WithWritableReportOrNotFoundProps & { /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ transaction: OnyxEntry; @@ -51,17 +48,16 @@ type IOURequestStepTimeProps = WithWritableReportOrNotFoundProps; }; -function IOURequestStepTime({ +function DynamicIOURequestStepTime({ route: { - params: {action, iouType, reportID, transactionID, backTo, backToReport}, + params: {action, iouType, reportID, transactionID, backToReport}, name, }, transaction, isLoadingTransaction, report, -}: IOURequestStepTimeProps) { +}: DynamicIOURequestStepTimeProps) { const styles = useThemeStyles(); - const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); const iouPolicyID = getIOURequestPolicyID(transaction, report); const {policy} = usePolicyForTransaction({ transaction, @@ -75,39 +71,17 @@ function IOURequestStepTime({ const currentDateAttributes = transaction?.comment?.customUnit?.attributes?.dates; const currentStartDate = currentDateAttributes?.start ? DateUtils.extractDate(currentDateAttributes.start) : undefined; const currentEndDate = currentDateAttributes?.end ? DateUtils.extractDate(currentDateAttributes.end) : undefined; - const isEditPage = name === SCREENS.MONEY_REQUEST.STEP_TIME_EDIT; + const isEditPage = name === SCREENS.MONEY_REQUEST.DYNAMIC_STEP_TIME_EDIT; const shouldShowNotFound = !isValidMoneyRequestType(iouType) || isEmptyObject(policy) || (isEditPage && isEmptyObject(transaction?.comment?.customUnit)); - const {login: currentUserLogin} = useCurrentUserPersonalDetails(); - const policiesWithPerDiemEnabled = useMemo(() => getActivePoliciesWithExpenseChatAndPerDiemEnabledAndHasRates(allPolicies, currentUserLogin), [allPolicies, currentUserLogin]); - const hasMoreThanOnePolicyWithPerDiemEnabled = policiesWithPerDiemEnabled.length > 1; - const navigateBack = () => { - if (isEditPage) { - Navigation.goBack(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(action, iouType, transactionID, reportID)); - return; - } - - if (backTo) { - Navigation.goBack(backTo); - return; - } - - if (transaction?.isFromGlobalCreate || iouType === CONST.IOU.TYPE.TRACK) { - // We want to navigate to destination step only when the first step was the workspace selector. - // If there is only one policy with per diem enabled, we want to navigate back to the start step because there is no separate destination step in that flow. - if (hasMoreThanOnePolicyWithPerDiemEnabled) { - Navigation.goBack( - createDynamicRoute(DYNAMIC_ROUTES.MONEY_REQUEST_STEP_DESTINATION.path, ROUTES.MONEY_REQUEST_CREATE.getRoute(action, iouType, transactionID, reportID, backToReport)), - ); - return; - } - - // If there is only one per diem policy, we can't override the reportID that is already on the stack to make sure we go back to the right screen. - Navigation.goBack(); - } + // Back removes this step's dynamic suffix from the current URL. In edit mode the suffix (`time-edit`) sits on the + // confirmation base, so back returns to confirmation. In the wizard flow the suffix (`time`) sits on the destination + // base (multi-policy) or the start base (single-policy), so back returns to whichever preceding step was appended to. + const backPath = useDynamicBackPath(isEditPage ? DYNAMIC_ROUTES.MONEY_REQUEST_STEP_TIME_EDIT.path : DYNAMIC_ROUTES.MONEY_REQUEST_STEP_TIME.path); - Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_PER_DIEM.getRoute(action, iouType, transactionID, reportID)); + const navigateBack = () => { + Navigation.goBack(backPath); }; const validate = (value: FormOnyxValues) => { @@ -164,11 +138,11 @@ function IOURequestStepTime({ return ( ): Partial> => { diff --git a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx index 42329aa91f52..b683f6ac6c3c 100644 --- a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx +++ b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx @@ -52,8 +52,8 @@ type MoneyRequestRouteName = | typeof SCREENS.MONEY_REQUEST.DYNAMIC_STEP_COMPANY_INFO | typeof SCREENS.MONEY_REQUEST.DYNAMIC_STEP_DESTINATION | typeof SCREENS.MONEY_REQUEST.DYNAMIC_STEP_DESTINATION_EDIT - | typeof SCREENS.MONEY_REQUEST.STEP_TIME - | typeof SCREENS.MONEY_REQUEST.STEP_TIME_EDIT + | typeof SCREENS.MONEY_REQUEST.DYNAMIC_STEP_TIME + | typeof SCREENS.MONEY_REQUEST.DYNAMIC_STEP_TIME_EDIT | typeof SCREENS.MONEY_REQUEST.STEP_SUBRATE | typeof SCREENS.MONEY_REQUEST.STEP_DISTANCE_MAP | typeof SCREENS.MONEY_REQUEST.STEP_DISTANCE_GPS diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx index c310b7a8dc45..e656d113d454 100644 --- a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx +++ b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx @@ -58,8 +58,8 @@ type MoneyRequestRouteName = | typeof SCREENS.MONEY_REQUEST.STEP_UPGRADE | typeof SCREENS.MONEY_REQUEST.DYNAMIC_STEP_DESTINATION | typeof SCREENS.MONEY_REQUEST.DYNAMIC_STEP_DESTINATION_EDIT - | typeof SCREENS.MONEY_REQUEST.STEP_TIME - | typeof SCREENS.MONEY_REQUEST.STEP_TIME_EDIT + | typeof SCREENS.MONEY_REQUEST.DYNAMIC_STEP_TIME + | typeof SCREENS.MONEY_REQUEST.DYNAMIC_STEP_TIME_EDIT | typeof SCREENS.MONEY_REQUEST.STEP_SUBRATE | typeof SCREENS.MONEY_REQUEST.EDIT_REPORT | typeof SCREENS.MONEY_REQUEST.DISTANCE_CREATE diff --git a/tests/navigation/getMatchingNewRouteTest.ts b/tests/navigation/getMatchingNewRouteTest.ts index 9682bbb65db8..baa3c3420a9c 100644 --- a/tests/navigation/getMatchingNewRouteTest.ts +++ b/tests/navigation/getMatchingNewRouteTest.ts @@ -452,4 +452,22 @@ describe('getBestMatchingPath', () => { expect(getMatchingNewRoute('/create/submit/start/123/456/per-diem-destination')).toBe(undefined); expect(getMatchingNewRoute('/create/submit/confirmation/123/456/per-diem-destination-edit')).toBe(undefined); }); + + it('redirects legacy per diem time step to the new destination-based dynamic route (#83850)', () => { + expect(getMatchingNewRoute('/create/submit/time/123/456')).toBe('/create/submit/destination/123/456/per-diem-time'); + }); + + // NOTE: the redirect appends the dynamic suffix (`/time`) so a deep-link restores the actual wizard step. Query + // preservation is intentionally not asserted: the OldRoutes matcher captures the trailing segment with a greedy + // `(.*)` that also swallows the query string, so a suffix cannot be appended after it while keeping `?backTo=…`. + // This is acceptable because no call site produces these legacy paths with a query anymore (all navigations moved to + // `createDynamicRoute` without `backTo`). + it('redirects legacy per diem time edit step to the new confirmation-based dynamic route (#83850)', () => { + expect(getMatchingNewRoute('/create/submit/time/123/456/edit')).toBe('/create/submit/confirmation/123/456/per-diem-time-edit'); + }); + + it('does not redirect the already-migrated per diem time dynamic routes (#83850)', () => { + expect(getMatchingNewRoute('/create/submit/start/123/456/per-diem-destination/per-diem-time')).toBe(undefined); + expect(getMatchingNewRoute('/create/submit/confirmation/123/456/per-diem-time-edit')).toBe(undefined); + }); }); From 2ef52aaee711a5ac286c5a86c55ea6b43d3eb046 Mon Sep 17 00:00:00 2001 From: Cong Pham Date: Wed, 29 Jul 2026 19:10:25 +0700 Subject: [PATCH 2/3] Fix subrate back target for single-policy per diem (#83850 part 2) Address codex review: IOURequestStepSubrate.goBack unconditionally rebuilt the Time route on the destination base, so in the single-policy flow (destination shown inline on the start page, Time opened on the start/CREATE base) pressing Back from Subrate inserted a destination step that was never in the stack. Mirror DynamicIOURequestStepDestination.updateDestination: derive openedFromStartPage (invert IOURequestStartPage's shouldShowWorkspaceSelectForPerDiem) and rebuild the Time route on the CREATE base when the destination was inline, otherwise on the destination base. Co-Authored-By: Claude --- .../request/step/IOURequestStepSubrate.tsx | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepSubrate.tsx b/src/pages/iou/request/step/IOURequestStepSubrate.tsx index 78ada3e49c3a..f663e4e5a08d 100644 --- a/src/pages/iou/request/step/IOURequestStepSubrate.tsx +++ b/src/pages/iou/request/step/IOURequestStepSubrate.tsx @@ -11,8 +11,10 @@ import TextInput from '@components/TextInput'; import ValuePicker from '@components/ValuePicker'; import useConfirmModal from '@hooks/useConfirmModal'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; import usePolicyForTransaction from '@hooks/usePolicyForTransaction'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -20,7 +22,7 @@ import {addErrorMessage} from '@libs/ErrorUtils'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import TransitionTracker from '@libs/Navigation/TransitionTracker'; -import {getPerDiemCustomUnit} from '@libs/PolicyUtils'; +import {getActivePoliciesWithExpenseChatAndPerDiemEnabledAndHasRates, getPerDiemCustomUnit, isControlPolicy, isPerDiemEnabled} from '@libs/PolicyUtils'; import {getIOURequestPolicyID} from '@userActions/IOU/MoneyRequest'; import {addSubrate, removeSubrate, updateSubrate} from '@userActions/IOU/PerDiem'; @@ -38,7 +40,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import {useNavigation} from '@react-navigation/native'; import {SafeString} from 'expensify-common'; -import React, {useCallback, useEffect, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; @@ -89,6 +91,14 @@ function IOURequestStepSubrate({ }); const customUnit = getPerDiemCustomUnit(policy); + const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); + const {login: currentUserLogin} = useCurrentUserPersonalDetails(); + const policiesWithPerDiemEnabled = useMemo(() => getActivePoliciesWithExpenseChatAndPerDiemEnabledAndHasRates(allPolicies, currentUserLogin), [allPolicies, currentUserLogin]); + const hasMoreThanOnePolicyWithPerDiemEnabled = policiesWithPerDiemEnabled.length > 1; + // Mirror IOURequestStartPage's `shouldShowWorkspaceSelectForPerDiem` (inverted) so the back target matches + // the base the Time step was actually opened on: inline destination on the start page (current policy is + // per-diem, or only one per-diem policy) -> CREATE/start base; workspace selector -> destination base. + const openedFromStartPage = !hasMoreThanOnePolicyWithPerDiemEnabled || (isControlPolicy(policy) && isPerDiemEnabled(policy)); const navigation = useNavigation(); const isFocused = navigation.isFocused(); const {translate} = useLocalize(); @@ -128,12 +138,13 @@ function IOURequestStepSubrate({ Navigation.goBack(backTo); return; } - Navigation.goBack( - createDynamicRoute( - DYNAMIC_ROUTES.MONEY_REQUEST_STEP_TIME.path, - createDynamicRoute(DYNAMIC_ROUTES.MONEY_REQUEST_STEP_DESTINATION.path, ROUTES.MONEY_REQUEST_CREATE.getRoute(action, iouType, transactionID, reportID, backToReport)), - ), - ); + // Rebuild the Time route on the same base it was opened on so Back returns to the actual previous step: + // the start/CREATE base when the destination was shown inline on the start page, otherwise the + // destination base. Matches the branch used by DynamicIOURequestStepDestination.updateDestination. + const timeBase = openedFromStartPage + ? ROUTES.MONEY_REQUEST_CREATE.getRoute(action, iouType, transactionID, reportID, backToReport) + : createDynamicRoute(DYNAMIC_ROUTES.MONEY_REQUEST_STEP_DESTINATION.path, ROUTES.MONEY_REQUEST_CREATE.getRoute(action, iouType, transactionID, reportID, backToReport)); + Navigation.goBack(createDynamicRoute(DYNAMIC_ROUTES.MONEY_REQUEST_STEP_TIME.path, timeBase)); }; const validate = (values: FormOnyxValues): Partial> => { From d32ac94d0e0c1b633c72118db06d427555e0f738 Mon Sep 17 00:00:00 2001 From: Cong Pham Date: Thu, 30 Jul 2026 09:30:08 +0700 Subject: [PATCH 3/3] Revert "Fix subrate back target for single-policy per diem (#83850 part 2)" This reverts commit 2ef52aaee711a5ac286c5a86c55ea6b43d3eb046. --- .../request/step/IOURequestStepSubrate.tsx | 27 ++++++------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepSubrate.tsx b/src/pages/iou/request/step/IOURequestStepSubrate.tsx index f663e4e5a08d..78ada3e49c3a 100644 --- a/src/pages/iou/request/step/IOURequestStepSubrate.tsx +++ b/src/pages/iou/request/step/IOURequestStepSubrate.tsx @@ -11,10 +11,8 @@ import TextInput from '@components/TextInput'; import ValuePicker from '@components/ValuePicker'; import useConfirmModal from '@hooks/useConfirmModal'; -import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; import usePolicyForTransaction from '@hooks/usePolicyForTransaction'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -22,7 +20,7 @@ import {addErrorMessage} from '@libs/ErrorUtils'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import TransitionTracker from '@libs/Navigation/TransitionTracker'; -import {getActivePoliciesWithExpenseChatAndPerDiemEnabledAndHasRates, getPerDiemCustomUnit, isControlPolicy, isPerDiemEnabled} from '@libs/PolicyUtils'; +import {getPerDiemCustomUnit} from '@libs/PolicyUtils'; import {getIOURequestPolicyID} from '@userActions/IOU/MoneyRequest'; import {addSubrate, removeSubrate, updateSubrate} from '@userActions/IOU/PerDiem'; @@ -40,7 +38,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import {useNavigation} from '@react-navigation/native'; import {SafeString} from 'expensify-common'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; @@ -91,14 +89,6 @@ function IOURequestStepSubrate({ }); const customUnit = getPerDiemCustomUnit(policy); - const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); - const {login: currentUserLogin} = useCurrentUserPersonalDetails(); - const policiesWithPerDiemEnabled = useMemo(() => getActivePoliciesWithExpenseChatAndPerDiemEnabledAndHasRates(allPolicies, currentUserLogin), [allPolicies, currentUserLogin]); - const hasMoreThanOnePolicyWithPerDiemEnabled = policiesWithPerDiemEnabled.length > 1; - // Mirror IOURequestStartPage's `shouldShowWorkspaceSelectForPerDiem` (inverted) so the back target matches - // the base the Time step was actually opened on: inline destination on the start page (current policy is - // per-diem, or only one per-diem policy) -> CREATE/start base; workspace selector -> destination base. - const openedFromStartPage = !hasMoreThanOnePolicyWithPerDiemEnabled || (isControlPolicy(policy) && isPerDiemEnabled(policy)); const navigation = useNavigation(); const isFocused = navigation.isFocused(); const {translate} = useLocalize(); @@ -138,13 +128,12 @@ function IOURequestStepSubrate({ Navigation.goBack(backTo); return; } - // Rebuild the Time route on the same base it was opened on so Back returns to the actual previous step: - // the start/CREATE base when the destination was shown inline on the start page, otherwise the - // destination base. Matches the branch used by DynamicIOURequestStepDestination.updateDestination. - const timeBase = openedFromStartPage - ? ROUTES.MONEY_REQUEST_CREATE.getRoute(action, iouType, transactionID, reportID, backToReport) - : createDynamicRoute(DYNAMIC_ROUTES.MONEY_REQUEST_STEP_DESTINATION.path, ROUTES.MONEY_REQUEST_CREATE.getRoute(action, iouType, transactionID, reportID, backToReport)); - Navigation.goBack(createDynamicRoute(DYNAMIC_ROUTES.MONEY_REQUEST_STEP_TIME.path, timeBase)); + Navigation.goBack( + createDynamicRoute( + DYNAMIC_ROUTES.MONEY_REQUEST_STEP_TIME.path, + createDynamicRoute(DYNAMIC_ROUTES.MONEY_REQUEST_STEP_DESTINATION.path, ROUTES.MONEY_REQUEST_CREATE.getRoute(action, iouType, transactionID, reportID, backToReport)), + ), + ); }; const validate = (values: FormOnyxValues): Partial> => {