Skip to content

Commit 24efb63

Browse files
committed
add intercept and validate dynamic suffixes
1 parent e0de0b7 commit 24efb63

File tree

7 files changed

+162
-13
lines changed

7 files changed

+162
-13
lines changed

src/ROUTES.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ type DynamicRoutes = Record<string, DynamicRouteConfig>;
7979
*/
8080
const DYNAMIC_ROUTES = {
8181
VERIFY_ACCOUNT: {
82-
path: 'verify-account',
82+
// The path is intentionally misspelled to avoid conflicts when dynamic routes logic isn't entirely ready
83+
path: 'verify-accountt',
8384
entryScreens: [],
8485
},
8586
} as const satisfies DynamicRoutes;

src/libs/Navigation/NavigationRoot.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N
121121
Navigation.navigate(ROUTES.MIGRATED_USER_WELCOME_MODAL.getRoute());
122122
});
123123

124-
return getAdaptedStateFromPath(lastVisitedPath, linkingConfig.config);
124+
return getAdaptedStateFromPath(lastVisitedPath);
125125
}
126126

127127
if (!account || account.isFromPublicDomain) {
@@ -147,8 +147,7 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N
147147
currentOnboardingCompanySize,
148148
onboardingInitialPath,
149149
onboardingValues,
150-
}),
151-
linkingConfig.config,
150+
}) as Route,
152151
);
153152
}
154153

@@ -160,7 +159,7 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N
160159

161160
if (!isSpecificDeepLink) {
162161
Log.info('Restoring last visited path on app startup', false, {lastVisitedPath, initialUrl, path});
163-
return getAdaptedStateFromPath(lastVisitedPath, linkingConfig.config);
162+
return getAdaptedStateFromPath(lastVisitedPath);
164163
}
165164
}
166165

