diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index c6446e1c5501..98f904f2e37f 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -30,14 +30,6 @@ 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', @@ -811,6 +803,13 @@ const ONYXKEYS = { * Key format: passkey_${userId} */ PASSKEY_CREDENTIALS: 'passkeyCredentials_', + + /** Holds information about device-specific biometrics which: + * - does need to be persisted across logout (but not uninstallation) + * - does not need to be kept in secure storage + * Key format: deviceBiometrics_${accountID} + */ + DEVICE_BIOMETRICS: 'deviceBiometrics_', }, /** List of Form ids */ @@ -1247,6 +1246,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.DOMAIN_ERRORS]: OnyxTypes.DomainErrors; [ONYXKEYS.COLLECTION.CODING_RULE_MATCHING_TRANSACTION]: OnyxTypes.CodingRuleMatchingTransaction; [ONYXKEYS.COLLECTION.PASSKEY_CREDENTIALS]: OnyxTypes.LocalPasskeyCredentialsEntry; + [ONYXKEYS.COLLECTION.DEVICE_BIOMETRICS]: OnyxTypes.DeviceBiometrics; }; type OnyxValuesMapping = { @@ -1475,7 +1475,6 @@ 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>; - [ONYXKEYS.DEVICE_BIOMETRICS]: OnyxTypes.DeviceBiometrics; }; type OnyxDerivedValuesMapping = { diff --git a/src/components/MultifactorAuthentication/Context/Main.tsx b/src/components/MultifactorAuthentication/Context/Main.tsx index 4dda859997f6..b78e685bfb48 100644 --- a/src/components/MultifactorAuthentication/Context/Main.tsx +++ b/src/components/MultifactorAuthentication/Context/Main.tsx @@ -7,32 +7,21 @@ import useBiometrics from '@components/MultifactorAuthentication/biometrics/useB import type {MultifactorAuthenticationScenario, MultifactorAuthenticationScenarioParams} from '@components/MultifactorAuthentication/config/types'; import addMFABreadcrumb from '@components/MultifactorAuthentication/observability/breadcrumbs'; import trackMFAFlowOutcome from '@components/MultifactorAuthentication/observability/trackMFAFlowOutcome'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useNetwork from '@hooks/useNetwork'; import {requestValidateCodeAction} from '@libs/actions/User'; import getPlatform from '@libs/getPlatform'; import type {ChallengeType, MultifactorAuthenticationCallbackInput} from '@libs/MultifactorAuthentication/shared/types'; import Navigation from '@navigation/Navigation'; -import {clearLocalMFAPublicKeyList, requestAuthorizationChallenge, requestRegistrationChallenge} from '@userActions/MultifactorAuthentication'; +import {clearLocalMFAPublicKeyList, getDeviceBiometricsOnyxKey, requestAuthorizationChallenge, requestRegistrationChallenge} from '@userActions/MultifactorAuthentication'; import {processRegistration, processScenarioAction} 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 {useMultifactorAuthenticationActions, useMultifactorAuthenticationState} from './State'; let deviceBiometricsState: OnyxEntry; -// Use Onyx.connectWithoutView instead of useOnyx hook to access the device biometrics state. -// This is a non-reactive read that allows us to check the current value (hasAcceptedSoftPrompt) -// from within the process() callback without triggering calling it too many times during the -// fresh registration flow -Onyx.connectWithoutView({ - key: ONYXKEYS.DEVICE_BIOMETRICS, - callback: (data) => { - deviceBiometricsState = data; - }, -}); - type ExecuteScenarioParams = MultifactorAuthenticationScenarioParams; type MultifactorAuthenticationContextValue = { @@ -71,10 +60,24 @@ function MultifactorAuthenticationContextProvider({children}: MultifactorAuthent const {dispatch} = useMultifactorAuthenticationActions(); const biometrics = useBiometrics(); + const {accountID} = useCurrentUserPersonalDetails(); const {isOffline} = useNetwork(); const platform = getPlatform(); const promptType = CONST.MULTIFACTOR_AUTHENTICATION.PROMPT_TYPE_MAP[biometrics.deviceVerificationType]; + useEffect(() => { + // Non-reactive read of deviceBiometrics. Using Onyx.connectWithoutView (set up in a useEffect + // inside the provider) instead of useOnyx to avoid triggering process() too many times during + // the fresh registration flow. + const connection = Onyx.connectWithoutView({ + key: getDeviceBiometricsOnyxKey(accountID), + callback: (data) => { + deviceBiometricsState = data; + }, + }); + return () => Onyx.disconnect(connection); + }, [accountID]); + /** * Handles the completion of a multifactor authentication scenario. * Invokes the scenario's callback function and navigates to the appropriate outcome screen. diff --git a/src/components/MultifactorAuthentication/Context/usePromptContent.ts b/src/components/MultifactorAuthentication/Context/usePromptContent.ts index 8cbf9dbbdbf3..cd6c734633ce 100644 --- a/src/components/MultifactorAuthentication/Context/usePromptContent.ts +++ b/src/components/MultifactorAuthentication/Context/usePromptContent.ts @@ -3,9 +3,10 @@ import type DotLottieAnimation from '@components/LottieAnimations/types'; import useBiometrics from '@components/MultifactorAuthentication/biometrics/useBiometrics'; import {MULTIFACTOR_AUTHENTICATION_PROMPT_UI} from '@components/MultifactorAuthentication/config'; import type {MultifactorAuthenticationPromptType} from '@components/MultifactorAuthentication/config/types'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useOnyx from '@hooks/useOnyx'; +import {getDeviceBiometricsOnyxKey} from '@libs/actions/MultifactorAuthentication'; import type {TranslationPaths} from '@src/languages/types'; -import ONYXKEYS from '@src/ONYXKEYS'; import type IconAsset from '@src/types/utils/IconAsset'; import {useMultifactorAuthenticationState} from './State'; @@ -29,8 +30,9 @@ type PromptContent = { function usePromptContent(promptType: MultifactorAuthenticationPromptType): PromptContent { const state = useMultifactorAuthenticationState(); const {areLocalCredentialsKnownToServer} = useBiometrics(); + const {accountID} = useCurrentUserPersonalDetails(); const [serverHasCredentials, setServerHasCredentials] = useState(false); - const [deviceBiometricsState] = useOnyx(ONYXKEYS.DEVICE_BIOMETRICS); + const [deviceBiometricsState] = useOnyx(getDeviceBiometricsOnyxKey(accountID)); const hasEverAcceptedSoftPrompt = deviceBiometricsState?.hasAcceptedSoftPrompt ?? false; // We need to know if server has this device's credentials specifically diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 0714e6464d2d..8ed76b1ca8d9 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -138,6 +138,7 @@ const KEYS_TO_PRESERVE: OnyxKey[] = [ ONYXKEYS.SHOULD_USE_STAGING_SERVER, ONYXKEYS.IS_DEBUG_MODE_ENABLED, ONYXKEYS.COLLECTION.PASSKEY_CREDENTIALS, + ONYXKEYS.COLLECTION.DEVICE_BIOMETRICS, // Preserve IS_USING_IMPORTED_STATE so that when the app restarts (especially in HybridApp mode), // we know if we're in imported state mode and should skip API calls that would cause infinite loading diff --git a/src/libs/actions/Delegate.ts b/src/libs/actions/Delegate.ts index 1e8d42ba6259..2c66412bc1fd 100644 --- a/src/libs/actions/Delegate.ts +++ b/src/libs/actions/Delegate.ts @@ -38,6 +38,7 @@ const KEYS_TO_PRESERVE_DELEGATE_ACCESS = [ ONYXKEYS.SHOULD_USE_STAGING_SERVER, ONYXKEYS.IS_DEBUG_MODE_ENABLED, ONYXKEYS.COLLECTION.PASSKEY_CREDENTIALS, + ONYXKEYS.COLLECTION.DEVICE_BIOMETRICS, ]; type WithDelegatedAccess = { diff --git a/src/libs/actions/MultifactorAuthentication/index.ts b/src/libs/actions/MultifactorAuthentication/index.ts index 043091f3dbf4..af8c4c4e498b 100644 --- a/src/libs/actions/MultifactorAuthentication/index.ts +++ b/src/libs/actions/MultifactorAuthentication/index.ts @@ -352,8 +352,12 @@ async function fireAndForgetDenyTransaction({transactionID}: DenyTransactionPara ); } -function markHasAcceptedSoftPrompt() { - Onyx.merge(ONYXKEYS.DEVICE_BIOMETRICS, { +function getDeviceBiometricsOnyxKey(accountID: number): `${typeof ONYXKEYS.COLLECTION.DEVICE_BIOMETRICS}${string}` { + return `${ONYXKEYS.COLLECTION.DEVICE_BIOMETRICS}${accountID}`; +} + +function markHasAcceptedSoftPrompt(accountID: number) { + Onyx.merge(getDeviceBiometricsOnyxKey(accountID), { hasAcceptedSoftPrompt: true, }); } @@ -370,6 +374,7 @@ export { requestAuthorizationChallenge, troubleshootMultifactorAuthentication, revokeMultifactorAuthenticationCredentials, + getDeviceBiometricsOnyxKey, markHasAcceptedSoftPrompt, clearLocalMFAPublicKeyList, setPersonalDetailsAndShipExpensifyCardsWithPIN, diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index 6b26bde5e3ef..629925660165 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -318,6 +318,7 @@ const KEYS_TO_PRESERVE_SUPPORTAL = [ // we know if we're in imported state mode and should skip API calls that would cause infinite loading ONYXKEYS.IS_USING_IMPORTED_STATE, ONYXKEYS.COLLECTION.PASSKEY_CREDENTIALS, + ONYXKEYS.COLLECTION.DEVICE_BIOMETRICS, ]; function signOutAndRedirectToSignIn(shouldResetToHome?: boolean, shouldStashSession?: boolean, shouldSignOutFromOldDot = true, shouldForceUseStashedSession?: boolean) { diff --git a/src/libs/actions/SignInRedirect.ts b/src/libs/actions/SignInRedirect.ts index e5fafa9d2fa4..8fcf14765ab0 100644 --- a/src/libs/actions/SignInRedirect.ts +++ b/src/libs/actions/SignInRedirect.ts @@ -57,6 +57,7 @@ function clearStorageAndRedirect(errorMessage?: string): Promise { keysToPreserve.push(ONYXKEYS.SHOULD_USE_STAGING_SERVER); keysToPreserve.push(ONYXKEYS.IS_DEBUG_MODE_ENABLED); keysToPreserve.push(ONYXKEYS.COLLECTION.PASSKEY_CREDENTIALS); + keysToPreserve.push(ONYXKEYS.COLLECTION.DEVICE_BIOMETRICS); // After signing out, set ourselves as offline if we were offline before logging out and we are not forcing it. // If we are forcing offline, ignore it while signed out, otherwise it would require a refresh because there's no way to toggle the switch to go back online while signed out. diff --git a/src/pages/MultifactorAuthentication/PromptPage.tsx b/src/pages/MultifactorAuthentication/PromptPage.tsx index cc63c213d029..4a74a29ca6ad 100644 --- a/src/pages/MultifactorAuthentication/PromptPage.tsx +++ b/src/pages/MultifactorAuthentication/PromptPage.tsx @@ -9,6 +9,7 @@ import {DefaultCancelConfirmModal} from '@components/MultifactorAuthentication/c import {useMultifactorAuthentication, useMultifactorAuthenticationActions, useMultifactorAuthenticationState, usePromptContent} from '@components/MultifactorAuthentication/Context'; import MultifactorAuthenticationPromptContent from '@components/MultifactorAuthentication/PromptContent'; import ScreenWrapper from '@components/ScreenWrapper'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -27,6 +28,7 @@ function MultifactorAuthenticationPromptPage({route}: MultifactorAuthenticationP const {cancel} = useMultifactorAuthentication(); const state = useMultifactorAuthenticationState(); const {dispatch} = useMultifactorAuthenticationActions(); + const {accountID} = useCurrentUserPersonalDetails(); const {isOffline} = useNetwork(); const {illustration, title, subtitle, shouldDisplayConfirmButton} = usePromptContent(route.params.promptType); @@ -34,7 +36,7 @@ function MultifactorAuthenticationPromptPage({route}: MultifactorAuthenticationP const [isCancelModalVisible, setCancelModalVisibility] = useState(false); const onConfirm = () => { - markHasAcceptedSoftPrompt(); + markHasAcceptedSoftPrompt(accountID); dispatch({type: 'SET_SOFT_PROMPT_APPROVED', payload: true}); }; diff --git a/src/types/onyx/DeviceBiometrics.ts b/src/types/onyx/DeviceBiometrics.ts index e906e0cc0f8f..f1ecc5685cbe 100644 --- a/src/types/onyx/DeviceBiometrics.ts +++ b/src/types/onyx/DeviceBiometrics.ts @@ -1,5 +1,5 @@ /** Holds information about device-specific biometrics which: - * - does need to be persisted + * - persists across logout/login (stored as a collection keyed by accountID) * - does not need to be kept in secure storage * - does not persist across uninstallations * (secure storage persists across uninstallation)