import React, { useRef } from 'react'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { useAppSelector } from '../app/hooks'; import { selectLastProfile, selectSignInContextLoaded, } from '../modules/auth/store/authSelectors'; import { FeatureKey } from '../constants/featureKeys'; import { useFeature } from '../features'; import type { AuthStackParamList } from './types'; import LoginScreen from '../modules/auth/screens/LoginScreen'; import RegisterScreen from '../modules/auth/screens/RegisterScreen'; import VerifyOtpScreen from '../modules/auth/screens/VerifyOtpScreen'; import CreateMpinScreen from '../modules/auth/screens/CreateMpinScreen'; import MpinLockedScreen from '../modules/auth/screens/MpinLockedScreen'; const Stack = createNativeStackNavigator(); export const AuthNavigator = () => { const lastProfile = useAppSelector(selectLastProfile); const contextLoaded = useAppSelector(selectSignInContextLoaded); /** * Which screen a customer owes us depends on whether this device has ever * completed registration: a stored profile means they only owe an MPIN, * nothing stored means registration. * * Decided here rather than by navigating out of the sign-in screen. An * effect-driven `replace()` on a screen's first render races the navigation * container's readiness and the action is silently dropped — and the warning * that would tell you so is stripped from release builds, so it fails only * where it is hardest to see. This is also flash-free. * * `contextLoaded` is what makes it safe: before the Keystore has been read, a * null profile means "unknown", not "never registered". */ /** * Registration is switchable, and turning it off unregisters the three screens * that make it up — so the initial route has to fall back to sign-in, or it * would name a route that no longer exists and the navigator would throw. * * A device with nothing stored then lands on a sign-in screen it cannot * complete, which is the correct outcome: the bank has stopped onboarding, and * the app should say nothing rather than offer a flow that will be refused. */ const registrationEnabled = useFeature(FeatureKey.AUTH_REGISTER); /** * Read once, on the navigator's first render, and frozen from then on. * * `initialRouteName` is not a live prop — React Navigation rebuilds its route * list when it changes, and the inputs here are anything but stable: * verify-otp records a `lastProfile` and flips this from 'Register' to * 'Login' in the very same tick that the OTP screen calls `reset()`. The * rebuild and the reset then describe two different stacks to Fabric in one * commit, and mounting the result throws "The specified child already has a * parent" — a red box on the way in, and a blank white screen once it is * dismissed, with no way to sign in. * * Freezing it costs nothing, because nothing here needs it to be live: * `lastProfile` arriving later is what `LoginScreen` already watches, and it * routes to registration itself. */ const frozenInitialRoute = useRef(null); if (frozenInitialRoute.current === null) { frozenInitialRoute.current = registrationEnabled && contextLoaded && !lastProfile ? 'Register' : 'Login'; } const initialRouteName = frozenInitialRoute.current; return ( {registrationEnabled && ( <> )} {/* Deliberately still registered when registration is switched off. A customer who has already passed OTP holds a single-use setup token — unregistering this screen under them would leave them unable to finish and unable to start again, because the token is spent. Switching the feature off closes the entrance; it does not trap anyone inside. No swipe back out of MPIN creation, for that same single-use reason. */} ); };