109 lines
4.6 KiB
TypeScript
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>
|
|
);
|
|
};
|