Skip to content
Merged
46 changes: 43 additions & 3 deletions src/libs/Navigation/helpers/linkTo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,47 @@ function isNavigatingToReportWithSameReportID(currentRoute: NavigationPartialRou
return currentParams?.reportID === newParams?.reportID;
}

function isRoutePreloaded(currentState: PlatformStackNavigationState<RootNavigatorParamList>, 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<typeof findFocusedRoute>,
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<RootNavigatorParamList> | 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?");
Expand Down Expand Up @@ -126,9 +167,8 @@ export default function linkTo(navigation: NavigationContainerRef<RootNavigatorP
const matchingFullScreenRoute = getMatchingFullScreenRoute(newFocusedRoute);

const lastFullScreenRoute = currentState.routes.findLast((route) => 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);
Expand Down
101 changes: 101 additions & 0 deletions tests/navigation/NavigateTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<TestNavigationContainer
initialState={{
index: 0,
routes: [
{
name: NAVIGATORS.REPORTS_SPLIT_NAVIGATOR,
state: {
index: 0,
routes: [
{
name: SCREENS.HOME,
},
{
name: SCREENS.REPORT,
params: {reportID: '1'},
},
],
},
},
],
}}
/>,
);

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(
<TestNavigationContainer
initialState={{
index: 0,
routes: [
{
name: NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR,
state: {
index: 0,
routes: [
{
name: SCREENS.SETTINGS.ROOT,
},
{
name: SCREENS.SETTINGS.PROFILE.ROOT,
},
],
},
},
],
}}
/>,
);

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);
});
});
});
24 changes: 24 additions & 0 deletions tests/utils/TestNavigationContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import navigationRef from '@libs/Navigation/navigationRef';
import type {
AuthScreensParamList,
ReportsSplitNavigatorParamList,
RightModalNavigatorParamList,
SearchFullscreenNavigatorParamList,
SettingsSplitNavigatorParamList,
WorkspaceSplitNavigatorParamList,
Expand All @@ -21,6 +22,7 @@ const ReportsSplit = createSplitNavigator<ReportsSplitNavigatorParamList>();
const SettingsSplit = createSplitNavigator<SettingsSplitNavigatorParamList>();
const SearchStack = createPlatformStackNavigator<SearchFullscreenNavigatorParamList>();
const WorkspaceSplit = createSplitNavigator<WorkspaceSplitNavigatorParamList>();
const RightModalNavigatorStack = createSplitNavigator<RightModalNavigatorParamList>();

const getEmptyComponent = () => jest.fn();

Expand Down Expand Up @@ -103,6 +105,10 @@ function TestSettingsSplitNavigator() {
name={SCREENS.SETTINGS.ABOUT}
getComponent={getEmptyComponent}
/>
<SettingsSplit.Screen
name={SCREENS.SETTINGS.SUBSCRIPTION.ROOT}
getComponent={getEmptyComponent}
/>
</SettingsSplit.Navigator>
);
}
Expand All @@ -122,6 +128,20 @@ function TestSearchFullscreenNavigator() {
);
}

function TestRightModalNavigator() {
return (
<RightModalNavigatorStack.Navigator
defaultCentralScreen={SCREENS.RIGHT_MODAL.SETTINGS}
parentRoute={CONST.NAVIGATION_TESTS.DEFAULT_PARENT_ROUTE}
>
<RightModalNavigatorStack.Screen
name={SCREENS.RIGHT_MODAL.SETTINGS}
getComponent={getEmptyComponent()}
/>
</RightModalNavigatorStack.Navigator>
);
}

function TestNavigationContainer({initialState}: TestNavigationContainerProps) {
return (
<NavigationContainer
Expand Down Expand Up @@ -149,6 +169,10 @@ function TestNavigationContainer({initialState}: TestNavigationContainerProps) {
name={SCREENS.VALIDATE_LOGIN}
component={getEmptyComponent()}
/>
<RootStack.Screen
name={NAVIGATORS.RIGHT_MODAL_NAVIGATOR}
component={TestRightModalNavigator}
/>
</RootStack.Navigator>
</NavigationContainer>
);
Expand Down
Loading