From 368e9fce540f74eb994e2f007afc219ec6d4e262 Mon Sep 17 00:00:00 2001 From: Chuck Dries Date: Thu, 12 Mar 2026 12:52:13 -0700 Subject: [PATCH 1/7] Revert "Merge pull request #85001 from Expensify/revert-79266-chuckdries/ReplaceTwoFactorDevice" This reverts commit f43469ce5c4f9aabb0b6378b741949e4eb569a40, reversing changes made to 56a68bff619cf1bf4b9ecbee6546df025355192a. --- src/CONST/index.ts | 2 + src/ROUTES.ts | 2 + src/SCREENS.ts | 2 + .../Pressable/PressableWithDelayToggle.tsx | 4 +- src/languages/de.ts | 6 + src/languages/en.ts | 6 + src/languages/es.ts | 6 + src/languages/fr.ts | 6 + src/languages/it.ts | 6 + src/languages/ja.ts | 6 + src/languages/nl.ts | 6 + src/languages/pl.ts | 6 + src/languages/pt-BR.ts | 6 + src/languages/zh-hans.ts | 6 + .../ReplaceTwoFactorDeviceParams.ts | 6 + src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + .../ModalStackNavigators/index.tsx | 2 + .../RELATIONS/SETTINGS_TO_RHP.ts | 2 + src/libs/Navigation/linkingConfig/config.ts | 8 ++ src/libs/TwoFactorAuthUtils.ts | 20 ++++ src/libs/actions/Session/index.ts | 54 +++++++++ .../Security/TwoFactorAuth/EnabledPage.tsx | 7 +- .../ReplaceDeviceVerifyNewPage.tsx | 106 ++++++++++++++++++ .../ReplaceDeviceVerifyOldPage.tsx | 83 ++++++++++++++ .../TwoFactorAuthSecretDisplay.tsx | 71 ++++++++++++ .../TwoFactorAuth/TwoFactorAuthWrapper.tsx | 2 + src/styles/index.ts | 4 + 28 files changed, 435 insertions(+), 3 deletions(-) create mode 100644 src/libs/API/parameters/ReplaceTwoFactorDeviceParams.ts create mode 100644 src/libs/TwoFactorAuthUtils.ts create mode 100644 src/pages/settings/Security/TwoFactorAuth/ReplaceDeviceVerifyNewPage.tsx create mode 100644 src/pages/settings/Security/TwoFactorAuth/ReplaceDeviceVerifyOldPage.tsx create mode 100644 src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthSecretDisplay.tsx diff --git a/src/CONST/index.ts b/src/CONST/index.ts index ad1dc229273ee..39e5ddb41c35b 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -5940,6 +5940,8 @@ const CONST = { ENABLED: 'ENABLED', DISABLED: 'DISABLED', DISABLE: 'DISABLE', + REPLACE_VERIFY_OLD: 'REPLACE_VERIFY_OLD', + REPLACE_VERIFY_NEW: 'REPLACE_VERIFY_NEW', }, MERGE_ACCOUNT_RESULTS: { SUCCESS: 'success', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 23188706a6e21..4d964e634d74f 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -609,6 +609,8 @@ const ROUTES = { }, SETTINGS_2FA_DISABLED: 'settings/security/two-factor-auth/disabled', SETTINGS_2FA_DISABLE: 'settings/security/two-factor-auth/disable', + SETTINGS_2FA_REPLACE_VERIFY_OLD: 'settings/security/two-factor-auth/replace/verify-old', + SETTINGS_2FA_REPLACE_VERIFY_NEW: 'settings/security/two-factor-auth/replace/verify-new', SETTINGS_STATUS: 'settings/profile/status', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 6c2599a91dc50..54bc9eb9bb18d 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -254,6 +254,8 @@ const SCREENS = { SUCCESS: 'Settings_TwoFactorAuth_Success', DISABLED: 'Settings_TwoFactorAuth_Disabled', DISABLE: 'Settings_TwoFactorAuth_Disable', + REPLACE_VERIFY_OLD: 'Settings_TwoFactorAuth_Replace_VerifyOld', + REPLACE_VERIFY_NEW: 'Settings_TwoFactorAuth_Replace_VerifyNew', }, SAVE_THE_WORLD: { ROOT: 'SaveTheWorld_Root', diff --git a/src/components/Pressable/PressableWithDelayToggle.tsx b/src/components/Pressable/PressableWithDelayToggle.tsx index e630688391372..d493d43e88ec1 100644 --- a/src/components/Pressable/PressableWithDelayToggle.tsx +++ b/src/components/Pressable/PressableWithDelayToggle.tsx @@ -166,17 +166,17 @@ function PressableWithDelayToggle({ > {({hovered, pressed}) => ( <> - {!inline && displayLabelText} {shouldShowIcon && ( )} + {!inline && displayLabelText} )} diff --git a/src/languages/de.ts b/src/languages/de.ts index c826e8b49e889..586a66b52325b 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -2115,6 +2115,12 @@ const translations: TranslationDeepObject = { twoFactorAuthIsRequiredCompany: 'Ihr Unternehmen verlangt eine Zwei-Faktor-Authentifizierung.', twoFactorAuthCannotDisable: '2FA kann nicht deaktiviert werden', twoFactorAuthRequired: 'Die Zwei-Faktor-Authentifizierung (2FA) ist für Ihre Xero-Verbindung erforderlich und kann nicht deaktiviert werden.', + replaceDevice: 'Gerät ersetzen', + replaceDeviceTitle: 'Zwei-Faktor-Gerät ersetzen', + verifyOldDeviceTitle: 'Altes Gerät verifizieren', + verifyOldDeviceDescription: 'Gib den sechsstelligen Code aus deiner aktuellen Authenticator-App ein, um zu bestätigen, dass du Zugriff darauf hast.', + verifyNewDeviceTitle: 'Neues Gerät einrichten', + verifyNewDeviceDescription: 'Scanne den QR-Code mit deinem neuen Gerät und gib dann den Code ein, um die Einrichtung abzuschließen.', }, recoveryCodeForm: { error: { diff --git a/src/languages/en.ts b/src/languages/en.ts index ea41723001910..ed83031233ba3 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2163,6 +2163,12 @@ const translations = { twoFactorAuthIsRequiredCompany: 'Your company requires two-factor authentication.', twoFactorAuthCannotDisable: 'Cannot disable 2FA', twoFactorAuthRequired: 'Two-factor authentication (2FA) is required for your Xero connection and cannot be disabled.', + replaceDevice: 'Replace device', + replaceDeviceTitle: 'Replace two-factor device', + verifyOldDeviceTitle: 'Verify old device', + verifyOldDeviceDescription: 'Enter the six-digit code from your current authenticator app to confirm you have access to it.', + verifyNewDeviceTitle: 'Set up new device', + verifyNewDeviceDescription: 'Scan the QR code with your new device, then enter the code to complete setup.', }, recoveryCodeForm: { error: { diff --git a/src/languages/es.ts b/src/languages/es.ts index fab0d5ec0ecc5..daa2e839d7b6a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1988,6 +1988,12 @@ const translations: TranslationDeepObject = { twoFactorAuthIsRequiredCompany: 'Tu empresa requiere el uso de autenticación de dos factores. Por favor, habilítala para seguir usando Expensify.', twoFactorAuthCannotDisable: 'No se puede desactivar la autenticación de dos factores (2FA)', twoFactorAuthRequired: 'La autenticación de dos factores (2FA) es obligatoria para tu conexión a Xero y no se puede desactivar.', + replaceDevice: 'Reemplazar dispositivo', + replaceDeviceTitle: 'Reemplazar dispositivo de autenticación de dos factores', + verifyOldDeviceTitle: 'Verificar dispositivo anterior', + verifyOldDeviceDescription: 'Introduce el código de seis dígitos de tu aplicación de autenticación actual para confirmar que tienes acceso a ella.', + verifyNewDeviceTitle: 'Configurar nuevo dispositivo', + verifyNewDeviceDescription: 'Escanea el código QR con tu nuevo dispositivo y luego introduce el código para completar la configuración.', }, recoveryCodeForm: { error: { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index cdd84323b33ce..5ea1749ade1a8 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -2121,6 +2121,12 @@ const translations: TranslationDeepObject = { twoFactorAuthIsRequiredCompany: 'Votre entreprise exige l’authentification à deux facteurs.', twoFactorAuthCannotDisable: 'Impossible de désactiver la 2FA', twoFactorAuthRequired: 'L’authentification à deux facteurs (2FA) est requise pour votre connexion Xero et ne peut pas être désactivée.', + replaceDevice: 'Remplacer l’appareil', + replaceDeviceTitle: 'Remplacer l’appareil d’authentification à deux facteurs', + verifyOldDeviceTitle: 'Vérifier l’ancien appareil', + verifyOldDeviceDescription: 'Saisissez le code à six chiffres depuis votre application d’authentification actuelle pour confirmer que vous y avez accès.', + verifyNewDeviceTitle: 'Configurer un nouvel appareil', + verifyNewDeviceDescription: 'Scannez le code QR avec votre nouvel appareil, puis saisissez le code pour terminer la configuration.', }, recoveryCodeForm: { error: { diff --git a/src/languages/it.ts b/src/languages/it.ts index 77ddcd87941dc..be04480089599 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -2112,6 +2112,12 @@ const translations: TranslationDeepObject = { twoFactorAuthIsRequiredCompany: 'La tua azienda richiede l’autenticazione a due fattori.', twoFactorAuthCannotDisable: "Impossibile disabilitare l'autenticazione a due fattori", twoFactorAuthRequired: 'Per la connessione a Xero è richiesta l’autenticazione a due fattori (2FA) e non può essere disattivata.', + replaceDevice: 'Sostituisci dispositivo', + replaceDeviceTitle: "Sostituisci dispositivo per l'autenticazione a due fattori", + verifyOldDeviceTitle: 'Verifica dispositivo precedente', + verifyOldDeviceDescription: 'Inserisci il codice a sei cifre dalla tua attuale app di autenticazione per confermare di avere accesso ad essa.', + verifyNewDeviceTitle: 'Configura nuovo dispositivo', + verifyNewDeviceDescription: 'Scansiona il codice QR con il tuo nuovo dispositivo, quindi inserisci il codice per completare la configurazione.', }, recoveryCodeForm: { error: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index df3c49a88ab34..3c2e0df029b59 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -2099,6 +2099,12 @@ const translations: TranslationDeepObject = { twoFactorAuthIsRequiredCompany: 'あなたの会社では、2 要素認証が必須です。', twoFactorAuthCannotDisable: '2要素認証を無効にできません', twoFactorAuthRequired: 'Xero 連携には二要素認証(2FA)が必須で、無効にすることはできません。', + replaceDevice: 'デバイスを交換', + replaceDeviceTitle: '2 要素認証デバイスを交換', + verifyOldDeviceTitle: '古いデバイスを確認', + verifyOldDeviceDescription: '現在使用している認証アプリに表示されている6桁のコードを入力して、アクセスできることを確認してください。', + verifyNewDeviceTitle: '新しいデバイスをセットアップ', + verifyNewDeviceDescription: '新しいデバイスでQRコードをスキャンし、その後コードを入力して設定を完了してください。', }, recoveryCodeForm: { error: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 0258ad7e7da8c..2a9050aedc47d 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -2109,6 +2109,12 @@ const translations: TranslationDeepObject = { twoFactorAuthIsRequiredCompany: 'Je bedrijf vereist tweefactorauthenticatie.', twoFactorAuthCannotDisable: 'Kan 2FA niet uitschakelen', twoFactorAuthRequired: 'Tweestapsverificatie (2FA) is vereist voor je Xero-verbinding en kan niet worden uitgeschakeld.', + replaceDevice: 'Apparaat vervangen', + replaceDeviceTitle: 'Twee-factorapparaat vervangen', + verifyOldDeviceTitle: 'Oud apparaat verifiëren', + verifyOldDeviceDescription: 'Voer de zescijferige code uit je huidige authenticator-app in om te bevestigen dat je er toegang toe hebt.', + verifyNewDeviceTitle: 'Nieuw apparaat instellen', + verifyNewDeviceDescription: 'Scan de QR-code met je nieuwe apparaat en voer vervolgens de code in om de installatie te voltooien.', }, recoveryCodeForm: { error: { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index d9617682efe9d..93c25eb1fbb66 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -2109,6 +2109,12 @@ const translations: TranslationDeepObject = { twoFactorAuthIsRequiredCompany: 'Twoetapowe uwierzytelnianie jest wymagane przez Twoją firmę.', twoFactorAuthCannotDisable: 'Nie można wyłączyć 2FA', twoFactorAuthRequired: 'Dla połączenia z Xero wymagana jest weryfikacja dwuetapowa (2FA) i nie można jej wyłączyć.', + replaceDevice: 'Zastąp urządzenie', + replaceDeviceTitle: 'Wymień urządzenie uwierzytelniania dwuskładnikowego', + verifyOldDeviceTitle: 'Zweryfikuj stare urządzenie', + verifyOldDeviceDescription: 'Wprowadź sześciocyfrowy kod z bieżącej aplikacji uwierzytelniającej, aby potwierdzić, że masz do niej dostęp.', + verifyNewDeviceTitle: 'Skonfiguruj nowe urządzenie', + verifyNewDeviceDescription: 'Zeskanuj kod QR swoim nowym urządzeniem, a następnie wprowadź kod, aby zakończyć konfigurację.', }, recoveryCodeForm: { error: { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 5d4aec900dae3..950d6c16e7efe 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -2105,6 +2105,12 @@ const translations: TranslationDeepObject = { twoFactorAuthIsRequiredCompany: 'Sua empresa exige autenticação em duas etapas.', twoFactorAuthCannotDisable: 'Não é possível desativar a 2FA', twoFactorAuthRequired: 'A autenticação em duas etapas (2FA) é obrigatória para sua conexão com o Xero e não pode ser desativada.', + replaceDevice: 'Substituir dispositivo', + replaceDeviceTitle: 'Substituir dispositivo de autenticação em duas etapas', + verifyOldDeviceTitle: 'Verificar dispositivo antigo', + verifyOldDeviceDescription: 'Insira o código de seis dígitos do seu aplicativo autenticador atual para confirmar que você tem acesso a ele.', + verifyNewDeviceTitle: 'Configurar novo dispositivo', + verifyNewDeviceDescription: 'Escaneie o código QR com seu novo dispositivo e, em seguida, insira o código para concluir a configuração.', }, recoveryCodeForm: { error: { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index ecc478ec7836d..87a48fcc2345a 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -2071,6 +2071,12 @@ const translations: TranslationDeepObject = { twoFactorAuthIsRequiredCompany: '您的公司要求使用双重身份验证。', twoFactorAuthCannotDisable: '无法禁用双重验证', twoFactorAuthRequired: '您的 Xero 连接需要启用双重身份验证(2FA),且无法将其禁用。', + replaceDevice: '更换设备', + replaceDeviceTitle: '更换双重验证设备', + verifyOldDeviceTitle: '验证旧设备', + verifyOldDeviceDescription: '请输入您当前身份验证器应用中的六位数验证码,以确认您可以访问该应用。', + verifyNewDeviceTitle: '设置新设备', + verifyNewDeviceDescription: '使用新设备扫描二维码,然后输入代码以完成设置。', }, recoveryCodeForm: { error: { diff --git a/src/libs/API/parameters/ReplaceTwoFactorDeviceParams.ts b/src/libs/API/parameters/ReplaceTwoFactorDeviceParams.ts new file mode 100644 index 0000000000000..5b4db968abb30 --- /dev/null +++ b/src/libs/API/parameters/ReplaceTwoFactorDeviceParams.ts @@ -0,0 +1,6 @@ +type ReplaceTwoFactorDeviceParams = { + step: 'verify_old' | 'verify_new'; + twoFactorAuthCode: string; +}; + +export default ReplaceTwoFactorDeviceParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index f18688d3cee34..090511f4294ff 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -115,6 +115,7 @@ export type {default as ValidateLoginParams} from './ValidateLoginParams'; export type {default as ValidateSecondaryLoginParams} from './ValidateSecondaryLoginParams'; export type {default as ValidateTwoFactorAuthParams} from './ValidateTwoFactorAuthParams'; export type {default as DisableTwoFactorAuthParams} from './DisableTwoFactorAuthParams'; +export type {default as ReplaceTwoFactorDeviceParams} from './ReplaceTwoFactorDeviceParams'; export type {default as VerifyIdentityForBankAccountParams} from './VerifyIdentityForBankAccountParams'; export type {default as AnswerQuestionsForWalletParams} from './AnswerQuestionsForWalletParams'; export type {default as AddCommentOrAttachmentParams} from './AddCommentOrAttachmentParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 21c6fda54a0af..7cb35b11c1276 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -111,6 +111,7 @@ const WRITE_COMMANDS = { UNLINK_LOGIN: 'UnlinkLogin', ENABLE_TWO_FACTOR_AUTH: 'EnableTwoFactorAuth', DISABLE_TWO_FACTOR_AUTH: 'DisableTwoFactorAuth', + REPLACE_TWO_FACTOR_DEVICE: 'ReplaceTwoFactorDevice', ADD_COMMENT: 'AddComment', ADD_ATTACHMENT: 'AddAttachment', ADD_TEXT_AND_ATTACHMENT: 'AddTextAndAttachment', @@ -664,6 +665,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UNLINK_LOGIN]: Parameters.UnlinkLoginParams; [WRITE_COMMANDS.ENABLE_TWO_FACTOR_AUTH]: null; [WRITE_COMMANDS.DISABLE_TWO_FACTOR_AUTH]: Parameters.DisableTwoFactorAuthParams; + [WRITE_COMMANDS.REPLACE_TWO_FACTOR_DEVICE]: Parameters.ReplaceTwoFactorDeviceParams; [WRITE_COMMANDS.ADD_COMMENT]: Parameters.AddCommentOrAttachmentParams; [WRITE_COMMANDS.ADD_ATTACHMENT]: Parameters.AddCommentOrAttachmentParams; [WRITE_COMMANDS.CREATE_APP_REPORT]: Parameters.CreateAppReportParams; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 30d5ba47887e8..b4a9c4321f7d6 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -930,6 +930,8 @@ const TwoFactorAuthenticatorStackNavigator = createModalStackNavigator require('../../../../pages/settings/Security/TwoFactorAuth/DisabledPage').default, [SCREENS.TWO_FACTOR_AUTH.DISABLE]: () => require('../../../../pages/settings/Security/TwoFactorAuth/DisablePage').default, [SCREENS.TWO_FACTOR_AUTH.SUCCESS]: () => require('../../../../pages/settings/Security/TwoFactorAuth/SuccessPage').default, + [SCREENS.TWO_FACTOR_AUTH.REPLACE_VERIFY_OLD]: () => require('../../../../pages/settings/Security/TwoFactorAuth/ReplaceDeviceVerifyOldPage').default, + [SCREENS.TWO_FACTOR_AUTH.REPLACE_VERIFY_NEW]: () => require('../../../../pages/settings/Security/TwoFactorAuth/ReplaceDeviceVerifyNewPage').default, }); const SearchRouterModalStackNavigator = createModalStackNavigator({ diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts index 36e20c9b14ad8..664676d27e0dd 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts @@ -90,6 +90,8 @@ const SETTINGS_TO_RHP: Partial['config'] = { path: ROUTES.SETTINGS_2FA_DISABLE, exact: true, }, + [SCREENS.TWO_FACTOR_AUTH.REPLACE_VERIFY_OLD]: { + path: ROUTES.SETTINGS_2FA_REPLACE_VERIFY_OLD, + exact: true, + }, + [SCREENS.TWO_FACTOR_AUTH.REPLACE_VERIFY_NEW]: { + path: ROUTES.SETTINGS_2FA_REPLACE_VERIFY_NEW, + exact: true, + }, }, }, [SCREENS.RIGHT_MODAL.PRIVATE_NOTES]: { diff --git a/src/libs/TwoFactorAuthUtils.ts b/src/libs/TwoFactorAuthUtils.ts new file mode 100644 index 0000000000000..0680cf9e30cfe --- /dev/null +++ b/src/libs/TwoFactorAuthUtils.ts @@ -0,0 +1,20 @@ +/** + * Splits the two-factor auth secret key in 4 chunks of 4 characters each + */ +function splitSecretInChunks(secret: string): string { + if (secret.length !== 16) { + return secret; + } + + return `${secret.slice(0, 4)} ${secret.slice(4, 8)} ${secret.slice(8, 12)} ${secret.slice(12, secret.length)}`; +} + +/** + * Builds the URL string to generate the QRCode, using the otpauth:// protocol, + * so it can be detected by authenticator apps + */ +function buildAuthenticatorUrl(contactMethod: string, secretKey: string): string { + return `otpauth://totp/Expensify:${contactMethod}?secret=${secretKey}&issuer=Expensify`; +} + +export {splitSecretInChunks, buildAuthenticatorUrl}; diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index 7ebdac9a989fc..4547ec3508c6f 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -16,6 +16,7 @@ import type { BeginSignInParams, DisableTwoFactorAuthParams, LogOutParams, + ReplaceTwoFactorDeviceParams, RequestNewValidateCodeParams, RequestUnlinkValidationLinkParams, ResetSMSDeliveryFailureStatusParams, @@ -1273,6 +1274,9 @@ function validateTwoFactorAuth(twoFactorAuthCode: string, shouldClearData: boole key: ONYXKEYS.ACCOUNT, value: { isLoading: false, + // Clear the secret key once we know we no longer need to show it + // This is necessary in case the user needs to complete the replaceTwoFactorDevice flow on this device at some point in the future - that flow uses the presence of this key to know when to navigate from one step to the next + twoFactorAuthSecretKey: null, }, }, ]; @@ -1306,6 +1310,54 @@ function validateTwoFactorAuth(twoFactorAuthCode: string, shouldClearData: boole }); } +function replaceTwoFactorDevice(step: 'verify_old' | 'verify_new', twoFactorAuthCode: string) { + const optimisticData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.ACCOUNT, + value: { + isLoading: true, + errors: null, + }, + }, + ]; + + const successData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.ACCOUNT, + value: { + isLoading: false, + errors: null, + // clear out the secret key to signal to the view that the call succeeded + ...(step === 'verify_new' ? {twoFactorAuthSecretKey: null} : {}), + }, + }, + ]; + + const failureData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.ACCOUNT, + value: { + isLoading: false, + }, + }, + ]; + + const params: ReplaceTwoFactorDeviceParams = {step, twoFactorAuthCode}; + + return API.write(WRITE_COMMANDS.REPLACE_TWO_FACTOR_DEVICE, params, {optimisticData, successData, failureData}); +} + +/** + * Clears the two-factor auth secret key from account data. + * Used when starting the device replacement flow to ensure clean state. + */ +function clearTwoFactorAuthSecretKey() { + Onyx.merge(ONYXKEYS.ACCOUNT, {twoFactorAuthSecretKey: undefined}); +} + /** * Waits for a user to sign in. * @@ -1587,6 +1639,8 @@ export { isAnonymousUser, toggleTwoFactorAuth, validateTwoFactorAuth, + replaceTwoFactorDevice, + clearTwoFactorAuthSecretKey, waitForUserSignIn, hasAuthToken, isExpiredSession, diff --git a/src/pages/settings/Security/TwoFactorAuth/EnabledPage.tsx b/src/pages/settings/Security/TwoFactorAuth/EnabledPage.tsx index f5533c6de00ff..dcf81967b212b 100644 --- a/src/pages/settings/Security/TwoFactorAuth/EnabledPage.tsx +++ b/src/pages/settings/Security/TwoFactorAuth/EnabledPage.tsx @@ -26,7 +26,7 @@ import TwoFactorAuthWrapper from './TwoFactorAuthWrapper'; function EnabledPage() { const theme = useTheme(); const styles = useThemeStyles(); - const icons = useMemoizedLazyExpensifyIcons(['Close']); + const icons = useMemoizedLazyExpensifyIcons(['Close', 'Sync']); const {asset: ShieldYellow} = useMemoizedLazyAsset(() => loadIllustration('ShieldYellow' as IllustrationName)); const {login} = useCurrentUserPersonalDetails(); @@ -64,6 +64,11 @@ function EnabledPage() { {translate('twoFactorAuth.whatIsTwoFactorAuth')} + Navigation.navigate(ROUTES.SETTINGS_2FA_REPLACE_VERIFY_OLD)} + icon={icons.Sync} + /> { diff --git a/src/pages/settings/Security/TwoFactorAuth/ReplaceDeviceVerifyNewPage.tsx b/src/pages/settings/Security/TwoFactorAuth/ReplaceDeviceVerifyNewPage.tsx new file mode 100644 index 0000000000000..8c0f3b6f3323e --- /dev/null +++ b/src/pages/settings/Security/TwoFactorAuth/ReplaceDeviceVerifyNewPage.tsx @@ -0,0 +1,106 @@ +import React, {useEffect, useRef} from 'react'; +import {InteractionManager, View} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import type {ScrollView as RNScrollView} from 'react-native'; +import Button from '@components/Button'; +import FixedFooter from '@components/FixedFooter'; +import ScrollView from '@components/ScrollView'; +import Text from '@components/Text'; +import TwoFactorAuthForm from '@components/TwoFactorAuthForm'; +import type {BaseTwoFactorAuthFormRef} from '@components/TwoFactorAuthForm/types'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {getLatestErrorMessage} from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import {getContactMethod} from '@libs/UserUtils'; +import {clearAccountMessages, replaceTwoFactorDevice} from '@userActions/Session'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import TwoFactorAuthSecretDisplay from './TwoFactorAuthSecretDisplay'; +import TwoFactorAuthWrapper from './TwoFactorAuthWrapper'; + +function ReplaceDeviceVerifyNewPage() { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const [session] = useOnyx(ONYXKEYS.SESSION); + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const contactMethod = getContactMethod(account?.primaryLogin, session?.email); + const formRef = useRef(null); + + const scrollViewRef = useRef(null); + + const errorMessage = getLatestErrorMessage(account); + + const clearAccountErrorsIfPresent = () => { + if (!account?.errors) { + return; + } + clearAccountMessages(); + }; + + // Navigate back to 2FA settings after successful device replacement + useEffect(() => { + if (!account || account.twoFactorAuthSecretKey) { + return; + } + Navigation.navigate(ROUTES.SETTINGS_2FA_SUCCESS.route, {forceReplace: true}); + }, [account, account?.twoFactorAuthSecretKey]); + + const handleInputFocus = () => { + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + requestAnimationFrame(() => { + scrollViewRef.current?.scrollToEnd({animated: true}); + }); + }); + }; + + return ( + + + + {translate('twoFactorAuth.verifyNewDeviceDescription')}} + /> + {translate('twoFactorAuth.enterCode')} + { + replaceTwoFactorDevice('verify_new', code); + }} + onInputChange={clearAccountErrorsIfPresent} + errorMessage={errorMessage} + onFocus={handleInputFocus} + /> + + + +