import React, { useEffect, useState } from 'react'; import { StyleSheet, Switch, Text, View } from 'react-native'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; import Ionicons from '@react-native-vector-icons/ionicons'; import { Button, Card, ScreenShell } from '../../../components'; import { useAppDispatch, useAppSelector } from '../../../app/hooks'; import { useTheme, fontFamily } from '../../../theme'; import { useApiErrorToast } from '../../../hooks/useApiErrorToast'; import { biometrics } from '../../../services/security/biometrics'; import { authActions } from '../store/authSlice'; import { selectRegistration } from '../store/authSelectors'; import { completeMpinSetup, rememberRegisteredProfile } from '../store/authThunk'; import { MPIN_LENGTH, describeMpinProblem, isValidMpin } from '../utils/authValidation'; import { CodeInput } from '../components/CodeInput'; import type { AuthStackParamList } from '../../../navigation/types'; type Props = Readonly>; export default function CreateMpinScreen({ navigation }: Props) { const dispatch = useAppDispatch(); const theme = useTheme(); const showApiError = useApiErrorToast(); const registration = useAppSelector(selectRegistration); const [mpin, setMpin] = useState(''); const [confirmMpin, setConfirmMpin] = useState(''); const [rememberForBiometrics, setRememberForBiometrics] = useState(false); const [biometricsAvailable, setBiometricsAvailable] = useState(false); const [submitted, setSubmitted] = useState(false); const isLoading = registration.status === 'loading'; useEffect(() => { void biometrics.isAvailable().then(setBiometricsAvailable); }, []); // A setup token this screen never received cannot be recovered from here — // the five-minute window has to be reopened from the start. useEffect(() => { if (!registration.setupToken && !isLoading && !submitted) { dispatch(authActions.resetRegistration()); navigation.navigate('Register'); } }, [dispatch, isLoading, navigation, registration.setupToken, submitted]); useEffect(() => { if (!submitted || registration.status !== 'failed' || !registration.error) { return; } setSubmitted(false); showApiError(registration.error); const { code } = registration.error; if (code === 'OTP_NOT_VERIFIED') { dispatch(authActions.resetRegistration()); navigation.navigate('Register'); return; } if (code === 'MPIN_ALREADY_SET') { // verify-otp said this profile still owed an MPIN and the server now says // otherwise, so the profile has to be recorded here — nothing earlier in // this path stored one, and sign-in would read that as a device that has // never registered and send them back for another OTP. void dispatch( rememberRegisteredProfile({ orgId: registration.orgId, mobileNumber: registration.mobileNumber, }), ); dispatch(authActions.resetRegistration()); navigation.navigate('Login'); return; } setMpin(''); setConfirmMpin(''); }, [ dispatch, navigation, registration.error, registration.mobileNumber, registration.orgId, registration.status, showApiError, submitted, ]); const mpinProblem = describeMpinProblem(mpin); const confirmProblem = confirmMpin.length === MPIN_LENGTH && confirmMpin !== mpin ? 'Both entries must match' : undefined; const canSubmit = isValidMpin(mpin) && confirmMpin === mpin && !isLoading; const handleSubmit = () => { if (!canSubmit) { return; } setSubmitted(true); void dispatch( completeMpinSetup({ mpin, rememberForBiometrics: rememberForBiometrics && biometricsAvailable, }), ); }; return ( {`CHOOSE A ${MPIN_LENGTH}-DIGIT MPIN`} {biometricsAvailable && ( Sign in with biometrics {/* Worth being straight about: the API opens a session only against the MPIN, so enabling this keeps the MPIN on the device behind the system prompt. */} Stores your MPIN in this device's secure hardware, unlocked by your fingerprint or face. )} Three wrong MPINs lock your profile, and only your branch can unlock it. Pick something you will remember.