diff --git a/src/components/MultifactorAuthentication/mfaNavigation.ts b/src/components/MultifactorAuthentication/mfaNavigation.ts index d204808552d1..127f341845a8 100644 --- a/src/components/MultifactorAuthentication/mfaNavigation.ts +++ b/src/components/MultifactorAuthentication/mfaNavigation.ts @@ -1,5 +1,7 @@ import {createNavigationContainerRef, StackActions} from '@react-navigation/native'; import type {MultifactorAuthenticationModalNavigatorParamList} from '@libs/Navigation/types'; +import CONFIG from '@src/CONFIG'; +import SCREENS from '@src/SCREENS'; /** * Internal placeholder used only as a mount-time buffer inside this module. @@ -14,6 +16,22 @@ type MultifactorAuthenticationModalNavigatorInternalParamList = MultifactorAuthe const mfaNavigationRef = createNavigationContainerRef(); +// Outcome screens are terminal states the flow ends on. +const OUTCOME_SCREENS = new Set([ + SCREENS.MULTIFACTOR_AUTHENTICATION.OUTCOME_SUCCESS, + SCREENS.MULTIFACTOR_AUTHENTICATION.OUTCOME_FAILURE, +]); + +// Screens that live inside this independent overlay navigator. REVOKE and AUTHORIZE_TRANSACTION are intentionally excluded: they +// render in the main RHP modal stack, not this tree. +const MFA_OVERLAY_SCREENS = new Set([ + MFA_INITIAL_SCREEN, + SCREENS.MULTIFACTOR_AUTHENTICATION.MAGIC_CODE, + SCREENS.MULTIFACTOR_AUTHENTICATION.PROMPT, + SCREENS.MULTIFACTOR_AUTHENTICATION.OUTCOME_SUCCESS, + SCREENS.MULTIFACTOR_AUTHENTICATION.OUTCOME_FAILURE, +]); + let pendingNavigation: {screen: string; params?: Record} | undefined; /** @@ -53,6 +71,16 @@ function navigate | undefined>(undefined); + type ScreenWrapperChildrenProps = { insets: EdgeInsets; safeAreaPaddingBottomStyle?: { @@ -190,10 +194,21 @@ function ScreenWrapper({ const initialURLWithoutParams = initialURL?.replaceAll(/\?.*/g, ''); const doesInitialURLMatchActiveRoute = !!initialURLWithoutParams?.endsWith(Navigation.getActiveRouteWithoutParams() || initialActiveRouteWithoutParams); - usePreventRemove(isSingleNewDotEntry && doesInitialURLMatchActiveRoute && !shouldBlockSingleEntryOldAppExit, () => { + // A multifactor authentication flow (e.g. Face ID to reveal UK/EU card details) renders in an independent navigation tree + // overlaid above the single NewDot entry, and each MFA screen renders its own ScreenWrapper. Navigation.getActiveRouteWithoutParams() + // still reports the underlying NewDot route while the overlay is up, so doesInitialURLMatchActiveRoute is true on the MFA screens too. + // usePreventRemove calls e.preventDefault() unconditionally whenever the guard is active, so guarding these instances would consume the + // MFA navigator's own stack actions (e.g. replacing the magic-code page with the Face ID prompt, or popping the outcome screen on close), + // blocking transitions within the flow. Guard only the outer NewDot ScreenWrapper, never the ScreenWrappers inside the MFA overlay. + // NavigationRouteContext is undefined when tests mock @react-navigation/native without re-exporting it, so fall back to a noop context to keep useContext valid. + const route = useContext(NavigationRouteContext ?? FallbackRouteContext); + const isMfaOverlayScreen = !!route && MFA_OVERLAY_SCREENS.has(route.name); + + usePreventRemove(isSingleNewDotEntry && doesInitialURLMatchActiveRoute && !shouldBlockSingleEntryOldAppExit && !isMfaOverlayScreen, () => { if (!CONFIG.IS_HYBRID_APP) { return; } + closeReactNativeApp({shouldSetNVP: false, isTrackingGPS: false}); });