Files
CBS-Mobile-App/src/navigation/AuthNavigator.tsx
T

109 lines
4.6 KiB
TypeScript

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<AuthStackParamList>();
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<keyof AuthStackParamList | null>(null);
if (frozenInitialRoute.current === null) {
frozenInitialRoute.current =
registrationEnabled && contextLoaded && !lastProfile ? 'Register' : 'Login';
}
const initialRouteName = frozenInitialRoute.current;
return (
<Stack.Navigator
initialRouteName={initialRouteName}
screenOptions={{
headerShown: false,
animation: 'slide_from_right',
}}
>
<Stack.Screen name="Login" component={LoginScreen} />
{registrationEnabled && (
<>
<Stack.Screen name="Register" component={RegisterScreen} />
<Stack.Screen name="VerifyOtp" component={VerifyOtpScreen} />
</>
)}
{/* 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. */}
<Stack.Screen
name="CreateMpin"
component={CreateMpinScreen}
options={{ gestureEnabled: false }}
/>
<Stack.Screen
name="MpinLocked"
component={MpinLockedScreen}
options={{ gestureEnabled: false, animation: 'fade' }}
/>
</Stack.Navigator>
);
};