From 0019f1b74947888d98d5835d99bcaf7d4754adc2 Mon Sep 17 00:00:00 2001 From: huutech <20178761+huult@users.noreply.github.com> Date: Mon, 16 Mar 2026 22:27:32 +0700 Subject: [PATCH 1/6] migrate DETAILS_CONSTANT_PICKER_PAGE --- src/ROUTES.ts | 18 ++-- src/SCREENS.ts | 2 +- .../ModalStackNavigators/index.tsx | 2 +- .../Navigation/helpers/getStateFromPath.ts | 48 +++++++-- src/libs/Navigation/linkingConfig/config.ts | 5 +- src/libs/Navigation/types.ts | 4 +- src/pages/Debug/ConstantSelector.tsx | 5 +- .../DynamicDebugDetailsConstantPickerPage.tsx | 98 +++++++++++++++++++ 8 files changed, 159 insertions(+), 23 deletions(-) create mode 100644 src/pages/Debug/DynamicDebugDetailsConstantPickerPage.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 23188706a6e2..f3a7128760fb 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -112,6 +112,18 @@ const DYNAMIC_ROUTES = { getRoute: (country = '') => `country?country=${country}`, queryParams: ['country'], }, + DETAILS_CONSTANT_PICKER: { + path: 'constant-picker', + entryScreens: [SCREENS.DEBUG.REPORT, SCREENS.DEBUG.REPORT_ACTION, SCREENS.DEBUG.TRANSACTION, SCREENS.DEBUG.TRANSACTION_VIOLATION], + getRoute: (formType: string, fieldName: string, fieldValue?: string, policyID?: string) => + getUrlWithParams('constant-picker', { + formType, + fieldName, + fieldValue, + policyID, + }), + queryParams: ['formType', 'fieldName', 'fieldValue', 'policyID'], + }, } as const satisfies DynamicRoutes; const ROUTES = { @@ -3858,12 +3870,6 @@ const ROUTES = { route: 'debug/report/:reportID/actions/:reportActionID/preview', getRoute: (reportID: string, reportActionID: string) => `debug/report/${reportID}/actions/${reportActionID}/preview` as const, }, - DETAILS_CONSTANT_PICKER_PAGE: { - route: 'debug/:formType/details/constant/:fieldName', - getRoute: (formType: string, fieldName: string, fieldValue?: string, policyID?: string, backTo?: string) => - // eslint-disable-next-line no-restricted-syntax -- Legacy route generation - getUrlWithBackToParam(`debug/${formType}/details/constant/${fieldName}?fieldValue=${fieldValue}&policyID=${policyID}`, backTo), - }, DETAILS_DATE_TIME_PICKER_PAGE: { route: 'debug/details/datetime/:fieldName', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 6c2599a91dc5..cf47672365a9 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -917,7 +917,7 @@ const SCREENS = { REPORT: 'Debug_Report', REPORT_ACTION: 'Debug_Report_Action', REPORT_ACTION_CREATE: 'Debug_Report_Action_Create', - DETAILS_CONSTANT_PICKER_PAGE: 'Debug_Details_Constant_Picker_Page', + DYNAMIC_DETAILS_CONSTANT_PICKER_PAGE: 'Dynamic_Debug_Details_Constant_Picker_Page', DETAILS_DATE_TIME_PICKER_PAGE: 'Debug_Details_Date_Time_Picker_Page', TRANSACTION: 'Debug_Transaction', TRANSACTION_VIOLATION_CREATE: 'Debug_Transaction_Violation_Create', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 30d5ba47887e..c4ea3aaeff61 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -1076,7 +1076,7 @@ const DebugModalStackNavigator = createModalStackNavigator({ [SCREENS.DEBUG.REPORT]: () => require('../../../../pages/Debug/Report/DebugReportPage').default, [SCREENS.DEBUG.REPORT_ACTION]: () => require('../../../../pages/Debug/ReportAction/DebugReportActionPage').default, [SCREENS.DEBUG.REPORT_ACTION_CREATE]: () => require('../../../../pages/Debug/ReportAction/DebugReportActionCreatePage').default, - [SCREENS.DEBUG.DETAILS_CONSTANT_PICKER_PAGE]: () => require('../../../../pages/Debug/DebugDetailsConstantPickerPage').default, + [SCREENS.DEBUG.DYNAMIC_DETAILS_CONSTANT_PICKER_PAGE]: () => require('../../../../pages/Debug/DynamicDebugDetailsConstantPickerPage').default, [SCREENS.DEBUG.DETAILS_DATE_TIME_PICKER_PAGE]: () => require('../../../../pages/Debug/DebugDetailsDateTimePickerPage').default, [SCREENS.DEBUG.TRANSACTION]: () => require('../../../../pages/Debug/Transaction/DebugTransactionPage').default, [SCREENS.DEBUG.TRANSACTION_VIOLATION_CREATE]: () => require('../../../../pages/Debug/TransactionViolation/DebugTransactionViolationCreatePage').default, diff --git a/src/libs/Navigation/helpers/getStateFromPath.ts b/src/libs/Navigation/helpers/getStateFromPath.ts index 2c4d7a11dcce..d58d60aad592 100644 --- a/src/libs/Navigation/helpers/getStateFromPath.ts +++ b/src/libs/Navigation/helpers/getStateFromPath.ts @@ -12,6 +12,34 @@ import getStateForDynamicRoute from './dynamicRoutesUtils/getStateForDynamicRout import getMatchingNewRoute from './getMatchingNewRoute'; import getRedirectedPath from './getRedirectedPath'; +type RouteChainItem = { + name?: string; + params?: Record; +}; + +function getRouteChain(state?: PartialState | NavigationState): RouteChainItem[] { + const routeChain: RouteChainItem[] = []; + let currentState = state; + + while (currentState?.routes?.length) { + const routeIndex = currentState.index ?? currentState.routes.length - 1; + const currentRoute = currentState.routes.at(routeIndex); + + if (!currentRoute) { + break; + } + + routeChain.push({ + name: currentRoute.name, + params: currentRoute.params as Record | undefined, + }); + + currentState = currentRoute.state as PartialState | NavigationState | undefined; + } + + return routeChain; +} + /** * @param path - The path to parse * @returns - It's possible that there is no navigation action for the given path @@ -31,15 +59,23 @@ function getStateFromPath(path: Route): PartialState { // Find the dynamic route key that matches the extracted suffix const dynamicRoute: string = dynamicRouteKeys.find((key) => DYNAMIC_ROUTES[key].path === dynamicRouteSuffix) ?? ''; - // Get the currently focused route from the base path to check permissions - const focusedRoute = findFocusedRoute(getStateFromPath(pathWithoutDynamicSuffix) ?? {}); const entryScreens: Screen[] = DYNAMIC_ROUTES[dynamicRoute as DynamicRouteKey]?.entryScreens ?? []; + // Get the currently focused route from the base path to check permissions + const baseState = getStateFromPath(pathWithoutDynamicSuffix); + const focusedRoute = findFocusedRoute(baseState ?? {}); + const allowedParentRoute = getRouteChain(baseState) + .reverse() + .find((route) => route.name && entryScreens.includes(route.name as Screen)); // Check if the focused route is allowed to access this dynamic route - if (focusedRoute?.name) { - if (entryScreens.includes(focusedRoute.name as Screen)) { + if (focusedRoute?.name || allowedParentRoute?.name) { + if (entryScreens.includes(focusedRoute?.name as Screen) || !!allowedParentRoute?.name) { // Generate navigation state for the dynamic route - const dynamicRouteState = getStateForDynamicRoute(normalizedPath, dynamicRoute as DynamicRouteKey, focusedRoute?.params as Record | undefined); + const dynamicRouteState = getStateForDynamicRoute( + normalizedPath, + dynamicRoute as DynamicRouteKey, + (allowedParentRoute?.params ?? focusedRoute?.params) as Record | undefined, + ); return dynamicRouteState; } @@ -51,7 +87,7 @@ function getStateFromPath(path: Route): PartialState { } // Log an error to quickly identify and add forgotten screens to the Dynamic Routes configuration - Log.warn(`[getStateFromPath.ts][DynamicRoute] Focused route ${focusedRoute.name} is not allowed to access dynamic route with suffix ${dynamicRouteSuffix}`); + Log.warn(`[getStateFromPath.ts][DynamicRoute] Focused route ${focusedRoute?.name ?? 'unknown'} is not allowed to access dynamic route with suffix ${dynamicRouteSuffix}`); } } diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index b533a716fbef..c05a8145f02f 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -2007,10 +2007,7 @@ const config: LinkingOptions['config'] = { path: ROUTES.DEBUG_REPORT_ACTION_CREATE.route, exact: true, }, - [SCREENS.DEBUG.DETAILS_CONSTANT_PICKER_PAGE]: { - path: ROUTES.DETAILS_CONSTANT_PICKER_PAGE.route, - exact: true, - }, + [SCREENS.DEBUG.DYNAMIC_DETAILS_CONSTANT_PICKER_PAGE]: DYNAMIC_ROUTES.DETAILS_CONSTANT_PICKER.path, [SCREENS.DEBUG.DETAILS_DATE_TIME_PICKER_PAGE]: { path: ROUTES.DETAILS_DATE_TIME_PICKER_PAGE.route, exact: true, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 072269fe167f..6286229c8ecc 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -3100,13 +3100,11 @@ type DebugParamList = { [SCREENS.DEBUG.REPORT_ACTION_CREATE]: { reportID: string; }; - [SCREENS.DEBUG.DETAILS_CONSTANT_PICKER_PAGE]: { + [SCREENS.DEBUG.DYNAMIC_DETAILS_CONSTANT_PICKER_PAGE]: { formType: string; fieldName: string; fieldValue?: string; policyID?: 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?: string; }; [SCREENS.DEBUG.DETAILS_DATE_TIME_PICKER_PAGE]: { fieldName: string; diff --git a/src/pages/Debug/ConstantSelector.tsx b/src/pages/Debug/ConstantSelector.tsx index 637370390169..33509de90ac3 100644 --- a/src/pages/Debug/ConstantSelector.tsx +++ b/src/pages/Debug/ConstantSelector.tsx @@ -4,9 +4,10 @@ import React, {useEffect} from 'react'; import type {View} from 'react-native'; import type {ValueOf} from 'type-fest'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; type ConstantSelectorProps = { /** Form error text. e.g when no constant is selected */ @@ -63,7 +64,7 @@ function ConstantSelector( brickRoadIndicator={errorText ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} errorText={errorText} onPress={() => { - Navigation.navigate(ROUTES.DETAILS_CONSTANT_PICKER_PAGE.getRoute(formType, name, value, policyID, Navigation.getActiveRoute())); + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.DETAILS_CONSTANT_PICKER.getRoute(formType, name, value, policyID))); }} shouldShowRightIcon /> diff --git a/src/pages/Debug/DynamicDebugDetailsConstantPickerPage.tsx b/src/pages/Debug/DynamicDebugDetailsConstantPickerPage.tsx new file mode 100644 index 000000000000..a4511c18a841 --- /dev/null +++ b/src/pages/Debug/DynamicDebugDetailsConstantPickerPage.tsx @@ -0,0 +1,98 @@ +import React, {useCallback} from 'react'; +import {View} from 'react-native'; +import CategoryPicker from '@components/CategoryPicker'; +import CurrencySelectionList from '@components/CurrencySelectionList'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import type {ListItem} from '@components/SelectionListWithSections/types'; +import useDynamicBackPath from '@hooks/useDynamicBackPath'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {DebugParamList} from '@libs/Navigation/types'; +import {appendParam} from '@libs/Url'; +import CONST from '@src/CONST'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import TRANSACTION_FORM_INPUT_IDS from '@src/types/form/DebugTransactionForm'; +import ConstantPicker from './ConstantPicker'; +import DebugTagPicker from './DebugTagPicker'; + +type DynamicDebugDetailsConstantPickerPageProps = PlatformStackScreenProps; + +function DynamicDebugDetailsConstantPickerPage({ + route: { + params: {formType, fieldName, fieldValue, policyID = ''}, + }, +}: DynamicDebugDetailsConstantPickerPageProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const backPath = useDynamicBackPath(DYNAMIC_ROUTES.DETAILS_CONSTANT_PICKER.path); + const onSubmit = useCallback( + (item: ListItem) => { + const value = item.text === fieldValue ? '' : (item.text ?? ''); + Navigation.goBack(appendParam(backPath, fieldName, value), {compareParams: false}); + }, + [backPath, fieldName, fieldValue], + ); + + const renderPicker = useCallback(() => { + if (([TRANSACTION_FORM_INPUT_IDS.CURRENCY, TRANSACTION_FORM_INPUT_IDS.MODIFIED_CURRENCY, TRANSACTION_FORM_INPUT_IDS.ORIGINAL_CURRENCY] as string[]).includes(fieldName)) { + return ( + + onSubmit({ + text: currencyCode, + }) + } + searchInputLabel={translate('common.search')} + /> + ); + } + if (formType === CONST.DEBUG.FORMS.TRANSACTION) { + if (fieldName === TRANSACTION_FORM_INPUT_IDS.CATEGORY) { + return ( + + ); + } + if (fieldName === TRANSACTION_FORM_INPUT_IDS.TAG) { + return ( + + ); + } + } + + return ( + + ); + }, [fieldName, fieldValue, formType, onSubmit, policyID, translate]); + + return ( + + { + Navigation.goBack(fieldValue ? appendParam(backPath, fieldName, fieldValue) : backPath, {compareParams: false}); + }} + /> + {renderPicker()} + + ); +} + +export default DynamicDebugDetailsConstantPickerPage; From 67db369b3d4e13b0d6d5f9f6e254b47b7e8b515b Mon Sep 17 00:00:00 2001 From: huutech <20178761+huult@users.noreply.github.com> Date: Mon, 16 Mar 2026 22:36:46 +0700 Subject: [PATCH 2/6] migrate DETAILS_DATE_TIME_PICKER_PAGE --- src/ROUTES.ts | 17 ++++--- src/SCREENS.ts | 2 +- .../ModalStackNavigators/index.tsx | 2 +- src/libs/Navigation/linkingConfig/config.ts | 5 +- src/libs/Navigation/types.ts | 4 +- src/pages/Debug/DateTimeSelector.tsx | 5 +- .../Debug/DebugDetailsConstantPickerPage.tsx | 36 +++++++------- ...DynamicDebugDetailsDateTimePickerPage.tsx} | 49 ++++++++++--------- 8 files changed, 62 insertions(+), 58 deletions(-) rename src/pages/Debug/{DebugDetailsDateTimePickerPage.tsx => DynamicDebugDetailsDateTimePickerPage.tsx} (51%) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index f3a7128760fb..7efa25fe2e3f 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -124,6 +124,17 @@ const DYNAMIC_ROUTES = { }), queryParams: ['formType', 'fieldName', 'fieldValue', 'policyID'], }, + DETAILS_DATE_TIME_PICKER: { + path: 'datetime-picker', + entryScreens: [SCREENS.DEBUG.REPORT, SCREENS.DEBUG.REPORT_ACTION, SCREENS.DEBUG.TRANSACTION, SCREENS.DEBUG.TRANSACTION_VIOLATION], + getRoute: (fieldName: string, fieldValue?: string, policyID?: string) => + getUrlWithParams('datetime-picker', { + fieldName, + fieldValue, + policyID, + }), + queryParams: ['fieldName', 'fieldValue', 'policyID'], + }, } as const satisfies DynamicRoutes; const ROUTES = { @@ -3870,12 +3881,6 @@ const ROUTES = { route: 'debug/report/:reportID/actions/:reportActionID/preview', getRoute: (reportID: string, reportActionID: string) => `debug/report/${reportID}/actions/${reportActionID}/preview` as const, }, - DETAILS_DATE_TIME_PICKER_PAGE: { - route: 'debug/details/datetime/:fieldName', - - // eslint-disable-next-line no-restricted-syntax -- Legacy route generation - getRoute: (fieldName: string, fieldValue?: string, backTo?: string) => getUrlWithBackToParam(`debug/details/datetime/${fieldName}?fieldValue=${fieldValue}`, backTo), - }, DEBUG_TRANSACTION: { route: 'debug/transaction/:transactionID', getRoute: (transactionID: string) => `debug/transaction/${transactionID}` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index cf47672365a9..75b3099daed2 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -918,7 +918,7 @@ const SCREENS = { REPORT_ACTION: 'Debug_Report_Action', REPORT_ACTION_CREATE: 'Debug_Report_Action_Create', DYNAMIC_DETAILS_CONSTANT_PICKER_PAGE: 'Dynamic_Debug_Details_Constant_Picker_Page', - DETAILS_DATE_TIME_PICKER_PAGE: 'Debug_Details_Date_Time_Picker_Page', + DYNAMIC_DETAILS_DATE_TIME_PICKER_PAGE: 'Dynamic_Debug_Details_Date_Time_Picker_Page', TRANSACTION: 'Debug_Transaction', TRANSACTION_VIOLATION_CREATE: 'Debug_Transaction_Violation_Create', TRANSACTION_VIOLATION: 'Debug_Transaction_Violation', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index c4ea3aaeff61..3a86a76b25ef 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -1077,7 +1077,7 @@ const DebugModalStackNavigator = createModalStackNavigator({ [SCREENS.DEBUG.REPORT_ACTION]: () => require('../../../../pages/Debug/ReportAction/DebugReportActionPage').default, [SCREENS.DEBUG.REPORT_ACTION_CREATE]: () => require('../../../../pages/Debug/ReportAction/DebugReportActionCreatePage').default, [SCREENS.DEBUG.DYNAMIC_DETAILS_CONSTANT_PICKER_PAGE]: () => require('../../../../pages/Debug/DynamicDebugDetailsConstantPickerPage').default, - [SCREENS.DEBUG.DETAILS_DATE_TIME_PICKER_PAGE]: () => require('../../../../pages/Debug/DebugDetailsDateTimePickerPage').default, + [SCREENS.DEBUG.DYNAMIC_DETAILS_DATE_TIME_PICKER_PAGE]: () => require('../../../../pages/Debug/DynamicDebugDetailsDateTimePickerPage').default, [SCREENS.DEBUG.TRANSACTION]: () => require('../../../../pages/Debug/Transaction/DebugTransactionPage').default, [SCREENS.DEBUG.TRANSACTION_VIOLATION_CREATE]: () => require('../../../../pages/Debug/TransactionViolation/DebugTransactionViolationCreatePage').default, [SCREENS.DEBUG.TRANSACTION_VIOLATION]: () => require('../../../../pages/Debug/TransactionViolation/DebugTransactionViolationPage').default, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index c05a8145f02f..d3f44d4d56d9 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -2008,10 +2008,7 @@ const config: LinkingOptions['config'] = { exact: true, }, [SCREENS.DEBUG.DYNAMIC_DETAILS_CONSTANT_PICKER_PAGE]: DYNAMIC_ROUTES.DETAILS_CONSTANT_PICKER.path, - [SCREENS.DEBUG.DETAILS_DATE_TIME_PICKER_PAGE]: { - path: ROUTES.DETAILS_DATE_TIME_PICKER_PAGE.route, - exact: true, - }, + [SCREENS.DEBUG.DYNAMIC_DETAILS_DATE_TIME_PICKER_PAGE]: DYNAMIC_ROUTES.DETAILS_DATE_TIME_PICKER.path, [SCREENS.DEBUG.TRANSACTION]: { path: ROUTES.DEBUG_TRANSACTION.route, exact: true, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 6286229c8ecc..9bc78234d15b 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -3106,11 +3106,9 @@ type DebugParamList = { fieldValue?: string; policyID?: string; }; - [SCREENS.DEBUG.DETAILS_DATE_TIME_PICKER_PAGE]: { + [SCREENS.DEBUG.DYNAMIC_DETAILS_DATE_TIME_PICKER_PAGE]: { fieldName: string; fieldValue?: 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?: string; }; [SCREENS.DEBUG.TRANSACTION]: { transactionID: string; diff --git a/src/pages/Debug/DateTimeSelector.tsx b/src/pages/Debug/DateTimeSelector.tsx index f9da6e064c9f..4dd552c1bc02 100644 --- a/src/pages/Debug/DateTimeSelector.tsx +++ b/src/pages/Debug/DateTimeSelector.tsx @@ -3,9 +3,10 @@ import type {ForwardedRef} from 'react'; import React, {useEffect} from 'react'; import type {View} from 'react-native'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; type DateTimeSelectorProps = { /** Form error text. e.g when no datetime is selected */ @@ -57,7 +58,7 @@ function DateTimeSelector( brickRoadIndicator={errorText ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} errorText={errorText} onPress={() => { - Navigation.navigate(ROUTES.DETAILS_DATE_TIME_PICKER_PAGE.getRoute(name, value, Navigation.getActiveRoute())); + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.DETAILS_DATE_TIME_PICKER.getRoute(name, value))); }} shouldShowRightIcon /> diff --git a/src/pages/Debug/DebugDetailsConstantPickerPage.tsx b/src/pages/Debug/DebugDetailsConstantPickerPage.tsx index a1ce28ebd4b0..a4511c18a841 100644 --- a/src/pages/Debug/DebugDetailsConstantPickerPage.tsx +++ b/src/pages/Debug/DebugDetailsConstantPickerPage.tsx @@ -5,6 +5,7 @@ import CurrencySelectionList from '@components/CurrencySelectionList'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import type {ListItem} from '@components/SelectionListWithSections/types'; +import useDynamicBackPath from '@hooks/useDynamicBackPath'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; @@ -12,37 +13,28 @@ import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavig import type {DebugParamList} from '@libs/Navigation/types'; import {appendParam} from '@libs/Url'; import CONST from '@src/CONST'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import TRANSACTION_FORM_INPUT_IDS from '@src/types/form/DebugTransactionForm'; import ConstantPicker from './ConstantPicker'; import DebugTagPicker from './DebugTagPicker'; -type DebugDetailsConstantPickerPageProps = PlatformStackScreenProps; +type DynamicDebugDetailsConstantPickerPageProps = PlatformStackScreenProps; -function DebugDetailsConstantPickerPage({ +function DynamicDebugDetailsConstantPickerPage({ route: { - params: {formType, fieldName, fieldValue, policyID = '', backTo = ''}, + params: {formType, fieldName, fieldValue, policyID = ''}, }, - navigation, -}: DebugDetailsConstantPickerPageProps) { +}: DynamicDebugDetailsConstantPickerPageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const backPath = useDynamicBackPath(DYNAMIC_ROUTES.DETAILS_CONSTANT_PICKER.path); const onSubmit = useCallback( (item: ListItem) => { const value = item.text === fieldValue ? '' : (item.text ?? ''); - // Check the navigation state and "backTo" parameter to decide navigation behavior - if (navigation.getState().routes.length === 1 && !backTo) { - // If there is only one route and "backTo" is empty, go back in navigation - Navigation.goBack(); - } else if (!!backTo && navigation.getState().routes.length === 1) { - // If "backTo" is not empty and there is only one route, go back to the specific route defined in "backTo" with a country parameter - Navigation.goBack(appendParam(backTo, fieldName, value)); - } else { - // Otherwise, navigate to the specific route defined in "backTo" with a country parameter - Navigation.navigate(appendParam(backTo, fieldName, value)); - } + Navigation.goBack(appendParam(backPath, fieldName, value), {compareParams: false}); }, - [backTo, fieldName, fieldValue, navigation], + [backPath, fieldName, fieldValue], ); const renderPicker = useCallback(() => { @@ -91,10 +83,16 @@ function DebugDetailsConstantPickerPage({ return ( - + { + Navigation.goBack(fieldValue ? appendParam(backPath, fieldName, fieldValue) : backPath, {compareParams: false}); + }} + /> {renderPicker()} ); } -export default DebugDetailsConstantPickerPage; +export default DynamicDebugDetailsConstantPickerPage; diff --git a/src/pages/Debug/DebugDetailsDateTimePickerPage.tsx b/src/pages/Debug/DynamicDebugDetailsDateTimePickerPage.tsx similarity index 51% rename from src/pages/Debug/DebugDetailsDateTimePickerPage.tsx rename to src/pages/Debug/DynamicDebugDetailsDateTimePickerPage.tsx index 91598772b9fb..7ded25114de6 100644 --- a/src/pages/Debug/DebugDetailsDateTimePickerPage.tsx +++ b/src/pages/Debug/DynamicDebugDetailsDateTimePickerPage.tsx @@ -1,5 +1,5 @@ import {format} from 'date-fns'; -import React, {useState} from 'react'; +import React, {useCallback, useState} from 'react'; import {View} from 'react-native'; import DatePicker from '@components/DatePicker'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -7,6 +7,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import TimePicker from '@components/TimePicker/TimePicker'; +import useDynamicBackPath from '@hooks/useDynamicBackPath'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; @@ -14,22 +15,38 @@ import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {DebugParamList} from '@libs/Navigation/types'; import {appendParam} from '@libs/Url'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -type DebugDetailsDateTimePickerPageProps = PlatformStackScreenProps; +type DynamicDebugDetailsDateTimePickerPageProps = PlatformStackScreenProps; -function DebugDetailsDateTimePickerPage({ +function DynamicDebugDetailsDateTimePickerPage({ route: { - params: {fieldName, fieldValue = '', backTo = ''}, + params: {fieldName, fieldValue = ''}, }, - navigation, -}: DebugDetailsDateTimePickerPageProps) { +}: DynamicDebugDetailsDateTimePickerPageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const backPath = useDynamicBackPath(DYNAMIC_ROUTES.DETAILS_DATE_TIME_PICKER.path); const [date, setDate] = useState(() => DateUtils.extractDate(fieldValue)); + + const handleSubmit = useCallback( + (time: string) => { + const formattedDateTime = format(new Date(`${date} ${time}`), 'yyyy-MM-dd HH:mm:ss.SSS'); + Navigation.goBack(appendParam(backPath, fieldName, formattedDateTime), {compareParams: false}); + }, + [date, fieldName, backPath], + ); + return ( - - + + { + Navigation.goBack(fieldValue ? appendParam(backPath, fieldName, fieldValue) : backPath, {compareParams: false}); + }} + /> {translate('debug.date')} @@ -42,19 +59,7 @@ function DebugDetailsDateTimePickerPage({ {translate('debug.time')} { - // Check the navigation state and "backTo" parameter to decide navigation behavior - if (navigation.getState().routes.length === 1 && !backTo) { - // If there is only one route and "backTo" is empty, go back in navigation - Navigation.goBack(); - } else if (!!backTo && navigation.getState().routes.length === 1) { - // If "backTo" is not empty and there is only one route, go back to the specific route defined in "backTo" with a country parameter - Navigation.goBack(appendParam(backTo, fieldName, format(new Date(`${date} ${time}`), 'yyyy-MM-dd HH:mm:ss.SSS'))); - } else { - // Otherwise, navigate to the specific route defined in "backTo" with a country parameter - Navigation.navigate(appendParam(backTo, fieldName, format(new Date(`${date} ${time}`), 'yyyy-MM-dd HH:mm:ss.SSS'))); - } - }} + onSubmit={handleSubmit} defaultValue={fieldValue} shouldValidate={false} showFullFormat @@ -65,4 +70,4 @@ function DebugDetailsDateTimePickerPage({ ); } -export default DebugDetailsDateTimePickerPage; +export default DynamicDebugDetailsDateTimePickerPage; From 2a2470bb78fe61f92db9cc263dca28f1676bd645 Mon Sep 17 00:00:00 2001 From: huutech <20178761+huult@users.noreply.github.com> Date: Mon, 16 Mar 2026 22:49:00 +0700 Subject: [PATCH 3/6] add test --- tests/navigation/getStateFromPathTests.ts | 35 +++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/navigation/getStateFromPathTests.ts b/tests/navigation/getStateFromPathTests.ts index 5808b6f164e4..8d18ba2b5321 100644 --- a/tests/navigation/getStateFromPathTests.ts +++ b/tests/navigation/getStateFromPathTests.ts @@ -71,6 +71,41 @@ describe('getStateFromPath', () => { expect(mockGetStateForDynamicRoute).toHaveBeenCalledWith(fullPath, 'VERIFY_ACCOUNT', focusedRouteParams); }); + it('should generate dynamic state when focused screen is not authorized but an authorized parent is in route chain', () => { + const fullPath = '/settings/wallet/verify-account'; + const allowedParentParams = {workspaceID: '123'}; + const baseRouteState = { + routes: [ + { + name: 'Root', + state: { + routes: [ + { + name: 'Settings', + params: allowedParentParams, + state: { + routes: [{name: 'RightModal', params: {modal: 'test'}}], + }, + }, + ], + }, + }, + ], + }; + + mockRNGetStateFromPath.mockReturnValue(baseRouteState); + mockFindFocusedRoute.mockReturnValue({name: 'RightModal', params: {modal: 'test'}}); + + const expectedDynamicState = {routes: [{name: 'DynamicRoot'}]}; + mockGetStateForDynamicRoute.mockReturnValue(expectedDynamicState); + + const result = getStateFromPath(fullPath as unknown as Route); + + expect(result).toBe(expectedDynamicState); + expect(mockGetStateForDynamicRoute).toHaveBeenCalledWith(fullPath, 'VERIFY_ACCOUNT', allowedParentParams); + expect(mockLogWarn).not.toHaveBeenCalled(); + }); + it('should fallback to standard RN parsing if focused screen is NOT authorized for dynamic route', () => { const fullPath = '/chat/verify-account'; From ea6363f0e70510bea7eec060baa70bee061e3c22 Mon Sep 17 00:00:00 2001 From: huutech <20178761+huult@users.noreply.github.com> Date: Mon, 16 Mar 2026 22:51:58 +0700 Subject: [PATCH 4/6] remove file unused --- .../Debug/DebugDetailsConstantPickerPage.tsx | 98 ------------------- 1 file changed, 98 deletions(-) delete mode 100644 src/pages/Debug/DebugDetailsConstantPickerPage.tsx diff --git a/src/pages/Debug/DebugDetailsConstantPickerPage.tsx b/src/pages/Debug/DebugDetailsConstantPickerPage.tsx deleted file mode 100644 index a4511c18a841..000000000000 --- a/src/pages/Debug/DebugDetailsConstantPickerPage.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import React, {useCallback} from 'react'; -import {View} from 'react-native'; -import CategoryPicker from '@components/CategoryPicker'; -import CurrencySelectionList from '@components/CurrencySelectionList'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import type {ListItem} from '@components/SelectionListWithSections/types'; -import useDynamicBackPath from '@hooks/useDynamicBackPath'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import Navigation from '@libs/Navigation/Navigation'; -import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {DebugParamList} from '@libs/Navigation/types'; -import {appendParam} from '@libs/Url'; -import CONST from '@src/CONST'; -import {DYNAMIC_ROUTES} from '@src/ROUTES'; -import type SCREENS from '@src/SCREENS'; -import TRANSACTION_FORM_INPUT_IDS from '@src/types/form/DebugTransactionForm'; -import ConstantPicker from './ConstantPicker'; -import DebugTagPicker from './DebugTagPicker'; - -type DynamicDebugDetailsConstantPickerPageProps = PlatformStackScreenProps; - -function DynamicDebugDetailsConstantPickerPage({ - route: { - params: {formType, fieldName, fieldValue, policyID = ''}, - }, -}: DynamicDebugDetailsConstantPickerPageProps) { - const {translate} = useLocalize(); - const styles = useThemeStyles(); - const backPath = useDynamicBackPath(DYNAMIC_ROUTES.DETAILS_CONSTANT_PICKER.path); - const onSubmit = useCallback( - (item: ListItem) => { - const value = item.text === fieldValue ? '' : (item.text ?? ''); - Navigation.goBack(appendParam(backPath, fieldName, value), {compareParams: false}); - }, - [backPath, fieldName, fieldValue], - ); - - const renderPicker = useCallback(() => { - if (([TRANSACTION_FORM_INPUT_IDS.CURRENCY, TRANSACTION_FORM_INPUT_IDS.MODIFIED_CURRENCY, TRANSACTION_FORM_INPUT_IDS.ORIGINAL_CURRENCY] as string[]).includes(fieldName)) { - return ( - - onSubmit({ - text: currencyCode, - }) - } - searchInputLabel={translate('common.search')} - /> - ); - } - if (formType === CONST.DEBUG.FORMS.TRANSACTION) { - if (fieldName === TRANSACTION_FORM_INPUT_IDS.CATEGORY) { - return ( - - ); - } - if (fieldName === TRANSACTION_FORM_INPUT_IDS.TAG) { - return ( - - ); - } - } - - return ( - - ); - }, [fieldName, fieldValue, formType, onSubmit, policyID, translate]); - - return ( - - { - Navigation.goBack(fieldValue ? appendParam(backPath, fieldName, fieldValue) : backPath, {compareParams: false}); - }} - /> - {renderPicker()} - - ); -} - -export default DynamicDebugDetailsConstantPickerPage; From f2c531f795d208286673110ae9264f6441c19692 Mon Sep 17 00:00:00 2001 From: huutech <20178761+huult@users.noreply.github.com> Date: Wed, 18 Mar 2026 21:29:02 +0700 Subject: [PATCH 5/6] remove code unused --- .../Navigation/helpers/getStateFromPath.ts | 48 +++---------------- tests/navigation/getStateFromPathTests.ts | 35 -------------- 2 files changed, 6 insertions(+), 77 deletions(-) diff --git a/src/libs/Navigation/helpers/getStateFromPath.ts b/src/libs/Navigation/helpers/getStateFromPath.ts index d58d60aad592..2c4d7a11dcce 100644 --- a/src/libs/Navigation/helpers/getStateFromPath.ts +++ b/src/libs/Navigation/helpers/getStateFromPath.ts @@ -12,34 +12,6 @@ import getStateForDynamicRoute from './dynamicRoutesUtils/getStateForDynamicRout import getMatchingNewRoute from './getMatchingNewRoute'; import getRedirectedPath from './getRedirectedPath'; -type RouteChainItem = { - name?: string; - params?: Record; -}; - -function getRouteChain(state?: PartialState | NavigationState): RouteChainItem[] { - const routeChain: RouteChainItem[] = []; - let currentState = state; - - while (currentState?.routes?.length) { - const routeIndex = currentState.index ?? currentState.routes.length - 1; - const currentRoute = currentState.routes.at(routeIndex); - - if (!currentRoute) { - break; - } - - routeChain.push({ - name: currentRoute.name, - params: currentRoute.params as Record | undefined, - }); - - currentState = currentRoute.state as PartialState | NavigationState | undefined; - } - - return routeChain; -} - /** * @param path - The path to parse * @returns - It's possible that there is no navigation action for the given path @@ -59,23 +31,15 @@ function getStateFromPath(path: Route): PartialState { // Find the dynamic route key that matches the extracted suffix const dynamicRoute: string = dynamicRouteKeys.find((key) => DYNAMIC_ROUTES[key].path === dynamicRouteSuffix) ?? ''; - const entryScreens: Screen[] = DYNAMIC_ROUTES[dynamicRoute as DynamicRouteKey]?.entryScreens ?? []; // Get the currently focused route from the base path to check permissions - const baseState = getStateFromPath(pathWithoutDynamicSuffix); - const focusedRoute = findFocusedRoute(baseState ?? {}); - const allowedParentRoute = getRouteChain(baseState) - .reverse() - .find((route) => route.name && entryScreens.includes(route.name as Screen)); + const focusedRoute = findFocusedRoute(getStateFromPath(pathWithoutDynamicSuffix) ?? {}); + const entryScreens: Screen[] = DYNAMIC_ROUTES[dynamicRoute as DynamicRouteKey]?.entryScreens ?? []; // Check if the focused route is allowed to access this dynamic route - if (focusedRoute?.name || allowedParentRoute?.name) { - if (entryScreens.includes(focusedRoute?.name as Screen) || !!allowedParentRoute?.name) { + if (focusedRoute?.name) { + if (entryScreens.includes(focusedRoute.name as Screen)) { // Generate navigation state for the dynamic route - const dynamicRouteState = getStateForDynamicRoute( - normalizedPath, - dynamicRoute as DynamicRouteKey, - (allowedParentRoute?.params ?? focusedRoute?.params) as Record | undefined, - ); + const dynamicRouteState = getStateForDynamicRoute(normalizedPath, dynamicRoute as DynamicRouteKey, focusedRoute?.params as Record | undefined); return dynamicRouteState; } @@ -87,7 +51,7 @@ function getStateFromPath(path: Route): PartialState { } // Log an error to quickly identify and add forgotten screens to the Dynamic Routes configuration - Log.warn(`[getStateFromPath.ts][DynamicRoute] Focused route ${focusedRoute?.name ?? 'unknown'} is not allowed to access dynamic route with suffix ${dynamicRouteSuffix}`); + Log.warn(`[getStateFromPath.ts][DynamicRoute] Focused route ${focusedRoute.name} is not allowed to access dynamic route with suffix ${dynamicRouteSuffix}`); } } diff --git a/tests/navigation/getStateFromPathTests.ts b/tests/navigation/getStateFromPathTests.ts index a495990cce0b..39548f069a16 100644 --- a/tests/navigation/getStateFromPathTests.ts +++ b/tests/navigation/getStateFromPathTests.ts @@ -112,41 +112,6 @@ describe('getStateFromPath', () => { expect(mockGetStateForDynamicRoute).toHaveBeenCalledWith(fullPath, 'SUFFIX_A', focusedRouteParams); }); - it('should generate dynamic state when focused screen is not authorized but an authorized parent is in route chain', () => { - const fullPath = '/settings/wallet/verify-account'; - const allowedParentParams = {workspaceID: '123'}; - const baseRouteState = { - routes: [ - { - name: 'Root', - state: { - routes: [ - { - name: 'Settings', - params: allowedParentParams, - state: { - routes: [{name: 'RightModal', params: {modal: 'test'}}], - }, - }, - ], - }, - }, - ], - }; - - mockRNGetStateFromPath.mockReturnValue(baseRouteState); - mockFindFocusedRoute.mockReturnValue({name: 'RightModal', params: {modal: 'test'}}); - - const expectedDynamicState = {routes: [{name: 'DynamicRoot'}]}; - mockGetStateForDynamicRoute.mockReturnValue(expectedDynamicState); - - const result = getStateFromPath(fullPath as unknown as Route); - - expect(result).toBe(expectedDynamicState); - expect(mockGetStateForDynamicRoute).toHaveBeenCalledWith(fullPath, 'VERIFY_ACCOUNT', allowedParentParams); - expect(mockLogWarn).not.toHaveBeenCalled(); - }); - it('should fallback to standard RN parsing if focused screen is NOT authorized for dynamic route', () => { const fullPath = '/unknown/suffix-b-unauth'; const standardState = {routes: [{name: 'FallbackRoute'}]}; From 385fa1725443638b557c46988dac5dc28ad51301 Mon Sep 17 00:00:00 2001 From: huutech <20178761+huult@users.noreply.github.com> Date: Sat, 28 Mar 2026 09:43:50 +0700 Subject: [PATCH 6/6] remove useCallback --- .../Debug/DynamicDebugDetailsConstantPickerPage.tsx | 11 ++++------- .../Debug/DynamicDebugDetailsDateTimePickerPage.tsx | 13 +++++-------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/pages/Debug/DynamicDebugDetailsConstantPickerPage.tsx b/src/pages/Debug/DynamicDebugDetailsConstantPickerPage.tsx index 1ee7aa37527a..9ceb714752b7 100644 --- a/src/pages/Debug/DynamicDebugDetailsConstantPickerPage.tsx +++ b/src/pages/Debug/DynamicDebugDetailsConstantPickerPage.tsx @@ -28,13 +28,10 @@ function DynamicDebugDetailsConstantPickerPage({ const {translate} = useLocalize(); const styles = useThemeStyles(); const backPath = useDynamicBackPath(DYNAMIC_ROUTES.DETAILS_CONSTANT_PICKER.path); - const onSubmit = useCallback( - (item: {text?: string; keyForList?: string}) => { - const value = item.text === fieldValue ? '' : (item.text ?? ''); - Navigation.goBack(appendParam(backPath, fieldName, value), {compareParams: false}); - }, - [backPath, fieldName, fieldValue], - ); + const onSubmit = (item: {text?: string; keyForList?: string}) => { + const value = item.text === fieldValue ? '' : (item.text ?? ''); + Navigation.goBack(appendParam(backPath, fieldName ?? '', value), {compareParams: false}); + }; const renderPicker = useCallback(() => { if (([TRANSACTION_FORM_INPUT_IDS.CURRENCY, TRANSACTION_FORM_INPUT_IDS.MODIFIED_CURRENCY, TRANSACTION_FORM_INPUT_IDS.ORIGINAL_CURRENCY] as string[]).includes(fieldName)) { diff --git a/src/pages/Debug/DynamicDebugDetailsDateTimePickerPage.tsx b/src/pages/Debug/DynamicDebugDetailsDateTimePickerPage.tsx index 7ded25114de6..b5d024a41b1b 100644 --- a/src/pages/Debug/DynamicDebugDetailsDateTimePickerPage.tsx +++ b/src/pages/Debug/DynamicDebugDetailsDateTimePickerPage.tsx @@ -1,5 +1,5 @@ import {format} from 'date-fns'; -import React, {useCallback, useState} from 'react'; +import React, {useState} from 'react'; import {View} from 'react-native'; import DatePicker from '@components/DatePicker'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -30,13 +30,10 @@ function DynamicDebugDetailsDateTimePickerPage({ const backPath = useDynamicBackPath(DYNAMIC_ROUTES.DETAILS_DATE_TIME_PICKER.path); const [date, setDate] = useState(() => DateUtils.extractDate(fieldValue)); - const handleSubmit = useCallback( - (time: string) => { - const formattedDateTime = format(new Date(`${date} ${time}`), 'yyyy-MM-dd HH:mm:ss.SSS'); - Navigation.goBack(appendParam(backPath, fieldName, formattedDateTime), {compareParams: false}); - }, - [date, fieldName, backPath], - ); + const handleSubmit = (time: string) => { + const formattedDateTime = format(new Date(`${date} ${time}`), 'yyyy-MM-dd HH:mm:ss.SSS'); + Navigation.goBack(appendParam(backPath, fieldName ?? '', formattedDateTime), {compareParams: false}); + }; return (