From 443ab2db1ae1a77fc806994b898d7a28daa80ecc Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Tue, 3 Feb 2026 14:26:28 +0100 Subject: [PATCH 01/10] add intercept and validate dynamic suffixes --- src/ROUTES.ts | 3 +- src/libs/Navigation/NavigationRoot.tsx | 4 +- .../helpers/getAdaptedStateFromPath.ts | 46 +++++++++++-- .../helpers/getLastSuffixFromPath.ts | 21 ++++++ .../helpers/getStateForDynamicRoute.ts | 67 +++++++++++++++++++ .../Navigation/helpers/getStateFromPath.ts | 28 +++++++- src/libs/actions/Welcome/OnboardingFlow.ts | 3 +- 7 files changed, 161 insertions(+), 11 deletions(-) create mode 100644 src/libs/Navigation/helpers/getLastSuffixFromPath.ts create mode 100644 src/libs/Navigation/helpers/getStateForDynamicRoute.ts diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 13f056926a3d..fea0dd60ccb6 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -79,7 +79,8 @@ type DynamicRoutes = Record; */ const DYNAMIC_ROUTES = { VERIFY_ACCOUNT: { - path: 'verify-account', + // The path is intentionally misspelled to avoid conflicts when dynamic routes logic isn't entirely ready + path: 'verify-accountt', entryScreens: [], }, } as const satisfies DynamicRoutes; diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index 7f65fb9dc465..a251aedf6214 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -109,7 +109,7 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N Navigation.navigate(ROUTES.MIGRATED_USER_WELCOME_MODAL.getRoute()); }); - return getAdaptedStateFromPath(lastVisitedPath, linkingConfig.config); + return getAdaptedStateFromPath(lastVisitedPath); } if (!account || account.isFromPublicDomain) { @@ -132,7 +132,7 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N if (!isSpecificDeepLink) { Log.info('Restoring last visited path on app startup', false, {lastVisitedPath, initialUrl, path}); - return getAdaptedStateFromPath(lastVisitedPath, linkingConfig.config); + return getAdaptedStateFromPath(lastVisitedPath); } } diff --git a/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts b/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts index fdae94a668e1..dfbabdad248c 100644 --- a/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts +++ b/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts @@ -1,21 +1,24 @@ -import type {NavigationState, PartialState, Route} from '@react-navigation/native'; -import {findFocusedRoute, getStateFromPath} from '@react-navigation/native'; +import type {NavigationState, PartialState, getStateFromPath as RNGetStateFromPath, Route} from '@react-navigation/native'; +import {findFocusedRoute} from '@react-navigation/native'; import pick from 'lodash/pick'; import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import getInitialSplitNavigatorState from '@libs/Navigation/AppNavigator/createSplitNavigator/getInitialSplitNavigatorState'; -import {config} from '@libs/Navigation/linkingConfig/config'; import {RHP_TO_DOMAIN, RHP_TO_SEARCH, RHP_TO_SETTINGS, RHP_TO_SIDEBAR, RHP_TO_WORKSPACE, RHP_TO_WORKSPACES_LIST} from '@libs/Navigation/linkingConfig/RELATIONS'; import type {NavigationPartialRoute, RootNavigatorParamList} from '@libs/Navigation/types'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {Route as RoutePath} from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import type {Report} from '@src/types/onyx'; +import getLastSuffixFromPath from './getLastSuffixFromPath'; import getMatchingNewRoute from './getMatchingNewRoute'; import getParamsFromRoute from './getParamsFromRoute'; import getRedirectedPath from './getRedirectedPath'; +import getStateFromPath from './getStateFromPath'; +import isDynamicRouteSuffix from './isDynamicRouteSuffix'; import {isFullScreenName} from './isNavigatorName'; import normalizePath from './normalizePath'; import replacePathInNestedState from './replacePathInNestedState'; @@ -31,7 +34,7 @@ Onyx.connect({ type GetAdaptedStateReturnType = ReturnType; -type GetAdaptedStateFromPath = (...args: [...Parameters, shouldReplacePathInNestedState?: boolean]) => GetAdaptedStateReturnType; +type GetAdaptedStateFromPath = (...args: [...Parameters, shouldReplacePathInNestedState?: boolean]) => GetAdaptedStateReturnType; // The function getPathFromState that we are using in some places isn't working correctly without defined index. const getRoutesWithIndex = (routes: NavigationPartialRoute[]): PartialState => ({routes, index: routes.length - 1}); @@ -64,7 +67,7 @@ function getSearchScreenNameForRoute(route: NavigationPartialRoute): string { function getMatchingFullScreenRoute(route: NavigationPartialRoute) { // Check for backTo param. One screen with different backTo value may need different screens visible under the overlay. if (isRouteWithBackToParam(route)) { - const stateForBackTo = getStateFromPath(route.params.backTo, config); + const stateForBackTo = getStateFromPath(route.params.backTo as RoutePath); // This may happen if the backTo url is invalid. const lastRoute = stateForBackTo?.routes.at(-1); @@ -87,6 +90,37 @@ function getMatchingFullScreenRoute(route: NavigationPartialRoute) { // If not, get the matching full screen route for the back to state. return getMatchingFullScreenRoute(focusedStateForBackToRoute); } + + // Handle dynamic routes: find the appropriate full screen route + const dynamicRouteSuffix = getLastSuffixFromPath(route.path); + if (isDynamicRouteSuffix(dynamicRouteSuffix)) { + // Remove dynamic suffix to get the base path + const pathWithoutDynamicSuffix = route.path?.replace(`/${dynamicRouteSuffix}`, ''); + + // Get navigation state for the base path without dynamic suffix + const stateUnderDynamicRoute = getStateFromPath(pathWithoutDynamicSuffix as RoutePath); + const lastRoute = stateUnderDynamicRoute?.routes.at(-1); + + if (!stateUnderDynamicRoute || !lastRoute || lastRoute.name === SCREENS.NOT_FOUND) { + return undefined; + } + + const isLastRouteFullScreen = isFullScreenName(lastRoute.name); + + if (isLastRouteFullScreen) { + return lastRoute; + } + + const focusedStateForDynamicRoute = findFocusedRoute(stateUnderDynamicRoute); + + if (!focusedStateForDynamicRoute) { + return undefined; + } + + // Recursively find the matching full screen route for the focused dynamic route + return getMatchingFullScreenRoute(focusedStateForDynamicRoute); + } + const routeNameForLookup = getSearchScreenNameForRoute(route); if (RHP_TO_SEARCH[routeNameForLookup]) { const paramsFromRoute = getParamsFromRoute(RHP_TO_SEARCH[routeNameForLookup]); @@ -278,7 +312,7 @@ const getAdaptedStateFromPath: GetAdaptedStateFromPath = (path, options, shouldR normalizedPath = '/'; } - const state = getStateFromPath(normalizedPath, options) as PartialState>; + const state = getStateFromPath(normalizedPath as RoutePath) as PartialState>; if (shouldReplacePathInNestedState) { replacePathInNestedState(state, normalizedPath); } diff --git a/src/libs/Navigation/helpers/getLastSuffixFromPath.ts b/src/libs/Navigation/helpers/getLastSuffixFromPath.ts new file mode 100644 index 000000000000..3094f923de21 --- /dev/null +++ b/src/libs/Navigation/helpers/getLastSuffixFromPath.ts @@ -0,0 +1,21 @@ +/** + * Extracts the last segment from a URL path, removing query parameters and trailing slashes. + * + * @param path - The URL path to extract the suffix from (can be undefined) + * @returns The last segment of the path as a string + */ +function getLastSuffixFromPath(path: string | undefined): string { + const pathWithoutParams = path?.split('?').at(0); + + if (!pathWithoutParams) { + throw new Error('Failed to parse the path, path is empty'); + } + + const pathWithoutTrailingSlash = pathWithoutParams.endsWith('/') ? pathWithoutParams.slice(0, -1) : pathWithoutParams; + + const lastSuffix = pathWithoutTrailingSlash.split('/').pop() ?? ''; + + return lastSuffix; +} + +export default getLastSuffixFromPath; diff --git a/src/libs/Navigation/helpers/getStateForDynamicRoute.ts b/src/libs/Navigation/helpers/getStateForDynamicRoute.ts new file mode 100644 index 000000000000..60bb53d0e13f --- /dev/null +++ b/src/libs/Navigation/helpers/getStateForDynamicRoute.ts @@ -0,0 +1,67 @@ +import {normalizedConfigs} from '@libs/Navigation/linkingConfig/config'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; +import type {DynamicRouteSuffix} from '@src/ROUTES'; + +type LeafRoute = { + name: string; + path: string; +}; + +type NestedRoute = { + name: string; + state: { + routes: [RouteNode]; + index: 0; + }; +}; + +type RouteNode = LeafRoute | NestedRoute; + +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 Object.entries(normalizedConfigs)) { + if (config.path === dynamicRouteName) { + return config.routeNames; + } + } + + return null; +} + +function getStateForDynamicRoute(path: string, dynamicRouteName: keyof typeof DYNAMIC_ROUTES) { + const routeConfig = getRouteNamesForDynamicRoute(DYNAMIC_ROUTES[dynamicRouteName].path); + + if (!routeConfig) { + throw new Error(`No route configuration found for dynamic route '${dynamicRouteName}'`); + } + + // Build navigation state by creating nested structure + const buildNestedState = (routes: string[], currentIndex: number): RouteNode => { + const currentRoute = routes.at(currentIndex); + + // If this is the last route, create leaf node with path + if (currentIndex === routes.length - 1) { + return { + name: currentRoute ?? '', + path, + }; + } + + // Create intermediate node with nested state + return { + name: currentRoute ?? '', + state: { + routes: [buildNestedState(routes, currentIndex + 1)], + index: 0, + }, + }; + }; + + // Start building from the first route + const rootRoute = {routes: [buildNestedState(routeConfig, 0)]}; + + return rootRoute; +} + +export default getStateForDynamicRoute; diff --git a/src/libs/Navigation/helpers/getStateFromPath.ts b/src/libs/Navigation/helpers/getStateFromPath.ts index b08d003e3c13..1e5725617c8e 100644 --- a/src/libs/Navigation/helpers/getStateFromPath.ts +++ b/src/libs/Navigation/helpers/getStateFromPath.ts @@ -1,9 +1,14 @@ import type {NavigationState, PartialState} from '@react-navigation/native'; -import {getStateFromPath as RNGetStateFromPath} from '@react-navigation/native'; +import {findFocusedRoute, getStateFromPath as RNGetStateFromPath} from '@react-navigation/native'; import {linkingConfig} from '@libs/Navigation/linkingConfig'; import type {Route} from '@src/ROUTES'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; +import type {Screen} from '@src/SCREENS'; +import getLastSuffixFromPath from './getLastSuffixFromPath'; import getMatchingNewRoute from './getMatchingNewRoute'; import getRedirectedPath from './getRedirectedPath'; +import getStateForDynamicRoute from './getStateForDynamicRoute'; +import isDynamicRouteSuffix from './isDynamicRouteSuffix'; /** * @param path - The path to parse @@ -14,6 +19,27 @@ function getStateFromPath(path: Route): PartialState { const redirectedPath = getRedirectedPath(normalizedPath); const normalizedPathAfterRedirection = getMatchingNewRoute(redirectedPath) ?? redirectedPath; + const dynamicRouteSuffix = getLastSuffixFromPath(path); + if (isDynamicRouteSuffix(dynamicRouteSuffix)) { + const pathWithoutDynamicSuffix = path.replace(`/${dynamicRouteSuffix}`, ''); + + type DynamicRouteKey = keyof typeof DYNAMIC_ROUTES; + + // Find the dynamic route key that matches the extracted suffix + const dynamicRoute: string = Object.keys(DYNAMIC_ROUTES).find((key) => DYNAMIC_ROUTES[key as DynamicRouteKey].path === dynamicRouteSuffix) ?? ''; + + // Get the currently focused route from the base path to check permissions + const focusedRoute = findFocusedRoute(getStateFromPath(pathWithoutDynamicSuffix as Route) ?? {}); + const entryScreens: Screen[] = DYNAMIC_ROUTES[dynamicRoute as DynamicRouteKey]?.entryScreens ?? []; + + // Check if the focused route is allowed to access this dynamic route + if (focusedRoute?.name && entryScreens.includes(focusedRoute.name as Screen)) { + // Generate navigation state for the dynamic route + const verifyAccountState = getStateForDynamicRoute(normalizedPath, dynamicRoute as DynamicRouteKey); + return verifyAccountState; + } + } + // This function is used in the linkTo function where we want to use default getStateFromPath function. const state = RNGetStateFromPath(normalizedPathAfterRedirection, linkingConfig.config); diff --git a/src/libs/actions/Welcome/OnboardingFlow.ts b/src/libs/actions/Welcome/OnboardingFlow.ts index 0a6c01e8adfe..c99144e73600 100644 --- a/src/libs/actions/Welcome/OnboardingFlow.ts +++ b/src/libs/actions/Welcome/OnboardingFlow.ts @@ -14,6 +14,7 @@ import IntlStore from '@src/languages/IntlStore'; import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {Route} from '@src/ROUTES'; import {hasCompletedGuidedSetupFlowSelector} from '@src/selectors/Onboarding'; import type {Locale, Onboarding} from '@src/types/onyx'; @@ -83,7 +84,7 @@ Onyx.connectWithoutView({ */ function startOnboardingFlow(startOnboardingFlowParams: GetOnboardingInitialPathParamsType) { const currentRoute = navigationRef.getCurrentRoute(); - const adaptedState = getAdaptedStateFromPath(getOnboardingInitialPath(startOnboardingFlowParams), linkingConfig.config, false); + const adaptedState = getAdaptedStateFromPath(getOnboardingInitialPath(startOnboardingFlowParams) as Route, linkingConfig.config, false); const focusedRoute = findFocusedRoute(adaptedState as PartialState>); if (focusedRoute?.name === currentRoute?.name) { return; From 0ceeb409179fcba1382243fa08f1b88b4d54f853 Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Wed, 4 Feb 2026 10:23:31 +0100 Subject: [PATCH 02/10] no need to add misspelled route name, there's no entry screens --- src/ROUTES.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index fea0dd60ccb6..13f056926a3d 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -79,8 +79,7 @@ type DynamicRoutes = Record; */ const DYNAMIC_ROUTES = { VERIFY_ACCOUNT: { - // The path is intentionally misspelled to avoid conflicts when dynamic routes logic isn't entirely ready - path: 'verify-accountt', + path: 'verify-account', entryScreens: [], }, } as const satisfies DynamicRoutes; From 4ea27616c22477e342c3ca7b44f36152a2363416 Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Wed, 4 Feb 2026 11:18:36 +0100 Subject: [PATCH 03/10] add all neccessary files to create first dynamic page --- src/hooks/useDynamicBackPath.ts | 17 +++++++++ src/libs/AppState/index.ts | 5 +-- .../ModalStackNavigators/index.tsx | 1 + .../syncBrowserHistory/index.web.ts | 5 +-- src/libs/Navigation/Navigation.ts | 5 ++- .../Navigation/helpers/getPathFromState.ts | 37 +++++++++++++++++++ .../settings/DynamicVerifyAccountPage.tsx | 18 +++++++++ 7 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 src/hooks/useDynamicBackPath.ts create mode 100644 src/libs/Navigation/helpers/getPathFromState.ts create mode 100644 src/pages/settings/DynamicVerifyAccountPage.tsx diff --git a/src/hooks/useDynamicBackPath.ts b/src/hooks/useDynamicBackPath.ts new file mode 100644 index 000000000000..aafa57b17662 --- /dev/null +++ b/src/hooks/useDynamicBackPath.ts @@ -0,0 +1,17 @@ +import {useNavigationState} from '@react-navigation/native'; +import getPathFromState from '@libs/Navigation/helpers/getPathFromState'; +import type {DynamicRouteSuffix, Route} from '@src/ROUTES'; +import ROUTES from '@src/ROUTES'; + +/** + * Returns the back path for a dynamic route by removing the dynamic suffix from the current URL. + * @param dynamicRouteSuffix The dynamic route suffix to remove from the current path + * @returns The back path without the dynamic route suffix + */ +function useDynamicBackPath(dynamicRouteSuffix: DynamicRouteSuffix): Route { + const path = useNavigationState((state) => getPathFromState(state)); + const backPath = path ? (path.replace(`/${dynamicRouteSuffix}`, '') as Route) : ROUTES.HOME; + return backPath; +} + +export default useDynamicBackPath; diff --git a/src/libs/AppState/index.ts b/src/libs/AppState/index.ts index 38e3d3f574d3..ddf8d4a82ac2 100644 --- a/src/libs/AppState/index.ts +++ b/src/libs/AppState/index.ts @@ -1,9 +1,8 @@ -import {getPathFromState} from '@react-navigation/native'; import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import Log from '@libs/Log'; -import {linkingConfig} from '@libs/Navigation/linkingConfig'; +import getPathFromState from '@libs/Navigation/helpers/getPathFromState'; import {navigationRef} from '@libs/Navigation/Navigation'; import {isAuthenticating as isAuthenticatingNetworkStore} from '@libs/Network/NetworkStore'; import CONST from '@src/CONST'; @@ -41,7 +40,7 @@ function captureNavigationState(): NavigationStateInfo { return {currentPath: undefined}; } - const routeFromState = getPathFromState(navigationRef.getRootState(), linkingConfig.config); + const routeFromState = getPathFromState(navigationRef.getRootState()); return { currentPath: routeFromState || undefined, }; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 8e34f4043826..61089f417453 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -353,6 +353,7 @@ const ConsoleModalStackNavigator = createModalStackNavigator({ + [SCREENS.SETTINGS.DYNAMIC_VERIFY_ACCOUNT]: () => require('../../../../pages/settings/DynamicVerifyAccountPage').default, [SCREENS.SETTINGS.SHARE_CODE]: () => require('../../../../pages/ShareCodePage').default, [SCREENS.SETTINGS.PROFILE.PRONOUNS]: () => require('../../../../pages/settings/Profile/PronounsPage').default, [SCREENS.SETTINGS.PROFILE.DISPLAY_NAME]: () => require('../../../../pages/settings/Profile/DisplayNamePage').default, diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/syncBrowserHistory/index.web.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/syncBrowserHistory/index.web.ts index 77431c8ca14b..a650fb00f8c7 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/syncBrowserHistory/index.web.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/syncBrowserHistory/index.web.ts @@ -1,11 +1,10 @@ import type {ParamListBase, StackNavigationState} from '@react-navigation/native'; -import {getPathFromState} from '@react-navigation/native'; -import {linkingConfig} from '@libs/Navigation/linkingConfig'; +import getPathFromState from '@libs/Navigation/helpers/getPathFromState'; function syncBrowserHistory(state: StackNavigationState) { // We reset the URL as the browser sets it in a way that doesn't match the navigation state // eslint-disable-next-line no-restricted-globals - history.replaceState({}, '', getPathFromState(state, linkingConfig.config)); + history.replaceState({}, '', getPathFromState(state)); } export default syncBrowserHistory; diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 35c74372fcf5..9f00beae8507 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -1,6 +1,6 @@ import {findFocusedRoute, getActionFromState} from '@react-navigation/core'; import type {EventArg, NavigationAction, NavigationContainerEventMap, NavigationState, PartialState} from '@react-navigation/native'; -import {CommonActions, getPathFromState, StackActions} from '@react-navigation/native'; +import {CommonActions, StackActions} from '@react-navigation/native'; import {Str} from 'expensify-common'; // eslint-disable-next-line you-dont-need-lodash-underscore/omit import omit from 'lodash/omit'; @@ -25,6 +25,7 @@ import type {Account, SidePanel} from '@src/types/onyx'; import getInitialSplitNavigatorState from './AppNavigator/createSplitNavigator/getInitialSplitNavigatorState'; import getSearchTopmostReportParams from './getSearchTopmostReportParams'; import originalCloseRHPFlow from './helpers/closeRHPFlow'; +import getPathFromState from './helpers/getPathFromState'; import getStateFromPath from './helpers/getStateFromPath'; import getTopmostReportParams from './helpers/getTopmostReportParams'; import {isFullScreenName, isOnboardingFlowName, isSplitNavigatorName} from './helpers/isNavigatorName'; @@ -212,7 +213,7 @@ function getActiveRoute(): string { return ''; } - const routeFromState = getPathFromState(navigationRef.getRootState(), linkingConfig.config); + const routeFromState = getPathFromState(navigationRef.getRootState()); if (routeFromState) { return routeFromState; diff --git a/src/libs/Navigation/helpers/getPathFromState.ts b/src/libs/Navigation/helpers/getPathFromState.ts new file mode 100644 index 000000000000..7718dbfdc405 --- /dev/null +++ b/src/libs/Navigation/helpers/getPathFromState.ts @@ -0,0 +1,37 @@ +import {findFocusedRoute, getPathFromState as RNGetPathFromState} from '@react-navigation/native'; +import type {NavigationState, PartialState} from '@react-navigation/routers'; +import {linkingConfig} from '@libs/Navigation/linkingConfig'; +import {normalizedConfigs} from '@libs/Navigation/linkingConfig/config'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; +import type {Screen} from '@src/SCREENS'; + +type State = NavigationState | Omit, 'stale'>; + +/** + * Checks if a screen name is a dynamic route screen + */ +function isDynamicRouteScreen(screenName: Screen): boolean { + for (const {path} of Object.values(DYNAMIC_ROUTES)) { + if (normalizedConfigs[screenName]?.path === path) { + return true; + } + } + return false; +} + +const getPathFromState = (state: State): string => { + const focusedRoute = findFocusedRoute(state); + const screenName = focusedRoute?.name ?? ''; + + // Handle dynamic route screens that require special path that is placed in state + if (isDynamicRouteScreen(screenName as Screen) && focusedRoute?.path) { + return focusedRoute.path; + } + + // For regular routes, use React Navigation's default path generation + const path = RNGetPathFromState(state, linkingConfig.config); + + return path; +}; + +export default getPathFromState; diff --git a/src/pages/settings/DynamicVerifyAccountPage.tsx b/src/pages/settings/DynamicVerifyAccountPage.tsx new file mode 100644 index 000000000000..d842e3c44c46 --- /dev/null +++ b/src/pages/settings/DynamicVerifyAccountPage.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import useDynamicBackPath from '@hooks/useDynamicBackPath'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; +import VerifyAccountPageBase from './VerifyAccountPageBase'; + +function DynamicVerifyAccountPage() { + const backPath = useDynamicBackPath(DYNAMIC_ROUTES.VERIFY_ACCOUNT.path); + // currently, the default behavior of this component after completing verification is to navigate back + const forwardPath = backPath; + return ( + + ); +} + +export default DynamicVerifyAccountPage; From 1a2c199de5db08dcf3237f5dadafdfcdd1f6c4b3 Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Wed, 4 Feb 2026 13:36:30 +0100 Subject: [PATCH 04/10] fix test --- tests/navigation/isActiveRouteTests.tsx | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/tests/navigation/isActiveRouteTests.tsx b/tests/navigation/isActiveRouteTests.tsx index 0e88faa69b58..a592eb7f6d33 100644 --- a/tests/navigation/isActiveRouteTests.tsx +++ b/tests/navigation/isActiveRouteTests.tsx @@ -1,5 +1,5 @@ import {afterEach, beforeEach, describe, expect, it, jest} from '@jest/globals'; -import type {getPathFromState as GetPathFromState} from '@react-navigation/native'; +import getPathFromState from '@libs/Navigation/helpers/getPathFromState'; import Navigation from '@libs/Navigation/Navigation'; import navigationRef from '@libs/Navigation/navigationRef'; import type {Route} from '@src/ROUTES'; @@ -18,14 +18,7 @@ jest.mock('@libs/Navigation/navigationRef', () => { }; }); -jest.mock('@react-navigation/native', () => { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - const actual = jest.requireActual('@react-navigation/native') as {getPathFromState: typeof GetPathFromState}; - return { - ...actual, - getPathFromState: jest.fn(() => '/settings/profile?backTo=settings'), - }; -}); +jest.mock('@libs/Navigation/helpers/getPathFromState', () => jest.fn()); describe('Navigation', () => { afterEach(() => { @@ -39,8 +32,10 @@ describe('Navigation', () => { isReady: jest.Mock; }; + (getPathFromState as jest.Mock).mockReturnValue('/settings/profile?backTo=settings'); + navigationRefMock.current.getCurrentRoute.mockReturnValue({name: 'test'}); - navigationRefMock.getRootState.mockReturnValue({} as ReturnType); + navigationRefMock.getRootState.mockReturnValue({}); navigationRefMock.isReady.mockReturnValue(true); }); From e1c6fa7bfa41eca8122ca797f8d288311247fecd Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Fri, 6 Feb 2026 15:28:00 +0100 Subject: [PATCH 05/10] add a guard to prevent path = undefined --- .../helpers/getAdaptedStateFromPath.ts | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts b/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts index dfbabdad248c..b64b96f66c91 100644 --- a/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts +++ b/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts @@ -92,33 +92,35 @@ function getMatchingFullScreenRoute(route: NavigationPartialRoute) { } // Handle dynamic routes: find the appropriate full screen route - const dynamicRouteSuffix = getLastSuffixFromPath(route.path); - if (isDynamicRouteSuffix(dynamicRouteSuffix)) { - // Remove dynamic suffix to get the base path - const pathWithoutDynamicSuffix = route.path?.replace(`/${dynamicRouteSuffix}`, ''); - - // Get navigation state for the base path without dynamic suffix - const stateUnderDynamicRoute = getStateFromPath(pathWithoutDynamicSuffix as RoutePath); - const lastRoute = stateUnderDynamicRoute?.routes.at(-1); + if (route.path) { + const dynamicRouteSuffix = getLastSuffixFromPath(route.path); + if (isDynamicRouteSuffix(dynamicRouteSuffix)) { + // Remove dynamic suffix to get the base path + const pathWithoutDynamicSuffix = route.path?.replace(`/${dynamicRouteSuffix}`, ''); + + // Get navigation state for the base path without dynamic suffix + const stateUnderDynamicRoute = getStateFromPath(pathWithoutDynamicSuffix as RoutePath); + const lastRoute = stateUnderDynamicRoute?.routes.at(-1); + + if (!stateUnderDynamicRoute || !lastRoute || lastRoute.name === SCREENS.NOT_FOUND) { + return undefined; + } - if (!stateUnderDynamicRoute || !lastRoute || lastRoute.name === SCREENS.NOT_FOUND) { - return undefined; - } + const isLastRouteFullScreen = isFullScreenName(lastRoute.name); - const isLastRouteFullScreen = isFullScreenName(lastRoute.name); + if (isLastRouteFullScreen) { + return lastRoute; + } - if (isLastRouteFullScreen) { - return lastRoute; - } + const focusedStateForDynamicRoute = findFocusedRoute(stateUnderDynamicRoute); - const focusedStateForDynamicRoute = findFocusedRoute(stateUnderDynamicRoute); + if (!focusedStateForDynamicRoute) { + return undefined; + } - if (!focusedStateForDynamicRoute) { - return undefined; + // Recursively find the matching full screen route for the focused dynamic route + return getMatchingFullScreenRoute(focusedStateForDynamicRoute); } - - // Recursively find the matching full screen route for the focused dynamic route - return getMatchingFullScreenRoute(focusedStateForDynamicRoute); } const routeNameForLookup = getSearchScreenNameForRoute(route); From 74175863fa05acf9db306f00ea0857c254295a2a Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Fri, 6 Feb 2026 15:48:54 +0100 Subject: [PATCH 06/10] add filename to the error text --- src/libs/Navigation/helpers/getLastSuffixFromPath.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Navigation/helpers/getLastSuffixFromPath.ts b/src/libs/Navigation/helpers/getLastSuffixFromPath.ts index 3094f923de21..1178acbf8539 100644 --- a/src/libs/Navigation/helpers/getLastSuffixFromPath.ts +++ b/src/libs/Navigation/helpers/getLastSuffixFromPath.ts @@ -8,7 +8,7 @@ function getLastSuffixFromPath(path: string | undefined): string { const pathWithoutParams = path?.split('?').at(0); if (!pathWithoutParams) { - throw new Error('Failed to parse the path, path is empty'); + throw new Error('[getLastSuffixFromPath.ts] Failed to parse the path, path is empty'); } const pathWithoutTrailingSlash = pathWithoutParams.endsWith('/') ? pathWithoutParams.slice(0, -1) : pathWithoutParams; From d2822ca48e8322c38ecebf99d765bc67febccad7 Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Tue, 10 Feb 2026 17:14:26 +0100 Subject: [PATCH 07/10] add and fix all suggestions left by bots --- src/hooks/useDynamicBackPath.ts | 25 +++++++++++++++--- .../Navigation/helpers/createDynamicRoute.ts | 15 ++++++++--- .../Navigation/helpers/getPathFromState.ts | 11 ++++++-- .../helpers/getStateForDynamicRoute.ts | 4 ++- .../Navigation/helpers/getStateFromPath.ts | 26 ++++++++++++++++--- .../Navigation/helpers/splitPathAndQuery.ts | 13 ++++++++++ 6 files changed, 80 insertions(+), 14 deletions(-) create mode 100644 src/libs/Navigation/helpers/splitPathAndQuery.ts diff --git a/src/hooks/useDynamicBackPath.ts b/src/hooks/useDynamicBackPath.ts index aafa57b17662..ffdfcb26ea58 100644 --- a/src/hooks/useDynamicBackPath.ts +++ b/src/hooks/useDynamicBackPath.ts @@ -1,17 +1,36 @@ import {useNavigationState} from '@react-navigation/native'; import getPathFromState from '@libs/Navigation/helpers/getPathFromState'; +import splitPathAndQuery from '@libs/Navigation/helpers/splitPathAndQuery'; import type {DynamicRouteSuffix, Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; /** * Returns the back path for a dynamic route by removing the dynamic suffix from the current URL. + * Only removes the suffix if it's the last segment of the path (ignoring trailing slashes and query parameters). * @param dynamicRouteSuffix The dynamic route suffix to remove from the current path - * @returns The back path without the dynamic route suffix + * @returns The back path without the dynamic route suffix, or HOME if path is null/undefined */ function useDynamicBackPath(dynamicRouteSuffix: DynamicRouteSuffix): Route { const path = useNavigationState((state) => getPathFromState(state)); - const backPath = path ? (path.replace(`/${dynamicRouteSuffix}`, '') as Route) : ROUTES.HOME; - return backPath; + + if (!path) { + return ROUTES.HOME; + } + + // Remove leading slashes for consistent processing + const pathWithoutLeadingSlash = path.replace(/^\/+/, ''); + + const [normalizedPath, query] = splitPathAndQuery(pathWithoutLeadingSlash); + + if (normalizedPath?.endsWith(`/${dynamicRouteSuffix}`)) { + const backPathWithoutQuery = normalizedPath.slice(0, -(dynamicRouteSuffix.length + 1)); + const backPath = `${backPathWithoutQuery}${query ? `?${query}` : ''}`; + + return backPath as Route; + } + + // If suffix is not the last segment, return the original path + return pathWithoutLeadingSlash as Route; } export default useDynamicBackPath; diff --git a/src/libs/Navigation/helpers/createDynamicRoute.ts b/src/libs/Navigation/helpers/createDynamicRoute.ts index 903d82f5143c..0b15a35b7376 100644 --- a/src/libs/Navigation/helpers/createDynamicRoute.ts +++ b/src/libs/Navigation/helpers/createDynamicRoute.ts @@ -1,13 +1,20 @@ 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 [basePath, params] = path.split('?'); - let newPath = basePath.endsWith('/') ? `${basePath}${suffix}` : `${basePath}/${suffix}`; + const [normalizedPath, query] = splitPathAndQuery(path); - if (params) { - newPath += `?${params}`; + // This should never happen as the path should always be defined + if (!normalizedPath) { + throw new Error('Path is undefined or empty'); + } + + let newPath = `${normalizedPath}/${suffix}`; + + if (query) { + newPath += `?${query}`; } return newPath as Route; }; diff --git a/src/libs/Navigation/helpers/getPathFromState.ts b/src/libs/Navigation/helpers/getPathFromState.ts index 7718dbfdc405..4c8b7219e170 100644 --- a/src/libs/Navigation/helpers/getPathFromState.ts +++ b/src/libs/Navigation/helpers/getPathFromState.ts @@ -11,8 +11,15 @@ type State = NavigationState | Omit, 'stale'>; * Checks if a screen name is a dynamic route screen */ function isDynamicRouteScreen(screenName: Screen): boolean { - for (const {path} of Object.values(DYNAMIC_ROUTES)) { - if (normalizedConfigs[screenName]?.path === path) { + const dynamicRouteEntries = Object.values(DYNAMIC_ROUTES); + const screenPath = normalizedConfigs[screenName]?.path; + + if (!screenPath) { + return false; + } + + for (const {path} of dynamicRouteEntries) { + if (screenPath === path) { return true; } } diff --git a/src/libs/Navigation/helpers/getStateForDynamicRoute.ts b/src/libs/Navigation/helpers/getStateForDynamicRoute.ts index 60bb53d0e13f..eab4689f041a 100644 --- a/src/libs/Navigation/helpers/getStateForDynamicRoute.ts +++ b/src/libs/Navigation/helpers/getStateForDynamicRoute.ts @@ -18,9 +18,11 @@ type NestedRoute = { type RouteNode = LeafRoute | NestedRoute; function getRouteNamesForDynamicRoute(dynamicRouteName: DynamicRouteSuffix): string[] | null { + const configEntries = Object.entries(normalizedConfigs); + // 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 Object.entries(normalizedConfigs)) { + for (const [, config] of configEntries) { if (config.path === dynamicRouteName) { return config.routeNames; } diff --git a/src/libs/Navigation/helpers/getStateFromPath.ts b/src/libs/Navigation/helpers/getStateFromPath.ts index 1e5725617c8e..5bf0ae2d18ba 100644 --- a/src/libs/Navigation/helpers/getStateFromPath.ts +++ b/src/libs/Navigation/helpers/getStateFromPath.ts @@ -1,5 +1,6 @@ import type {NavigationState, PartialState} from '@react-navigation/native'; import {findFocusedRoute, getStateFromPath as RNGetStateFromPath} from '@react-navigation/native'; +import Log from '@libs/Log'; import {linkingConfig} from '@libs/Navigation/linkingConfig'; import type {Route} from '@src/ROUTES'; import {DYNAMIC_ROUTES} from '@src/ROUTES'; @@ -33,10 +34,27 @@ function getStateFromPath(path: Route): PartialState { const entryScreens: Screen[] = DYNAMIC_ROUTES[dynamicRoute as DynamicRouteKey]?.entryScreens ?? []; // Check if the focused route is allowed to access this dynamic route - if (focusedRoute?.name && entryScreens.includes(focusedRoute.name as Screen)) { - // Generate navigation state for the dynamic route - const verifyAccountState = getStateForDynamicRoute(normalizedPath, dynamicRoute as DynamicRouteKey); - return verifyAccountState; + if (focusedRoute?.name) { + if (entryScreens.includes(focusedRoute.name as Screen)) { + // Generate navigation state for the dynamic route + const verifyAccountState = getStateForDynamicRoute(normalizedPath, dynamicRoute as DynamicRouteKey); + return verifyAccountState; + } + + // Fallback to root parsing so users can't land on /verify-account directly. + // This ensures navigation redirects back to the previous screen (root handles that). + if (pathWithoutDynamicSuffix === '/' || pathWithoutDynamicSuffix === '') { + const state = RNGetStateFromPath(pathWithoutDynamicSuffix, linkingConfig.config); + + if (!state) { + throw new Error('Failed to parse the path to a navigation state.'); + } + + return state; + } + + // 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}`); } } diff --git a/src/libs/Navigation/helpers/splitPathAndQuery.ts b/src/libs/Navigation/helpers/splitPathAndQuery.ts new file mode 100644 index 000000000000..9c87238b8391 --- /dev/null +++ b/src/libs/Navigation/helpers/splitPathAndQuery.ts @@ -0,0 +1,13 @@ +/** + * Splits a full path into its path and query components. + * @param fullPath - The full URL path (e.g., '/settings/wallet?param=value') + * @returns A tuple where the first element is the path without trailing slash + * and the second element is the query string (if any). + */ +function splitPathAndQuery(fullPath: string | undefined): [string | undefined, string | undefined] { + const [path, query] = fullPath?.split('?') ?? [undefined, undefined]; + const normalizedPath = path?.endsWith('/') && path.length > 1 ? path.slice(0, -1) : path; + return [normalizedPath, query]; +} + +export default splitPathAndQuery; From 52f74d39187dcb455bd9e8c1a4c0e736360e0c61 Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Wed, 11 Feb 2026 10:22:01 +0100 Subject: [PATCH 08/10] fix getStateFromPath handling dynamic routes and change imports for getPathFromState --- src/libs/Navigation/NavigationRoot.tsx | 5 +++-- .../Navigation/helpers/getStateFromPath.ts | 18 ++++++------------ src/libs/actions/App.ts | 5 ++--- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index 452ef3301ea4..8a56092104b2 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -1,5 +1,5 @@ import type {NavigationState} from '@react-navigation/native'; -import {DarkTheme, DefaultTheme, findFocusedRoute, getPathFromState, NavigationContainer} from '@react-navigation/native'; +import {DarkTheme, DefaultTheme, findFocusedRoute, NavigationContainer} from '@react-navigation/native'; import {hasCompletedGuidedSetupFlowSelector, wasInvitedToNewDotSelector} from '@selectors/Onboarding'; import React, {useCallback, useContext, useEffect, useMemo, useRef} from 'react'; import {useOnboardingValues} from '@components/OnyxListItemProvider'; @@ -29,6 +29,7 @@ import ROUTES from '@src/ROUTES'; import AppNavigator from './AppNavigator'; import {cleanPreservedNavigatorStates} from './AppNavigator/createSplitNavigator/usePreserveNavigatorState'; import getAdaptedStateFromPath from './helpers/getAdaptedStateFromPath'; +import getPathFromState from './helpers/getPathFromState'; import {isWorkspacesTabScreenName} from './helpers/isNavigatorName'; import {saveSettingsTabPathToSessionStorage, saveWorkspacesTabPathToSessionStorage} from './helpers/lastVisitedTabPathUtils'; import {linkingConfig} from './linkingConfig'; @@ -56,7 +57,7 @@ function parseAndLogRoute(state: NavigationState) { return; } - const currentPath = getPathFromState(state, linkingConfig.config); + const currentPath = getPathFromState(state); const focusedRoute = findFocusedRoute(state); diff --git a/src/libs/Navigation/helpers/getStateFromPath.ts b/src/libs/Navigation/helpers/getStateFromPath.ts index 5bf0ae2d18ba..ca011426ec17 100644 --- a/src/libs/Navigation/helpers/getStateFromPath.ts +++ b/src/libs/Navigation/helpers/getStateFromPath.ts @@ -20,9 +20,9 @@ function getStateFromPath(path: Route): PartialState { const redirectedPath = getRedirectedPath(normalizedPath); const normalizedPathAfterRedirection = getMatchingNewRoute(redirectedPath) ?? redirectedPath; - const dynamicRouteSuffix = getLastSuffixFromPath(path); + const dynamicRouteSuffix = getLastSuffixFromPath(normalizedPathAfterRedirection); if (isDynamicRouteSuffix(dynamicRouteSuffix)) { - const pathWithoutDynamicSuffix = path.replace(`/${dynamicRouteSuffix}`, ''); + const pathWithoutDynamicSuffix = normalizedPathAfterRedirection.replace(`/${dynamicRouteSuffix}`, ''); type DynamicRouteKey = keyof typeof DYNAMIC_ROUTES; @@ -37,18 +37,13 @@ function getStateFromPath(path: Route): PartialState { if (focusedRoute?.name) { if (entryScreens.includes(focusedRoute.name as Screen)) { // Generate navigation state for the dynamic route - const verifyAccountState = getStateForDynamicRoute(normalizedPath, dynamicRoute as DynamicRouteKey); - return verifyAccountState; + const dynamicRouteState = getStateForDynamicRoute(normalizedPath, dynamicRoute as DynamicRouteKey); + return dynamicRouteState; } - // Fallback to root parsing so users can't land on /verify-account directly. - // This ensures navigation redirects back to the previous screen (root handles that). + // Fallback to not found page so users can't land on dynamic suffix directly. if (pathWithoutDynamicSuffix === '/' || pathWithoutDynamicSuffix === '') { - const state = RNGetStateFromPath(pathWithoutDynamicSuffix, linkingConfig.config); - - if (!state) { - throw new Error('Failed to parse the path to a navigation state.'); - } + const state = {routes: [{name: 'not-found', path: normalizedPathAfterRedirection}]}; return state; } @@ -64,7 +59,6 @@ function getStateFromPath(path: Route): PartialState { if (!state) { throw new Error('Failed to parse the path to a navigation state.'); } - return state; } diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 8b64dde19dae..2a93c758e917 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -1,5 +1,4 @@ // Issue - https://github.com/Expensify/App/issues/26719 -import {getPathFromState} from '@react-navigation/native'; import {Str} from 'expensify-common'; import type {AppStateStatus} from 'react-native'; import {AppState} from 'react-native'; @@ -11,7 +10,6 @@ import {SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import DateUtils from '@libs/DateUtils'; import Log from '@libs/Log'; import getCurrentUrl from '@libs/Navigation/currentUrl'; -import {linkingConfig} from '@libs/Navigation/linkingConfig'; import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; import Performance from '@libs/Performance'; import {isPublicRoom, isValidReport} from '@libs/ReportUtils'; @@ -19,6 +17,7 @@ import {isLoggingInAsNewUser as isLoggingInAsNewUserSessionUtils} from '@libs/Se import {clearSoundAssetsCache} from '@libs/Sound'; import {cancelAllSpans, endSpan, getSpan, startSpan} from '@libs/telemetry/activeSpans'; import CONST from '@src/CONST'; +import getPathFromState from '@src/libs/Navigation/helpers/getPathFromState'; import type {OnyxKey} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; @@ -242,7 +241,7 @@ function saveCurrentPathBeforeBackground() { return; } - const currentPath = getPathFromState(currentState, linkingConfig.config); + const currentPath = getPathFromState(currentState); if (currentPath) { Log.info('Saving current path before background', false, {currentPath}); From 0ec218a48b05817cd971b6dfb54a5690bd52da22 Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Wed, 11 Feb 2026 10:50:05 +0100 Subject: [PATCH 09/10] add comment that expains why we should left options arg and remove options from args when calling getAdaptedStateFromPath --- src/libs/Navigation/NavigationRoot.tsx | 1 - src/libs/Navigation/helpers/getAdaptedStateFromPath.ts | 5 +++-- src/libs/actions/Welcome/OnboardingFlow.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index 8a56092104b2..294baabc1924 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -149,7 +149,6 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N onboardingInitialPath, onboardingValues, }), - linkingConfig.config, ); } diff --git a/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts b/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts index 3c1761993f6a..396911a1d12e 100644 --- a/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts +++ b/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts @@ -304,11 +304,12 @@ function getAdaptedState(state: PartialState { let normalizedPath = !path.startsWith('/') ? `/${path}` : path; normalizedPath = getRedirectedPath(normalizedPath); diff --git a/src/libs/actions/Welcome/OnboardingFlow.ts b/src/libs/actions/Welcome/OnboardingFlow.ts index 797982c9b464..1723ead6cd33 100644 --- a/src/libs/actions/Welcome/OnboardingFlow.ts +++ b/src/libs/actions/Welcome/OnboardingFlow.ts @@ -83,7 +83,7 @@ Onyx.connectWithoutView({ */ function startOnboardingFlow(startOnboardingFlowParams: GetOnboardingInitialPathParamsType) { const currentRoute = navigationRef.getCurrentRoute(); - const adaptedState = getAdaptedStateFromPath(getOnboardingInitialPath(startOnboardingFlowParams) as Route, linkingConfig.config, false); + const adaptedState = getAdaptedStateFromPath(getOnboardingInitialPath(startOnboardingFlowParams) as Route, undefined, false); const focusedRoute = findFocusedRoute(adaptedState as PartialState>); if (focusedRoute?.name === currentRoute?.name) { return; From 1735d03dc8170acfc55cda2d1068b764a66d563a Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Wed, 11 Feb 2026 12:39:44 +0100 Subject: [PATCH 10/10] change string screen name to const from SCREENS --- src/libs/Navigation/helpers/getStateFromPath.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/Navigation/helpers/getStateFromPath.ts b/src/libs/Navigation/helpers/getStateFromPath.ts index ca011426ec17..75dd252b3c2f 100644 --- a/src/libs/Navigation/helpers/getStateFromPath.ts +++ b/src/libs/Navigation/helpers/getStateFromPath.ts @@ -5,6 +5,7 @@ import {linkingConfig} from '@libs/Navigation/linkingConfig'; import type {Route} from '@src/ROUTES'; import {DYNAMIC_ROUTES} from '@src/ROUTES'; import type {Screen} from '@src/SCREENS'; +import SCREENS from '@src/SCREENS'; import getLastSuffixFromPath from './getLastSuffixFromPath'; import getMatchingNewRoute from './getMatchingNewRoute'; import getRedirectedPath from './getRedirectedPath'; @@ -43,7 +44,7 @@ function getStateFromPath(path: Route): PartialState { // Fallback to not found page so users can't land on dynamic suffix directly. if (pathWithoutDynamicSuffix === '/' || pathWithoutDynamicSuffix === '') { - const state = {routes: [{name: 'not-found', path: normalizedPathAfterRedirection}]}; + const state = {routes: [{name: SCREENS.NOT_FOUND, path: normalizedPathAfterRedirection}]}; return state; }