From 42ab0587a773a1fa43236d45c7b6afe88e93e0a9 Mon Sep 17 00:00:00 2001 From: Cong Pham Date: Wed, 12 Aug 2026 11:49:14 +0700 Subject: [PATCH 1/2] Migrate money request merchant, description and date steps to dynamic routes Replaces the legacy `backTo` param on MONEY_REQUEST_STEP_MERCHANT, MONEY_REQUEST_STEP_DESCRIPTION and MONEY_REQUEST_STEP_DATE with dynamic routes, following the MONEY_REQUEST_STEP_TAX_RATE query-param template. - add `expense-merchant`, `expense-description` and `expense-date` entries to DYNAMIC_ROUTES and drop the static route definitions - rename the pages to Dynamic* and resolve the back path with useDynamicBackPath - register the new DYNAMIC_STEP_* screens in the linking config, navigation types, modal stack navigator and the step HOC unions - update every call site (confirmation list fields, MoneyRequestView, SplitExpenseEditPage) to createDynamicRoute - add OldRoutes redirects (with the optional reportActionID variants) and unit tests Part 1 of #83852. --- src/ROUTES.ts | 78 ++++++++++++------- src/SCREENS.ts | 6 +- .../sections/DateField.tsx | 5 +- .../sections/DescriptionField.tsx | 7 +- .../sections/MerchantField.tsx | 5 +- .../ReportActionItem/MoneyRequestView.tsx | 24 ++---- .../ModalStackNavigators/index.tsx | 6 +- .../Navigation/linkingConfig/OldRoutes.ts | 9 +++ src/libs/Navigation/linkingConfig/config.ts | 6 +- src/libs/Navigation/types.ts | 12 +-- src/pages/iou/SplitExpenseEditPage.tsx | 21 +++-- ...Date.tsx => DynamicIOURequestStepDate.tsx} | 23 +++--- ...x => DynamicIOURequestStepDescription.tsx} | 21 ++--- ....tsx => DynamicIOURequestStepMerchant.tsx} | 19 +++-- .../step/withFullTransactionOrNotFound.tsx | 6 +- .../step/withWritableReportOrNotFound.tsx | 6 +- tests/navigation/getMatchingNewRouteTest.ts | 27 +++++++ tests/unit/useActiveRouteTest.ts | 3 +- 18 files changed, 163 insertions(+), 121 deletions(-) rename src/pages/iou/request/step/{IOURequestStepDate.tsx => DynamicIOURequestStepDate.tsx} (91%) rename src/pages/iou/request/step/{IOURequestStepDescription.tsx => DynamicIOURequestStepDescription.tsx} (91%) rename src/pages/iou/request/step/{IOURequestStepMerchant.tsx => DynamicIOURequestStepMerchant.tsx} (94%) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 9f4555bb4c56..a38fe64fa267 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -302,6 +302,54 @@ const DYNAMIC_ROUTES = { getUrlWithParams('expense-tag', {action, iouType, orderWeight: String(orderWeight), transactionID, reportID, reportActionID}), queryParams: ['action', 'iouType', 'orderWeight', 'transactionID', 'reportID', 'reportActionID'], }, + MONEY_REQUEST_STEP_MERCHANT: { + path: 'expense-merchant', + entryScreens: [ + SCREENS.MONEY_REQUEST.STEP_CONFIRMATION, + // In the new manual expense flow the confirmation fields are inlined on the create page, so it is also an entry point. + SCREENS.MONEY_REQUEST.CREATE, + SCREENS.SHARE.SUBMIT_DETAILS, + SCREENS.REPORT, + SCREENS.RIGHT_MODAL.SEARCH_REPORT, + SCREENS.RIGHT_MODAL.EXPENSE_REPORT, + SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT, + ], + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, reportActionID?: string) => + getUrlWithParams('expense-merchant', {action, iouType, transactionID, reportID, reportActionID}), + queryParams: ['action', 'iouType', 'transactionID', 'reportID', 'reportActionID'], + }, + MONEY_REQUEST_STEP_DESCRIPTION: { + path: 'expense-description', + entryScreens: [ + SCREENS.MONEY_REQUEST.STEP_CONFIRMATION, + SCREENS.MONEY_REQUEST.CREATE, + SCREENS.SHARE.SUBMIT_DETAILS, + SCREENS.REPORT, + SCREENS.RIGHT_MODAL.SEARCH_REPORT, + SCREENS.RIGHT_MODAL.EXPENSE_REPORT, + SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT, + SCREENS.MONEY_REQUEST.SPLIT_EXPENSE_EDIT, + ], + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, reportActionID?: string) => + getUrlWithParams('expense-description', {action, iouType, transactionID, reportID, reportActionID}), + queryParams: ['action', 'iouType', 'transactionID', 'reportID', 'reportActionID'], + }, + MONEY_REQUEST_STEP_DATE: { + path: 'expense-date', + entryScreens: [ + SCREENS.MONEY_REQUEST.STEP_CONFIRMATION, + SCREENS.MONEY_REQUEST.CREATE, + SCREENS.SHARE.SUBMIT_DETAILS, + SCREENS.REPORT, + SCREENS.RIGHT_MODAL.SEARCH_REPORT, + SCREENS.RIGHT_MODAL.EXPENSE_REPORT, + SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT, + SCREENS.MONEY_REQUEST.SPLIT_EXPENSE_EDIT, + ], + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, reportActionID?: string) => + getUrlWithParams('expense-date', {action, iouType, transactionID, reportID, reportActionID}), + queryParams: ['action', 'iouType', 'transactionID', 'reportID', 'reportActionID'], + }, MONEY_REQUEST_STEP_DESTINATION: { path: 'per-diem-destination', entryScreens: [SCREENS.MONEY_REQUEST.CREATE], @@ -2409,26 +2457,6 @@ const ROUTES = { getRoute: (policyID: string, categoryName: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/category/${encodeURIComponent(categoryName)}/gl-code` as const, backTo), }, - MONEY_REQUEST_STEP_DATE: { - route: ':action/:iouType/date/:transactionID/:reportID/:reportActionID?', - getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backTo = '', reportActionID?: string) => { - if (!transactionID || !reportID) { - Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_STEP_DATE route'); - } - - return getUrlWithBackToParam(`${action as string}/${iouType as string}/date/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo); - }, - }, - MONEY_REQUEST_STEP_DESCRIPTION: { - route: ':action/:iouType/description/:transactionID/:reportID/:reportActionID?', - getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backTo = '', reportActionID?: string) => { - if (!transactionID || !reportID) { - Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_STEP_DESCRIPTION route'); - } - - return getUrlWithBackToParam(`${action as string}/${iouType as string}/description/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo); - }, - }, MONEY_REQUEST_STEP_DISTANCE: { route: ':action/:iouType/distance/:transactionID/:reportID/:reportActionID?', getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backTo = '', reportActionID?: string) => { @@ -2469,16 +2497,6 @@ const ROUTES = { return getUrlWithBackToParam(`${action as string}/${iouType as string}/distanceRate/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo); }, }, - MONEY_REQUEST_STEP_MERCHANT: { - route: ':action/:iouType/merchant/:transactionID/:reportID/:reportActionID?', - getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backTo = '', reportActionID?: string) => { - if (!transactionID || !reportID) { - Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_STEP_MERCHANT route'); - } - - return getUrlWithBackToParam(`${action as string}/${iouType as string}/merchant/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo); - }, - }, MONEY_REQUEST_STEP_PARTICIPANTS: { route: ':action/:iouType/participants/:transactionID/:reportID', getRoute: (iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backTo = '', action: IOUAction = 'create', isWorkspacesOnly = false) => { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index c43f60cd2071..5efd9d249b05 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -333,11 +333,11 @@ const SCREENS = { STEP_AMOUNT: 'Money_Request_Step_Amount', STEP_CATEGORY: 'Money_Request_Step_Category', STEP_CATEGORY_CREATE: 'Money_Request_Step_Category_Create', - STEP_DATE: 'Money_Request_Step_Date', - STEP_DESCRIPTION: 'Money_Request_Step_Description', + DYNAMIC_STEP_DATE: 'Dynamic_Money_Request_Step_Date', + DYNAMIC_STEP_DESCRIPTION: 'Dynamic_Money_Request_Step_Description', STEP_DISTANCE: 'Money_Request_Step_Distance', STEP_DISTANCE_RATE: 'Money_Request_Step_Rate', - STEP_MERCHANT: 'Money_Request_Step_Merchant', + DYNAMIC_STEP_MERCHANT: 'Dynamic_Money_Request_Step_Merchant', STEP_PARTICIPANTS: 'Money_Request_Step_Participants', STEP_SCAN: 'Money_Request_Step_Scan', DYNAMIC_STEP_TAG: 'Dynamic_Money_Request_Step_Tag', diff --git a/src/components/MoneyRequestConfirmationList/sections/DateField.tsx b/src/components/MoneyRequestConfirmationList/sections/DateField.tsx index 128a19a2e8e2..deba313892d7 100644 --- a/src/components/MoneyRequestConfirmationList/sections/DateField.tsx +++ b/src/components/MoneyRequestConfirmationList/sections/DateField.tsx @@ -12,6 +12,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {setMoneyRequestCreated, updateDistanceRateOnExpenseDateChange} from '@libs/actions/IOU/MoneyRequest'; import {shouldUseTransactionDraft} from '@libs/IOUUtils'; +import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import {isPolicyExpenseChat as isPolicyExpenseChatReportUtil} from '@libs/ReportUtils'; @@ -20,7 +21,7 @@ import {setDraftSplitTransaction} from '@userActions/IOU/Split'; import CONST from '@src/CONST'; import type {IOUAction, IOUType} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/MoneyRequestDateForm'; import {format} from 'date-fns'; @@ -150,7 +151,7 @@ function DateField({ return; } - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute(), reportActionID)); + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(action, iouType, transactionID, reportID, reportActionID))); }} disabled={didConfirm} interactive={!isReadOnly} diff --git a/src/components/MoneyRequestConfirmationList/sections/DescriptionField.tsx b/src/components/MoneyRequestConfirmationList/sections/DescriptionField.tsx index a99215a6e529..703fe3e2640e 100644 --- a/src/components/MoneyRequestConfirmationList/sections/DescriptionField.tsx +++ b/src/components/MoneyRequestConfirmationList/sections/DescriptionField.tsx @@ -10,6 +10,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {setMoneyRequestDescription} from '@libs/actions/IOU/MoneyRequest'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; +import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import Parser from '@libs/Parser'; @@ -20,7 +21,7 @@ import {setDraftSplitTransaction} from '@userActions/IOU/Split'; import CONST from '@src/CONST'; import type {IOUAction, IOUType} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -147,9 +148,7 @@ function DescriptionField({ return; } - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute(), reportActionID), - ); + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(action, iouType, transactionID, reportID, reportActionID))); }} style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} diff --git a/src/components/MoneyRequestConfirmationList/sections/MerchantField.tsx b/src/components/MoneyRequestConfirmationList/sections/MerchantField.tsx index 36327adec420..022ba4ca3cb6 100644 --- a/src/components/MoneyRequestConfirmationList/sections/MerchantField.tsx +++ b/src/components/MoneyRequestConfirmationList/sections/MerchantField.tsx @@ -7,6 +7,7 @@ import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import {clearMoneyRequestMerchant, setMoneyRequestMerchant} from '@libs/actions/IOU/MoneyRequest'; +import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import {isInvalidMerchantValue, isValidInputLength} from '@libs/ValidationUtils'; @@ -15,7 +16,7 @@ import {setDraftSplitTransaction} from '@userActions/IOU/Split'; import CONST from '@src/CONST'; import type {IOUAction, IOUType} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; import React from 'react'; import {View} from 'react-native'; @@ -134,7 +135,7 @@ function MerchantField({ return; } - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute(), reportActionID)); + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(action, iouType, transactionID, reportID, reportActionID))); }} disabled={didConfirm} interactive={!isReadOnly} diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 2080754f5ef5..69c45446676d 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -1250,12 +1250,8 @@ function MoneyRequestView({ titleStyle={styles.flex1} onPress={() => { Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute( - CONST.IOU.ACTION.EDIT, - iouType, - transaction.transactionID, - transactionThreadReport?.reportID, - getReportRHPActiveRoute(), + createDynamicRoute( + DYNAMIC_ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, transactionThreadReport?.reportID), ), ); }} @@ -1280,12 +1276,8 @@ function MoneyRequestView({ titleStyle={styles.flex1} onPress={() => { Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute( - CONST.IOU.ACTION.EDIT, - iouType, - transaction.transactionID, - transactionThreadReport?.reportID, - getReportRHPActiveRoute(), + createDynamicRoute( + DYNAMIC_ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, transactionThreadReport?.reportID), ), ); }} @@ -1309,12 +1301,8 @@ function MoneyRequestView({ titleStyle={styles.flex1} onPress={() => { Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DATE.getRoute( - CONST.IOU.ACTION.EDIT, - iouType, - transaction.transactionID, - transactionThreadReport?.reportID, - getReportRHPActiveRoute(), + createDynamicRoute( + DYNAMIC_ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, transactionThreadReport?.reportID), ), ); }} diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 03147c48c96c..36bf7c6b4beb 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -184,11 +184,11 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../../pages/iou/request/step/IOURequestStepCategory').default, [SCREENS.MONEY_REQUEST.STEP_CATEGORY_CREATE]: () => require('../../../../pages/iou/request/step/IOURequestStepCategoryCreate').default, [SCREENS.MONEY_REQUEST.STEP_VENDOR]: () => require('../../../../pages/iou/request/step/IOURequestStepVendor').default, - [SCREENS.MONEY_REQUEST.STEP_DATE]: () => require('../../../../pages/iou/request/step/IOURequestStepDate').default, - [SCREENS.MONEY_REQUEST.STEP_DESCRIPTION]: () => require('../../../../pages/iou/request/step/IOURequestStepDescription').default, + [SCREENS.MONEY_REQUEST.DYNAMIC_STEP_DATE]: () => require('../../../../pages/iou/request/step/DynamicIOURequestStepDate').default, + [SCREENS.MONEY_REQUEST.DYNAMIC_STEP_DESCRIPTION]: () => require('../../../../pages/iou/request/step/DynamicIOURequestStepDescription').default, [SCREENS.MONEY_REQUEST.STEP_DISTANCE]: () => require('../../../../pages/iou/request/step/IOURequestStepDistance').default, [SCREENS.MONEY_REQUEST.STEP_DISTANCE_RATE]: () => require('@pages/iou/request/step/IOURequestStepDistanceRate').default, - [SCREENS.MONEY_REQUEST.STEP_MERCHANT]: () => require('../../../../pages/iou/request/step/IOURequestStepMerchant').default, + [SCREENS.MONEY_REQUEST.DYNAMIC_STEP_MERCHANT]: () => require('../../../../pages/iou/request/step/DynamicIOURequestStepMerchant').default, [SCREENS.MONEY_REQUEST.STEP_PARTICIPANTS]: () => require('../../../../pages/iou/request/step/IOURequestStepParticipants').default, [SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORIES_ROOT]: () => require('../../../../pages/workspace/categories/WorkspaceCategoriesPage').default, [SCREENS.SETTINGS_TAGS.SETTINGS_TAGS_ROOT]: () => require('../../../../pages/workspace/tags/WorkspaceTagsPage').default, diff --git a/src/libs/Navigation/linkingConfig/OldRoutes.ts b/src/libs/Navigation/linkingConfig/OldRoutes.ts index 3af6014870a5..6ecb53e3233a 100644 --- a/src/libs/Navigation/linkingConfig/OldRoutes.ts +++ b/src/libs/Navigation/linkingConfig/OldRoutes.ts @@ -153,6 +153,15 @@ const oldRoutes: Record = { '/*/*/taxRate/*/*': '/r/$4/taxRate?action=$1&iouType=$2&transactionID=$3&reportID=$4', '/*/*/taxAmount/*/*': '/r/$4/taxAmount?action=$1&iouType=$2&transactionID=$3&reportID=$4', '/*/*/attendees/*/*': '/r/$4/attendees?action=$1&iouType=$2&transactionID=$3&reportID=$4', + // Merchant/description/date steps migrated to dynamic routes (#83852). Suffixes are namespaced `expense-*` because + // dynamic route paths must be globally unique and bare `description` is already taken by the report description + // suffix. As with the report/tag entries above, the optional `reportActionID` segment needs its own longer pattern. + '/*/*/merchant/*/*/*': '/r/$4/expense-merchant?action=$1&iouType=$2&transactionID=$3&reportID=$4&reportActionID=$5', + '/*/*/merchant/*/*': '/r/$4/expense-merchant?action=$1&iouType=$2&transactionID=$3&reportID=$4', + '/*/*/description/*/*/*': '/r/$4/expense-description?action=$1&iouType=$2&transactionID=$3&reportID=$4&reportActionID=$5', + '/*/*/description/*/*': '/r/$4/expense-description?action=$1&iouType=$2&transactionID=$3&reportID=$4', + '/*/*/date/*/*/*': '/r/$4/expense-date?action=$1&iouType=$2&transactionID=$3&reportID=$4&reportActionID=$5', + '/*/*/date/*/*': '/r/$4/expense-date?action=$1&iouType=$2&transactionID=$3&reportID=$4', /* eslint-enable @typescript-eslint/naming-convention */ }; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index f9f7da3da3b1..6b4ab1d4a034 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1779,8 +1779,8 @@ const config: LinkingOptions['config'] = { [SCREENS.MONEY_REQUEST.STEP_CATEGORY_CREATE]: ROUTES.MONEY_REQUEST_STEP_CATEGORY_CREATE.route, [SCREENS.MONEY_REQUEST.STEP_VENDOR]: ROUTES.MONEY_REQUEST_STEP_VENDOR.route, [SCREENS.MONEY_REQUEST.STEP_CONFIRMATION]: ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.route, - [SCREENS.MONEY_REQUEST.STEP_DATE]: ROUTES.MONEY_REQUEST_STEP_DATE.route, - [SCREENS.MONEY_REQUEST.STEP_DESCRIPTION]: ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.route, + [SCREENS.MONEY_REQUEST.DYNAMIC_STEP_DATE]: DYNAMIC_ROUTES.MONEY_REQUEST_STEP_DATE.path, + [SCREENS.MONEY_REQUEST.DYNAMIC_STEP_DESCRIPTION]: DYNAMIC_ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.path, [SCREENS.MONEY_REQUEST.STEP_DISTANCE]: ROUTES.MONEY_REQUEST_STEP_DISTANCE.route, [SCREENS.MONEY_REQUEST.STEP_DISTANCE_MANUAL]: ROUTES.MONEY_REQUEST_STEP_DISTANCE_MANUAL.route, [SCREENS.MONEY_REQUEST.STEP_DISTANCE_ODOMETER]: ROUTES.MONEY_REQUEST_STEP_DISTANCE_ODOMETER.route, @@ -1790,7 +1790,7 @@ const config: LinkingOptions['config'] = { [SCREENS.MONEY_REQUEST.HOLD]: ROUTES.MONEY_REQUEST_HOLD_REASON.route, [SCREENS.MONEY_REQUEST.REJECT]: ROUTES.REJECT_MONEY_REQUEST_REASON.route, [SCREENS.MONEY_REQUEST.REPORT_REJECT]: ROUTES.REJECT_EXPENSE_REPORT.route, - [SCREENS.MONEY_REQUEST.STEP_MERCHANT]: ROUTES.MONEY_REQUEST_STEP_MERCHANT.route, + [SCREENS.MONEY_REQUEST.DYNAMIC_STEP_MERCHANT]: DYNAMIC_ROUTES.MONEY_REQUEST_STEP_MERCHANT.path, [SCREENS.MONEY_REQUEST.STEP_PARTICIPANTS]: ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.route, [SCREENS.MONEY_REQUEST.STEP_SCAN]: ROUTES.MONEY_REQUEST_STEP_SCAN.route, [SCREENS.MONEY_REQUEST.RECEIPT_VIEW]: ROUTES.MONEY_REQUEST_RECEIPT_VIEW.route, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index fde882542f08..c8d9d5698936 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -2070,22 +2070,18 @@ type MoneyRequestNavigatorParamList = { // 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; }; - [SCREENS.MONEY_REQUEST.STEP_DATE]: { + [SCREENS.MONEY_REQUEST.DYNAMIC_STEP_DATE]: { action: IOUAction; iouType: Exclude; transactionID: string; reportID: 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; reportActionID?: string; }; - [SCREENS.MONEY_REQUEST.STEP_DESCRIPTION]: { + [SCREENS.MONEY_REQUEST.DYNAMIC_STEP_DESCRIPTION]: { action: IOUAction; iouType: Exclude; transactionID: string; reportID: 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; reportActionID: string; }; [SCREENS.MONEY_REQUEST.STEP_CATEGORY]: { @@ -2143,13 +2139,11 @@ type MoneyRequestNavigatorParamList = { pageIndex: string; transactionID: string; }; - [SCREENS.MONEY_REQUEST.STEP_MERCHANT]: { + [SCREENS.MONEY_REQUEST.DYNAMIC_STEP_MERCHANT]: { action: IOUAction; iouType: Exclude; transactionID: string; reportID: 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; reportActionID?: string; }; [SCREENS.IOU_SEND.ENABLE_PAYMENTS]: undefined; diff --git a/src/pages/iou/SplitExpenseEditPage.tsx b/src/pages/iou/SplitExpenseEditPage.tsx index 1d6ffb2c3557..83dc59a7c2cd 100644 --- a/src/pages/iou/SplitExpenseEditPage.tsx +++ b/src/pages/iou/SplitExpenseEditPage.tsx @@ -328,12 +328,13 @@ function SplitExpenseEditPage({route}: SplitExpensePageProps) { title={currentDescription} onPress={() => { Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute( - CONST.IOU.ACTION.EDIT, - CONST.IOU.TYPE.SPLIT_EXPENSE, - CONST.IOU.OPTIMISTIC_TRANSACTION_ID, - reportID, - Navigation.getActiveRoute(), + createDynamicRoute( + DYNAMIC_ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute( + CONST.IOU.ACTION.EDIT, + CONST.IOU.TYPE.SPLIT_EXPENSE, + CONST.IOU.OPTIMISTIC_TRANSACTION_ID, + reportID, + ), ), ); }} @@ -430,12 +431,8 @@ function SplitExpenseEditPage({route}: SplitExpensePageProps) { numberOfLinesTitle={2} onPress={() => { Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DATE.getRoute( - CONST.IOU.ACTION.EDIT, - CONST.IOU.TYPE.SPLIT_EXPENSE, - CONST.IOU.OPTIMISTIC_TRANSACTION_ID, - reportID, - Navigation.getActiveRoute(), + createDynamicRoute( + DYNAMIC_ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(CONST.IOU.ACTION.EDIT, CONST.IOU.TYPE.SPLIT_EXPENSE, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, reportID), ), ); }} diff --git a/src/pages/iou/request/step/IOURequestStepDate.tsx b/src/pages/iou/request/step/DynamicIOURequestStepDate.tsx similarity index 91% rename from src/pages/iou/request/step/IOURequestStepDate.tsx rename to src/pages/iou/request/step/DynamicIOURequestStepDate.tsx index a63775b12bc7..c9aee87cbf48 100644 --- a/src/pages/iou/request/step/IOURequestStepDate.tsx +++ b/src/pages/iou/request/step/DynamicIOURequestStepDate.tsx @@ -8,6 +8,7 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' import useDelegateAccountID from '@hooks/useDelegateAccountID'; import useDistanceRateOriginalPolicy from '@hooks/useDistanceRateOriginalPolicy'; import useDuplicateTransactionsAndViolations from '@hooks/useDuplicateTransactionsAndViolations'; +import useDynamicBackPath from '@hooks/useDynamicBackPath'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; @@ -32,6 +33,7 @@ import {updateMoneyRequestDate} from '@userActions/IOU/UpdateMoneyRequest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import {personalDetailsLoginSelector} from '@src/selectors/PersonalDetails'; import INPUT_IDS from '@src/types/form/MoneyRequestDateForm'; @@ -49,7 +51,7 @@ import StepScreenWrapper from './StepScreenWrapper'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; -type IOURequestStepDateProps = WithWritableReportOrNotFoundProps & { +type DynamicIOURequestStepDateProps = WithWritableReportOrNotFoundProps & { /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ transaction: OnyxEntry; @@ -57,13 +59,14 @@ type IOURequestStepDateProps = WithWritableReportOrNotFoundProps; }; -function IOURequestStepDate({ +function DynamicIOURequestStepDate({ route: { - params: {action, iouType, reportID, backTo, reportActionID, transactionID}, + params: {action, iouType, reportID, reportActionID, transactionID}, }, transaction, report, -}: IOURequestStepDateProps) { +}: DynamicIOURequestStepDateProps) { + const backPath = useDynamicBackPath(DYNAMIC_ROUTES.MONEY_REQUEST_STEP_DATE.path); const {getCurrencyDecimals, getCurrencySymbol} = useCurrencyListActions(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -102,11 +105,11 @@ function IOURequestStepDate({ const shouldShowNotFound = useShowNotFoundPageInIOUStep(action, iouType, reportActionID, report, transaction); const navigateBack = () => { - Navigation.goBack(backTo); + Navigation.goBack(backPath); }; const saveAndNavigateBack = () => { - Navigation.goBack(backTo, {shouldSkipFocusRestore: true}); + Navigation.goBack(backPath, {shouldSkipFocusRestore: true}); }; const updateDate = (value: FormOnyxValues) => { @@ -191,7 +194,7 @@ function IOURequestStepDate({ onBackButtonPress={navigateBack} shouldShowNotFoundPage={shouldShowNotFound} shouldShowWrapper - testID="IOURequestStepDate" + testID="DynamicIOURequestStepDate" includeSafeAreaPaddingBottom > & { +type DynamicIOURequestStepDescriptionProps = WithWritableReportOrNotFoundProps & { /** Holds data related to Expense view state, rather than the underlying Expense data. */ transaction: OnyxEntry; }; -function IOURequestStepDescription({ +function DynamicIOURequestStepDescription({ route: { - params: {action, iouType, reportID, backTo, reportActionID, transactionID}, + params: {action, iouType, reportID, reportActionID, transactionID}, }, transaction, report, -}: IOURequestStepDescriptionProps) { +}: DynamicIOURequestStepDescriptionProps) { + const backPath = useDynamicBackPath(DYNAMIC_ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.path); const policy = usePolicy(report?.policyID); const [splitDraftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report?.policyID}`); @@ -116,7 +119,7 @@ function IOURequestStepDescription({ [translate], ); - const {navigateBack, armNavigateBack} = useNavigateBackOnSave(isSaved, backTo); + const {navigateBack, armNavigateBack} = useNavigateBackOnSave(isSaved, backPath); const updateDescriptionRef = (value: string) => { setCurrentDescription(value); @@ -190,7 +193,7 @@ function IOURequestStepDescription({ headerTitle={translate('common.description')} onBackButtonPress={navigateBack} shouldShowWrapper - testID="IOURequestStepDescription" + testID="DynamicIOURequestStepDescription" shouldShowNotFoundPage={shouldShowNotFoundPage} > & - WithFullTransactionOrNotFoundProps; +type DynamicIOURequestStepMerchantProps = WithWritableReportOrNotFoundProps & + WithFullTransactionOrNotFoundProps; -function IOURequestStepMerchant({ +function DynamicIOURequestStepMerchant({ route: { - params: {transactionID, reportID, backTo, action, iouType, reportActionID}, + params: {transactionID, reportID, action, iouType, reportActionID}, }, transaction, report, -}: IOURequestStepMerchantProps) { +}: DynamicIOURequestStepMerchantProps) { + const backPath = useDynamicBackPath(DYNAMIC_ROUTES.MONEY_REQUEST_STEP_MERCHANT.path); const policy = usePolicy(report?.policyID); const [splitDraftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report?.policyID}`); @@ -87,7 +90,7 @@ function IOURequestStepMerchant({ const isMerchantRequired = isPolicyExpenseChat(report) || isExpenseRequest(report) || transaction?.participants?.some((participant) => !!participant.isPolicyExpenseChat); - const {navigateBack, armNavigateBack} = useNavigateBackOnSave(isSaved, backTo); + const {navigateBack, armNavigateBack} = useNavigateBackOnSave(isSaved, backPath); const validate = useCallback( (value: FormOnyxValues) => { @@ -183,7 +186,7 @@ function IOURequestStepMerchant({ headerTitle={translate('common.merchant')} onBackButtonPress={navigateBack} shouldShowWrapper - testID="IOURequestStepMerchant" + testID="DynamicIOURequestStepMerchant" shouldShowNotFoundPage={shouldShowNotFoundPage} > { expect(getMatchingNewRoute('/r/456/expense-report-edit?action=edit&iouType=submit&reportID=456')).toBe(undefined); expect(getMatchingNewRoute('/r/456/expense-tag?action=edit&iouType=submit&orderWeight=0&transactionID=123&reportID=456')).toBe(undefined); }); + + it('redirects the legacy money request merchant step to the new dynamic route (#83852)', () => { + expect(getMatchingNewRoute('/edit/submit/merchant/123/456')).toBe('/r/456/expense-merchant?action=edit&iouType=submit&transactionID=123&reportID=456'); + expect(getMatchingNewRoute('/edit/submit/merchant/123/456/789')).toBe('/r/456/expense-merchant?action=edit&iouType=submit&transactionID=123&reportID=456&reportActionID=789'); + }); + + it('redirects the legacy money request description step to the new dynamic route (#83852)', () => { + expect(getMatchingNewRoute('/edit/submit/description/123/456')).toBe('/r/456/expense-description?action=edit&iouType=submit&transactionID=123&reportID=456'); + expect(getMatchingNewRoute('/edit/submit/description/123/456/789')).toBe('/r/456/expense-description?action=edit&iouType=submit&transactionID=123&reportID=456&reportActionID=789'); + }); + + it('redirects the legacy money request date step to the new dynamic route (#83852)', () => { + expect(getMatchingNewRoute('/edit/submit/date/123/456')).toBe('/r/456/expense-date?action=edit&iouType=submit&transactionID=123&reportID=456'); + expect(getMatchingNewRoute('/edit/submit/date/123/456/789')).toBe('/r/456/expense-date?action=edit&iouType=submit&transactionID=123&reportID=456&reportActionID=789'); + }); + + it('does not redirect the already-migrated money request merchant, description and date dynamic routes (#83852)', () => { + expect(getMatchingNewRoute('/r/456/expense-merchant?action=edit&iouType=submit&transactionID=123&reportID=456')).toBe(undefined); + expect(getMatchingNewRoute('/r/456/expense-description?action=edit&iouType=submit&transactionID=123&reportID=456')).toBe(undefined); + expect(getMatchingNewRoute('/r/456/expense-date?action=edit&iouType=submit&transactionID=123&reportID=456')).toBe(undefined); + }); + + it('does not let the new money request patterns swallow unrelated merchant/description/date routes (#83852)', () => { + // The transaction duplicate review merchant route and the report description route keep their own mappings. + expect(getMatchingNewRoute('/r/123/duplicates/review/merchant')).toBe('/r/123/merchant/123'); + expect(getMatchingNewRoute('/r/123/description')).toBe('/r/123/description'); + }); }); diff --git a/tests/unit/useActiveRouteTest.ts b/tests/unit/useActiveRouteTest.ts index 9cee1f47fa22..90fd0d82938c 100644 --- a/tests/unit/useActiveRouteTest.ts +++ b/tests/unit/useActiveRouteTest.ts @@ -4,7 +4,6 @@ import useActiveRoute from '@hooks/useActiveRoute'; import Navigation from '@libs/Navigation/Navigation'; -import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; describe('useActiveRoute', () => { @@ -19,7 +18,7 @@ describe('useActiveRoute', () => { expect(actualActiveRoute).toBe(expectedActiveRoute); // When getting the active route multiple times - navigation.mockReturnValueOnce(ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.CREATE, '1', '1')); + navigation.mockReturnValueOnce(ROUTES.SEARCH_REPORT.getRoute({reportID: '2'})); const actualActiveRoute2 = result.current.getReportRHPActiveRoute(); // Then it should return the first active route value From 060f0538f58961a59feefdbccb76421d13f709fc Mon Sep 17 00:00:00 2001 From: Cong Pham Date: Wed, 12 Aug 2026 13:13:15 +0700 Subject: [PATCH 2/2] Allow the split bill details screen to open the merchant, description and date steps An incomplete split bill still renders `MoneyRequestConfirmationList` with editable Merchant/Description/Date rows for its original actor, so those taps append the `expense-*` suffix to `/r/:reportID/split/:reportActionID`. Without the split details screen in `entryScreens`, `getStateFromPath` rejects the suffix and the app falls through to `/not-found`. Co-Authored-By: Claude Opus 5 --- src/ROUTES.ts | 4 ++++ src/libs/Navigation/linkingConfig/OldRoutes.ts | 3 --- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index a38fe64fa267..4e055652d90c 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -313,6 +313,8 @@ const DYNAMIC_ROUTES = { SCREENS.RIGHT_MODAL.SEARCH_REPORT, SCREENS.RIGHT_MODAL.EXPENSE_REPORT, SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT, + // An incomplete split bill still renders the editable confirmation fields for its original actor. + SCREENS.SPLIT_DETAILS.DYNAMIC_ROOT, ], getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, reportActionID?: string) => getUrlWithParams('expense-merchant', {action, iouType, transactionID, reportID, reportActionID}), @@ -329,6 +331,7 @@ const DYNAMIC_ROUTES = { SCREENS.RIGHT_MODAL.EXPENSE_REPORT, SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT, SCREENS.MONEY_REQUEST.SPLIT_EXPENSE_EDIT, + SCREENS.SPLIT_DETAILS.DYNAMIC_ROOT, ], getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, reportActionID?: string) => getUrlWithParams('expense-description', {action, iouType, transactionID, reportID, reportActionID}), @@ -345,6 +348,7 @@ const DYNAMIC_ROUTES = { SCREENS.RIGHT_MODAL.EXPENSE_REPORT, SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT, SCREENS.MONEY_REQUEST.SPLIT_EXPENSE_EDIT, + SCREENS.SPLIT_DETAILS.DYNAMIC_ROOT, ], getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, reportActionID?: string) => getUrlWithParams('expense-date', {action, iouType, transactionID, reportID, reportActionID}), diff --git a/src/libs/Navigation/linkingConfig/OldRoutes.ts b/src/libs/Navigation/linkingConfig/OldRoutes.ts index 6ecb53e3233a..dbe1f96ebe5b 100644 --- a/src/libs/Navigation/linkingConfig/OldRoutes.ts +++ b/src/libs/Navigation/linkingConfig/OldRoutes.ts @@ -153,9 +153,6 @@ const oldRoutes: Record = { '/*/*/taxRate/*/*': '/r/$4/taxRate?action=$1&iouType=$2&transactionID=$3&reportID=$4', '/*/*/taxAmount/*/*': '/r/$4/taxAmount?action=$1&iouType=$2&transactionID=$3&reportID=$4', '/*/*/attendees/*/*': '/r/$4/attendees?action=$1&iouType=$2&transactionID=$3&reportID=$4', - // Merchant/description/date steps migrated to dynamic routes (#83852). Suffixes are namespaced `expense-*` because - // dynamic route paths must be globally unique and bare `description` is already taken by the report description - // suffix. As with the report/tag entries above, the optional `reportActionID` segment needs its own longer pattern. '/*/*/merchant/*/*/*': '/r/$4/expense-merchant?action=$1&iouType=$2&transactionID=$3&reportID=$4&reportActionID=$5', '/*/*/merchant/*/*': '/r/$4/expense-merchant?action=$1&iouType=$2&transactionID=$3&reportID=$4', '/*/*/description/*/*/*': '/r/$4/expense-description?action=$1&iouType=$2&transactionID=$3&reportID=$4&reportActionID=$5',