Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7466,7 +7466,7 @@ const CONST = {
CASH_BACK: 'earnedCashback',
},

EXCLUDE_FROM_LAST_VISITED_PATH: [SCREENS.NOT_FOUND, SCREENS.SAML_SIGN_IN, SCREENS.VALIDATE_LOGIN, SCREENS.MIGRATED_USER_WELCOME_MODAL.ROOT, SCREENS.MONEY_REQUEST.STEP_SCAN] as string[],
EXCLUDE_FROM_LAST_VISITED_PATH: [SCREENS.NOT_FOUND, SCREENS.SAML_SIGN_IN, SCREENS.VALIDATE_LOGIN, SCREENS.MIGRATED_USER_WELCOME_MODAL.ROOT, SCREENS.MONEY_REQUEST.STEP_SCAN, ...Object.values(SCREENS.MULTIFACTOR_AUTHENTICATION)] as string[],

CANCELLATION_TYPE: {
MANUAL: 'manual',
Expand Down
9 changes: 9 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ const ONYXKEYS = {
/** A unique ID for the device */
DEVICE_ID: 'deviceID',

/** Holds information about device-specific biometrics which:
* - does need to be persisted
* - does not need to be kept in secure storage
* - does not persist across uninstallations
* (secure storage persists across uninstallation)
*/
DEVICE_BIOMETRICS: 'deviceBiometrics',

/** Boolean flag set whenever the sidebar has loaded */
IS_SIDEBAR_LOADED: 'isSidebarLoaded',

Expand Down Expand Up @@ -1417,6 +1425,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.IS_OPEN_CONFIRM_NAVIGATE_EXPENSIFY_CLASSIC_MODAL_OPEN]: boolean;
[ONYXKEYS.PERSONAL_POLICY_ID]: string;
[ONYXKEYS.TRANSACTION_IDS_HIGHLIGHT_ON_SEARCH_ROUTE]: Record<string, Record<string, boolean>>;
[ONYXKEYS.DEVICE_BIOMETRICS]: OnyxTypes.DeviceBiometrics;
};

