diff --git a/components/Passcode.tsx b/components/Passcode.tsx index 74bdca93cd..1768999e81 100644 --- a/components/Passcode.tsx +++ b/components/Passcode.tsx @@ -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 = props => { + useEffect(() => { + sendImpressionEvent(getImpressionEventData('App Login', 'Passcode')); + }, []); -export const Passcode: React.FC = (props) => { return ( = props => { useEffect(() => { if (isVerified) { props.onSuccess(); + setIsVerified(false); } }, [isVerified]); @@ -21,11 +22,17 @@ export const PasscodeVerify: React.FC = 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); } } }; diff --git a/machines/QrLoginMachine.ts b/machines/QrLoginMachine.ts index 1cc07e9f9b..34cc66be6a 100644 --- a/machines/QrLoginMachine.ts +++ b/machines/QrLoginMachine.ts @@ -227,7 +227,7 @@ export const qrLoginMachine = }, }, success: { - entry: [() => sendEndEvent(getEndEventData('QR login'))], + entry: [() => sendEndEvent(getEndEventData('QR login', 'SUCCESS'))], on: { CONFIRM: { target: 'done', diff --git a/machines/VCItemMachine/ExistingMosipVCItem/ExistingMosipVCItemMachine.ts b/machines/VCItemMachine/ExistingMosipVCItem/ExistingMosipVCItemMachine.ts index ea1302d800..ff239ccab5 100644 --- a/machines/VCItemMachine/ExistingMosipVCItem/ExistingMosipVCItemMachine.ts +++ b/machines/VCItemMachine/ExistingMosipVCItem/ExistingMosipVCItemMachine.ts @@ -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, + }; } }), diff --git a/machines/VCItemMachine/ExistingMosipVCItem/ExistingMosipVCItemMachine.typegen.ts b/machines/VCItemMachine/ExistingMosipVCItem/ExistingMosipVCItemMachine.typegen.ts index 3625c7f70d..ba48f5e9ec 100644 --- a/machines/VCItemMachine/ExistingMosipVCItem/ExistingMosipVCItemMachine.typegen.ts +++ b/machines/VCItemMachine/ExistingMosipVCItem/ExistingMosipVCItemMachine.typegen.ts @@ -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]' diff --git a/machines/biometrics.ts b/machines/biometrics.ts index 7177276714..7b645f3318 100644 --- a/machines/biometrics.ts +++ b/machines/biometrics.ts @@ -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; +} diff --git a/machines/bleShare/scan/scanMachine.ts b/machines/bleShare/scan/scanMachine.ts index eeb28c4c85..98c314e2fd 100644 --- a/machines/bleShare/scan/scanMachine.ts +++ b/machines/bleShare/scan/scanMachine.ts @@ -572,7 +572,7 @@ export const scanMachine = accepted: { entry: [ 'logShared', - () => sendEndEvent(getEndEventData('VC share')), + () => sendEndEvent(getEndEventData('VC share', 'SUCCESS')), ], on: { DISMISS: { diff --git a/screens/AuthScreen.tsx b/screens/AuthScreen.tsx index bb21ef07f6..1a30a9efb5 100644 --- a/screens/AuthScreen.tsx +++ b/screens/AuthScreen.tsx @@ -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 = 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 ( = props => { testID="usePasscode" type="clear" title={t('usePasscode')} - onPress={controller.usePasscode} + onPress={() => handleUsePasscodeButtonPress()} /> diff --git a/screens/AuthScreenController.ts b/screens/AuthScreenController.ts index 1814e1520c..a46d8756d8 100644 --- a/screens/AuthScreenController.ts +++ b/screens/AuthScreenController.ts @@ -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')); } diff --git a/screens/BiometricScreen.tsx b/screens/BiometricScreen.tsx index 30efe1a9e6..dd0a639508 100644 --- a/screens/BiometricScreen.tsx +++ b/screens/BiometricScreen.tsx @@ -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 = (props) => { - const { t } = useTranslation('BiometricScreen'); +export const BiometricScreen: React.FC = props => { + const {t} = useTranslation('BiometricScreen'); const controller = useBiometricScreen(props); + const handlePasscodeMismatch = (error: string) => { + incrementPasscodeRetryCount(getEventType(props.route.params?.setup)); + controller.onError(error); + }; + return ( = (props) => { controller.onSuccess()} - onError={(value: string) => controller.onError(value)} + onError={handlePasscodeMismatch} storedPasscode={controller.storedPasscode} onDismiss={() => controller.onDismiss()} error={controller.error} + salt={controller.passcodeSalt} /> )} diff --git a/screens/BiometricScreenController.ts b/screens/BiometricScreenController.ts index 732d4cf27f..fab1da8781 100644 --- a/screens/BiometricScreenController.ts +++ b/screens/BiometricScreenController.ts @@ -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, diff --git a/screens/Home/MyVcsTab.tsx b/screens/Home/MyVcsTab.tsx index 16df6db75f..132fff661e 100644 --- a/screens/Home/MyVcsTab.tsx +++ b/screens/Home/MyVcsTab.tsx @@ -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 = 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 ( diff --git a/screens/PasscodeScreen.tsx b/screens/PasscodeScreen.tsx index 2f8979f530..3cd62d54e3 100644 --- a/screens/PasscodeScreen.tsx +++ b/screens/PasscodeScreen.tsx @@ -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 = 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 === '' ? ( @@ -63,7 +105,7 @@ export const PasscodeScreen: React.FC = props => { @@ -76,7 +118,7 @@ export const PasscodeScreen: React.FC = props => { padding="32" backgroundColor={Theme.Colors.whiteBackgroundColor}> - {props.route.params?.setup ? ( + {isSettingUp ? ( {passcodeSetup} @@ -92,7 +134,7 @@ export const PasscodeScreen: React.FC = props => { diff --git a/screens/PasscodeScreenController.ts b/screens/PasscodeScreenController.ts index 78ab5b5444..ddfabc7283 100644 --- a/screens/PasscodeScreenController.ts +++ b/screens/PasscodeScreenController.ts @@ -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]); diff --git a/screens/WelcomeScreenController.ts b/screens/WelcomeScreenController.ts index 027a08437d..ac55c6c65a 100644 --- a/screens/WelcomeScreenController.ts +++ b/screens/WelcomeScreenController.ts @@ -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'); } diff --git a/shared/telemetry/TelemetryUtils.js b/shared/telemetry/TelemetryUtils.js index 1504ddf571..333bb79b1a 100644 --- a/shared/telemetry/TelemetryUtils.js +++ b/shared/telemetry/TelemetryUtils.js @@ -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',