mirror of
https://github.com/mosip/inji-wallet.git
synced 2026-01-10 14:07:59 -05:00
Improve code, logic and validation of Biometrics auth
This commit is contained in:
@@ -13,7 +13,8 @@ const model = createModel(
|
||||
{
|
||||
events: {
|
||||
SETUP_PASSCODE: (passcode: string) => ({ passcode }),
|
||||
SETUP_BIOMETRICS: () => ({}),
|
||||
SETUP_BIOMETRICS: (biometrics: string) => ({ biometrics }),
|
||||
RESET_BIOMETRICS: () => ({}),
|
||||
LOGOUT: () => ({}),
|
||||
LOGIN: () => ({}),
|
||||
STORE_RESPONSE: (response?: unknown) => ({ response }),
|
||||
@@ -23,6 +24,9 @@ const model = createModel(
|
||||
|
||||
export const AuthEvents = model.events;
|
||||
|
||||
|
||||
type SetupBiometricsEvent = EventFrom<typeof model, 'SETUP_BIOMETRICS'>;
|
||||
|
||||
export const authMachine = model.createMachine(
|
||||
{
|
||||
tsTypes: {} as import('./auth.typegen').Typegen0,
|
||||
@@ -74,6 +78,10 @@ export const authMachine = model.createMachine(
|
||||
unauthorized: {
|
||||
on: {
|
||||
LOGIN: 'authorized',
|
||||
RESET_BIOMETRICS: {
|
||||
target: 'settingUp',
|
||||
actions: ['setBiometrics', 'storeContext'],
|
||||
}
|
||||
},
|
||||
},
|
||||
authorized: {
|
||||
@@ -109,15 +117,19 @@ export const authMachine = model.createMachine(
|
||||
}),
|
||||
|
||||
setBiometrics: model.assign({
|
||||
biometrics: () => 'true',
|
||||
biometrics: (_, event: SetupBiometricsEvent) => event.biometrics,
|
||||
}),
|
||||
},
|
||||
|
||||
guards: {
|
||||
hasData: (_, event: StoreResponseEvent) => event.response != null,
|
||||
|
||||
hasPasscodeSet: (context) => context.passcode !== '',
|
||||
hasBiometricSet: (context) => context.biometrics !== ''
|
||||
hasPasscodeSet: (context) => {
|
||||
return context.passcode !== ''
|
||||
},
|
||||
hasBiometricSet: (context) => {
|
||||
return context.biometrics !== ''
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createModel } from "xstate/lib/model";
|
||||
import * as LocalAuthentication from 'expo-local-authentication';
|
||||
import { assign, StateFrom } from "xstate";
|
||||
import { assign, send, StateFrom } from "xstate";
|
||||
|
||||
|
||||
// --- CREATE MODEL -----------------------------------------------------------
|
||||
@@ -9,11 +9,13 @@ const model = createModel(
|
||||
isAvailable : false,
|
||||
authTypes : [],
|
||||
isEnrolled : false,
|
||||
status : null
|
||||
status : null,
|
||||
retry : false,
|
||||
},
|
||||
{
|
||||
events: {
|
||||
AUTHENTICATE: () => ({})
|
||||
AUTHENTICATE: () => ({}),
|
||||
RETRY_AUTHENTICATE: () => ({})
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -90,11 +92,11 @@ export const biometricsMachine = model.createMachine(
|
||||
authenticating: {
|
||||
invoke: {
|
||||
src: () => async () => {
|
||||
console.log('[BIOMETRIC_MACHINE] authenticating invoked');
|
||||
// console.log('[BIOMETRIC_MACHINE] authenticating invoked');
|
||||
let res = await LocalAuthentication.authenticateAsync({
|
||||
promptMessage: 'Biometric Authentication',
|
||||
})
|
||||
console.log("[BIOMETRIC_MACHINE] authenticating result", res)
|
||||
// console.log("[BIOMETRIC_MACHINE] authenticating result", res)
|
||||
return res.success;
|
||||
},
|
||||
onError: 'failure',
|
||||
@@ -105,6 +107,18 @@ export const biometricsMachine = model.createMachine(
|
||||
},
|
||||
},
|
||||
|
||||
reauthenticating: {
|
||||
always: [
|
||||
{
|
||||
target: 'authenticating',
|
||||
cond: 'checkIfAvailable'
|
||||
},
|
||||
{
|
||||
target: 'failure.unenrolled'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// checks authentication status
|
||||
authentication: {
|
||||
always: [
|
||||
@@ -130,12 +144,21 @@ export const biometricsMachine = model.createMachine(
|
||||
meta: 'Device does not support Biometrics'
|
||||
},
|
||||
unenrolled: {
|
||||
meta: 'To use Biometrics, please enroll your fingerprint in your device settings'
|
||||
meta: 'To use Biometrics, please enroll your fingerprint in your device settings',
|
||||
on: {
|
||||
RETRY_AUTHENTICATE: {
|
||||
actions: assign({retry: (context, event) => true}),
|
||||
target: [
|
||||
'#biometrics.initEnrolled',
|
||||
'#biometrics.reauthenticating'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
failed: {
|
||||
after: {
|
||||
// after 3 seconds, transition to available
|
||||
3000: { target: '..available' }
|
||||
// after 1 seconds, transition to available
|
||||
1000: { target: '#biometrics.available' }
|
||||
},
|
||||
meta: 'Failed to authenticate with Biometrics'
|
||||
},
|
||||
@@ -149,7 +172,8 @@ export const biometricsMachine = model.createMachine(
|
||||
},
|
||||
|
||||
{
|
||||
actions: {},
|
||||
actions: {
|
||||
},
|
||||
guards: {
|
||||
checkIfAvailable: ctx => ctx.isAvailable && ctx.isEnrolled,
|
||||
checkIfUnavailable: ctx => !ctx.isAvailable,
|
||||
@@ -167,6 +191,28 @@ export const BiometricsEvents = model.events;
|
||||
|
||||
type State = StateFrom<typeof biometricsMachine>;
|
||||
|
||||
export function selectFailMessage(state: State) {
|
||||
return Object.values(state.meta).join(', ');
|
||||
}
|
||||
|
||||
export function selectIsEnabled(state: State) {
|
||||
return state.matches('available');
|
||||
return state.matches('available') ||
|
||||
state.matches({ failure: 'unenrolled' });
|
||||
}
|
||||
|
||||
export function selectIsUnvailable(state: State) {
|
||||
return state.matches({ failure: 'unavailable' });
|
||||
}
|
||||
|
||||
export function selectIsSuccess(state: State) {
|
||||
return state.matches('success');
|
||||
}
|
||||
|
||||
export function selectError(state: State) {
|
||||
return state.matches({failure: 'error'}) ? selectFailMessage(state) : '';
|
||||
}
|
||||
|
||||
export function selectUnenrolledNotice(state: State) {
|
||||
// console.log("the unenrolled context is?", state.context)
|
||||
return state.matches({ failure: 'unenrolled' }) && state.context.retry ? selectFailMessage(state) : '';
|
||||
}
|
||||
@@ -8,14 +8,13 @@ import { useAuthScreen } from './AuthScreenController';
|
||||
|
||||
export const AuthScreen: React.FC<RootRouteProps> = (props) => {
|
||||
const controller = useAuthScreen(props);
|
||||
// console.log('-------->CONTROLLER', controller)
|
||||
|
||||
return (
|
||||
<Column fill padding="32" backgroundColor={Colors.White}>
|
||||
<MessageOverlay
|
||||
isVisible={controller.hasAlertMsg != null}
|
||||
isVisible={controller.alertMsg != ''}
|
||||
onBackdropPress={controller.hideAlert}
|
||||
title={controller.hasAlertMsg}
|
||||
title={controller.alertMsg}
|
||||
/>
|
||||
<Column>
|
||||
<Text align="center">
|
||||
@@ -29,7 +28,7 @@ export const AuthScreen: React.FC<RootRouteProps> = (props) => {
|
||||
<Button
|
||||
title="Use biometrics"
|
||||
margin="0 0 8 0"
|
||||
disabled={!controller.enableBiometric}
|
||||
disabled={!controller.isEnabledBio}
|
||||
onPress={controller.useBiometrics}
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -1,22 +1,37 @@
|
||||
import { useInterpret, useMachine, useSelector } from '@xstate/react';
|
||||
import { useMachine, useSelector } from '@xstate/react';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { AuthEvents, selectSettingUp, selectAuthorized } from '../machines/auth';
|
||||
import { RootRouteProps } from '../routes';
|
||||
import { GlobalContext } from '../shared/GlobalContext';
|
||||
import { biometricsMachine, selectIsEnabled } from '../machines/biometrics';
|
||||
import {
|
||||
biometricsMachine,
|
||||
selectError,
|
||||
selectIsEnabled,
|
||||
selectIsSuccess,
|
||||
selectIsUnvailable,
|
||||
selectUnenrolledNotice
|
||||
} from '../machines/biometrics';
|
||||
|
||||
export function useAuthScreen(props: RootRouteProps) {
|
||||
const { appService } = useContext(GlobalContext);
|
||||
const authService = appService.children.get('auth');
|
||||
const [hasAlertMsg, setHasAlertMsg] = useState(null);
|
||||
const authService = appService.children.get('auth');
|
||||
|
||||
const isSettingUp = useSelector(authService, selectSettingUp);
|
||||
const isAuthorized = useSelector(authService, selectAuthorized);
|
||||
const isSettingUp = useSelector(authService, selectSettingUp);
|
||||
const isAuthorized = useSelector(authService, selectAuthorized);
|
||||
|
||||
const biometricService = useInterpret(biometricsMachine);
|
||||
const [biometricState, biometricSend] = useMachine(biometricsMachine);
|
||||
const [alertMsg, setHasAlertMsg] = useState('');
|
||||
const [
|
||||
biometricState,
|
||||
biometricSend,
|
||||
bioService
|
||||
] = useMachine(biometricsMachine);
|
||||
|
||||
const isEnabledBio:boolean = useSelector(bioService, selectIsEnabled);
|
||||
const isUnavailableBio:boolean = useSelector(bioService, selectIsUnvailable);
|
||||
const isSuccessBio:boolean = useSelector(bioService, selectIsSuccess);
|
||||
const errorMsgBio:string = useSelector(bioService, selectError);
|
||||
const unEnrolledNoticeBio:string = useSelector(bioService, selectUnenrolledNotice);
|
||||
|
||||
const enableBiometric:boolean = useSelector(biometricService, selectIsEnabled);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -28,43 +43,52 @@ export function useAuthScreen(props: RootRouteProps) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log("[BIOMETRICS] value", biometricState.value);
|
||||
console.log("[BIOMETRICS] context", biometricState.context);
|
||||
|
||||
// if biometic state is success then lets send auth service BIOMETRICS
|
||||
if (biometricState.matches('success')) {
|
||||
authService.send(AuthEvents.SETUP_BIOMETRICS());
|
||||
if (isSuccessBio) {
|
||||
authService.send(AuthEvents.SETUP_BIOMETRICS('true'));
|
||||
|
||||
// handle biometric failure unknown error
|
||||
} else if (errorMsgBio) {
|
||||
// show alert message whenever biometric state gets failure
|
||||
setHasAlertMsg(errorMsgBio);
|
||||
|
||||
|
||||
// handle any unenrolled notice
|
||||
} else if (unEnrolledNoticeBio) {
|
||||
setHasAlertMsg(unEnrolledNoticeBio);
|
||||
|
||||
|
||||
// we dont need to see this page to user once biometric is unavailable on its device
|
||||
} else if (isUnavailableBio) {
|
||||
props.navigation.navigate('Passcode', { setup: isSettingUp });
|
||||
}
|
||||
|
||||
|
||||
}, [
|
||||
isAuthorized,
|
||||
isSuccessBio,
|
||||
isUnavailableBio,
|
||||
errorMsgBio,
|
||||
unEnrolledNoticeBio,
|
||||
]);
|
||||
|
||||
const useBiometrics = () => {
|
||||
if (biometricState.matches({failure: 'unenrolled'})) {
|
||||
biometricSend({ type: 'RETRY_AUTHENTICATE' });
|
||||
return;
|
||||
}
|
||||
|
||||
// handle biometric failure
|
||||
if (biometricState.matches('failure')) {
|
||||
|
||||
// we dont need to see this page to user once biometric is unavailable on its device
|
||||
if (biometricState.matches({ failure: 'unavailable' })) {
|
||||
props.navigation.navigate('Passcode', { setup: isSettingUp });
|
||||
return;
|
||||
}
|
||||
|
||||
// show alert message whenever biometric state gets failure
|
||||
setHasAlertMsg(Object.values(biometricState.meta).join(', '));
|
||||
}
|
||||
|
||||
}, [isAuthorized, biometricState]);
|
||||
|
||||
|
||||
const useBiometrics = () => {
|
||||
biometricSend({ type: 'AUTHENTICATE' });
|
||||
};
|
||||
|
||||
const hideAlert = () => {
|
||||
setHasAlertMsg(null);
|
||||
setHasAlertMsg('');
|
||||
}
|
||||
|
||||
return {
|
||||
isSettingUp,
|
||||
hasAlertMsg,
|
||||
enableBiometric,
|
||||
alertMsg,
|
||||
isEnabledBio,
|
||||
hideAlert,
|
||||
useBiometrics,
|
||||
usePasscode: () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMachine, useSelector } from '@xstate/react';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { AuthEvents, selectAuthorized } from '../machines/auth';
|
||||
import { RootRouteProps } from '../routes';
|
||||
import { GlobalContext } from '../shared/GlobalContext';
|
||||
@@ -9,7 +9,8 @@ export function useBiometricScreen(props: RootRouteProps) {
|
||||
const { appService } = useContext(GlobalContext);
|
||||
const authService = appService.children.get('auth');
|
||||
|
||||
const [biometricState, biometricSend] = useMachine(biometricsMachine);
|
||||
const [initAuthBio, updateInitAuthBio] = useState(true);
|
||||
const [bioState, bioSend] = useMachine(biometricsMachine);
|
||||
|
||||
const isAuthorized = useSelector(authService, selectAuthorized);
|
||||
|
||||
@@ -22,19 +23,36 @@ export function useBiometricScreen(props: RootRouteProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[BIOMETRICS] value", biometricState.value);
|
||||
console.log("[BIOMETRICS] context", biometricState.context);
|
||||
if (initAuthBio && bioState.matches('available')) {
|
||||
bioSend({ type: 'AUTHENTICATE' });
|
||||
|
||||
// so we only init authentication of biometrics just once
|
||||
updateInitAuthBio(false);
|
||||
}
|
||||
|
||||
// if biometic state is success then lets send auth service BIOMETRICS
|
||||
if (biometricState.matches('success')) {
|
||||
if (bioState.matches('success')) {
|
||||
authService.send(AuthEvents.LOGIN());
|
||||
return;
|
||||
}
|
||||
|
||||
}, [isAuthorized, biometricState]);
|
||||
if (
|
||||
bioState.matches({ failure: 'unavailable' }) ||
|
||||
bioState.matches({ failure: 'unenrolled' })
|
||||
) {
|
||||
authService.send(AuthEvents.RESET_BIOMETRICS());
|
||||
|
||||
props.navigation.reset({
|
||||
index: 0,
|
||||
routes: [{ name: 'Auth' }],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
}, [isAuthorized, bioState]);
|
||||
|
||||
const useBiometrics = () => {
|
||||
biometricSend({ type: 'AUTHENTICATE' });
|
||||
bioSend({ type: 'AUTHENTICATE' });
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -18,7 +18,7 @@ export function useWelcomeScreen(props: RootRouteProps) {
|
||||
unlockPage: () => {
|
||||
if (!isSettingUp && passcode !== '') {
|
||||
props.navigation.navigate('Passcode', { setup: isSettingUp });
|
||||
} else if (!isSettingUp && biometrics) {
|
||||
} else if (!isSettingUp && biometrics !== '') {
|
||||
props.navigation.navigate('Biometric', { setup: isSettingUp });
|
||||
} else {
|
||||
props.navigation.navigate('Auth');
|
||||
|
||||
Reference in New Issue
Block a user