src/libs/Navigation/helpers/getAdaptedStateFromPath.ts

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
1-
import type {NavigationState, PartialState, Route} from '@react-navigation/native';
2-
import {findFocusedRoute, getStateFromPath} from '@react-navigation/native';
1+
import type {NavigationState, PartialState, getStateFromPath as RNGetStateFromPath, Route} from '@react-navigation/native';
2+
import {findFocusedRoute} from '@react-navigation/native';
33
import pick from 'lodash/pick';
44
import type {OnyxCollection} from 'react-native-onyx';
55
import Onyx from 'react-native-onyx';
66
import getInitialSplitNavigatorState from '@libs/Navigation/AppNavigator/createSplitNavigator/getInitialSplitNavigatorState';
7-
import {config} from '@libs/Navigation/linkingConfig/config';
87
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';
98
import type {NavigationPartialRoute, RootNavigatorParamList} from '@libs/Navigation/types';
109
import CONST from '@src/CONST';
1110
import NAVIGATORS from '@src/NAVIGATORS';
1211
import ONYXKEYS from '@src/ONYXKEYS';
1312
import ROUTES from '@src/ROUTES';
13+
import type {Route as RoutePath} from '@src/ROUTES';
1414
import SCREENS from '@src/SCREENS';
1515
import type {Report} from '@src/types/onyx';
16+
import getLastSuffixFromPath from './getLastSuffixFromPath';
1617
import getMatchingNewRoute from './getMatchingNewRoute';
1718
import getParamsFromRoute from './getParamsFromRoute';
19+
import getStateFromPath from './getStateFromPath';
20+
import isDynamicRouteSuffix from './isDynamicRouteSuffix';
1821
import {isFullScreenName} from './isNavigatorName';
1922
import normalizePath from './normalizePath';
2023
import replacePathInNestedState from './replacePathInNestedState';
@@ -30,7 +33,7 @@ Onyx.connect({
3033

3134
type GetAdaptedStateReturnType = ReturnType<typeof getStateFromPath>;
3235

33-
type GetAdaptedStateFromPath = (...args: [...Parameters<typeof getStateFromPath>, shouldReplacePathInNestedState?: boolean]) => GetAdaptedStateReturnType;
36+
type GetAdaptedStateFromPath = (...args: [...Parameters<typeof RNGetStateFromPath>, shouldReplacePathInNestedState?: boolean]) => GetAdaptedStateReturnType;
3437

3538
// The function getPathFromState that we are using in some places isn't working correctly without defined index.
3639
const getRoutesWithIndex = (routes: NavigationPartialRoute[]): PartialState<NavigationState> => ({routes, index: routes.length - 1});
@@ -63,7 +66,7 @@ function getSearchScreenNameForRoute(route: NavigationPartialRoute): string {
6366
function getMatchingFullScreenRoute(route: NavigationPartialRoute) {
6467
// Check for backTo param. One screen with different backTo value may need different screens visible under the overlay.
6568
if (isRouteWithBackToParam(route)) {
66-
const stateForBackTo = getStateFromPath(route.params.backTo, config);
69+
const stateForBackTo = getStateFromPath(route.params.backTo as RoutePath);
6770

6871
// This may happen if the backTo url is invalid.
6972
const lastRoute = stateForBackTo?.routes.at(-1);
@@ -86,6 +89,37 @@ function getMatchingFullScreenRoute(route: NavigationPartialRoute) {
8689
// If not, get the matching full screen route for the back to state.
8790
return getMatchingFullScreenRoute(focusedStateForBackToRoute);
8891
}
92+
93+
// Handle dynamic routes: find the appropriate full screen route
94+
const dynamicRouteSuffix = getLastSuffixFromPath(route.path);
95+
if (isDynamicRouteSuffix(dynamicRouteSuffix)) {
96+
// Remove dynamic suffix to get the base path
97+
const pathWithoutDynamicSuffix = route.path?.replace(`/${dynamicRouteSuffix}`, '');
98+
99+
// Get navigation state for the base path without dynamic suffix
100+
const stateUnderDynamicRoute = getStateFromPath(pathWithoutDynamicSuffix as RoutePath);
101+
const lastRoute = stateUnderDynamicRoute?.routes.at(-1);
102+
103+
if (!stateUnderDynamicRoute || !lastRoute || lastRoute.name === SCREENS.NOT_FOUND) {
104+
return undefined;
105+
}
106+
107+
const isLastRouteFullScreen = isFullScreenName(lastRoute.name);
108+
109+
if (isLastRouteFullScreen) {
110+
return lastRoute;
111+
}
112+
113+
const focusedStateForDynamicRoute = findFocusedRoute(stateUnderDynamicRoute);
114+
115+
if (!focusedStateForDynamicRoute) {
116+
return undefined;
117+
}
118+
119+
// Recursively find the matching full screen route for the focused dynamic route
120+
return getMatchingFullScreenRoute(focusedStateForDynamicRoute);
121+
}
122+
89123
const routeNameForLookup = getSearchScreenNameForRoute(route);
90124
if (RHP_TO_SEARCH[routeNameForLookup]) {
91125
const paramsFromRoute = getParamsFromRoute(RHP_TO_SEARCH[routeNameForLookup]);
@@ -277,7 +311,7 @@ const getAdaptedStateFromPath: GetAdaptedStateFromPath = (path, options, shouldR
277311
normalizedPath = '/';
278312
}
279313

280-
const state = getStateFromPath(normalizedPath, options) as PartialState<NavigationState<RootNavigatorParamList>>;
314+
const state = getStateFromPath(normalizedPath as RoutePath) as PartialState<NavigationState<RootNavigatorParamList>>;
281315
if (shouldReplacePathInNestedState) {
282316
replacePathInNestedState(state, normalizedPath);
283317
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Extracts the last segment from a URL path, removing query parameters and trailing slashes.
3+
*
4+
* @param path - The URL path to extract the suffix from (can be undefined)
5+
* @returns The last segment of the path as a string
6+
*/
7+
function getLastSuffixFromPath(path: string | undefined): string {
8+
const pathWithoutParams = path?.split('?').at(0);
9+
10+
if (!pathWithoutParams) {
11+
throw new Error('Failed to parse the path, path is empty');
12+
}
13+
14+
const pathWithoutTrailingSlash = pathWithoutParams.endsWith('/') ? pathWithoutParams.slice(0, -1) : pathWithoutParams;
15+
16+
const lastSuffix = pathWithoutTrailingSlash.split('/').pop() ?? '';
17+
18+
return lastSuffix;
19+
}
20+
21+
export default getLastSuffixFromPath;
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import {normalizedConfigs} from '@libs/Navigation/linkingConfig/config';
2+
import {DYNAMIC_ROUTES} from '@src/ROUTES';
3+
import type {DynamicRouteSuffix} from '@src/ROUTES';
4+
5+
type LeafRoute = {
6+
name: string;
7+
path: string;
8+
};
9+
10+
type NestedRoute = {
11+
name: string;
12+
state: {
13+
routes: [RouteNode];
14+
index: 0;
15+
};
16+
};
17+
18+
type RouteNode = LeafRoute | NestedRoute;
19+
20+
function getRouteNamesForDynamicRoute(dynamicRouteName: DynamicRouteSuffix): string[] | null {
21+
// Search through normalized configs to find matching path and extract navigation hierarchy
22+
// routeNames contains the sequence of screen/navigator names that should be present in the navigation state
23+
for (const [, config] of Object.entries(normalizedConfigs)) {
24+
if (config.path === dynamicRouteName) {
25+
return config.routeNames;
26+
}
27+
}
28+
29+
return null;
30+
}
31+
32+
function getStateForDynamicRoute(path: string, dynamicRouteName: keyof typeof DYNAMIC_ROUTES) {
33+
const routeConfig = getRouteNamesForDynamicRoute(DYNAMIC_ROUTES[dynamicRouteName].path);
34+
35+
if (!routeConfig) {
36+
throw new Error(`No route configuration found for dynamic route '${dynamicRouteName}'`);
37+
}
38+
39+
// Build navigation state by creating nested structure
40+
const buildNestedState = (routes: string[], currentIndex: number): RouteNode => {
41+
const currentRoute = routes.at(currentIndex);
42+
43+
// If this is the last route, create leaf node with path
44+
if (currentIndex === routes.length - 1) {
45+
return {
46+
name: currentRoute ?? '',
47+
path,
48+
};
49+
}
50+
51+
// Create intermediate node with nested state
52+
return {
53+
name: currentRoute ?? '',
54+
state: {
55+
routes: [buildNestedState(routes, currentIndex + 1)],
56+
index: 0,
57+
},
58+
};
59+
};
60+
61+
// Start building from the first route
62+
const rootRoute = {routes: [buildNestedState(routeConfig, 0)]};
63+
64+
return rootRoute;
65+
}
66+
67+
export default getStateForDynamicRoute;

src/libs/Navigation/helpers/getStateFromPath.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import type {NavigationState, PartialState} from '@react-navigation/native';
2-
import {getStateFromPath as RNGetStateFromPath} from '@react-navigation/native';
2+
import {findFocusedRoute, getStateFromPath as RNGetStateFromPath} from '@react-navigation/native';
33
import {linkingConfig} from '@libs/Navigation/linkingConfig';
44
import type {Route} from '@src/ROUTES';
5+
import {DYNAMIC_ROUTES} from '@src/ROUTES';
6+
import type {Screen} from '@src/SCREENS';
7+
import getLastSuffixFromPath from './getLastSuffixFromPath';
58
import getMatchingNewRoute from './getMatchingNewRoute';
9+
import getStateForDynamicRoute from './getStateForDynamicRoute';
10+
import isDynamicRouteSuffix from './isDynamicRouteSuffix';
611

712
/**
813
* @param path - The path to parse
@@ -12,6 +17,27 @@ function getStateFromPath(path: Route): PartialState<NavigationState> {
1217
const normalizedPath = !path.startsWith('/') ? `/${path}` : path;
1318
const normalizedPathAfterRedirection = getMatchingNewRoute(normalizedPath) ?? normalizedPath;
1419

20+
const dynamicRouteSuffix = getLastSuffixFromPath(path);
21+
if (isDynamicRouteSuffix(dynamicRouteSuffix)) {
22+
const pathWithoutDynamicSuffix = path.replace(`/${dynamicRouteSuffix}`, '');
23+
24+
type DynamicRouteKey = keyof typeof DYNAMIC_ROUTES;
25+
26+
// Find the dynamic route key that matches the extracted suffix
27+
const dynamicRoute: string = Object.keys(DYNAMIC_ROUTES).find((key) => DYNAMIC_ROUTES[key as DynamicRouteKey].path === dynamicRouteSuffix) ?? '';
28+
29+
// Get the currently focused route from the base path to check permissions
30+
const focusedRoute = findFocusedRoute(getStateFromPath(pathWithoutDynamicSuffix as Route) ?? {});
31+
const entryScreens: Screen[] = DYNAMIC_ROUTES[dynamicRoute as DynamicRouteKey]?.entryScreens ?? [];
32+
33+
// Check if the focused route is allowed to access this dynamic route
34+
if (focusedRoute?.name && entryScreens.includes(focusedRoute.name as Screen)) {
35+
// Generate navigation state for the dynamic route
36+
const verifyAccountState = getStateForDynamicRoute(normalizedPath, dynamicRoute as DynamicRouteKey);
37+
return verifyAccountState;
38+
}
39+
}
40+
1541
// This function is used in the linkTo function where we want to use default getStateFromPath function.
1642
const state = RNGetStateFromPath(normalizedPathAfterRedirection, linkingConfig.config);
1743

src/libs/actions/Welcome/OnboardingFlow.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import IntlStore from '@src/languages/IntlStore';
1414
import NAVIGATORS from '@src/NAVIGATORS';
1515
import ONYXKEYS from '@src/ONYXKEYS';
1616
import ROUTES from '@src/ROUTES';
17+
import type {Route} from '@src/ROUTES';
1718
import {hasCompletedGuidedSetupFlowSelector} from '@src/selectors/Onboarding';
1819
import type {Locale, Onboarding} from '@src/types/onyx';
1920

@@ -83,7 +84,7 @@ Onyx.connectWithoutView({
8384
*/
8485
function startOnboardingFlow(startOnboardingFlowParams: GetOnboardingInitialPathParamsType) {
8586
const currentRoute = navigationRef.getCurrentRoute();
86-
const adaptedState = getAdaptedStateFromPath(getOnboardingInitialPath(startOnboardingFlowParams), linkingConfig.config, false);
87+
const adaptedState = getAdaptedStateFromPath(getOnboardingInitialPath(startOnboardingFlowParams) as Route, linkingConfig.config, false);
8788
const focusedRoute = findFocusedRoute(adaptedState as PartialState<NavigationState<RootNavigatorParamList>>);
8889
if (focusedRoute?.name === currentRoute?.name) {
8990
return;

0 commit comments

Comments
 (0)