diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 2a170150d23da..0a9c8efd01354 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -5948,6 +5948,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 f84f0da1aba12..9f79f4495b71e 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -2123,6 +2123,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: 'Geben Sie den sechsstelligen Code aus Ihrer aktuellen Authentifizierungs-App ein, um zu bestätigen, dass Sie Zugriff darauf haben.', + verifyNewDeviceTitle: 'Neues Gerät einrichten', + verifyNewDeviceDescription: 'Scannen Sie den QR-Code mit Ihrem neuen Gerät und geben Sie dann den Code ein, um die Einrichtung abzuschließen.', }, recoveryCodeForm: { error: { diff --git a/src/languages/en.ts b/src/languages/en.ts index c92d5bd001659..a16df4d35c13e 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2175,6 +2175,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 1baa6afa156d0..231378cba9af5 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2000,6 +2000,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 b65c07543413a..d8a0a4bd749f6 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -2129,6 +2129,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 de 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 babe2339b9f66..2773f9ec3ff0f 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -2120,6 +2120,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 a due fattori', + verifyOldDeviceTitle: 'Verifica il vecchio dispositivo', + verifyOldDeviceDescription: 'Inserisci il codice a sei cifre dalla tua attuale app di autenticazione per confermare che hai accesso ad essa.', + verifyNewDeviceTitle: 'Configura nuovo dispositivo', + verifyNewDeviceDescription: 'Scansiona il codice QR con il tuo nuovo dispositivo, poi inserisci il codice per completare la configurazione.', }, recoveryCodeForm: { error: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index b3a7c6691b6c9..6cafa9c92353f 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -2107,6 +2107,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 be5b70b72ab6f..40218171baaf1 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -2117,6 +2117,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: 'Tweefactorauthenticatie-apparaat vervangen', + verifyOldDeviceTitle: 'Oud apparaat verifiëren', + verifyOldDeviceDescription: 'Voer de zescijferige code uit je huidige authenticator-app in om te bevestigen dat je daar toegang toe hebt.', + verifyNewDeviceTitle: 'Nieuw apparaat instellen', + verifyNewDeviceDescription: 'Scan de QR-code met je nieuwe apparaat en voer daarna de code in om de installatie te voltooien.', }, recoveryCodeForm: { error: { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 2fb31cbf66bd7..ce4095974281a 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -2117,6 +2117,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: 'Zastąp urządzenie dwuetapowej weryfikacji', + verifyOldDeviceTitle: 'Zweryfikuj stare urządzenie', + verifyOldDeviceDescription: 'Wpisz sześciocyfrowy kod z bieżącej aplikacji uwierzytelniającej, żeby potwierdzić, że masz do niej dostęp.', + verifyNewDeviceTitle: 'Skonfiguruj nowe urządzenie', + verifyNewDeviceDescription: 'Zeskanuj kod QR nowym urządzeniem, a następnie wpisz ten kod, aby zakończyć konfigurację.', }, recoveryCodeForm: { error: { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index eaf08826159ef..6c7b4931b6d82 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -2113,6 +2113,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 dois fatores', + verifyOldDeviceTitle: 'Verificar dispositivo antigo', + verifyOldDeviceDescription: 'Digite 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 depois 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 b4f23ecbec1c9..cd612d7b440ac 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -2079,6 +2079,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 44b4a085a26c1..e6cbd85ac07a9 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts @@ -89,6 +89,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..bbb2a9cb089ec 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: null}); +} + /** * 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..e1772c73583d6 --- /dev/null +++ b/src/pages/settings/Security/TwoFactorAuth/ReplaceDeviceVerifyNewPage.tsx @@ -0,0 +1,113 @@ +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(); + }; + + useEffect(() => { + clearAccountMessages(); + 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} + /> + + + +