Bring back Replace Two Factor Device Flow#85134
Conversation
|
@shubham1206agra Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button] |
🦜 Polyglot Parrot! 🦜Squawk! Looks like you added some shiny new English strings. Allow me to parrot them back to you in other tongues: View the translation diffdiff --git a/src/languages/de.ts b/src/languages/de.ts
index 586a66b5..06933ab0 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -2118,9 +2118,9 @@ const translations: TranslationDeepObject<typeof en> = {
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.',
+ 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: 'Scanne den QR-Code mit deinem neuen Gerät und gib dann den Code ein, um die Einrichtung abzuschließen.',
+ 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/fr.ts b/src/languages/fr.ts
index 5ea1749a..9eb76431 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -2124,7 +2124,7 @@ const translations: TranslationDeepObject<typeof en> = {
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.',
+ 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.',
},
diff --git a/src/languages/it.ts b/src/languages/it.ts
index be044800..4a48f4bd 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -2113,11 +2113,11 @@ const translations: TranslationDeepObject<typeof en> = {
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.',
+ 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, quindi inserisci il codice per completare la configurazione.',
+ 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 3c2e0df0..34ccb095 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -2100,11 +2100,11 @@ const translations: TranslationDeepObject<typeof en> = {
twoFactorAuthCannotDisable: '2要素認証を無効にできません',
twoFactorAuthRequired: 'Xero 連携には二要素認証(2FA)が必須で、無効にすることはできません。',
replaceDevice: 'デバイスを交換',
- replaceDeviceTitle: '2 要素認証デバイスを交換',
- verifyOldDeviceTitle: '古いデバイスを確認',
+ replaceDeviceTitle: '2 要素認証デバイスを変更',
+ verifyOldDeviceTitle: '古い端末を確認',
verifyOldDeviceDescription: '現在使用している認証アプリに表示されている6桁のコードを入力して、アクセスできることを確認してください。',
- verifyNewDeviceTitle: '新しいデバイスをセットアップ',
- verifyNewDeviceDescription: '新しいデバイスでQRコードをスキャンし、その後コードを入力して設定を完了してください。',
+ verifyNewDeviceTitle: '新しいデバイスを設定',
+ verifyNewDeviceDescription: '新しいデバイスでQRコードをスキャンし、表示されたコードを入力して設定を完了してください。',
},
recoveryCodeForm: {
error: {
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index 2a9050ae..6993d5a0 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -2110,11 +2110,11 @@ const translations: TranslationDeepObject<typeof en> = {
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',
+ 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 er toegang toe hebt.',
+ 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 vervolgens de code in om de installatie te voltooien.',
+ 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 93c25eb1..7c02226f 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -2110,11 +2110,11 @@ const translations: TranslationDeepObject<typeof en> = {
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',
+ replaceDeviceTitle: 'Zastąp urządzenie dwuetapowej weryfikacji',
verifyOldDeviceTitle: 'Zweryfikuj stare urządzenie',
- verifyOldDeviceDescription: 'Wprowadź sześciocyfrowy kod z bieżącej aplikacji uwierzytelniającej, aby potwierdzić, że masz do niej dostęp.',
+ 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 swoim nowym urządzeniem, a następnie wprowadź kod, aby zakończyć konfigurację.',
+ 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 950d6c16..fc4f6485 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -2106,11 +2106,11 @@ const translations: TranslationDeepObject<typeof en> = {
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',
+ replaceDeviceTitle: 'Substituir dispositivo de dois fatores',
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.',
+ 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, em seguida, insira o código para concluir a configuração.',
+ 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 87a48fcc..dcd25a4a 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -2074,9 +2074,9 @@ const translations: TranslationDeepObject<typeof en> = {
replaceDevice: '更换设备',
replaceDeviceTitle: '更换双重验证设备',
verifyOldDeviceTitle: '验证旧设备',
- verifyOldDeviceDescription: '请输入您当前身份验证器应用中的六位数验证码,以确认您可以访问该应用。',
+ verifyOldDeviceDescription: '请输入您当前身份验证器应用中的六位数验证码,以确认您仍可访问该应用。',
verifyNewDeviceTitle: '设置新设备',
- verifyNewDeviceDescription: '使用新设备扫描二维码,然后输入代码以完成设置。',
+ verifyNewDeviceDescription: '使用新设备扫描二维码,然后输入代码完成设置。',
},
recoveryCodeForm: {
error: {
Note You can apply these changes to your branch by copying the patch to your clipboard, then running |
| @@ -0,0 +1,113 @@ | |||
| import React, {useEffect, useRef} from 'react'; | |||
| import {InteractionManager, View} from 'react-native'; | |||
| // eslint-disable-next-line no-restricted-imports | |||
There was a problem hiding this comment.
❌ CONSISTENCY-5 (docs)
This eslint-disable-next-line no-restricted-imports lacks a justification comment explaining why the restricted import is necessary here.
Add a comment explaining the reason, for example:
// eslint-disable-next-line no-restricted-imports -- We need the RN ScrollView type for the ref, but the component import uses @components/ScrollView
import type {ScrollView as RNScrollView} from 'react-native';Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
| }, [account, account?.twoFactorAuthSecretKey]); | ||
|
|
||
| const handleInputFocus = () => { | ||
| // eslint-disable-next-line @typescript-eslint/no-deprecated |
There was a problem hiding this comment.
❌ CONSISTENCY-5 (docs)
This eslint-disable-next-line @typescript-eslint/no-deprecated lacks a justification comment explaining why the deprecated API is being used.
Add a comment explaining the reason, for example:
// eslint-disable-next-line @typescript-eslint/no-deprecated -- InteractionManager.runAfterInteractions is needed to defer scroll until animations complete; no non-deprecated alternative available
InteractionManager.runAfterInteractions(() => {Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
| * Splits the two-factor auth secret key in 4 chunks of 4 characters each | ||
| */ | ||
| function splitSecretInChunks(secret: string): string { | ||
| if (secret.length !== 16) { |
There was a problem hiding this comment.
❌ CONSISTENCY-2 (docs)
The number 16 is a magic number representing the expected length of a TOTP secret key. It should be extracted to a named constant for clarity.
Define a constant in CONST or at the top of this file:
const TOTP_SECRET_KEY_LENGTH = 16;
function splitSecretInChunks(secret: string): string {
if (secret.length !== TOTP_SECRET_KEY_LENGTH) {
return secret;
}
// ...
}Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
| /** | ||
| * Splits the two-factor auth secret key in 4 chunks of 4 characters each | ||
| */ | ||
| function splitSecretInChunks(secret: string): string { |
There was a problem hiding this comment.
❌ CONSISTENCY-3 (docs)
The splitSecretInChunks and buildAuthenticatorUrl functions extracted here are exact duplicates of the local functions already defined in src/pages/settings/Security/TwoFactorAuth/VerifyPage.tsx (lines 61-75). The new TwoFactorAuthSecretDisplay component correctly imports from this shared utility, but VerifyPage.tsx was not updated to also use it, leaving duplicate implementations in the codebase.
Update VerifyPage.tsx to import from the new shared utility and remove its local copies:
import {buildAuthenticatorUrl, splitSecretInChunks} from '@libs/TwoFactorAuthUtils';Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
| description?: React.ReactNode; | ||
| }; | ||
|
|
||
| function TwoFactorAuthSecretDisplay({contactMethod, secretKey, description}: TwoFactorAuthSecretDisplayProps) { |
There was a problem hiding this comment.
❌ CONSISTENCY-3 (docs)
This new TwoFactorAuthSecretDisplay component renders the QR code, secret key text, and copy button -- the exact same UI pattern already present in src/pages/settings/Security/TwoFactorAuth/VerifyPage.tsx (lines 109-136). However, VerifyPage.tsx was not updated to use this new shared component, leaving significant duplication of the secret key display logic.
Consider updating VerifyPage.tsx to use TwoFactorAuthSecretDisplay for its QR code and secret key section, consolidating the duplicated rendering logic into the shared component.
Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e1d43d2073
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
src/pages/settings/Security/TwoFactorAuth/ReplaceDeviceVerifyOldPage.tsx
Show resolved
Hide resolved
src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthSecretDisplay.tsx
Outdated
Show resolved
Hide resolved
|
@chuckdries can you take a look at codex comments above, please? |
Codecov Report✅ Changes either increased or maintained existing code coverage, great job!
|
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppAndroid: mWeb ChromeiOS: HybridAppiOS: mWeb SafariMacOS: Chrome / Safari564173715-dacef0cb-697d-4708-9d68-99e52a4288f6.mov |
|
@chu everything seems fine to me, I can't reproduce any more bugs: Screen.Recording.2026-03-16.at.10.53.26.movHowever, do you think we can add some unit tests? We also have some conflicts here. Thank you very much. |
|
@brunovjk can you finish your review please? 🙏 |
|
@roryabraham Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button] |
|
🚧 @lakchote has triggered a test Expensify/App build. You can view the workflow run here. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
🚀 Deployed to staging by https://github.com/lakchote in version: 9.3.41-0 🚀
Bundle Size Analysis (Sentry): |
|
🚀 Deployed to production by https://github.com/cristipaval in version: 9.3.41-4 🚀
|
Explanation of Change
This PR reinstates #79266, adding protection against #84986
Fixed Issues
$ https://github.com/Expensify/Expensify/issues/583049
PROPOSAL:
Tests
Happy path
Invalid codes are rejected
Invalid code error is cleared when user leaves page
Offline tests
N/A
QA Steps
Same as tests
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectiontoggleReportand notonIconClick)src/languages/*files and using the translation methodSTYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.ScrollViewcomponent to make it scrollable when more elements are added to the page.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps./** comment above it */thisproperly so there are no scoping issues (i.e. foronClick={this.submit}the methodthis.submitshould be bound tothisin the constructor)thisare necessary to be bound (i.e. avoidthis.submit = this.submit.bind(this);ifthis.submitis never passed to a component event handler likeonClick)Screenshots/Videos
Android: Native
replace.2fa.device.android.native.mp4
iOS: Native
replace.2fa.device.ios.native.mp4
iOS: mWeb Safari
replace.2fa.device.mobile.safari.mp4
MacOS: Chrome / Safari
replace.2fa.happy.path.mp4
invalid.code.errors.appear.and.are.cleared.mp4