type OnyxDerivedValuesMapping = {
Expand Down
22 changes: 21 additions & 1 deletion src/components/MultifactorAuthentication/Context/Main.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React, {createContext, useCallback, useContext, useEffect, useMemo} from 'react';
import type {ReactNode} from 'react';
import Onyx from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import {MULTIFACTOR_AUTHENTICATION_SCENARIO_CONFIG} from '@components/MultifactorAuthentication/config';
import {getOutcomePaths} from '@components/MultifactorAuthentication/config/outcomePaths';
import type {MultifactorAuthenticationScenario, MultifactorAuthenticationScenarioParams} from '@components/MultifactorAuthentication/config/types';
Expand All @@ -11,11 +13,21 @@ import Navigation from '@navigation/Navigation';
import {requestAuthorizationChallenge, requestRegistrationChallenge} from '@userActions/MultifactorAuthentication';
import {processRegistration, processScenario} from '@userActions/MultifactorAuthentication/processing';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {DeviceBiometrics} from '@src/types/onyx';
import {useMultifactorAuthenticationState} from './State';
import useNativeBiometrics from './useNativeBiometrics';
import type {AuthorizeResult, RegisterResult} from './useNativeBiometrics';

let deviceBiometricsState: OnyxEntry<DeviceBiometrics>;
Onyx.connectWithoutView({
key: ONYXKEYS.DEVICE_BIOMETRICS,
callback: (data) => {
deviceBiometricsState = data;
},
});

type ExecuteScenarioParams<T extends MultifactorAuthenticationScenario> = MultifactorAuthenticationScenarioParams<T> & Partial<OutcomePaths>;

type MultifactorAuthenticationContextValue = {
Expand Down Expand Up @@ -194,10 +206,18 @@ function MultifactorAuthenticationContextProvider({children}: MultifactorAuthent
return;
}

// Registration isn't required, but they have never seen the soft prompt
// this happens on ios if they delete and reinstall the app. Their keys are preserved in the secure store, but
// they'll be shown the "do you want to enable FaceID again" system prompt, so we want to show them the soft prompt
if (!deviceBiometricsState?.hasAcceptedSoftPrompt) {
Navigation.navigate(ROUTES.MULTIFACTOR_AUTHENTICATION_PROMPT.getRoute(CONST.MULTIFACTOR_AUTHENTICATION.PROMPT.ENABLE_BIOMETRICS), {forceReplace: true});
return;
}

// 4. Authorize the user if that has not already been done
if (!isAuthorizationComplete) {
if (!Navigation.isActiveRoute(ROUTES.MULTIFACTOR_AUTHENTICATION_PROMPT.getRoute('enable-biometrics'))) {
Navigation.navigate(ROUTES.MULTIFACTOR_AUTHENTICATION_PROMPT.getRoute('enable-biometrics'), { forceReplace: true });
Navigation.navigate(ROUTES.MULTIFACTOR_AUTHENTICATION_PROMPT.getRoute('enable-biometrics'), {forceReplace: true});
}

// Request authorization challenge if not already fetched
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,13 @@ function serverHasRegisteredCredentials(data: OnyxEntry<Account>) {
function usePromptContent(promptType: MultifactorAuthenticationPromptType): PromptContent {
const {state} = useMultifactorAuthenticationState();
const [serverHasCredentials = false] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true, selector: serverHasRegisteredCredentials});
const [deviceBiometricsState] = useOnyx(ONYXKEYS.DEVICE_BIOMETRICS, {canBeMissing: true});
const hasEverAcceptedSoftPrompt = deviceBiometricsState?.hasAcceptedSoftPrompt ?? false;

const contentData = MULTIFACTOR_AUTHENTICATION_PROMPT_UI[promptType];

// Returning user: server has credentials, but user hasn't approved soft prompt yet
const isReturningUser = serverHasCredentials && !state.softPromptApproved;
const isReturningUser = hasEverAcceptedSoftPrompt && serverHasCredentials && !state.softPromptApproved;

let title: TranslationPaths = contentData.title;
let subtitle: TranslationPaths | undefined = contentData.subtitle;
Expand All @@ -54,15 +56,15 @@ function usePromptContent(promptType: MultifactorAuthenticationPromptType): Prom
if (isReturningUser) {
title = 'multifactorAuthentication.letsAuthenticateYou';
subtitle = undefined;
} else if (state.isRegistrationComplete) {
} else if (state.isRegistrationComplete && hasEverAcceptedSoftPrompt) {
title = 'multifactorAuthentication.nowLetsAuthenticateYou';
subtitle = undefined;
}

// Display confirm button only for new users during their first biometric registration.
// Hide it for: users who already approved the soft prompt, users who finished registration,
// or returning users with existing server credentials. The button prompts users to enable biometrics.
const shouldDisplayConfirmButton = !state.softPromptApproved && !state.isRegistrationComplete && !serverHasCredentials;
const shouldDisplayConfirmButton = !hasEverAcceptedSoftPrompt || (!state.softPromptApproved && !state.isRegistrationComplete && !serverHasCredentials);

return {
animation: contentData.animation,
Expand Down
15 changes: 14 additions & 1 deletion src/libs/actions/MultifactorAuthentication/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,17 @@ async function revokeMultifactorAuthenticationCredentials() {
}
}

export {registerAuthenticationKey, requestRegistrationChallenge, requestAuthorizationChallenge, troubleshootMultifactorAuthentication, revokeMultifactorAuthenticationCredentials};
function markHasAcceptedSoftPrompt() {
Onyx.merge(ONYXKEYS.DEVICE_BIOMETRICS, {
hasAcceptedSoftPrompt: true,
});
}

export {
registerAuthenticationKey,
requestRegistrationChallenge,
requestAuthorizationChallenge,
troubleshootMultifactorAuthentication,
revokeMultifactorAuthenticationCredentials,
markHasAcceptedSoftPrompt,
};
2 changes: 2 additions & 0 deletions src/pages/MultifactorAuthentication/PromptPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import ScreenWrapper from '@components/ScreenWrapper';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
import {markHasAcceptedSoftPrompt} from '@libs/actions/MultifactorAuthentication';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {MultifactorAuthenticationParamList} from '@libs/Navigation/types';
import Navigation from '@navigation/Navigation';
Expand All @@ -31,6 +32,7 @@ function MultifactorAuthenticationPromptPage({route}: MultifactorAuthenticationP
const [isCancelModalVisible, setCancelModalVisibility] = useState(false);

const onConfirm = () => {
markHasAcceptedSoftPrompt();
dispatch({type: 'SET_SOFT_PROMPT_APPROVED', payload: true});
};

Expand Down
12 changes: 12 additions & 0 deletions src/types/onyx/DeviceBiometrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/** Holds information about device-specific biometrics which:
* - does need to be persisted
* - does not need to be kept in secure storage
* - does not persist across uninstallations
* (secure storage persists across uninstallation)
*/
type DeviceBiometrics = {
/** Whether the user has been shown the Biometrics Soft Prompt screen and accepted it */
hasAcceptedSoftPrompt: boolean;
};

export default DeviceBiometrics;
2 changes: 2 additions & 0 deletions src/types/onyx/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import type {
TodoMetadata,
TodosDerivedValue,
} from './DerivedValues';
import type DeviceBiometrics from './DeviceBiometrics';
import type DismissedProductTraining from './DismissedProductTraining';
import type DismissedReferralBanners from './DismissedReferralBanners';
import type Domain from './Domain';
Expand Down Expand Up @@ -349,4 +350,5 @@ export type {
DomainSecurityGroup,
CodingRuleMatchingTransaction,
UserSecurityGroupData,
DeviceBiometrics,
};
Loading