From e738c816d9de938ee5e424e11aa89094d9471c40 Mon Sep 17 00:00:00 2001 From: "daledah (via MelvinBot)" Date: Wed, 27 May 2026 07:20:25 +0000 Subject: [PATCH 1/3] Wait for TabNavigator to mount before resolving waitForProtectedRoutes navContainsProtectedRoutes previously only checked state.routeNames for CONCIERGE, but routeNames reflects declared screens, not mounted ones. TabNavigator appears in routeNames simultaneously with Concierge, so waitForProtectedRoutes resolved before TabNavigator's child router was registered. Deep link navigation dispatched during this window produced an unhandled NAVIGATE action error. Now additionally verify that the TabNavigator route exists in state.routes with stale === false (set by useNavigationBuilder after mount), ensuring the navigator can handle nested navigation actions. Co-authored-by: daledah --- src/libs/Navigation/Navigation.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 63e8fc8ecb43..7fb26fbd9b3b 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -698,8 +698,17 @@ 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. + const tabRoute = state.routes?.find((route) => route.name === NAVIGATORS.TAB_NAVIGATOR); + return tabRoute?.state?.stale === false; } /** From 57796ecf1a01245ede9ee6f51fcf1c5b9c205a06 Mon Sep 17 00:00:00 2001 From: "daledah (via MelvinBot)" Date: Wed, 27 May 2026 11:37:38 +0000 Subject: [PATCH 2/3] Add stale guard in subscribe.ts to skip URL forwarding before TabNavigator mounts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The subscribe listener forwards URLs directly to React Navigation via listener(url). When TabNavigator exists in routes but hasn't finished mounting (stale !== false), this dispatch hits an unregistered child router and throws an unhandled NAVIGATE action error. Skip forwarding in this window — deep links for protected screens are already handled by openReportFromDeepLink via waitForProtectedRoutes(). Co-authored-by: daledah --- src/libs/Navigation/linkingConfig/subscribe.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/libs/Navigation/linkingConfig/subscribe.ts b/src/libs/Navigation/linkingConfig/subscribe.ts index 7824115a8e70..24f24cae8c4c 100644 --- a/src/libs/Navigation/linkingConfig/subscribe.ts +++ b/src/libs/Navigation/linkingConfig/subscribe.ts @@ -5,6 +5,7 @@ import continuePlaidOAuth from '@libs/continuePlaidOAuth'; import navigationRef from '@libs/Navigation/navigationRef'; import type {RootNavigatorParamList} from '@libs/Navigation/types'; import CONST from '@src/CONST'; +import NAVIGATORS from '@src/NAVIGATORS'; import ROUTES from '@src/ROUTES'; const subscribe: LinkingOptions['subscribe'] = (listener) => { @@ -31,6 +32,17 @@ 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(); + const tabRoute = state?.routes?.find((route) => route.name === NAVIGATORS.TAB_NAVIGATOR); + if (tabRoute && tabRoute.state?.stale !== false) { + return; + } + listener(url); }); return () => subscription.remove(); From 8776ce3b08b2fa8516ea2b4190256f6b7312ba08 Mon Sep 17 00:00:00 2001 From: "daledah (via MelvinBot)" Date: Wed, 27 May 2026 11:53:25 +0000 Subject: [PATCH 3/3] Extract isTabNavigatorReady helper to deduplicate stale check The TabNavigator readiness check (find route, verify stale === false) was duplicated between subscribe.ts and navContainsProtectedRoutes in Navigation.ts. Extract into a shared helper so the criteria only need to be updated in one place. Co-authored-by: daledah --- src/libs/Navigation/Navigation.ts | 4 ++-- .../Navigation/helpers/isTabNavigatorReady.ts | 15 +++++++++++++++ src/libs/Navigation/linkingConfig/subscribe.ts | 5 ++--- 3 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 src/libs/Navigation/helpers/isTabNavigatorReady.ts diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 7fb26fbd9b3b..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'; @@ -707,8 +708,7 @@ function navContainsProtectedRoutes(state: State | undefined): boolean { // 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. - const tabRoute = state.routes?.find((route) => route.name === NAVIGATORS.TAB_NAVIGATOR); - return tabRoute?.state?.stale === false; + 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 24f24cae8c4c..14381547c962 100644 --- a/src/libs/Navigation/linkingConfig/subscribe.ts +++ b/src/libs/Navigation/linkingConfig/subscribe.ts @@ -2,10 +2,10 @@ 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'; -import NAVIGATORS from '@src/NAVIGATORS'; import ROUTES from '@src/ROUTES'; const subscribe: LinkingOptions['subscribe'] = (listener) => { @@ -38,8 +38,7 @@ const subscribe: LinkingOptions['subscribe'] = (listener // error. Protected-screen deep links will be handled separately by // openReportFromDeepLink via waitForProtectedRoutes(). const state = navigationRef.current?.getRootState(); - const tabRoute = state?.routes?.find((route) => route.name === NAVIGATORS.TAB_NAVIGATOR); - if (tabRoute && tabRoute.state?.stale !== false) { + if (!isTabNavigatorReady(state)) { return; }