diff --git a/contributingGuides/NAVIGATION.md b/contributingGuides/NAVIGATION.md index fb76ad9dd99ad..9cee8c3b9f0a9 100644 --- a/contributingGuides/NAVIGATION.md +++ b/contributingGuides/NAVIGATION.md @@ -26,6 +26,8 @@ The navigation in the app is built on top of the `react-navigation` library. To - [Dynamic routes configuration](#dynamic-routes-configuration) - [Entry screens (access control)](#entry-screens-access-control) - [Current limitations (work in progress)](#current-limitations-work-in-progress) + - [Multi-segment dynamic routes](#multi-segment-dynamic-routes) + - [Dynamic routes with query parameters](#dynamic-routes-with-query-parameters) - [How to add a new dynamic route](#how-to-add-a-new-dynamic-route) - [Migrating from backTo to dynamic routes](#migrating-from-backto-to-dynamic-routes) - [How to remove backTo from URL (Legacy)](#how-to-remove-backto-from-url) @@ -700,7 +702,6 @@ A dynamic route is a URL suffix (e.g. `verify-account`) that can be appended to Do not use dynamic routes when: - Your use case falls under the [current limitations](#current-limitations-work-in-progress): - You need to stack multiple dynamic route suffixes (e.g. `/a/verify-account/another-flow`). - - Your suffix includes path or query parameters (e.g. `verify-account/:id` or `verify-account?tab=details`). - The screen has a single, fixed entry and a fixed back destination. In this case, use a normal static route instead. ### Dynamic routes configuration @@ -732,21 +733,119 @@ When adding or extending a dynamic route, list every screen that should be able ### Current limitations (work in progress) - **Stacking:** Multiple dynamic route suffixes on top of each other (e.g. `/a/verify-account/another-flow`) are not supported. Only one dynamic suffix per path is allowed. -- **Suffix shape:** Suffixes must be a single path segment. Compound suffixes with extra path segments (e.g. `a/b`) are not supported. -- **Parameters:** Suffixes must not include path params (e.g. `a/:reportID`) or query params (e.g. `a?foo=bar`). Use a single literal segment like `verify-account` only. +- **Path parameters:** Suffixes must not include path params (e.g. `a/:reportID`). Query parameters are supported - see [Dynamic routes with query parameters](#dynamic-routes-with-query-parameters). If you try to use dynamic routes for these cases now, you will either fail to navigate to the page at all or end up on a non-existent page, and the navigation will be broken. -### How to add a new dynamic route +### Multi-segment dynamic routes + +Dynamic route suffixes are not limited to a single path segment - +they can span multiple segments separated by `/`. +For example, the suffix `add-bank-account/verify-account` is a valid +multi-segment suffix that combines two segments into one dynamic route. + +When the URL is parsed, the matching algorithm +iterates from the longest candidate suffix to the shortest, +so overlapping registrations are resolved deterministically. +For instance, if both `verify-account` and `add-bank-account/verify-account` +are registered, a path ending with `/add-bank-account/verify-account` +will always match the longer, more specific suffix. + +### Dynamic routes with query parameters + +Dynamic route suffixes can carry query parameters +(e.g. `country?country=US`). +This is useful when a dynamic screen needs initial data passed +through the URL - for example, a country selector that +pre-selects the current country. + +#### How to add query parameters to a dynamic route + +1. In `DYNAMIC_ROUTES` (in [`src/ROUTES.ts`](../src/ROUTES.ts)), add a `getRoute` function +that returns the suffix with query parameters and a `queryParams` +array listing every parameter key that the suffix may add: + +```ts +ADDRESS_COUNTRY: { + path: 'country', + entryScreens: [SCREENS.SETTINGS.PROFILE.ADDRESS, /* ... */], + getRoute: (country = '') => `country${country ? `?country=${country}` : ''}`, + queryParams: ['country'], +}, +``` -1. Add to `DYNAMIC_ROUTES` in `src/ROUTES.ts`: define `path` and `entryScreens` (screen names that may open this route). -2. Add a screen constant in `src/SCREENS.ts`. The name must start with the `DYNAMIC_` prefix (e.g. `SETTINGS.DYNAMIC_VERIFY_ACCOUNT`) so dynamic screens can be distinguished from static ones. -3. Register in linking config in `src/libs/Navigation/linkingConfig/config.ts`: map the new screen to `DYNAMIC_ROUTES..path`. -4. Implement the page component: Use `createDynamicRoute(DYNAMIC_ROUTES..path)` to navigate to the flow and `useDynamicBackPath(DYNAMIC_ROUTES..path)` to get the back path. Pass these into your base UI (e.g. `VerifyAccountPageBase` with `navigateBackTo` / `navigateForwardTo`). -5. Register the screen in the appropriate modal/stack navigator (e.g. `src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx`). -6. Types: Add the screen to the navigator param list in `src/libs/Navigation/types.ts` (no params for the new screen). +2. Navigate using `createDynamicRoute` with the output of `getRoute`: + +```ts +Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.ADDRESS_COUNTRY.getRoute(countryCode))); +// Produces e.g. /settings/profile/address/country?country=US +``` + +3. In the page component, read query params from `route.params` as usual: + +```ts +const currentCountry = route.params?.country ?? ''; +``` + +> [!CAUTION] +> **`queryParams` array is mandatory.** +> When you define a `getRoute` function that adds query parameters +> after `?` in a dynamic route, you **must** also define a `queryParams` +> array containing **every** parameter key that `getRoute` may produce. +> The `queryParams` array is used to strip suffix-specific parameters when navigating back. +> Without it, query parameters will leak into the parent path +> and break back navigation. + +> [!CAUTION] +> **Query parameter names must not collide with inherited entry screen parameters.** +> Do not use a query parameter name in a dynamic suffix that +> already exists as a parameter of any entry screen listed +> in `entryScreens`. +> For example, if an entry screen has a `country` query parameter, +> do not add `country` as a query parameter in your dynamic route. +> When both paths carry the same parameter key, +> `createDynamicRoute` will throw an error: +> `Query param "X" exists in both base path and dynamic suffix.` +> `This is not allowed.` +> This guard prevents non-deterministic parameter handling +> and accidental overwriting of inherited parameters. + +The Address Country flow is the reference implementation for +dynamic routes with query parameters: +see [`src/components/CountrySelector.tsx`](../src/components/CountrySelector.tsx) (navigation call) +and [`src/pages/settings/Profile/PersonalDetails/DynamicCountrySelectionPage.tsx`](../src/pages/settings/Profile/PersonalDetails/DynamicCountrySelectionPage.tsx) +(page component). + +### How to add a new dynamic route -The Verify Account flow (Wallet → Dynamic Verify Account) is the reference implementation: see `src/pages/settings/DynamicVerifyAccountPage.tsx` and the Wallet entry point in `src/pages/settings/Wallet/WalletPage/index.tsx`. +1. Add to `DYNAMIC_ROUTES` in [`src/ROUTES.ts`](../src/ROUTES.ts): define `path` and +`entryScreens` (screen names that may open this route). +If the suffix needs query parameters, also define `getRoute` +and `queryParams` - see +[Dynamic routes with query parameters](#dynamic-routes-with-query-parameters). +2. Add a screen constant in [`src/SCREENS.ts`](../src/SCREENS.ts). +The name must start with the `DYNAMIC_` prefix +(e.g. `SETTINGS.DYNAMIC_VERIFY_ACCOUNT`) so dynamic screens +can be distinguished from static ones. +3. Register in linking config in +[`src/libs/Navigation/linkingConfig/config.ts`](../src/libs/Navigation/linkingConfig/config.ts): +map the new screen to `DYNAMIC_ROUTES..path`. +4. Implement the page component: +Use `createDynamicRoute(DYNAMIC_ROUTES..path)` to navigate +to the flow and `useDynamicBackPath(DYNAMIC_ROUTES..path)` +to get the back path. Pass these into your base UI +(e.g. `VerifyAccountPageBase` with `navigateBackTo` / +`navigateForwardTo`). +5. Register the screen in the appropriate modal/stack navigator +(e.g. [`src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx`](../src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx)). +6. Types: Add the screen to the navigator param list in +[`src/libs/Navigation/types.ts`](../src/libs/Navigation/types.ts) (no params for the new screen). + +The Verify Account flow (Wallet → Dynamic Verify Account) is the +reference implementation: +see [`src/pages/settings/DynamicVerifyAccountPage.tsx`](../src/pages/settings/DynamicVerifyAccountPage.tsx) and the +Wallet entry point in +[`src/pages/settings/Wallet/WalletPage/index.tsx`](../src/pages/settings/Wallet/WalletPage/index.tsx). ### Migrating from backTo to dynamic routes diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 0d6ac95e1b7d4..49466bce956eb 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -56,6 +56,8 @@ const VERIFY_ACCOUNT = 'verify-account'; type DynamicRouteConfig = { path: string; entryScreens: Screen[]; + getRoute?: (...args: never[]) => string; + queryParams?: readonly string[]; }; type DynamicRoutes = Record; @@ -97,6 +99,19 @@ const DYNAMIC_ROUTES = { path: 'owner-selector', entryScreens: [], }, + ADDRESS_COUNTRY: { + path: 'country', + entryScreens: [ + SCREENS.SETTINGS.PROFILE.ADDRESS, + SCREENS.WORKSPACE.ADDRESS, + SCREENS.SETTINGS.WALLET.CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS, + SCREENS.DOMAIN_CARD.DOMAIN_CARD_UPDATE_ADDRESS, + SCREENS.TRAVEL.WORKSPACE_ADDRESS, + SCREENS.SETTINGS.ADD_US_BANK_ACCOUNT, + ], + getRoute: (country = '') => `country?country=${country}`, + queryParams: ['country'], + }, } as const satisfies DynamicRoutes; const ROUTES = { @@ -523,12 +538,6 @@ const ROUTES = { SETTINGS_DATE_OF_BIRTH: 'settings/profile/date-of-birth', SETTINGS_PHONE_NUMBER: 'settings/profile/phone', SETTINGS_ADDRESS: 'settings/profile/address', - SETTINGS_ADDRESS_COUNTRY: { - route: 'settings/profile/address/country', - - // eslint-disable-next-line no-restricted-syntax -- Legacy route generation - getRoute: (country: string, backTo?: string) => getUrlWithBackToParam(`settings/profile/address/country?country=${country}`, backTo), - }, SETTINGS_ADDRESS_STATE: { route: 'settings/profile/address/state', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 2ce38561b3013..7561556ed2957 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -142,7 +142,7 @@ const SCREENS = { PHONE_NUMBER: 'Settings_PhoneNumber', ADDRESS: 'Settings_Address', AVATAR: 'Settings_Avatar', - ADDRESS_COUNTRY: 'Settings_Address_Country', + DYNAMIC_ADDRESS_COUNTRY: 'Dynamic_Address_Country', ADDRESS_STATE: 'Settings_Address_State', }, diff --git a/src/components/CountrySelector.tsx b/src/components/CountrySelector.tsx index 9c64311a1aeac..20e495670d138 100644 --- a/src/components/CountrySelector.tsx +++ b/src/components/CountrySelector.tsx @@ -5,10 +5,11 @@ import type {View} from 'react-native'; import useGeographicalStateAndCountryFromRoute from '@hooks/useGeographicalStateAndCountryFromRoute'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import type {Country} from '@src/CONST'; -import ROUTES from '@src/ROUTES'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; type CountrySelectorProps = { @@ -78,9 +79,8 @@ function CountrySelector({errorText = '', value: countryCode, onInputChange = () description={translate('common.country')} errorText={errorText} onPress={() => { - const activeRoute = Navigation.getActiveRoute(); didOpenCountrySelector.current = true; - Navigation.navigate(ROUTES.SETTINGS_ADDRESS_COUNTRY.getRoute(countryCode ?? '', activeRoute)); + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.ADDRESS_COUNTRY.getRoute(countryCode ?? ''))); }} /> ); diff --git a/src/components/WorkspaceConfirmationForm.tsx b/src/components/WorkspaceConfirmationForm.tsx index b2158325084e9..10d5194797b11 100644 --- a/src/components/WorkspaceConfirmationForm.tsx +++ b/src/components/WorkspaceConfirmationForm.tsx @@ -14,7 +14,7 @@ import {generateDefaultWorkspaceName, generatePolicyID} from '@libs/actions/Poli import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; import {addErrorMessage} from '@libs/ErrorUtils'; import getFirstAlphaNumericCharacter from '@libs/getFirstAlphaNumericCharacter'; -import createDynamicRoute from '@libs/Navigation/helpers/createDynamicRoute'; +import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils'; import {isRequiredFulfilled} from '@libs/ValidationUtils'; diff --git a/src/hooks/useDynamicBackPath.ts b/src/hooks/useDynamicBackPath.ts index 1bebe7f6e814c..b2a33637cf7e3 100644 --- a/src/hooks/useDynamicBackPath.ts +++ b/src/hooks/useDynamicBackPath.ts @@ -1,6 +1,6 @@ +import getPathWithoutDynamicSuffix from '@libs/Navigation/helpers/dynamicRoutesUtils/getPathWithoutDynamicSuffix'; +import splitPathAndQuery from '@libs/Navigation/helpers/dynamicRoutesUtils/splitPathAndQuery'; import getPathFromState from '@libs/Navigation/helpers/getPathFromState'; -import getPathWithoutDynamicSuffix from '@libs/Navigation/helpers/getPathWithoutDynamicSuffix'; -import splitPathAndQuery from '@libs/Navigation/helpers/splitPathAndQuery'; import type {State} from '@libs/Navigation/types'; import type {DynamicRouteSuffix, Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 2f4b49da7822a..61b6fad5ad353 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -389,7 +389,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Profile/PersonalDetails/DateOfBirthPage').default, [SCREENS.SETTINGS.PROFILE.PHONE_NUMBER]: () => require('../../../../pages/settings/Profile/PersonalDetails/PhoneNumberPage').default, [SCREENS.SETTINGS.PROFILE.ADDRESS]: () => require('../../../../pages/settings/Profile/PersonalDetails/PersonalAddressPage').default, - [SCREENS.SETTINGS.PROFILE.ADDRESS_COUNTRY]: () => require('../../../../pages/settings/Profile/PersonalDetails/CountrySelectionPage').default, + [SCREENS.SETTINGS.PROFILE.DYNAMIC_ADDRESS_COUNTRY]: () => require('../../../../pages/settings/Profile/PersonalDetails/DynamicCountrySelectionPage').default, [SCREENS.SETTINGS.PROFILE.ADDRESS_STATE]: () => require('../../../../pages/settings/Profile/PersonalDetails/StateSelectionPage').default, [SCREENS.SETTINGS.PROFILE.AVATAR]: () => require('../../../../pages/settings/Profile/Avatar/AvatarPage').default, [SCREENS.SETTINGS.PROFILE.CONTACT_METHODS]: () => require('../../../../pages/settings/Profile/Contacts/ContactMethodsPage').default, diff --git a/src/libs/Navigation/helpers/createDynamicRoute.ts b/src/libs/Navigation/helpers/createDynamicRoute.ts deleted file mode 100644 index 51b62ac3bb71e..0000000000000 --- a/src/libs/Navigation/helpers/createDynamicRoute.ts +++ /dev/null @@ -1,35 +0,0 @@ -import Log from '@libs/Log'; -import Navigation from '@libs/Navigation/Navigation'; -import type {DynamicRouteSuffix, Route} from '@src/ROUTES'; -import isDynamicRouteSuffix from './isDynamicRouteSuffix'; -import splitPathAndQuery from './splitPathAndQuery'; - -const combinePathAndSuffix = (path: string, suffix: string): Route => { - const [normalizedPath, query] = splitPathAndQuery(path); - - // This should never happen as the path should always be defined - if (!normalizedPath) { - Log.warn('[createDynamicRoute.ts] Path is undefined or empty, returning suffix only', {path, suffix}); - return suffix as Route; - } - - let newPath = normalizedPath === '/' ? `/${suffix}` : `${normalizedPath}/${suffix}`; - - if (query) { - newPath += `?${query}`; - } - return newPath as Route; -}; - -/** Adds dynamic route name to the current URL and returns it */ -const createDynamicRoute = (dynamicRouteSuffix: DynamicRouteSuffix): Route => { - if (!isDynamicRouteSuffix(dynamicRouteSuffix)) { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - throw new Error(`The route name ${dynamicRouteSuffix} is not supported in createDynamicRoute`); - } - - const activeRoute = Navigation.getActiveRoute(); - return combinePathAndSuffix(activeRoute, dynamicRouteSuffix); -}; - -export default createDynamicRoute; diff --git a/src/libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute.ts b/src/libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute.ts new file mode 100644 index 0000000000000..5ab1511a9957e --- /dev/null +++ b/src/libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute.ts @@ -0,0 +1,69 @@ +import Log from '@libs/Log'; +import Navigation from '@libs/Navigation/Navigation'; +import type {Route} from '@src/ROUTES'; +import isDynamicRouteSuffix from './isDynamicRouteSuffix'; +import splitPathAndQuery from './splitPathAndQuery'; + +/** + * Merges two query strings into one. If both contain the same key, + * the error is thrown. + * @param baseQuery - The query string of the base path + * @param suffixQuery - The query string of the suffix + * @returns The merged query string or an empty string if both are empty + * + * @private - Internal helper. Do not export or use outside this file. + * + * @example + * mergeQueryStrings('foo=bar', 'foo=baz') => '?foo=bar&baz=qux' + * mergeQueryStrings('foo=bar', 'foo=baz') => throws an error + */ +const mergeQueryStrings = (baseQuery = '', suffixQuery = ''): string => { + if (!baseQuery && !suffixQuery) { + return ''; + } + const params = new URLSearchParams(baseQuery); + const suffixParams = new URLSearchParams(suffixQuery); + const suffixParamsEntries = suffixParams.entries(); + for (const [key, value] of suffixParamsEntries) { + if (params.has(key)) { + throw new Error(`[createDynamicRoute] Query param "${key}" exists in both base path and dynamic suffix. This is not allowed.`); + } + params.set(key, value); + } + const result = params.toString(); + return result ? `?${result}` : ''; +}; + +/** + * Combines a base path with a dynamic route suffix, merging their query parameters. + * + * @private - Internal helper. Do not export or use outside this file. + */ +const combinePathAndSuffix = (basePath: string, suffixWithQuery: string): Route => { + const [normalizedBasePath, baseQuery] = splitPathAndQuery(basePath); + const [suffixPath, suffixQuery] = splitPathAndQuery(suffixWithQuery); + + if (!normalizedBasePath) { + Log.warn('[createDynamicRoute.ts] Path is undefined or empty, returning suffix only', {basePath, suffixWithQuery}); + return suffixWithQuery as Route; + } + + const combinedPath = normalizedBasePath === '/' ? `/${suffixPath}` : `${normalizedBasePath}/${suffixPath}`; + const mergedQuery = mergeQueryStrings(baseQuery, suffixQuery); + + return `${combinedPath}${mergedQuery}` as Route; +}; + +/** Adds dynamic route name (with optional query params) to the current URL and returns it */ +const createDynamicRoute = (dynamicRouteSuffixWithParams: string): Route => { + const [suffixPath] = splitPathAndQuery(dynamicRouteSuffixWithParams); + + if (!suffixPath || !isDynamicRouteSuffix(suffixPath)) { + throw new Error(`The route name ${suffixPath} is not supported in createDynamicRoute`); + } + + const activeRoute = Navigation.getActiveRoute(); + return combinePathAndSuffix(activeRoute, dynamicRouteSuffixWithParams); +}; + +export default createDynamicRoute; diff --git a/src/libs/Navigation/helpers/findMatchingDynamicSuffix.ts b/src/libs/Navigation/helpers/dynamicRoutesUtils/findMatchingDynamicSuffix.ts similarity index 100% rename from src/libs/Navigation/helpers/findMatchingDynamicSuffix.ts rename to src/libs/Navigation/helpers/dynamicRoutesUtils/findMatchingDynamicSuffix.ts diff --git a/src/libs/Navigation/helpers/dynamicRoutesUtils/getPathWithoutDynamicSuffix.ts b/src/libs/Navigation/helpers/dynamicRoutesUtils/getPathWithoutDynamicSuffix.ts new file mode 100644 index 0000000000000..2382b5cec186d --- /dev/null +++ b/src/libs/Navigation/helpers/dynamicRoutesUtils/getPathWithoutDynamicSuffix.ts @@ -0,0 +1,58 @@ +import type {Route} from '@src/ROUTES'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; +import splitPathAndQuery from './splitPathAndQuery'; + +/** + * Returns the query parameter names that belong to a dynamic route suffix and should be + * stripped when removing that suffix from the path. Looks up the matching DYNAMIC_ROUTES + * config and returns its `queryParams` array if defined. + * + * @param dynamicSuffix - The dynamic route path segment (e.g., 'country') + * @returns The list of query param keys to strip, or undefined if no match or no queryParams config + * + * @private - Internal helper. Do not export or use outside this file. + */ +function getQueryParamsToStrip(dynamicSuffix: string): readonly string[] | undefined { + const keys = Object.keys(DYNAMIC_ROUTES) as Array; + const match = keys.find((key) => DYNAMIC_ROUTES[key].path === dynamicSuffix); + if (!match) { + return undefined; + } + const config = DYNAMIC_ROUTES[match]; + if (!('queryParams' in config)) { + return undefined; + } + return config.queryParams; +} + +/** + * Returns the path without a dynamic route suffix, stripping suffix-specific query parameters + * (derived from the matching DYNAMIC_ROUTES.getRoute output) and preserving any remaining ones. + * + * @param fullPath - The full URL path possibly containing query params (e.g., '/settings/profile/address/country?country=US') + * @param dynamicSuffix - The dynamic suffix to strip (e.g., 'country') + * @returns The path without the suffix and with only base-path query params preserved + */ +function getPathWithoutDynamicSuffix(fullPath: string, dynamicSuffix: string): Route { + const [pathWithoutQuery, query] = splitPathAndQuery(fullPath); + const pathWithoutDynamicSuffix = pathWithoutQuery?.slice(0, -(dynamicSuffix.length + 1)) ?? ''; + + if (!pathWithoutDynamicSuffix || pathWithoutDynamicSuffix === '/') { + return ''; + } + + const paramsToStrip = getQueryParamsToStrip(dynamicSuffix); + let filteredQuery = query; + if (paramsToStrip?.length && query) { + const params = new URLSearchParams(query); + for (const key of paramsToStrip) { + params.delete(key); + } + const result = params.toString(); + filteredQuery = result || undefined; + } + + return `${pathWithoutDynamicSuffix}${filteredQuery ? `?${filteredQuery}` : ''}` as Route; +} + +export default getPathWithoutDynamicSuffix; diff --git a/src/libs/Navigation/helpers/getStateForDynamicRoute.ts b/src/libs/Navigation/helpers/dynamicRoutesUtils/getStateForDynamicRoute.ts similarity index 70% rename from src/libs/Navigation/helpers/getStateForDynamicRoute.ts rename to src/libs/Navigation/helpers/dynamicRoutesUtils/getStateForDynamicRoute.ts index fc9fb1a378b1f..ce35dc567c20a 100644 --- a/src/libs/Navigation/helpers/getStateForDynamicRoute.ts +++ b/src/libs/Navigation/helpers/dynamicRoutesUtils/getStateForDynamicRoute.ts @@ -1,10 +1,12 @@ import {normalizedConfigs} from '@libs/Navigation/linkingConfig/config'; import {DYNAMIC_ROUTES} from '@src/ROUTES'; import type {DynamicRouteSuffix} from '@src/ROUTES'; +import splitPathAndQuery from './splitPathAndQuery'; type LeafRoute = { name: string; path: string; + params?: Record; }; type NestedRoute = { @@ -19,9 +21,30 @@ type RouteNode = LeafRoute | NestedRoute; const configEntries = Object.entries(normalizedConfigs); +/** + * Parses a query string into a key-value record. + * + * @private - Internal helper. Do not export or use outside this file. + */ +function getParamsFromQuery(query: string | undefined): Record | undefined { + if (!query) { + return undefined; + } + + const entries = Array.from(new URLSearchParams(query).entries()); + if (entries.length === 0) { + return undefined; + } + + return Object.fromEntries(entries); +} + +/** + * Looks up the navigation screen hierarchy (routeNames) for a given dynamic route suffix. + * + * @private - Internal helper. Do not export or use outside this file. + */ function getRouteNamesForDynamicRoute(dynamicRouteName: DynamicRouteSuffix): string[] | null { - // Search through normalized configs to find matching path and extract navigation hierarchy - // routeNames contains the sequence of screen/navigator names that should be present in the navigation state for (const [, config] of configEntries) { if (config.path === dynamicRouteName) { return config.routeNames; @@ -33,6 +56,8 @@ function getRouteNamesForDynamicRoute(dynamicRouteName: DynamicRouteSuffix): str function getStateForDynamicRoute(path: string, dynamicRouteName: keyof typeof DYNAMIC_ROUTES) { const routeConfig = getRouteNamesForDynamicRoute(DYNAMIC_ROUTES[dynamicRouteName].path); + const [, query] = splitPathAndQuery(path); + const params = getParamsFromQuery(query); if (!routeConfig) { throw new Error(`No route configuration found for dynamic route '${dynamicRouteName}'`); @@ -47,6 +72,7 @@ function getStateForDynamicRoute(path: string, dynamicRouteName: keyof typeof DY return { name: currentRoute ?? '', path, + params, }; } diff --git a/src/libs/Navigation/helpers/isDynamicRouteSuffix.ts b/src/libs/Navigation/helpers/dynamicRoutesUtils/isDynamicRouteSuffix.ts similarity index 100% rename from src/libs/Navigation/helpers/isDynamicRouteSuffix.ts rename to src/libs/Navigation/helpers/dynamicRoutesUtils/isDynamicRouteSuffix.ts diff --git a/src/libs/Navigation/helpers/splitPathAndQuery.ts b/src/libs/Navigation/helpers/dynamicRoutesUtils/splitPathAndQuery.ts similarity index 100% rename from src/libs/Navigation/helpers/splitPathAndQuery.ts rename to src/libs/Navigation/helpers/dynamicRoutesUtils/splitPathAndQuery.ts diff --git a/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts b/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts index 33f7996fb8f46..930862e77ce54 100644 --- a/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts +++ b/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts @@ -11,10 +11,10 @@ import NAVIGATORS from '@src/NAVIGATORS'; import type {Route as RoutePath} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; -import findMatchingDynamicSuffix from './findMatchingDynamicSuffix'; +import findMatchingDynamicSuffix from './dynamicRoutesUtils/findMatchingDynamicSuffix'; +import getPathWithoutDynamicSuffix from './dynamicRoutesUtils/getPathWithoutDynamicSuffix'; import getMatchingNewRoute from './getMatchingNewRoute'; import getParamsFromRoute from './getParamsFromRoute'; -import getPathWithoutDynamicSuffix from './getPathWithoutDynamicSuffix'; import getRedirectedPath from './getRedirectedPath'; import getStateFromPath from './getStateFromPath'; import {isFullScreenName} from './isNavigatorName'; diff --git a/src/libs/Navigation/helpers/getPathFromState.ts b/src/libs/Navigation/helpers/getPathFromState.ts index fa48471b6f8d3..0362ce72a1489 100644 --- a/src/libs/Navigation/helpers/getPathFromState.ts +++ b/src/libs/Navigation/helpers/getPathFromState.ts @@ -4,7 +4,7 @@ import {linkingConfig} from '@libs/Navigation/linkingConfig'; import {normalizedConfigs} from '@libs/Navigation/linkingConfig/config'; import type {DynamicRouteSuffix} from '@src/ROUTES'; import type {Screen} from '@src/SCREENS'; -import {dynamicRoutePaths} from './isDynamicRouteSuffix'; +import {dynamicRoutePaths} from './dynamicRoutesUtils/isDynamicRouteSuffix'; type State = NavigationState | Omit, 'stale'>; diff --git a/src/libs/Navigation/helpers/getPathWithoutDynamicSuffix.ts b/src/libs/Navigation/helpers/getPathWithoutDynamicSuffix.ts deleted file mode 100644 index da9c2bbceab11..0000000000000 --- a/src/libs/Navigation/helpers/getPathWithoutDynamicSuffix.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type {Route} from '@src/ROUTES'; -import splitPathAndQuery from './splitPathAndQuery'; - -/** - * Returns the path without a dynamic route suffix while preserving query parameters. - * - * @param fullPath - The full URL path possibly containing query params (e.g., '/settings/wallet/verify-account?param=value') - * @param dynamicSuffix - The dynamic suffix to strip (e.g., 'verify-account') - * @returns The path without the suffix and with query params re-appended - */ -function getPathWithoutDynamicSuffix(fullPath: string, dynamicSuffix: string): Route { - const [pathWithoutQuery, query] = splitPathAndQuery(fullPath); - const pathWithoutDynamicSuffix = pathWithoutQuery?.slice(0, -(dynamicSuffix.length + 1)) ?? ''; - - if (!pathWithoutDynamicSuffix || pathWithoutDynamicSuffix === '/') { - return ''; - } - - return `${pathWithoutDynamicSuffix}${query ? `?${query}` : ''}` as Route; -} - -export default getPathWithoutDynamicSuffix; diff --git a/src/libs/Navigation/helpers/getStateFromPath.ts b/src/libs/Navigation/helpers/getStateFromPath.ts index ca01d5a6b5fc0..92119fdbf2281 100644 --- a/src/libs/Navigation/helpers/getStateFromPath.ts +++ b/src/libs/Navigation/helpers/getStateFromPath.ts @@ -6,11 +6,11 @@ import type {Route} from '@src/ROUTES'; import {DYNAMIC_ROUTES} from '@src/ROUTES'; import type {Screen} from '@src/SCREENS'; import SCREENS from '@src/SCREENS'; -import findMatchingDynamicSuffix from './findMatchingDynamicSuffix'; +import findMatchingDynamicSuffix from './dynamicRoutesUtils/findMatchingDynamicSuffix'; +import getPathWithoutDynamicSuffix from './dynamicRoutesUtils/getPathWithoutDynamicSuffix'; +import getStateForDynamicRoute from './dynamicRoutesUtils/getStateForDynamicRoute'; import getMatchingNewRoute from './getMatchingNewRoute'; -import getPathWithoutDynamicSuffix from './getPathWithoutDynamicSuffix'; import getRedirectedPath from './getRedirectedPath'; -import getStateForDynamicRoute from './getStateForDynamicRoute'; /** * @param path - The path to parse diff --git a/src/libs/Navigation/helpers/linkTo/index.ts b/src/libs/Navigation/helpers/linkTo/index.ts index 63126c40e1c72..88fedac311ee9 100644 --- a/src/libs/Navigation/helpers/linkTo/index.ts +++ b/src/libs/Navigation/helpers/linkTo/index.ts @@ -1,6 +1,7 @@ import {getActionFromState} from '@react-navigation/core'; import type {NavigationContainerRef, NavigationState, PartialState} from '@react-navigation/native'; import {findFocusedRoute, StackActions} from '@react-navigation/native'; +import findMatchingDynamicSuffix from '@libs/Navigation/helpers/dynamicRoutesUtils/findMatchingDynamicSuffix'; import {getMatchingFullScreenRoute, isFullScreenName} from '@libs/Navigation/helpers/getAdaptedStateFromPath'; import getStateFromPath from '@libs/Navigation/helpers/getStateFromPath'; import normalizePath from '@libs/Navigation/helpers/normalizePath'; @@ -151,10 +152,12 @@ export default function linkTo(navigation: NavigationContainerRef['config'] = { path: ROUTES.SETTINGS_ADDRESS, exact: true, }, - [SCREENS.SETTINGS.PROFILE.ADDRESS_COUNTRY]: { - path: ROUTES.SETTINGS_ADDRESS_COUNTRY.route, - exact: true, - }, + [SCREENS.SETTINGS.PROFILE.DYNAMIC_ADDRESS_COUNTRY]: DYNAMIC_ROUTES.ADDRESS_COUNTRY.path, [SCREENS.SETTINGS.PROFILE.ADDRESS_STATE]: { path: ROUTES.SETTINGS_ADDRESS_STATE.route, exact: true, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index ccea4a0183ad0..09a44d85090bf 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -98,10 +98,8 @@ type SettingsNavigatorParamList = { [SCREENS.SETTINGS.PROFILE.ADDRESS]: { country?: Country | ''; }; - [SCREENS.SETTINGS.PROFILE.ADDRESS_COUNTRY]: { - // 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; - country: string; + [SCREENS.SETTINGS.PROFILE.DYNAMIC_ADDRESS_COUNTRY]: { + country?: string; }; [SCREENS.SETTINGS.PROFILE.CONTACT_METHODS]: { // 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 diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index df9a305b9cabc..6ac536e761e03 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -24,7 +24,7 @@ import type {SaveCorpayOnboardingCompanyDetails} from '@libs/API/parameters/Save import type SaveCorpayOnboardingDirectorInformationParams from '@libs/API/parameters/SaveCorpayOnboardingDirectorInformationParams'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; -import createDynamicRoute from '@libs/Navigation/helpers/createDynamicRoute'; +import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import type {MemberForList} from '@libs/OptionsListUtils'; import {getFormattedStreet} from '@libs/PersonalDetailsUtils'; diff --git a/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.tsx b/src/pages/settings/Profile/PersonalDetails/DynamicCountrySelectionPage.tsx similarity index 72% rename from src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.tsx rename to src/pages/settings/Profile/PersonalDetails/DynamicCountrySelectionPage.tsx index 34c9197f158bf..e83eecb21dd90 100644 --- a/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.tsx +++ b/src/pages/settings/Profile/PersonalDetails/DynamicCountrySelectionPage.tsx @@ -3,6 +3,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/ListItem/RadioListItem'; +import useDynamicBackPath from '@hooks/useDynamicBackPath'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -13,15 +14,16 @@ import StringUtils from '@libs/StringUtils'; import {appendParam} from '@libs/Url'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; -import type {Route} from '@src/ROUTES'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -type CountrySelectionPageProps = PlatformStackScreenProps; +type DynamicCountrySelectionPageProps = PlatformStackScreenProps; -function CountrySelectionPage({route}: CountrySelectionPageProps) { +function DynamicCountrySelectionPage({route}: DynamicCountrySelectionPageProps) { const [searchValue, setSearchValue] = useState(''); const {translate} = useLocalize(); const currentCountry = route.params.country; + const backPath = useDynamicBackPath(DYNAMIC_ROUTES.ADDRESS_COUNTRY.path); const countries = useMemo( () => @@ -42,17 +44,9 @@ function CountrySelectionPage({route}: CountrySelectionPageProps) { const selectCountry = useCallback( (option: Option) => { - const backTo = route.params.backTo ?? ''; - - // Check the "backTo" parameter to decide navigation behavior - if (!backTo) { - Navigation.goBack(); - } else { - // Set compareParams to false because we want to go back to this particular screen and update params (country). - Navigation.goBack(appendParam(backTo, 'country', option.value), {compareParams: false}); - } + Navigation.goBack(appendParam(backPath, 'country', option.value), {compareParams: false}); }, - [route.params.backTo], + [backPath], ); const textInputOptions = useMemo( @@ -67,16 +61,14 @@ function CountrySelectionPage({route}: CountrySelectionPageProps) { return ( { - const backTo = route.params.backTo ?? ''; - const backToRoute = backTo ? `${backTo}?country=${currentCountry}` : ''; - Navigation.goBack(backToRoute as Route, {compareParams: false}); + Navigation.goBack(currentCountry ? appendParam(backPath, 'country', currentCountry) : backPath, {compareParams: false}); }} /> @@ -93,4 +85,4 @@ function CountrySelectionPage({route}: CountrySelectionPageProps) { ); } -export default CountrySelectionPage; +export default DynamicCountrySelectionPage; diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index 0b458f0778f11..1cee08078c3df 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -34,7 +34,7 @@ import { lastFourNumbersFromCardName, maskCardNumber, } from '@libs/CardUtils'; -import createDynamicRoute from '@libs/Navigation/helpers/createDynamicRoute'; +import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import {formatPaymentMethods} from '@libs/PaymentUtils'; import {getDescriptionForPolicyDomainCard} from '@libs/PolicyUtils'; diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index 3195f3bb4a733..0048af9b6fbfb 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -36,7 +36,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {hasDisplayableAssignedCards, isDirectFeed, maskCardNumber} from '@libs/CardUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; -import createDynamicRoute from '@libs/Navigation/helpers/createDynamicRoute'; +import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import {formatPaymentMethods, getPaymentMethodDescription} from '@libs/PaymentUtils'; import {getActiveAdminWorkspaces, getDescriptionForPolicyDomainCard, hasActiveAdminWorkspaces, hasEligibleActiveAdminFromWorkspaces, isPaidGroupPolicy} from '@libs/PolicyUtils'; diff --git a/tests/navigation/createDynamicRouteTests.ts b/tests/navigation/createDynamicRouteTests.ts index a8bc48ca63fe8..9acbfff659729 100644 --- a/tests/navigation/createDynamicRouteTests.ts +++ b/tests/navigation/createDynamicRouteTests.ts @@ -1,4 +1,4 @@ -import createDynamicRoute from '@libs/Navigation/helpers/createDynamicRoute'; +import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import type {DynamicRouteSuffix} from '@src/ROUTES'; @@ -6,6 +6,10 @@ jest.mock('@libs/Navigation/Navigation', () => ({ getActiveRoute: jest.fn(), })); +jest.mock('@libs/Log', () => ({ + warn: jest.fn(), +})); + jest.mock('@src/ROUTES', () => ({ DYNAMIC_ROUTES: { VERIFY_ACCOUNT: {path: 'verify-account'}, @@ -13,6 +17,7 @@ jest.mock('@src/ROUTES', () => ({ DETAILS: {path: 'details'}, INVITE: {path: 'invite'}, FILTERS: {path: 'filters'}, + ADDRESS_COUNTRY: {path: 'country', getRoute: (country: string) => `country?country=${country}`}, }, })); @@ -78,4 +83,37 @@ describe('createDynamicRoute', () => { expect(result).toBe(expectedPath); }); + + it('should append suffix with its own query params to a simple path', () => { + const activeRoute = 'settings/profile/address'; + const suffixWithQuery = 'country?country=US'; + const expectedPath = 'settings/profile/address/country?country=US'; + + mockGetActiveRoute.mockReturnValue(activeRoute); + + const result = createDynamicRoute(suffixWithQuery); + + expect(result).toBe(expectedPath); + }); + + it('should merge suffix query params with base path query params', () => { + const activeRoute = 'settings/profile/address?existingParam=1'; + const suffixWithQuery = 'country?country=US'; + const expectedPath = 'settings/profile/address/country?existingParam=1&country=US'; + + mockGetActiveRoute.mockReturnValue(activeRoute); + + const result = createDynamicRoute(suffixWithQuery); + + expect(result).toBe(expectedPath); + }); + + it('should throw an error when suffix query param collides with base path query param', () => { + const activeRoute = 'settings/profile/address?country=GB'; + const suffixWithQuery = 'country?country=US'; + + mockGetActiveRoute.mockReturnValue(activeRoute); + + expect(() => createDynamicRoute(suffixWithQuery)).toThrow('[createDynamicRoute] Query param "country" exists in both base path and dynamic suffix. This is not allowed.'); + }); }); diff --git a/tests/navigation/findMatchingDynamicSuffixTests.ts b/tests/navigation/findMatchingDynamicSuffixTests.ts index 46e3fcefa20bc..6baaaf187d4cb 100644 --- a/tests/navigation/findMatchingDynamicSuffixTests.ts +++ b/tests/navigation/findMatchingDynamicSuffixTests.ts @@ -1,4 +1,4 @@ -import findMatchingDynamicSuffix from '@libs/Navigation/helpers/findMatchingDynamicSuffix'; +import findMatchingDynamicSuffix from '@libs/Navigation/helpers/dynamicRoutesUtils/findMatchingDynamicSuffix'; describe('findMatchingDynamicSuffix', () => { it('should match a single-segment dynamic suffix', () => { @@ -32,4 +32,8 @@ describe('findMatchingDynamicSuffix', () => { it('should not match a suffix that appears in the middle of the path', () => { expect(findMatchingDynamicSuffix('/verify-account/settings/wallet')).toBeUndefined(); }); + + it('should match a suffix when path has suffix-specific query params', () => { + expect(findMatchingDynamicSuffix('settings/profile/address/country?country=US')).toBe('country'); + }); }); diff --git a/tests/navigation/getPathWithoutDynamicSuffixTests.ts b/tests/navigation/getPathWithoutDynamicSuffixTests.ts index 72bed418726f5..f0b22e6a0ed43 100644 --- a/tests/navigation/getPathWithoutDynamicSuffixTests.ts +++ b/tests/navigation/getPathWithoutDynamicSuffixTests.ts @@ -1,4 +1,4 @@ -import getPathWithoutDynamicSuffix from '@libs/Navigation/helpers/getPathWithoutDynamicSuffix'; +import getPathWithoutDynamicSuffix from '@libs/Navigation/helpers/dynamicRoutesUtils/getPathWithoutDynamicSuffix'; describe('getPathWithoutDynamicSuffix', () => { it('should remove a single-segment suffix from a simple path', () => { @@ -36,4 +36,16 @@ describe('getPathWithoutDynamicSuffix', () => { expect(result).toBe('/settings/wallet'); }); + + it('should strip suffix-specific query params derived from DYNAMIC_ROUTES.getRoute', () => { + const result = getPathWithoutDynamicSuffix('/settings/profile/address/country?country=US', 'country'); + + expect(result).toBe('/settings/profile/address'); + }); + + it('should strip only suffix-specific params and preserve base path params', () => { + const result = getPathWithoutDynamicSuffix('/settings/profile/address/country?baseParam=1&country=US', 'country'); + + expect(result).toBe('/settings/profile/address?baseParam=1'); + }); }); diff --git a/tests/navigation/getStateForDynamicRouteTests.ts b/tests/navigation/getStateForDynamicRouteTests.ts index 0c2b36581595c..cf3dc7c588f0e 100644 --- a/tests/navigation/getStateForDynamicRouteTests.ts +++ b/tests/navigation/getStateForDynamicRouteTests.ts @@ -1,4 +1,4 @@ -import getStateForDynamicRoute from '@libs/Navigation/helpers/getStateForDynamicRoute'; +import getStateForDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/getStateForDynamicRoute'; import type {DYNAMIC_ROUTES} from '@src/ROUTES'; jest.mock('@libs/Navigation/linkingConfig/config', () => ({ diff --git a/tests/navigation/getStateFromPathTests.ts b/tests/navigation/getStateFromPathTests.ts index c6bac121f472f..ffa5cb2ace840 100644 --- a/tests/navigation/getStateFromPathTests.ts +++ b/tests/navigation/getStateFromPathTests.ts @@ -1,6 +1,6 @@ import {findFocusedRoute, getStateFromPath as RNGetStateFromPath} from '@react-navigation/native'; import Log from '@libs/Log'; -import getStateForDynamicRoute from '@libs/Navigation/helpers/getStateForDynamicRoute'; +import getStateForDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/getStateForDynamicRoute'; import getStateFromPath from '@libs/Navigation/helpers/getStateFromPath'; import type {Route} from '@src/ROUTES'; @@ -30,7 +30,7 @@ jest.mock('@src/ROUTES', () => ({ jest.mock('@libs/Navigation/helpers/getMatchingNewRoute', () => jest.fn()); jest.mock('@libs/Navigation/helpers/getRedirectedPath', () => jest.fn((path: string) => path)); -jest.mock('@libs/Navigation/helpers/getStateForDynamicRoute', () => jest.fn()); +jest.mock('@libs/Navigation/helpers/dynamicRoutesUtils/getStateForDynamicRoute', () => jest.fn()); describe('getStateFromPath', () => { const mockFindFocusedRoute = findFocusedRoute as jest.Mock; diff --git a/tests/navigation/splitPathAndQueryTests.ts b/tests/navigation/splitPathAndQueryTests.ts index a0dba6475e6fb..baa585bc695d9 100644 --- a/tests/navigation/splitPathAndQueryTests.ts +++ b/tests/navigation/splitPathAndQueryTests.ts @@ -1,4 +1,4 @@ -import splitPathAndQuery from '@libs/Navigation/helpers/splitPathAndQuery'; +import splitPathAndQuery from '@libs/Navigation/helpers/dynamicRoutesUtils/splitPathAndQuery'; describe('splitPathAndQuery', () => { it('should split path and query', () => { diff --git a/tests/navigation/useDynamicBackPathTests.ts b/tests/navigation/useDynamicBackPathTests.ts index 325a12508fd38..99dacded768ff 100644 --- a/tests/navigation/useDynamicBackPathTests.ts +++ b/tests/navigation/useDynamicBackPathTests.ts @@ -11,6 +11,7 @@ jest.mock('@src/ROUTES', () => ({ DYNAMIC_ROUTES: { VERIFY_ACCOUNT: {path: 'verify-account'}, CUSTOM_TEST_ROUTE: {path: 'custom-test-route'}, + ADDRESS_COUNTRY: {path: 'country'}, }, }));