diff --git a/src/libs/Navigation/helpers/linkTo/index.ts b/src/libs/Navigation/helpers/linkTo/index.ts index 6f3142148ea7..63126c40e1c7 100644 --- a/src/libs/Navigation/helpers/linkTo/index.ts +++ b/src/libs/Navigation/helpers/linkTo/index.ts @@ -64,6 +64,47 @@ function isNavigatingToReportWithSameReportID(currentRoute: NavigationPartialRou return currentParams?.reportID === newParams?.reportID; } +function isRoutePreloaded(currentState: PlatformStackNavigationState, matchingFullScreenRoute: NavigationPartialRoute) { + const lastRouteInMatchingFullScreen = matchingFullScreenRoute.state?.routes?.at(-1); + + const preloadedRoutes = currentState.preloadedRoutes; + + return preloadedRoutes.some((preloadedRoute) => { + const isMatchingFullScreenRoute = preloadedRoute.name === matchingFullScreenRoute.name; + + // If the matching fullscreen route does not have a last route, then we only need to compare the fullscreen route name + if (!lastRouteInMatchingFullScreen?.name) { + return isMatchingFullScreenRoute; + } + + // Compare the last route of the preloadedRoute and the last route of the matchingFullScreenRoute to ensure the preloaded route is accepted when matching subroutes as well + const isMatchingLastRoute = preloadedRoute.params && 'screen' in preloadedRoute.params && preloadedRoute.params.screen === lastRouteInMatchingFullScreen.name; + + return isMatchingFullScreenRoute && isMatchingLastRoute; + }); +} + +/** + * We will check whether we need to navigate with the target route along with the changes of the fullscreen route. + * When the fullscreen route needs to change, the background of the route will change according to the matchingFullScreenRoute. + */ +function shouldChangeToMatchingFullScreen( + newFocusedRoute: ReturnType, + matchingFullScreenRoute: NavigationPartialRoute, + lastFullScreenRoute: NavigationPartialRoute, +) { + if (matchingFullScreenRoute.name !== lastFullScreenRoute.name) { + return true; + } + + const lastRouteInLastFullScreenRoute = lastFullScreenRoute?.state?.routes.at(-1); + + // We always want the fullscreen route of SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD to be the SUBSCRIPTION tab of SCREENS.SETTINGS. + // The add payment card page can be opened via the Global create button from the create expense flow, so even when we are already on SCREENS.SETTINGS, with any tab currently open, + // the add payment card page can still be opened. Therefore, checking only the fullscreen name above is not sufficient, and the check below using the last route name is necessary. + return newFocusedRoute?.name === SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD && lastRouteInLastFullScreenRoute?.name !== SCREENS.SETTINGS.SUBSCRIPTION.ROOT; +} + export default function linkTo(navigation: NavigationContainerRef | null, path: Route, options?: LinkToOptions) { if (!navigation) { throw new Error("Couldn't find a navigation object. Is your component inside a screen in a navigator?"); @@ -126,9 +167,8 @@ export default function linkTo(navigation: NavigationContainerRef isFullScreenName(route.name)); - if (matchingFullScreenRoute && lastFullScreenRoute && matchingFullScreenRoute.name !== lastFullScreenRoute.name) { - const isMatchingRoutePreloaded = currentState.preloadedRoutes.some((preloadedRoute) => preloadedRoute.name === matchingFullScreenRoute.name); - if (isMatchingRoutePreloaded) { + if (matchingFullScreenRoute && lastFullScreenRoute && shouldChangeToMatchingFullScreen(newFocusedRoute, matchingFullScreenRoute, lastFullScreenRoute as NavigationPartialRoute)) { + if (isRoutePreloaded(currentState, matchingFullScreenRoute)) { navigation.dispatch(StackActions.push(matchingFullScreenRoute.name)); } else { const lastRouteInMatchingFullScreen = matchingFullScreenRoute.state?.routes?.at(-1); diff --git a/tests/navigation/NavigateTests.tsx b/tests/navigation/NavigateTests.tsx index 084c139cc84b..28825c199ff4 100644 --- a/tests/navigation/NavigateTests.tsx +++ b/tests/navigation/NavigateTests.tsx @@ -145,5 +145,106 @@ describe('Navigate', () => { expect(rootStateAfterNavigate?.index).toBe(1); expect(lastSplitAfterNavigate?.name).toBe(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR); }); + + it('to the sub-route from a different split navigator', () => { + // Given the initialized navigation on the narrow layout with the reports split navigator + render( + , + ); + + const rootStateBeforeNavigate = navigationRef.current?.getRootState(); + const lastSplitBeforeNavigate = rootStateBeforeNavigate?.routes.at(-1); + expect(rootStateBeforeNavigate?.index).toBe(0); + expect(lastSplitBeforeNavigate?.name).toBe(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR); + expect(lastSplitBeforeNavigate?.state?.routes.at(-1)?.name).toBe(SCREENS.REPORT); + + // When navigate to the page from the different split navigator + act(() => { + Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION_ADD_PAYMENT_CARD); + }); + + // Then push a new split navigator to the navigation state + const rootStateAfterNavigate = navigationRef.current?.getRootState(); + expect(rootStateAfterNavigate?.index).toBe(2); + + const middleSplitAfterNavigate = rootStateAfterNavigate?.routes.at(-2); + expect(middleSplitAfterNavigate?.name).toBe(NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR); + expect(middleSplitAfterNavigate?.state?.routes.at(-1)?.name).toBe(SCREENS.SETTINGS.SUBSCRIPTION.ROOT); + + const lastSplitAfterNavigate = rootStateAfterNavigate?.routes.at(-1); + expect(lastSplitAfterNavigate?.name).toBe(NAVIGATORS.RIGHT_MODAL_NAVIGATOR); + expect(lastSplitAfterNavigate?.state?.routes.at(-1)?.name).toBe(SCREENS.RIGHT_MODAL.SETTINGS); + }); + + it('to the sub-route from a same split navigator', () => { + // Given the initialized navigation on the narrow layout with the settings split navigator + render( + , + ); + + const rootStateBeforeNavigate = navigationRef.current?.getRootState(); + const lastSplitBeforeNavigate = rootStateBeforeNavigate?.routes.at(-1); + expect(rootStateBeforeNavigate?.index).toBe(0); + expect(lastSplitBeforeNavigate?.name).toBe(NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR); + expect(lastSplitBeforeNavigate?.state?.routes.at(-1)?.name).toBe(SCREENS.SETTINGS.PROFILE.ROOT); + + // When navigate to the page from the same split navigator + act(() => { + Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION_ADD_PAYMENT_CARD); + }); + + // Then push a new split navigator to the navigation state + const rootStateAfterNavigate = navigationRef.current?.getRootState(); + expect(rootStateAfterNavigate?.index).toBe(2); + + const middleSplitAfterNavigate = rootStateAfterNavigate?.routes.at(-2); + expect(middleSplitAfterNavigate?.name).toBe(NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR); + expect(middleSplitAfterNavigate?.state?.routes.at(-1)?.name).toBe(SCREENS.SETTINGS.SUBSCRIPTION.ROOT); + + const lastSplitAfterNavigate = rootStateAfterNavigate?.routes.at(-1); + expect(lastSplitAfterNavigate?.name).toBe(NAVIGATORS.RIGHT_MODAL_NAVIGATOR); + expect(lastSplitAfterNavigate?.state?.routes.at(-1)?.name).toBe(SCREENS.RIGHT_MODAL.SETTINGS); + }); }); }); diff --git a/tests/utils/TestNavigationContainer.tsx b/tests/utils/TestNavigationContainer.tsx index 0c3370f12de0..8ae0725f83bf 100644 --- a/tests/utils/TestNavigationContainer.tsx +++ b/tests/utils/TestNavigationContainer.tsx @@ -7,6 +7,7 @@ import navigationRef from '@libs/Navigation/navigationRef'; import type { AuthScreensParamList, ReportsSplitNavigatorParamList, + RightModalNavigatorParamList, SearchFullscreenNavigatorParamList, SettingsSplitNavigatorParamList, WorkspaceSplitNavigatorParamList, @@ -21,6 +22,7 @@ const ReportsSplit = createSplitNavigator(); const SettingsSplit = createSplitNavigator(); const SearchStack = createPlatformStackNavigator(); const WorkspaceSplit = createSplitNavigator(); +const RightModalNavigatorStack = createSplitNavigator(); const getEmptyComponent = () => jest.fn(); @@ -103,6 +105,10 @@ function TestSettingsSplitNavigator() { name={SCREENS.SETTINGS.ABOUT} getComponent={getEmptyComponent} /> + ); } @@ -122,6 +128,20 @@ function TestSearchFullscreenNavigator() { ); } +function TestRightModalNavigator() { + return ( + + + + ); +} + function TestNavigationContainer({initialState}: TestNavigationContainerProps) { return ( + );