mirror of
https://github.com/mosip/inji-wallet.git
synced 2026-01-09 13:38:01 -05:00
feat(Inji-402): track login & onboarding flow events in telemetry (#918)
* feat(INJI-402): track different flows of login and onboarding features in telemetry Signed-off-by: PuBHARGAVI <46226958+PuBHARGAVI@users.noreply.github.com> * feat(INJI-402): add missing events in the login flow Signed-off-by: PuBHARGAVI <46226958+PuBHARGAVI@users.noreply.github.com> * feat(INJI-402): track hardware keystore not supported error in telemetry Signed-off-by: PuBHARGAVI <46226958+PuBHARGAVI@users.noreply.github.com> * fix(INJI-402): send biometric event only when biometrics are enrolled in device Signed-off-by: PuBHARGAVI <46226958+PuBHARGAVI@users.noreply.github.com> * feat(INJI-402): send error event for every 5 passcode mismatch attempts Signed-off-by: PuBHARGAVI <46226958+PuBHARGAVI@users.noreply.github.com> * feat(INJI-402): add telemetry events to track passcode screen flow when biometrics change Signed-off-by: PuBHARGAVI <46226958+PuBHARGAVI@users.noreply.github.com> * feat(INJI-402): add subtype to impression and interact event Signed-off-by: PuBHARGAVI <46226958+PuBHARGAVI@users.noreply.github.com> * fix(INJI-402): remove additionalParamters in error event Signed-off-by: PuBHARGAVI <46226958+PuBHARGAVI@users.noreply.github.com> * feat(INJI-402): remove extra impression events and fix the biometrics reenabling flow Signed-off-by: PuBHARGAVI <46226958+PuBHARGAVI@users.noreply.github.com> * refactor(INJI-402): change getData method name to getStartEventData in telemetry utils Signed-off-by: PuBHARGAVI <46226958+PuBHARGAVI@users.noreply.github.com> * feat(INJI-402): don't show biometric failed alert message when user cancels the flow Signed-off-by: PuBHARGAVI <46226958+PuBHARGAVI@users.noreply.github.com> * refactor(INJI-402): change telemetry events name Signed-off-by: PuBHARGAVI <46226958+PuBHARGAVI@users.noreply.github.com> * fix(INJI-402): add missing functions in telemetry utils Signed-off-by: PuBHARGAVI <46226958+PuBHARGAVI@users.noreply.github.com> * refactor(INJI-402): add impression event in passcode screen and change Main to Home in event Signed-off-by: PuBHARGAVI <46226958+PuBHARGAVI@users.noreply.github.com> --------- Signed-off-by: PuBHARGAVI <46226958+PuBHARGAVI@users.noreply.github.com>
This commit is contained in:
@@ -1,11 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Modal as RNModal } from 'react-native';
|
||||
import { Icon } from 'react-native-elements';
|
||||
import { PasscodeVerify } from '../components/PasscodeVerify';
|
||||
import { Column, Text } from '../components/ui';
|
||||
import { Theme } from '../components/ui/styleUtils';
|
||||
import React, {useEffect} from 'react';
|
||||
import {Modal as RNModal} from 'react-native';
|
||||
import {Icon} from 'react-native-elements';
|
||||
import {PasscodeVerify} from '../components/PasscodeVerify';
|
||||
import {Column, Text} from '../components/ui';
|
||||
import {Theme} from '../components/ui/styleUtils';
|
||||
import {
|
||||
getImpressionEventData,
|
||||
sendImpressionEvent,
|
||||
} from '../shared/telemetry/TelemetryUtils';
|
||||
|
||||
export const Passcode: React.FC<PasscodeProps> = props => {
|
||||
useEffect(() => {
|
||||
sendImpressionEvent(getImpressionEventData('App Login', 'Passcode'));
|
||||
}, []);
|
||||
|
||||
export const Passcode: React.FC<PasscodeProps> = (props) => {
|
||||
return (
|
||||
<RNModal
|
||||
animationType="slide"
|
||||
|
||||
@@ -13,6 +13,7 @@ export const PasscodeVerify: React.FC<PasscodeVerifyProps> = props => {
|
||||
useEffect(() => {
|
||||
if (isVerified) {
|
||||
props.onSuccess();
|
||||
setIsVerified(false);
|
||||
}
|
||||
}, [isVerified]);
|
||||
|
||||
@@ -21,11 +22,17 @@ export const PasscodeVerify: React.FC<PasscodeVerifyProps> = props => {
|
||||
);
|
||||
|
||||
async function verify(value: string) {
|
||||
const hashedPasscode = await hashData(value, props.salt, argon2iConfig);
|
||||
if (props.passcode === hashedPasscode) {
|
||||
setIsVerified(true);
|
||||
} else {
|
||||
props.onError(t('passcodeMismatchError'));
|
||||
try {
|
||||
const hashedPasscode = await hashData(value, props.salt, argon2iConfig);
|
||||
if (props.passcode === hashedPasscode) {
|
||||
setIsVerified(true);
|
||||
} else {
|
||||
if (props.onError) {
|
||||
props.onError(t('passcodeMismatchError'));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('error:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -227,7 +227,7 @@ export const qrLoginMachine =
|
||||
},
|
||||
},
|
||||
success: {
|
||||
entry: [() => sendEndEvent(getEndEventData('QR login'))],
|
||||
entry: [() => sendEndEvent(getEndEventData('QR login', 'SUCCESS'))],
|
||||
on: {
|
||||
CONFIRM: {
|
||||
target: 'done',
|
||||
|
||||
@@ -419,7 +419,8 @@ export const ExistingMosipVCItemMachine =
|
||||
'setWalletBindingErrorEmpty',
|
||||
'sendWalletBindingSuccess',
|
||||
'logWalletBindingSuccess',
|
||||
() => sendEndEvent(getEndEventData('VC activation')),
|
||||
() =>
|
||||
sendEndEvent(getEndEventData('VC activation', 'SUCCESS')),
|
||||
],
|
||||
target: '#vc-item.kebabPopUp',
|
||||
},
|
||||
@@ -746,7 +747,7 @@ export const ExistingMosipVCItemMachine =
|
||||
'setWalletBindingErrorEmpty',
|
||||
'setWalletBindingSuccess',
|
||||
'logWalletBindingSuccess',
|
||||
() => sendEndEvent(getEndEventData('VC activation')),
|
||||
() => sendEndEvent(getEndEventData('VC activation', 'SUCCESS')),
|
||||
],
|
||||
target: 'idle',
|
||||
},
|
||||
@@ -1003,7 +1004,11 @@ export const ExistingMosipVCItemMachine =
|
||||
};
|
||||
case 'GET_VC_RESPONSE':
|
||||
case 'CREDENTIAL_DOWNLOADED':
|
||||
return {...context, ...event.vc, vcMetadata: context.vcMetadata};
|
||||
return {
|
||||
...context,
|
||||
...event.vc,
|
||||
vcMetadata: context.vcMetadata,
|
||||
};
|
||||
}
|
||||
}),
|
||||
|
||||
|
||||
@@ -208,15 +208,17 @@ export interface Typegen0 {
|
||||
| 'done.invoke.vc-item.kebabPopUp.updatingPrivateKey:invocation[0]'
|
||||
| 'done.invoke.vc-item.updatingPrivateKey:invocation[0]';
|
||||
markVcValid: 'done.invoke.vc-item.verifyingCredential:invocation[0]';
|
||||
refreshMyVcs: 'STORE_RESPONSE';
|
||||
removeTamperedVcItem: 'TAMPERED_VC';
|
||||
removeVcFromInProgressDownloads: 'STORE_RESPONSE';
|
||||
removeVcItem: 'CONFIRM';
|
||||
removeVcMetaDataFromStorage: 'STORE_ERROR';
|
||||
removeVcMetaDataFromVcMachine: 'DISMISS';
|
||||
removedVc: 'STORE_RESPONSE';
|
||||
requestStoredContext: 'GET_VC_RESPONSE' | 'REFRESH';
|
||||
requestVcContext: 'DISMISS' | 'xstate.init';
|
||||
resetWalletBindingSuccess: 'DISMISS';
|
||||
revokeVID: 'done.invoke.vc-item.requestingRevoke:invocation[0]';
|
||||
sendTamperedVc: 'TAMPERED_VC';
|
||||
sendVcUpdated: 'PIN_CARD';
|
||||
sendWalletBindingSuccess:
|
||||
| 'done.invoke.vc-item.kebabPopUp.addingWalletBindingId:invocation[0]'
|
||||
|
||||
@@ -11,6 +11,7 @@ const model = createModel(
|
||||
isEnrolled: false,
|
||||
status: null,
|
||||
retry: false,
|
||||
error: {},
|
||||
},
|
||||
{
|
||||
events: {
|
||||
@@ -22,7 +23,7 @@ const model = createModel(
|
||||
AUTHENTICATE: () => ({}),
|
||||
RETRY_AUTHENTICATE: () => ({}),
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
@@ -108,9 +109,20 @@ export const biometricsMachine = model.createMachine(
|
||||
// disableDeviceFallback: true,
|
||||
// fallbackLabel: 'Invalid fingerprint attempts, Please try again.'
|
||||
});
|
||||
|
||||
if (res.error) {
|
||||
throw new Error(JSON.stringify(res));
|
||||
}
|
||||
|
||||
return res.success;
|
||||
},
|
||||
onError: 'failure',
|
||||
onError: [
|
||||
{
|
||||
target: 'failure',
|
||||
actions: ['sendFailedEndEvent'],
|
||||
},
|
||||
],
|
||||
|
||||
onDone: {
|
||||
target: 'authentication',
|
||||
actions: ['setStatus'],
|
||||
@@ -192,6 +204,7 @@ export const biometricsMachine = model.createMachine(
|
||||
meta: {
|
||||
message: 'errors.generic',
|
||||
},
|
||||
exit: 'resetError',
|
||||
},
|
||||
},
|
||||
on: {
|
||||
@@ -222,15 +235,26 @@ export const biometricsMachine = model.createMachine(
|
||||
setRetry: model.assign({
|
||||
retry: () => true,
|
||||
}),
|
||||
|
||||
sendFailedEndEvent: model.assign({
|
||||
error: (_context, event) => {
|
||||
const res = JSON.parse((event.data as Error).message);
|
||||
return { res: res, stacktrace: event };
|
||||
},
|
||||
}),
|
||||
|
||||
resetError: model.assign({
|
||||
error: () => null,
|
||||
}),
|
||||
},
|
||||
guards: {
|
||||
isStatusSuccess: (ctx) => ctx.status,
|
||||
isStatusFail: (ctx) => !ctx.status,
|
||||
checkIfAvailable: (ctx) => ctx.isAvailable && ctx.isEnrolled,
|
||||
checkIfUnavailable: (ctx) => !ctx.isAvailable,
|
||||
checkIfUnenrolled: (ctx) => !ctx.isEnrolled,
|
||||
isStatusSuccess: ctx => ctx.status,
|
||||
isStatusFail: ctx => !ctx.status,
|
||||
checkIfAvailable: ctx => ctx.isAvailable && ctx.isEnrolled,
|
||||
checkIfUnavailable: ctx => !ctx.isAvailable,
|
||||
checkIfUnenrolled: ctx => !ctx.isEnrolled,
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -281,3 +305,7 @@ export function selectUnenrolledNotice(state: State) {
|
||||
? selectFailMessage(state)
|
||||
: null;
|
||||
}
|
||||
|
||||
export function selectErrorResponse(state: State) {
|
||||
return state.context.error;
|
||||
}
|
||||
|
||||
@@ -572,7 +572,7 @@ export const scanMachine =
|
||||
accepted: {
|
||||
entry: [
|
||||
'logShared',
|
||||
() => sendEndEvent(getEndEventData('VC share')),
|
||||
() => sendEndEvent(getEndEventData('VC share', 'SUCCESS')),
|
||||
],
|
||||
on: {
|
||||
DISMISS: {
|
||||
|
||||
@@ -6,11 +6,24 @@ import {Button, Column, Text} from '../components/ui';
|
||||
import {Theme} from '../components/ui/styleUtils';
|
||||
import {RootRouteProps} from '../routes';
|
||||
import {useAuthScreen} from './AuthScreenController';
|
||||
import {
|
||||
getStartEventData,
|
||||
getInteractEventData,
|
||||
sendInteractEvent,
|
||||
sendStartEvent,
|
||||
} from '../shared/telemetry/TelemetryUtils';
|
||||
|
||||
export const AuthScreen: React.FC<RootRouteProps> = props => {
|
||||
const {t} = useTranslation('AuthScreen');
|
||||
const controller = useAuthScreen(props);
|
||||
|
||||
const handleUsePasscodeButtonPress = () => {
|
||||
sendStartEvent(getStartEventData('App Onboarding'));
|
||||
sendInteractEvent(
|
||||
getInteractEventData('App Onboarding', 'TOUCH', 'Use Passcode Button'),
|
||||
);
|
||||
controller.usePasscode();
|
||||
};
|
||||
return (
|
||||
<Column
|
||||
fill
|
||||
@@ -54,7 +67,7 @@ export const AuthScreen: React.FC<RootRouteProps> = props => {
|
||||
testID="usePasscode"
|
||||
type="clear"
|
||||
title={t('usePasscode')}
|
||||
onPress={controller.usePasscode}
|
||||
onPress={() => handleUsePasscodeButtonPress()}
|
||||
/>
|
||||
</Column>
|
||||
</Column>
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { useMachine, useSelector } from '@xstate/react';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import {useMachine, useSelector} from '@xstate/react';
|
||||
import {useContext, useEffect, useState} from 'react';
|
||||
import * as LocalAuthentication from 'expo-local-authentication';
|
||||
|
||||
import {
|
||||
AuthEvents,
|
||||
selectSettingUp,
|
||||
selectAuthorized,
|
||||
} from '../machines/auth';
|
||||
import { RootRouteProps } from '../routes';
|
||||
import { GlobalContext } from '../shared/GlobalContext';
|
||||
import {AuthEvents, selectSettingUp, selectAuthorized} from '../machines/auth';
|
||||
import {RootRouteProps} from '../routes';
|
||||
import {GlobalContext} from '../shared/GlobalContext';
|
||||
import {
|
||||
biometricsMachine,
|
||||
selectError,
|
||||
@@ -16,12 +12,23 @@ import {
|
||||
selectIsSuccess,
|
||||
selectIsUnvailable,
|
||||
selectUnenrolledNotice,
|
||||
selectErrorResponse,
|
||||
} from '../machines/biometrics';
|
||||
import { SettingsEvents } from '../machines/settings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {SettingsEvents} from '../machines/settings';
|
||||
import {useTranslation} from 'react-i18next';
|
||||
import {
|
||||
sendStartEvent,
|
||||
sendImpressionEvent,
|
||||
sendInteractEvent,
|
||||
getStartEventData,
|
||||
getInteractEventData,
|
||||
getImpressionEventData,
|
||||
getEndEventData,
|
||||
sendEndEvent,
|
||||
} from '../shared/telemetry/TelemetryUtils';
|
||||
|
||||
export function useAuthScreen(props: RootRouteProps) {
|
||||
const { appService } = useContext(GlobalContext);
|
||||
const {appService} = useContext(GlobalContext);
|
||||
const authService = appService.children.get('auth');
|
||||
const settingsService = appService.children.get('settings');
|
||||
|
||||
@@ -38,12 +45,13 @@ export function useAuthScreen(props: RootRouteProps) {
|
||||
const isSuccessBio = useSelector(bioService, selectIsSuccess);
|
||||
const errorMsgBio = useSelector(bioService, selectError);
|
||||
const unEnrolledNoticeBio = useSelector(bioService, selectUnenrolledNotice);
|
||||
const errorResponse = useSelector(bioService, selectErrorResponse);
|
||||
|
||||
const usePasscode = () => {
|
||||
props.navigation.navigate('Passcode', { setup: isSettingUp });
|
||||
props.navigation.navigate('Passcode', {setup: isSettingUp});
|
||||
};
|
||||
|
||||
const { t } = useTranslation('AuthScreen');
|
||||
const {t} = useTranslation('AuthScreen');
|
||||
|
||||
const fetchIsAvailable = async () => {
|
||||
const result = await LocalAuthentication.hasHardwareAsync();
|
||||
@@ -53,10 +61,12 @@ export function useAuthScreen(props: RootRouteProps) {
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthorized) {
|
||||
sendEndEvent(getEndEventData('App Onboarding', 'SUCCESS'));
|
||||
props.navigation.reset({
|
||||
index: 0,
|
||||
routes: [{ name: 'Main' }],
|
||||
routes: [{name: 'Main'}],
|
||||
});
|
||||
sendImpressionEvent(getImpressionEventData('App Onboarding', 'Home'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -69,8 +79,17 @@ export function useAuthScreen(props: RootRouteProps) {
|
||||
|
||||
// handle biometric failure unknown error
|
||||
} else if (errorMsgBio) {
|
||||
sendEndEvent(
|
||||
getEndEventData('App Onboarding', 'FAILURE', {
|
||||
errorId: errorResponse.res.error,
|
||||
errorMessage: errorResponse.res.warning,
|
||||
stackTrace: errorResponse.stacktrace,
|
||||
}),
|
||||
);
|
||||
// show alert message whenever biometric state gets failure
|
||||
setHasAlertMsg(t(errorMsgBio));
|
||||
if (errorResponse.res.error !== 'user_cancel') {
|
||||
setHasAlertMsg(t(errorMsgBio));
|
||||
}
|
||||
|
||||
// handle any unenrolled notice
|
||||
} else if (unEnrolledNoticeBio) {
|
||||
@@ -78,6 +97,7 @@ export function useAuthScreen(props: RootRouteProps) {
|
||||
|
||||
// we dont need to see this page to user once biometric is unavailable on its device
|
||||
} else if (isUnavailableBio) {
|
||||
sendStartEvent(getStartEventData('App Onboarding'));
|
||||
usePasscode();
|
||||
}
|
||||
}, [isSuccessBio, isUnavailableBio, errorMsgBio, unEnrolledNoticeBio]);
|
||||
@@ -85,12 +105,21 @@ export function useAuthScreen(props: RootRouteProps) {
|
||||
const useBiometrics = async () => {
|
||||
const isBiometricsEnrolled = await LocalAuthentication.isEnrolledAsync();
|
||||
if (isBiometricsEnrolled) {
|
||||
if (biometricState.matches({ failure: 'unenrolled' })) {
|
||||
biometricSend({ type: 'RETRY_AUTHENTICATE' });
|
||||
sendStartEvent(getStartEventData('App Onboarding'));
|
||||
sendInteractEvent(
|
||||
getInteractEventData(
|
||||
'App Onboarding',
|
||||
'TOUCH',
|
||||
'Use Biometrics Button',
|
||||
),
|
||||
);
|
||||
|
||||
if (biometricState.matches({failure: 'unenrolled'})) {
|
||||
biometricSend({type: 'RETRY_AUTHENTICATE'});
|
||||
return;
|
||||
}
|
||||
|
||||
biometricSend({ type: 'AUTHENTICATE' });
|
||||
biometricSend({type: 'AUTHENTICATE'});
|
||||
} else {
|
||||
setHasAlertMsg(t('errors.unenrolled'));
|
||||
}
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TouchableOpacity } from 'react-native';
|
||||
import { Icon } from 'react-native-elements';
|
||||
import { Button, Centered, Column } from '../components/ui';
|
||||
import { Theme } from '../components/ui/styleUtils';
|
||||
import { RootRouteProps } from '../routes';
|
||||
import { useBiometricScreen } from './BiometricScreenController';
|
||||
import { Passcode } from '../components/Passcode';
|
||||
import React, {useEffect} from 'react';
|
||||
import {useTranslation} from 'react-i18next';
|
||||
import {TouchableOpacity} from 'react-native';
|
||||
import {Icon} from 'react-native-elements';
|
||||
import {Button, Centered, Column} from '../components/ui';
|
||||
import {Theme} from '../components/ui/styleUtils';
|
||||
import {RootRouteProps} from '../routes';
|
||||
import {useBiometricScreen} from './BiometricScreenController';
|
||||
import {Passcode} from '../components/Passcode';
|
||||
import {
|
||||
getEventType,
|
||||
incrementPasscodeRetryCount,
|
||||
} from '../shared/telemetry/TelemetryUtils';
|
||||
|
||||
export const BiometricScreen: React.FC<RootRouteProps> = (props) => {
|
||||
const { t } = useTranslation('BiometricScreen');
|
||||
export const BiometricScreen: React.FC<RootRouteProps> = props => {
|
||||
const {t} = useTranslation('BiometricScreen');
|
||||
const controller = useBiometricScreen(props);
|
||||
|
||||
const handlePasscodeMismatch = (error: string) => {
|
||||
incrementPasscodeRetryCount(getEventType(props.route.params?.setup));
|
||||
controller.onError(error);
|
||||
};
|
||||
|
||||
return (
|
||||
<Column
|
||||
fill
|
||||
@@ -34,10 +43,11 @@ export const BiometricScreen: React.FC<RootRouteProps> = (props) => {
|
||||
<Passcode
|
||||
message="Enter your passcode to re-enable biometrics authentication."
|
||||
onSuccess={() => controller.onSuccess()}
|
||||
onError={(value: string) => controller.onError(value)}
|
||||
onError={handlePasscodeMismatch}
|
||||
storedPasscode={controller.storedPasscode}
|
||||
onDismiss={() => controller.onDismiss()}
|
||||
error={controller.error}
|
||||
salt={controller.passcodeSalt}
|
||||
/>
|
||||
)}
|
||||
</Column>
|
||||
|
||||
@@ -1,20 +1,37 @@
|
||||
import { useMachine, useSelector } from '@xstate/react';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import {useMachine, useSelector} from '@xstate/react';
|
||||
import {useContext, useEffect, useState} from 'react';
|
||||
import {Platform} from 'react-native';
|
||||
import RNFingerprintChange from 'react-native-biometrics-changed';
|
||||
import { AuthEvents, selectAuthorized, selectPasscode } from '../machines/auth';
|
||||
import { RootRouteProps } from '../routes';
|
||||
import { GlobalContext } from '../shared/GlobalContext';
|
||||
import {
|
||||
AuthEvents,
|
||||
selectAuthorized,
|
||||
selectPasscode,
|
||||
selectPasscodeSalt,
|
||||
} from '../machines/auth';
|
||||
import {
|
||||
biometricsMachine,
|
||||
selectError,
|
||||
selectErrorResponse,
|
||||
selectIsAvailable,
|
||||
selectIsSuccess,
|
||||
selectIsUnenrolled,
|
||||
selectIsUnvailable,
|
||||
} from '../machines/biometrics';
|
||||
import { Platform } from 'react-native';
|
||||
import {RootRouteProps} from '../routes';
|
||||
import {GlobalContext} from '../shared/GlobalContext';
|
||||
import {
|
||||
getStartEventData,
|
||||
getEndEventData,
|
||||
getImpressionEventData,
|
||||
getInteractEventData,
|
||||
sendEndEvent,
|
||||
sendImpressionEvent,
|
||||
sendInteractEvent,
|
||||
sendStartEvent,
|
||||
} from '../shared/telemetry/TelemetryUtils';
|
||||
|
||||
export function useBiometricScreen(props: RootRouteProps) {
|
||||
const { appService } = useContext(GlobalContext);
|
||||
const {appService} = useContext(GlobalContext);
|
||||
const authService = appService.children.get('auth');
|
||||
|
||||
const [error, setError] = useState('');
|
||||
@@ -27,12 +44,29 @@ export function useBiometricScreen(props: RootRouteProps) {
|
||||
const isUnavailable = useSelector(bioService, selectIsUnvailable);
|
||||
const isSuccessBio = useSelector(bioService, selectIsSuccess);
|
||||
const isUnenrolled = useSelector(bioService, selectIsUnenrolled);
|
||||
const errorMsgBio = useSelector(bioService, selectError);
|
||||
const errorResponse = useSelector(bioService, selectErrorResponse);
|
||||
const passcodeSalt = useSelector(authService, selectPasscodeSalt);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAvailable) {
|
||||
sendStartEvent(getStartEventData('App login'));
|
||||
sendInteractEvent(
|
||||
getInteractEventData(
|
||||
'App login',
|
||||
'TOUCH',
|
||||
'Unlock with Biometrics button',
|
||||
),
|
||||
);
|
||||
}
|
||||
}, [isAvailable]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthorized) {
|
||||
sendEndEvent(getEndEventData('App Login', 'SUCCESS'));
|
||||
props.navigation.reset({
|
||||
index: 0,
|
||||
routes: [{ name: 'Main' }],
|
||||
routes: [{name: 'Main'}],
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -50,13 +84,34 @@ export function useBiometricScreen(props: RootRouteProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (errorMsgBio && !isReEnabling) {
|
||||
sendEndEvent(
|
||||
getEndEventData('App Login', 'FAILURE', {
|
||||
errorId: errorResponse.res.error,
|
||||
errorMessage: errorResponse.res.warning,
|
||||
stackTrace: errorResponse.stacktrace,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (isUnavailable || isUnenrolled) {
|
||||
props.navigation.reset({
|
||||
index: 0,
|
||||
routes: [{ name: 'Passcode' }],
|
||||
routes: [{name: 'Passcode'}],
|
||||
});
|
||||
sendStartEvent(getStartEventData('App Login'));
|
||||
sendInteractEvent(
|
||||
getInteractEventData('App Login', 'TOUCH', 'Unlock application button'),
|
||||
);
|
||||
}
|
||||
}, [isAuthorized, isAvailable, isUnenrolled, isUnavailable, isSuccessBio]);
|
||||
}, [
|
||||
isAuthorized,
|
||||
isAvailable,
|
||||
isUnenrolled,
|
||||
isUnavailable,
|
||||
isSuccessBio,
|
||||
errorMsgBio,
|
||||
]);
|
||||
|
||||
const checkBiometricsChange = () => {
|
||||
if (Platform.OS === 'android') {
|
||||
@@ -66,9 +121,9 @@ export function useBiometricScreen(props: RootRouteProps) {
|
||||
if (biometricsHasChanged) {
|
||||
setReEnabling(true);
|
||||
} else {
|
||||
bioSend({ type: 'AUTHENTICATE' });
|
||||
bioSend({type: 'AUTHENTICATE'});
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// TODO: solution for iOS
|
||||
@@ -76,11 +131,20 @@ export function useBiometricScreen(props: RootRouteProps) {
|
||||
};
|
||||
|
||||
const useBiometrics = () => {
|
||||
bioSend({ type: 'AUTHENTICATE' });
|
||||
sendStartEvent(getStartEventData('App login'));
|
||||
sendInteractEvent(
|
||||
getInteractEventData(
|
||||
'App Login',
|
||||
'TOUCH',
|
||||
'Unlock with biometrics button',
|
||||
),
|
||||
);
|
||||
bioSend({type: 'AUTHENTICATE'});
|
||||
};
|
||||
|
||||
const onSuccess = () => {
|
||||
bioSend({ type: 'AUTHENTICATE' });
|
||||
bioSend({type: 'AUTHENTICATE'});
|
||||
setError('');
|
||||
};
|
||||
|
||||
const onError = (value: string) => {
|
||||
@@ -88,6 +152,12 @@ export function useBiometricScreen(props: RootRouteProps) {
|
||||
};
|
||||
|
||||
const onDismiss = () => {
|
||||
sendEndEvent(
|
||||
getEndEventData('App Login', 'FAILURE', {
|
||||
errorId: 'user_cancel',
|
||||
errorMessage: 'Authentication canceled',
|
||||
}),
|
||||
);
|
||||
setReEnabling(false);
|
||||
};
|
||||
|
||||
@@ -95,6 +165,7 @@ export function useBiometricScreen(props: RootRouteProps) {
|
||||
error,
|
||||
isReEnabling,
|
||||
isSuccessBio,
|
||||
passcodeSalt,
|
||||
storedPasscode: useSelector(authService, selectPasscode),
|
||||
useBiometrics,
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ import {groupBy} from '../../shared/javascript';
|
||||
import {isOpenId4VCIEnabled} from '../../shared/openId4VCI/Utils';
|
||||
import {VcItemContainer} from '../../components/VC/VcItemContainer';
|
||||
import {BannerNotification} from '../../components/BannerNotification';
|
||||
import {
|
||||
getErrorEventData,
|
||||
sendErrorEvent,
|
||||
} from '../../shared/telemetry/TelemetryUtils';
|
||||
import {Error} from '../../components/ui/Error';
|
||||
|
||||
const pinIconProps = {iconName: 'pushpin', iconType: 'antdesign'};
|
||||
@@ -47,6 +51,16 @@ export const MyVcsTab: React.FC<HomeScreenTabProps> = props => {
|
||||
if (controller.inProgressVcDownloads?.size > 0) {
|
||||
controller.SET_STORE_VC_ITEM_STATUS();
|
||||
}
|
||||
|
||||
if (controller.showHardwareKeystoreNotExistsAlert) {
|
||||
sendErrorEvent(
|
||||
getErrorEventData(
|
||||
'App Onboarding',
|
||||
'does_not_exist',
|
||||
'Some security features will be unavailable as hardware key store is not available',
|
||||
),
|
||||
);
|
||||
}
|
||||
}, [controller.areAllVcsLoaded, controller.inProgressVcDownloads]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, {useEffect} from 'react';
|
||||
import {useTranslation} from 'react-i18next';
|
||||
import {Image} from 'react-native';
|
||||
import {MAX_PIN, PasscodeVerify} from '../components/PasscodeVerify';
|
||||
@@ -9,16 +9,58 @@ import {PasscodeRouteProps} from '../routes';
|
||||
import {usePasscodeScreen} from './PasscodeScreenController';
|
||||
import {hashData} from '../shared/commonUtil';
|
||||
import {argon2iConfig} from '../shared/constants';
|
||||
import {
|
||||
getEndEventData,
|
||||
getEventType,
|
||||
getImpressionEventData,
|
||||
sendEndEvent,
|
||||
sendImpressionEvent,
|
||||
} from '../shared/telemetry/TelemetryUtils';
|
||||
import {BackHandler} from 'react-native';
|
||||
import {incrementPasscodeRetryCount} from '../shared/telemetry/TelemetryUtils';
|
||||
|
||||
export const PasscodeScreen: React.FC<PasscodeRouteProps> = props => {
|
||||
const {t} = useTranslation('PasscodeScreen');
|
||||
const controller = usePasscodeScreen(props);
|
||||
const isSettingUp = props.route.params?.setup;
|
||||
|
||||
useEffect(() => {
|
||||
sendImpressionEvent(
|
||||
getImpressionEventData(getEventType(isSettingUp), 'Passcode'),
|
||||
);
|
||||
}, [isSettingUp]);
|
||||
|
||||
const handleBackButtonPress = () => {
|
||||
sendEndEvent(
|
||||
getEndEventData(getEventType(isSettingUp), 'FAILURE', {
|
||||
errorId: 'user_cancel',
|
||||
errorMessage: 'Authentication canceled',
|
||||
}),
|
||||
);
|
||||
return false;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const backHandler = BackHandler.addEventListener(
|
||||
'hardwareBackPress',
|
||||
handleBackButtonPress,
|
||||
);
|
||||
|
||||
return () => {
|
||||
backHandler.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const setPasscode = async (passcode: string) => {
|
||||
const data = await hashData(passcode, controller.storedSalt, argon2iConfig);
|
||||
controller.setPasscode(data);
|
||||
};
|
||||
|
||||
const handlePasscodeMismatch = (error: string) => {
|
||||
incrementPasscodeRetryCount(getEventType(isSettingUp));
|
||||
controller.setError(error);
|
||||
};
|
||||
|
||||
const passcodeSetup =
|
||||
controller.passcode === '' ? (
|
||||
<React.Fragment>
|
||||
@@ -63,7 +105,7 @@ export const PasscodeScreen: React.FC<PasscodeRouteProps> = props => {
|
||||
</Column>
|
||||
<PasscodeVerify
|
||||
onSuccess={controller.SETUP_PASSCODE}
|
||||
onError={controller.setError}
|
||||
onError={handlePasscodeMismatch}
|
||||
passcode={controller.passcode}
|
||||
salt={controller.storedSalt}
|
||||
/>
|
||||
@@ -76,7 +118,7 @@ export const PasscodeScreen: React.FC<PasscodeRouteProps> = props => {
|
||||
padding="32"
|
||||
backgroundColor={Theme.Colors.whiteBackgroundColor}>
|
||||
<Image source={Theme.LockIcon} style={{alignSelf: 'center'}} />
|
||||
{props.route.params?.setup ? (
|
||||
{isSettingUp ? (
|
||||
<Column fill align="space-around" width="100%">
|
||||
{passcodeSetup}
|
||||
</Column>
|
||||
@@ -92,7 +134,7 @@ export const PasscodeScreen: React.FC<PasscodeRouteProps> = props => {
|
||||
</Text>
|
||||
<PasscodeVerify
|
||||
onSuccess={controller.LOGIN}
|
||||
onError={controller.setError}
|
||||
onError={handlePasscodeMismatch}
|
||||
passcode={controller.storedPasscode}
|
||||
salt={controller.storedSalt}
|
||||
/>
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { useSelector } from '@xstate/react';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import {useSelector} from '@xstate/react';
|
||||
import {useContext, useEffect, useState} from 'react';
|
||||
import {
|
||||
AuthEvents,
|
||||
selectAuthorized,
|
||||
selectPasscode,
|
||||
selectPasscodeSalt,
|
||||
} from '../machines/auth';
|
||||
import { PasscodeRouteProps } from '../routes';
|
||||
import { GlobalContext } from '../shared/GlobalContext';
|
||||
import {PasscodeRouteProps} from '../routes';
|
||||
import {GlobalContext} from '../shared/GlobalContext';
|
||||
import {
|
||||
getEndEventData,
|
||||
getEventType,
|
||||
sendEndEvent,
|
||||
} from '../shared/telemetry/TelemetryUtils';
|
||||
|
||||
export function usePasscodeScreen(props: PasscodeRouteProps) {
|
||||
const { appService } = useContext(GlobalContext);
|
||||
const {appService} = useContext(GlobalContext);
|
||||
const authService = appService.children.get('auth');
|
||||
|
||||
const isAuthorized = useSelector(authService, selectAuthorized);
|
||||
@@ -20,9 +25,12 @@ export function usePasscodeScreen(props: PasscodeRouteProps) {
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthorized) {
|
||||
sendEndEvent(
|
||||
getEndEventData(getEventType(props.route.params?.setup), 'SUCCESS'),
|
||||
);
|
||||
props.navigation.reset({
|
||||
index: 0,
|
||||
routes: [{ name: 'Main' }],
|
||||
routes: [{name: 'Main'}],
|
||||
});
|
||||
}
|
||||
}, [isAuthorized]);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useSelector } from '@xstate/react';
|
||||
import { useContext } from 'react';
|
||||
import {useSelector} from '@xstate/react';
|
||||
import {useContext} from 'react';
|
||||
import {
|
||||
AuthEvents,
|
||||
selectBiometrics,
|
||||
@@ -11,11 +11,19 @@ import {
|
||||
SettingsEvents,
|
||||
selectBiometricUnlockEnabled,
|
||||
} from '../machines/settings';
|
||||
import { RootRouteProps } from '../routes';
|
||||
import { GlobalContext } from '../shared/GlobalContext';
|
||||
import {RootRouteProps} from '../routes';
|
||||
import {GlobalContext} from '../shared/GlobalContext';
|
||||
import {
|
||||
getStartEventData,
|
||||
getImpressionEventData,
|
||||
getInteractEventData,
|
||||
sendImpressionEvent,
|
||||
sendInteractEvent,
|
||||
sendStartEvent,
|
||||
} from '../shared/telemetry/TelemetryUtils';
|
||||
|
||||
export function useWelcomeScreen(props: RootRouteProps) {
|
||||
const { appService } = useContext(GlobalContext);
|
||||
const {appService} = useContext(GlobalContext);
|
||||
const authService = appService.children.get('auth');
|
||||
const settingsService = appService.children.get('settings');
|
||||
|
||||
@@ -34,7 +42,7 @@ export function useWelcomeScreen(props: RootRouteProps) {
|
||||
const isLanguagesetup = useSelector(authService, selectLanguagesetup);
|
||||
const isBiometricUnlockEnabled = useSelector(
|
||||
settingsService,
|
||||
selectBiometricUnlockEnabled
|
||||
selectBiometricUnlockEnabled,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -55,9 +63,17 @@ export function useWelcomeScreen(props: RootRouteProps) {
|
||||
unlockPage: () => {
|
||||
// prioritize biometrics
|
||||
if (!isSettingUp && isBiometricUnlockEnabled && biometrics !== '') {
|
||||
props.navigation.navigate('Biometric', { setup: isSettingUp });
|
||||
props.navigation.navigate('Biometric', {setup: isSettingUp});
|
||||
} else if (!isSettingUp && passcode !== '') {
|
||||
props.navigation.navigate('Passcode', { setup: isSettingUp });
|
||||
sendStartEvent(getStartEventData('App Login'));
|
||||
sendInteractEvent(
|
||||
getInteractEventData(
|
||||
'App Login',
|
||||
'TOUCH',
|
||||
'Unlock application button',
|
||||
),
|
||||
);
|
||||
props.navigation.navigate('Passcode', {setup: isSettingUp});
|
||||
} else {
|
||||
props.navigation.navigate('Auth');
|
||||
}
|
||||
|
||||
@@ -135,12 +135,29 @@ export function getAppInfoEventData() {
|
||||
};
|
||||
}
|
||||
|
||||
let passcodeRetryCount = 1;
|
||||
|
||||
export const incrementPasscodeRetryCount = eventType => {
|
||||
if (passcodeRetryCount < 5) {
|
||||
passcodeRetryCount += 1;
|
||||
} else {
|
||||
sendErrorEvent(
|
||||
getErrorEventData(eventType, 'mismatch', 'Passcode did not match'),
|
||||
);
|
||||
passcodeRetryCount = 1;
|
||||
}
|
||||
};
|
||||
|
||||
export function configureTelemetry() {
|
||||
const config = getTelemetryConfigData();
|
||||
initializeTelemetry(config);
|
||||
sendAppInfoEvent(getAppInfoEventData());
|
||||
}
|
||||
|
||||
export function getEventType(isSettingUp) {
|
||||
return isSettingUp ? 'App Onboarding' : 'App Login';
|
||||
}
|
||||
|
||||
const languageCodeMap = {
|
||||
en: 'English',
|
||||
fil: 'Filipino',
|
||||
|
||||
Reference in New Issue
Block a user