Skip to content

Commit 6f59731

Browse files
authored
Merge pull request #91796 from Expensify/claude-fixTabNavigatorDeepLinkRace
Wait for TabNavigator mount before resolving waitForProtectedRoutes
2 parents c724929 + 8776ce3 commit 6f59731

3 files changed

Lines changed: 37 additions & 2 deletions

File tree

src/libs/Navigation/Navigation.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {isFullScreenName, isOnboardingFlowName, isSplitNavigatorName} from './he
3737
import isReportOpenInRHP from './helpers/isReportOpenInRHP';
3838
import isReportTopmostSplitNavigator from './helpers/isReportTopmostSplitNavigator';
3939
import isSideModalNavigator from './helpers/isSideModalNavigator';
40+
import isTabNavigatorReady from './helpers/isTabNavigatorReady';
4041
import linkTo from './helpers/linkTo';
4142
import getMinimalAction from './helpers/linkTo/getMinimalAction';
4243
import type {LinkToOptions} from './helpers/linkTo/types';
@@ -698,8 +699,16 @@ function navContainsProtectedRoutes(state: State | undefined): boolean {
698699
return false;
699700
}
700701

701-
// If one protected screen is in the routeNames then other screens are there as well.
702-
return state?.routeNames.includes(PROTECTED_SCREENS.CONCIERGE);
702+
if (!state.routeNames.includes(PROTECTED_SCREENS.CONCIERGE)) {
703+
return false;
704+
}
705+
706+
// routeNames only tells us screens are declared on the root navigator.
707+
// We also need TabNavigator to be mounted (its child router has run
708+
// useNavigationBuilder and produced a non-stale nested state), otherwise a
709+
// deferred NAVIGATE targeting a screen inside TabNavigator will be dispatched
710+
// before any child router is registered to handle it.
711+
return isTabNavigatorReady(state);
703712
}
704713

705714
/**
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type {NavigationState, PartialState} from '@react-navigation/native';
2+
import NAVIGATORS from '@src/NAVIGATORS';
3+
4+
/**
5+
* Checks whether TabNavigator's child router has mounted and produced a
6+
* non-stale nested state. Until `useNavigationBuilder` runs inside
7+
* TabNavigator, the route's nested state stays `stale: true` and React
8+
* Navigation cannot handle NAVIGATE actions targeting screens inside it.
9+
*/
10+
function isTabNavigatorReady(state: NavigationState | PartialState<NavigationState> | undefined): boolean {
11+
const tabRoute = state?.routes?.find((route) => route.name === NAVIGATORS.TAB_NAVIGATOR);
12+
return tabRoute?.state?.stale === false;
13+
}
14+
15+
export default isTabNavigatorReady;

src/libs/Navigation/linkingConfig/subscribe.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {LinkingOptions} from '@react-navigation/native';
22
import {findFocusedRoute} from '@react-navigation/native';
33
import {Linking} from 'react-native';
44
import continuePlaidOAuth from '@libs/continuePlaidOAuth';
5+
import isTabNavigatorReady from '@libs/Navigation/helpers/isTabNavigatorReady';
56
import navigationRef from '@libs/Navigation/navigationRef';
67
import type {RootNavigatorParamList} from '@libs/Navigation/types';
78
import CONST from '@src/CONST';
@@ -31,6 +32,16 @@ const subscribe: LinkingOptions<RootNavigatorParamList>['subscribe'] = (listener
3132
continuePlaidOAuth(url);
3233
return;
3334
}
35+
// Skip forwarding URLs while TabNavigator is mounting — its child
36+
// router hasn't run useNavigationBuilder yet, so React Navigation
37+
// can't handle nested NAVIGATE actions and throws an unhandled-action
38+
// error. Protected-screen deep links will be handled separately by
39+
// openReportFromDeepLink via waitForProtectedRoutes().
40+
const state = navigationRef.current?.getRootState();
41+
if (!isTabNavigatorReady(state)) {
42+
return;
43+
}
44+
3445
listener(url);
3546
});
3647
return () => subscription.remove();

0 commit comments

Comments
 (0)