Skip to content
Draft
3 changes: 1 addition & 2 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ type DynamicRoutes = Record<string, DynamicRouteConfig>;
const DYNAMIC_ROUTES = {
VERIFY_ACCOUNT: {
path: 'verify-account',
entryScreens: [],
entryScreens: [SCREENS.SETTINGS.WALLET.ROOT],
},
} as const satisfies DynamicRoutes;

Expand Down Expand Up @@ -333,7 +333,6 @@ const ROUTES = {
SETTINGS_ABOUT: 'settings/about',
SETTINGS_APP_DOWNLOAD_LINKS: 'settings/about/app-download-links',
SETTINGS_WALLET: 'settings/wallet',
SETTINGS_WALLET_VERIFY_ACCOUNT: `settings/wallet/${VERIFY_ACCOUNT}`,
SETTINGS_WALLET_DOMAIN_CARD: {
route: 'settings/wallet/card/:cardID?',
getRoute: (cardID: string) => `settings/wallet/card/${cardID}` as const,
Expand Down
1 change: 0 additions & 1 deletion src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,6 @@ const SCREENS = {

WALLET: {
ROOT: 'Settings_Wallet',
VERIFY_ACCOUNT: 'Settings_Wallet_VerifyAccount',
DOMAIN_CARD: 'Settings_Wallet_DomainCard',
DOMAIN_CARD_CONFIRM_MAGIC_CODE: 'Settings_Wallet_DomainCard_ConfirmMagicCode',
CARD_MISSING_DETAILS: 'Settings_Wallet_Card_MissingDetails',
Expand Down
33 changes: 33 additions & 0 deletions src/hooks/useDynamicBackPath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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, or HOME if path is null/undefined
*/
function useDynamicBackPath(dynamicRouteSuffix: DynamicRouteSuffix): Route {
const path = useNavigationState((state) => getPathFromState(state));

if (!path) {
return ROUTES.HOME;
}

const [normalizedPath, query] = splitPathAndQuery(path);

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 path as Route;
}

export default useDynamicBackPath;
5 changes: 2 additions & 3 deletions src/libs/AppState/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ const ConsoleModalStackNavigator = createModalStackNavigator<ConsoleNavigatorPar
});

const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorParamList>({
[SCREENS.SETTINGS.DYNAMIC_VERIFY_ACCOUNT]: () => require<ReactComponentModule>('../../../../pages/settings/DynamicVerifyAccountPage').default,
[SCREENS.SETTINGS.SHARE_CODE]: () => require<ReactComponentModule>('../../../../pages/ShareCodePage').default,
[SCREENS.SETTINGS.PROFILE.PRONOUNS]: () => require<ReactComponentModule>('../../../../pages/settings/Profile/PronounsPage').default,
[SCREENS.SETTINGS.PROFILE.DISPLAY_NAME]: () => require<ReactComponentModule>('../../../../pages/settings/Profile/DisplayNamePage').default,
Expand Down Expand Up @@ -381,7 +382,6 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.SETTINGS.CONSOLE]: () => require<ReactComponentModule>('../../../../pages/settings/AboutPage/ConsolePage').default,
[SCREENS.SETTINGS.SHARE_LOG]: () => require<ReactComponentModule>('../../../../pages/settings/AboutPage/ShareLogPage').default,
[SCREENS.SETTINGS.WALLET.CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS]: () => require<ReactComponentModule>('../../../../pages/settings/Profile/PersonalDetails/PersonalAddressPage').default,
[SCREENS.SETTINGS.WALLET.VERIFY_ACCOUNT]: () => require<ReactComponentModule>('../../../../pages/settings/Wallet/VerifyAccountPage').default,
[SCREENS.SETTINGS.WALLET.DOMAIN_CARD]: () => require<ReactComponentModule>('../../../../pages/settings/Wallet/ExpensifyCardPage/index').default,
[SCREENS.SETTINGS.WALLET.DOMAIN_CARD_CONFIRM_MAGIC_CODE]: () =>
require<ReactComponentModule>('../../../../pages/settings/Wallet/ExpensifyCardPage/ExpensifyCardVerifyAccountPage').default,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ParamListBase>) {
// 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;
5 changes: 3 additions & 2 deletions src/libs/Navigation/Navigation.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -212,7 +213,7 @@ function getActiveRoute(): string {
return '';
}

const routeFromState = getPathFromState(navigationRef.getRootState(), linkingConfig.config);
const routeFromState = getPathFromState(navigationRef.getRootState());

if (routeFromState) {
return routeFromState;
Expand Down
4 changes: 2 additions & 2 deletions src/libs/Navigation/NavigationRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);
}
}

Expand Down
27 changes: 27 additions & 0 deletions src/libs/Navigation/helpers/createDynamicRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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);
let newPath = `${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;
46 changes: 40 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} 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,37 @@
// 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]);
Expand Down Expand Up @@ -278,7 +312,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
23 changes: 23 additions & 0 deletions src/libs/Navigation/helpers/getLastSuffixFromPath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Log from '@libs/Log';
import splitPathAndQuery from './splitPathAndQuery';

/**
* 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 [normalizedPath] = splitPathAndQuery(path ?? '');

if (!normalizedPath) {
Log.warn('[getLastSuffixFromPath.ts] Provided path is undefined or empty.');
return '';
}

const lastSuffix = normalizedPath.split('/').pop() ?? '';

return lastSuffix;
}

export default getLastSuffixFromPath;
37 changes: 37 additions & 0 deletions src/libs/Navigation/helpers/getPathFromState.ts
Original file line number Diff line number Diff line change
@@ -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<PartialState<NavigationState>, '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;
Loading
Loading