From fe298060f2205e1765d351a4b792703eb06110ec Mon Sep 17 00:00:00 2001 From: dmkt9 Date: Tue, 11 Nov 2025 11:11:51 +0700 Subject: [PATCH 1/4] Fix - Add payment card RHP opens on the Profile page instead of Subscription --- src/libs/Navigation/helpers/linkTo/index.ts | 36 ++++++- tests/navigation/NavigateTests.tsx | 101 ++++++++++++++++++++ tests/utils/TestNavigationContainer.tsx | 24 +++++ 3 files changed, 158 insertions(+), 3 deletions(-) diff --git a/src/libs/Navigation/helpers/linkTo/index.ts b/src/libs/Navigation/helpers/linkTo/index.ts index 1649e9c7ab29..535754f57409 100644 --- a/src/libs/Navigation/helpers/linkTo/index.ts +++ b/src/libs/Navigation/helpers/linkTo/index.ts @@ -64,6 +64,36 @@ function isNavigatingToReportWithSameReportID(currentRoute: NavigationPartialRou return currentParams?.reportID === newParams?.reportID; } +function areFullScreenRoutesEqual(matchingFullScreenRoute: NavigationPartialRoute, lastFullScreenRoute: NavigationPartialRoute) { + const lastRouteInMatchingFullScreen = matchingFullScreenRoute.state?.routes?.at(-1); + const lastRouteInLastFullScreenRoute = lastFullScreenRoute.state?.routes?.at(-1); + + // We need to perform a manual check here, since it's possible to open the `WorkspaceRestrictedActionPage` via the FAB while still on the Settings page. + if (lastRouteInMatchingFullScreen?.name === SCREENS.SETTINGS.SUBSCRIPTION.ROOT && lastRouteInLastFullScreenRoute?.name !== SCREENS.SETTINGS.SUBSCRIPTION.ROOT) { + return false; + } + + const isEqualFullScreenRoute = matchingFullScreenRoute.name === lastFullScreenRoute.name; + + return isEqualFullScreenRoute; +} + +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; + + // 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 = + !lastRouteInMatchingFullScreen?.name || (preloadedRoute.params && 'screen' in preloadedRoute.params && preloadedRoute.params.screen === lastRouteInMatchingFullScreen?.name); + + return isMatchingFullScreenRoute && isMatchingLastRoute; + }); +} + 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 +156,9 @@ 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 && !areFullScreenRoutesEqual(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 ( + ); From 1cd19e5095508225da2adef2a908a2e6eeb9309d Mon Sep 17 00:00:00 2001 From: dmkt9 Date: Mon, 17 Nov 2025 19:05:46 +0700 Subject: [PATCH 2/4] Fix - Add payment card RHP opens on the Profile page instead of Subscription --- src/libs/Navigation/helpers/linkTo/index.ts | 23 +++++++-------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/libs/Navigation/helpers/linkTo/index.ts b/src/libs/Navigation/helpers/linkTo/index.ts index 535754f57409..73f750f6ec16 100644 --- a/src/libs/Navigation/helpers/linkTo/index.ts +++ b/src/libs/Navigation/helpers/linkTo/index.ts @@ -64,20 +64,6 @@ function isNavigatingToReportWithSameReportID(currentRoute: NavigationPartialRou return currentParams?.reportID === newParams?.reportID; } -function areFullScreenRoutesEqual(matchingFullScreenRoute: NavigationPartialRoute, lastFullScreenRoute: NavigationPartialRoute) { - const lastRouteInMatchingFullScreen = matchingFullScreenRoute.state?.routes?.at(-1); - const lastRouteInLastFullScreenRoute = lastFullScreenRoute.state?.routes?.at(-1); - - // We need to perform a manual check here, since it's possible to open the `WorkspaceRestrictedActionPage` via the FAB while still on the Settings page. - if (lastRouteInMatchingFullScreen?.name === SCREENS.SETTINGS.SUBSCRIPTION.ROOT && lastRouteInLastFullScreenRoute?.name !== SCREENS.SETTINGS.SUBSCRIPTION.ROOT) { - return false; - } - - const isEqualFullScreenRoute = matchingFullScreenRoute.name === lastFullScreenRoute.name; - - return isEqualFullScreenRoute; -} - function isRoutePreloaded(currentState: PlatformStackNavigationState, matchingFullScreenRoute: NavigationPartialRoute) { const lastRouteInMatchingFullScreen = matchingFullScreenRoute.state?.routes?.at(-1); @@ -156,8 +142,13 @@ export default function linkTo(navigation: NavigationContainerRef isFullScreenName(route.name)); - - if (matchingFullScreenRoute && lastFullScreenRoute && !areFullScreenRoutesEqual(matchingFullScreenRoute, lastFullScreenRoute as NavigationPartialRoute)) { + const lastRouteInLastFullScreenRoute = lastFullScreenRoute?.state?.routes.at(-1); + if ( + matchingFullScreenRoute && + lastFullScreenRoute && + (matchingFullScreenRoute.name !== lastFullScreenRoute.name || + (newFocusedRoute.name === SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD && lastRouteInLastFullScreenRoute?.name !== SCREENS.SETTINGS.SUBSCRIPTION.ROOT)) + ) { if (isRoutePreloaded(currentState, matchingFullScreenRoute)) { navigation.dispatch(StackActions.push(matchingFullScreenRoute.name)); } else { From 90ba4338a7914fc6ba10bc9cb9fe7fa7ab53302f Mon Sep 17 00:00:00 2001 From: dmkt9 Date: Fri, 19 Dec 2025 21:18:05 +0700 Subject: [PATCH 3/4] Fix - Add payment card RHP opens on the Profile page instead of Subscription --- src/libs/Navigation/helpers/linkTo/index.ts | 26 +++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/libs/Navigation/helpers/linkTo/index.ts b/src/libs/Navigation/helpers/linkTo/index.ts index 8207517f616c..6ca1726318a5 100644 --- a/src/libs/Navigation/helpers/linkTo/index.ts +++ b/src/libs/Navigation/helpers/linkTo/index.ts @@ -80,6 +80,24 @@ function isRoutePreloaded(currentState: PlatformStackNavigationState, + matchingFullScreenRoute: NavigationPartialRoute, + lastFullScreenRoute: NavigationPartialRoute, +) { + if (matchingFullScreenRoute.name !== lastFullScreenRoute.name) { + return true; + } + + const lastRouteInLastFullScreenRoute = lastFullScreenRoute?.state?.routes.at(-1); + // Always ensure that the fullscreen route for SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD is SCREENS.SETTINGS.SUBSCRIPTION + 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?"); @@ -142,13 +160,7 @@ export default function linkTo(navigation: NavigationContainerRef isFullScreenName(route.name)); - const lastRouteInLastFullScreenRoute = lastFullScreenRoute?.state?.routes.at(-1); - if ( - matchingFullScreenRoute && - lastFullScreenRoute && - (matchingFullScreenRoute.name !== lastFullScreenRoute.name || - (newFocusedRoute.name === SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD && lastRouteInLastFullScreenRoute?.name !== SCREENS.SETTINGS.SUBSCRIPTION.ROOT)) - ) { + if (matchingFullScreenRoute && lastFullScreenRoute && shouldChangeToMatchingFullScreen(newFocusedRoute, matchingFullScreenRoute, lastFullScreenRoute as NavigationPartialRoute)) { if (isRoutePreloaded(currentState, matchingFullScreenRoute)) { navigation.dispatch(StackActions.push(matchingFullScreenRoute.name)); } else { From 0814b81823b2eea987d74f92f9bc02c09f528b66 Mon Sep 17 00:00:00 2001 From: dmkt9 Date: Tue, 23 Dec 2025 10:23:29 +0700 Subject: [PATCH 4/4] Fix - Add payment card RHP opens on the Profile page instead of Subscription --- src/libs/Navigation/helpers/linkTo/index.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/libs/Navigation/helpers/linkTo/index.ts b/src/libs/Navigation/helpers/linkTo/index.ts index 6ca1726318a5..63126c40e1c7 100644 --- a/src/libs/Navigation/helpers/linkTo/index.ts +++ b/src/libs/Navigation/helpers/linkTo/index.ts @@ -72,9 +72,13 @@ function isRoutePreloaded(currentState: PlatformStackNavigationState { 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 = - !lastRouteInMatchingFullScreen?.name || (preloadedRoute.params && 'screen' in preloadedRoute.params && preloadedRoute.params.screen === lastRouteInMatchingFullScreen?.name); + const isMatchingLastRoute = preloadedRoute.params && 'screen' in preloadedRoute.params && preloadedRoute.params.screen === lastRouteInMatchingFullScreen.name; return isMatchingFullScreenRoute && isMatchingLastRoute; }); @@ -94,7 +98,10 @@ function shouldChangeToMatchingFullScreen( } const lastRouteInLastFullScreenRoute = lastFullScreenRoute?.state?.routes.at(-1); - // Always ensure that the fullscreen route for SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD is SCREENS.SETTINGS.SUBSCRIPTION + + // 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; }