Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/libs/Navigation/NavigationRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,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) {
Expand Down Expand Up @@ -160,7 +160,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);
}
}

Expand Down
26 changes: 26 additions & 0 deletions src/libs/Navigation/helpers/createDynamicRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Navigation from '@libs/Navigation/Navigation';
import type {DynamicRouteSuffix, Route} from '@src/ROUTES';
import isDynamicRouteSuffix from './isDynamicRouteSuffix';

const combinePathAndSuffix = (path: string, suffix: string): Route => {
const [basePath, params] = path.split('?');
let newPath = path.endsWith('/') ? `${basePath}${suffix}` : `${basePath}/${suffix}`;

if (params) {
newPath += `?${params}`;
}
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;
48 changes: 42 additions & 6 deletions src/libs/Navigation/helpers/getAdaptedStateFromPath.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
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, isPublicScreenName} from './isNavigatorName';
import normalizePath from './normalizePath';
import replacePathInNestedState from './replacePathInNestedState';

let allReports: OnyxCollection<Report>;
Onyx.connect({

Check warning on line 27 in src/libs/Navigation/helpers/getAdaptedStateFromPath.ts

View workflow job for this annotation

GitHub Actions / ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function

Check warning on line 27 in src/libs/Navigation/helpers/getAdaptedStateFromPath.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -31,7 +34,7 @@

type GetAdaptedStateReturnType = ReturnType<typeof getStateFromPath>;

type GetAdaptedStateFromPath = (...args: [...Parameters<typeof getStateFromPath>, shouldReplacePathInNestedState?: boolean]) => GetAdaptedStateReturnType;
type GetAdaptedStateFromPath = (...args: [...Parameters<typeof RNGetStateFromPath>, 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<NavigationState> => ({routes, index: routes.length - 1});
Expand Down Expand Up @@ -64,7 +67,7 @@
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);
Expand All @@ -87,6 +90,39 @@
// 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
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;
}

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]);
Expand Down Expand Up @@ -279,7 +315,7 @@
normalizedPath = '/';
}

const state = getStateFromPath(normalizedPath, options) as PartialState<NavigationState<RootNavigatorParamList>>;
const state = getStateFromPath(normalizedPath as RoutePath) as PartialState<NavigationState<RootNavigatorParamList>>;
if (shouldReplacePathInNestedState) {
replacePathInNestedState(state, normalizedPath);
}
Expand Down
21 changes: 21 additions & 0 deletions src/libs/Navigation/helpers/getLastSuffixFromPath.ts
Original file line number Diff line number Diff line change
@@ -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('[getLastSuffixFromPath.ts] 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;
67 changes: 67 additions & 0 deletions src/libs/Navigation/helpers/getStateForDynamicRoute.ts
Original file line number Diff line number Diff line change
@@ -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;
28 changes: 27 additions & 1 deletion src/libs/Navigation/helpers/getStateFromPath.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -14,6 +19,27 @@ function getStateFromPath(path: Route): PartialState<NavigationState> {
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);

Expand Down
11 changes: 11 additions & 0 deletions src/libs/Navigation/helpers/isDynamicRouteSuffix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {DYNAMIC_ROUTES} from '@src/ROUTES';
import type {DynamicRouteSuffix} from '@src/ROUTES';

/**
* Checks if a suffix matches any dynamic route path in DYNAMIC_ROUTES.
*/
function isDynamicRouteSuffix(suffix: string): suffix is DynamicRouteSuffix {
return Object.values(DYNAMIC_ROUTES).some((route) => route.path === suffix);
}

export default isDynamicRouteSuffix;
3 changes: 2 additions & 1 deletion src/libs/actions/Welcome/OnboardingFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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<NavigationState<RootNavigatorParamList>>);
if (focusedRoute?.name === currentRoute?.name) {
return;
Expand Down
Loading