diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 63e8fc8ecb43..f73878b602d3 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -37,6 +37,7 @@ import {isFullScreenName, isOnboardingFlowName, isSplitNavigatorName} from './he import isReportOpenInRHP from './helpers/isReportOpenInRHP'; import isReportTopmostSplitNavigator from './helpers/isReportTopmostSplitNavigator'; import isSideModalNavigator from './helpers/isSideModalNavigator'; +import isTabNavigatorReady from './helpers/isTabNavigatorReady'; import linkTo from './helpers/linkTo'; import getMinimalAction from './helpers/linkTo/getMinimalAction'; import type {LinkToOptions} from './helpers/linkTo/types'; @@ -698,8 +699,16 @@ function navContainsProtectedRoutes(state: State | undefined): boolean { return false; } - // If one protected screen is in the routeNames then other screens are there as well. - return state?.routeNames.includes(PROTECTED_SCREENS.CONCIERGE); + if (!state.routeNames.includes(PROTECTED_SCREENS.CONCIERGE)) { + return false; + } + + // routeNames only tells us screens are declared on the root navigator. + // We also need TabNavigator to be mounted (its child router has run + // useNavigationBuilder and produced a non-stale nested state), otherwise a + // deferred NAVIGATE targeting a screen inside TabNavigator will be dispatched + // before any child router is registered to handle it. + return isTabNavigatorReady(state); } /** diff --git a/src/libs/Navigation/helpers/isTabNavigatorReady.ts b/src/libs/Navigation/helpers/isTabNavigatorReady.ts new file mode 100644 index 000000000000..5ace84f36528 --- /dev/null +++ b/src/libs/Navigation/helpers/isTabNavigatorReady.ts @@ -0,0 +1,15 @@ +import type {NavigationState, PartialState} from '@react-navigation/native'; +import NAVIGATORS from '@src/NAVIGATORS'; + +/** + * Checks whether TabNavigator's child router has mounted and produced a + * non-stale nested state. Until `useNavigationBuilder` runs inside + * TabNavigator, the route's nested state stays `stale: true` and React + * Navigation cannot handle NAVIGATE actions targeting screens inside it. + */ +function isTabNavigatorReady(state: NavigationState | PartialState | undefined): boolean { + const tabRoute = state?.routes?.find((route) => route.name === NAVIGATORS.TAB_NAVIGATOR); + return tabRoute?.state?.stale === false; +} + +export default isTabNavigatorReady; diff --git a/src/libs/Navigation/linkingConfig/subscribe.ts b/src/libs/Navigation/linkingConfig/subscribe.ts index 7824115a8e70..14381547c962 100644 --- a/src/libs/Navigation/linkingConfig/subscribe.ts +++ b/src/libs/Navigation/linkingConfig/subscribe.ts @@ -2,6 +2,7 @@ import type {LinkingOptions} from '@react-navigation/native'; import {findFocusedRoute} from '@react-navigation/native'; import {Linking} from 'react-native'; import continuePlaidOAuth from '@libs/continuePlaidOAuth'; +import isTabNavigatorReady from '@libs/Navigation/helpers/isTabNavigatorReady'; import navigationRef from '@libs/Navigation/navigationRef'; import type {RootNavigatorParamList} from '@libs/Navigation/types'; import CONST from '@src/CONST'; @@ -31,6 +32,16 @@ const subscribe: LinkingOptions['subscribe'] = (listener continuePlaidOAuth(url); return; } + // Skip forwarding URLs while TabNavigator is mounting — its child + // router hasn't run useNavigationBuilder yet, so React Navigation + // can't handle nested NAVIGATE actions and throws an unhandled-action + // error. Protected-screen deep links will be handled separately by + // openReportFromDeepLink via waitForProtectedRoutes(). + const state = navigationRef.current?.getRootState(); + if (!isTabNavigatorReady(state)) { + return; + } + listener(url); }); return () => subscription.remove();