Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ca4b883
Add MultifactorAuthentication RevokePage
chuckdries Jan 26, 2026
bf86d35
Wire up isRegisteredForMultifactorAuthentication
chuckdries Jan 26, 2026
7f3d241
Wire up RevokeMultifactorAuthenticationCredentials
chuckdries Jan 27, 2026
9156d6f
Lint and prettify
chuckdries Jan 27, 2026
dfe8474
Add spanish translation
chuckdries Jan 27, 2026
6ab6ea1
Fix spelling
chuckdries Jan 27, 2026
52c1e5c
Address PR feedback and use multifactorAuthenticationPublicKeyIDs
chuckdries Jan 27, 2026
9c383e7
Merge remote-tracking branch 'origin/main' into chuckdries/3ds-revoke…
chuckdries Jan 27, 2026
97faf1b
Apply polyglot parrot patch
chuckdries Jan 27, 2026
c6cf0b6
Add basic error handling
chuckdries Jan 27, 2026
9ba3826
Apply polyglot parrot patch
chuckdries Jan 27, 2026
1eeb893
Address PR feedback and fix translation string typo
chuckdries Jan 27, 2026
9c35334
Apply polyglot parrot patch
chuckdries Jan 27, 2026
ea060d7
Merge remote-tracking branch 'origin/main' into chuckdries/3ds-revoke…
chuckdries Jan 27, 2026
8fd73d6
Run translation script
chuckdries Jan 27, 2026
3fb73e9
Merge remote-tracking branch 'origin/main' into chuckdries/3ds-revoke…
chuckdries Jan 27, 2026
8c9c687
Fix params type for revoke command
chuckdries Jan 27, 2026
014cee5
Apply polyglot parrot patch
chuckdries Jan 27, 2026
3149528
Update src/pages/MultifactorAuthentication/RevokePage.tsx
chuckdries Jan 27, 2026
b1f6545
Address PR feedback
chuckdries Jan 27, 2026
13eed52
Revert "Run translation script"
chuckdries Jan 27, 2026
0a9f331
Merge remote-tracking branch 'origin/main' into chuckdries/3ds-revoke…
chuckdries Jan 27, 2026
897c091
Merge remote-tracking branch 'origin/main' into chuckdries/3ds-revoke…
chuckdries Jan 27, 2026
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: 2 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3744,6 +3744,8 @@ const ROUTES = {
},

MULTIFACTOR_AUTHENTICATION_NOT_FOUND: 'multifactor-authentication/not-found',

MULTIFACTOR_AUTHENTICATION_REVOKE: 'multifactor-authentication/revoke',
} as const;

/**
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,7 @@ const SCREENS = {
OUTCOME: 'Multifactor_Authentication_Outcome',
PROMPT: 'Multifactor_Authentication_Prompt',
NOT_FOUND: 'Multifactor_Authentication_Not_Found',
REVOKE: 'Multifactor_Authentication_Revoke',
},
} as const;

Expand Down
20 changes: 17 additions & 3 deletions src/components/TestToolMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import React from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import useIsAuthenticated from '@hooks/useIsAuthenticated';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import {useSidebarOrderedReports} from '@hooks/useSidebarOrderedReports';
import useSingleExecution from '@hooks/useSingleExecution';
import useThemeStyles from '@hooks/useThemeStyles';
import useWaitForNavigation from '@hooks/useWaitForNavigation';
import {revokeMultifactorAuthenticationCredentials} from '@libs/actions/MultifactorAuthentication';
import {isUsingStagingApi} from '@libs/ApiUtils';
import Navigation from '@libs/Navigation/Navigation';
import {setShouldFailAllRequests, setShouldForceOffline, setShouldSimulatePoorConnection} from '@userActions/Network';
Expand All @@ -15,15 +17,17 @@ import {setIsDebugModeEnabled, setShouldUseStagingServer} from '@userActions/Use
import CONFIG from '@src/CONFIG';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {Account} from '@src/types/onyx';
import Button from './Button';
import SoftKillTestToolRow from './SoftKillTestToolRow';
import Switch from './Switch';
import TestCrash from './TestCrash';
import TestToolRow from './TestToolRow';
import Text from './Text';

// Temporary hardcoded value until MultifactorAuthenticationContext is implemented
const TEMP_BIOMETRICS_REGISTERED_STATUS = false;
function getHasBiometricsRegistered(data: OnyxEntry<Account>) {
return data?.multifactorAuthenticationPublicKeyIDs && data.multifactorAuthenticationPublicKeyIDs.length > 0;
}

function TestToolMenu() {
const [network] = useOnyx(ONYXKEYS.NETWORK, {canBeMissing: true});
Expand All @@ -33,6 +37,7 @@ function TestToolMenu() {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {clearLHNCache} = useSidebarOrderedReports();
const [hasBiometricsRegistered = false] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true, selector: getHasBiometricsRegistered});

const {singleExecution} = useSingleExecution();
const waitForNavigate = useWaitForNavigation();
Expand All @@ -51,7 +56,7 @@ function TestToolMenu() {
const isAuthenticated = useIsAuthenticated();

// Temporary hardcoded false, expected behavior: status fetched from the MultifactorAuthenticationContext
const biometricsTitle = translate('multifactorAuthentication.biometricsTest.troubleshootBiometricsStatus', {registered: TEMP_BIOMETRICS_REGISTERED_STATUS});
const biometricsTitle = translate('multifactorAuthentication.biometricsTest.troubleshootBiometricsStatus', {registered: hasBiometricsRegistered});

return (
<>
Expand Down Expand Up @@ -116,6 +121,15 @@ function TestToolMenu() {
text={translate('multifactorAuthentication.biometricsTest.test')}
onPress={() => navigateToBiometricsTestPage()}
/>
{hasBiometricsRegistered && (
<Button
small
text={translate('multifactorAuthentication.revoke.revoke')}
onPress={() => {
revokeMultifactorAuthenticationCredentials();
}}
Comment thread
chuckdries marked this conversation as resolved.
/>
)}
</View>
</TestToolRow>
</>
Expand Down
12 changes: 12 additions & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,18 @@ const translations: TranslationDeepObject<typeof en> = {
enableQuickVerification: {
biometrics: 'Schnelle, sichere Verifizierung mit deinem Gesicht oder Fingerabdruck aktivieren. Keine Passwörter oder Codes erforderlich.',
},
revoke: {
revoke: 'Widerrufen',
title: 'Gesichts-/Fingerabdruck & Passkeys',
explanation:
'Die Gesichts-/Fingerabdruck- oder Passkey-Verifizierung ist auf einem oder mehreren Geräten aktiviert. Das Widerrufen des Zugriffs erfordert beim nächsten Verifizierungsvorgang auf jedem Gerät einen Magic Code',
confirmationPrompt: 'Bist du sicher? Du benötigst einen magischen Code für die nächste Verifizierung auf jedem Gerät',
cta: 'Zugriff widerrufen',
noDevices:
'Sie haben keine Geräte für Gesichts-/Fingerabdruck- oder Passkey-Verifizierung registriert. Wenn Sie welche registrieren, können Sie den Zugriff hier wieder entziehen.',
dismiss: 'Verstanden',
error: 'Anfrage fehlgeschlagen. Versuchen Sie es später erneut.',
},
},
validateCodeModal: {
successfulSignInTitle: dedent(`
Expand Down
10 changes: 10 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,16 @@ const translations = {
enableQuickVerification: {
biometrics: 'Enable quick, secure verification using your face or fingerprint. No passwords or codes required.',
},
revoke: {
revoke: 'Revoke',
title: 'Face/fingerprint & passkeys',
explanation: 'Face/fingerprint or passkey verification are enabled on one or more devices. Revoking access will require a magic code for the next verification on any device',
Comment thread
chuckdries marked this conversation as resolved.
confirmationPrompt: "Are you sure? You'll need a magic code for the next verification on any device",
Comment thread
chuckdries marked this conversation as resolved.
cta: 'Revoke access',
noDevices: "You don't have any devices registered for face/fingerprint or passkey verification. If you register any, you will be able to revoke that access here.",
dismiss: 'Got it',
error: 'Request failed. Try again later.',
},
},
validateCodeModal: {
successfulSignInTitle: dedent(`
Expand Down
12 changes: 12 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,18 @@ const translations: TranslationDeepObject<typeof en> = {
enableQuickVerification: {
biometrics: 'Activa la verificación rápida y segura usando tu rostro o huella dactilar. No se requieren contraseñas ni códigos.',
},
revoke: {
revoke: 'Revocar',
title: 'Reconocimiento facial/huella digital y claves de acceso',
explanation:
'La verificación mediante reconocimiento facial, huella digital o clave de acceso está habilitada en uno o más dispositivos. Revocar el acceso requerirá un código mágico para la próxima verificación en cualquier dispositivo.',
confirmationPrompt: '¿Estás seguro? Necesitarás un código mágico para la próxima verificación en cualquier dispositivo.',
cta: 'Revocar acceso',
noDevices:
'No tienes ningún dispositivo registrado para la verificación mediante reconocimiento facial, huella digital o clave de acceso. Si registras alguno, podrás revocar ese acceso aquí.',
dismiss: 'Entendido',
error: 'La solicitud ha fallado. Inténtalo de nuevo más tarde.',
},
},
validateCodeModal: {
successfulSignInTitle: 'Abracadabra,\n¡sesión iniciada!',
Expand Down
12 changes: 12 additions & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,18 @@ const translations: TranslationDeepObject<typeof en> = {
enableQuickVerification: {
biometrics: 'Activez une vérification rapide et sécurisée avec votre visage ou votre empreinte digitale. Aucun mot de passe ou code requis.',
},
revoke: {
revoke: 'Révoquer',
title: 'Reconnaissance faciale/empreinte digitale et passkeys',
explanation:
'La vérification par reconnaissance faciale/empreinte digitale ou passkey est activée sur un ou plusieurs appareils. Révoquer l’accès nécessitera un code magique pour la prochaine vérification sur n’importe quel appareil',
confirmationPrompt: 'Êtes-vous sûr ? Vous aurez besoin d’un code magique pour la prochaine vérification sur n’importe quel appareil',
cta: 'Révoquer l’accès',
noDevices:
'Vous n’avez enregistré aucun appareil pour la vérification par reconnaissance faciale, empreinte digitale ou Passkey. Si vous en enregistrez, vous pourrez révoquer cet accès ici.',
dismiss: 'Compris',
error: 'La demande a échoué. Veuillez réessayer plus tard.',
},
},
validateCodeModal: {
successfulSignInTitle: dedent(`
Expand Down
11 changes: 11 additions & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,17 @@ const translations: TranslationDeepObject<typeof en> = {
enableQuickVerification: {
biometrics: 'Abilita una verifica rapida e sicura utilizzando il tuo viso o impronta digitale. Nessuna password o codice necessario.',
},
revoke: {
revoke: 'Revoca',
title: 'Face/impronta digitale e passkey',
explanation:
'La verifica tramite volto/impronta digitale o passkey è abilitata su uno o più dispositivi. La revoca dell’accesso richiederà un codice magico per la prossima verifica su qualsiasi dispositivo',
confirmationPrompt: 'Sei sicuro? Avrai bisogno di un codice magico per la prossima verifica su qualsiasi dispositivo',
cta: 'Revoca accesso',
noDevices: 'Non hai alcun dispositivo registrato per la verifica con volto/impronta digitale o passkey. Se ne registri uno, potrai revocare tale accesso qui.',
dismiss: 'Capito',
error: 'Richiesta non riuscita. Riprova più tardi.',
},
},
validateCodeModal: {
successfulSignInTitle: dedent(`
Expand Down
10 changes: 10 additions & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,16 @@ const translations: TranslationDeepObject<typeof en> = {
enableQuickVerification: {
biometrics: '顔または指紋を使用して、パスワードやコード不要の迅速かつ安全な認証を有効にしてください。',
},
revoke: {
revoke: '取り消す',
title: '顔認証/指紋認証 & パスキー',
explanation: '1 台以上のデバイスで、顔認証/指紋認証またはパスキー認証が有効になっています。アクセスを取り消すと、次回以降どのデバイスでも認証時にマジックコードが必要になります',
confirmationPrompt: '本当に実行してもよろしいですか?今後、どのデバイスでの認証にもマジックコードが必要になります',
cta: 'アクセスを取り消す',
noDevices: '顔/指紋認証またはパスキー認証用に登録されたデバイスがありません。デバイスを登録すると、そのアクセスをここで取り消せるようになります。',
dismiss: '了解しました',
error: 'リクエストに失敗しました。後でもう一度お試しください。',
},
},
validateCodeModal: {
successfulSignInTitle: dedent(`
Expand Down
11 changes: 11 additions & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,17 @@ const translations: TranslationDeepObject<typeof en> = {
enableQuickVerification: {
biometrics: 'Schakel snelle, veilige verificatie in met je gezicht of vingerafdruk. Geen wachtwoorden of codes nodig.',
},
revoke: {
revoke: 'Intrekken',
title: 'Gezichtsherkenning/vingerafdruk & passkeys',
explanation:
'Gezichts-/vingerafdruk- of passkeys-verificatie is ingeschakeld op één of meer apparaten. Toegang intrekken vereist een magische code voor de volgende verificatie op elk apparaat',
confirmationPrompt: 'Weet je het zeker? Je hebt een magische code nodig voor de volgende verificatie op elk apparaat',
cta: 'Toegang intrekken',
noDevices: 'Je hebt geen apparaten geregistreerd voor gezichts-/vingerafdruk- of passkeys-verificatie. Als je er een registreert, kun je hier die toegang intrekken.',
dismiss: 'Begrepen',
error: 'Verzoek mislukt. Probeer het later opnieuw.',
},
},
validateCodeModal: {
successfulSignInTitle: dedent(`
Expand Down
12 changes: 12 additions & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,18 @@ const translations: TranslationDeepObject<typeof en> = {
enableQuickVerification: {
biometrics: 'Włącz szybką i bezpieczną weryfikację za pomocą twarzy lub odcisku palca. Bez haseł ani kodów.',
},
revoke: {
revoke: 'Unieważnij',
title: 'Rozpoznawanie twarzy/odcisku palca i klucze dostępu',
explanation:
'Weryfikacja twarzą/odciskiem palca lub kluczem dostępu jest włączona na jednym lub kilku urządzeniach. Odwołanie dostępu spowoduje, że przy następnej weryfikacji na dowolnym urządzeniu wymagany będzie magiczny kod',
confirmationPrompt: 'Czy na pewno? Będziesz potrzebować magicznego kodu do kolejnej weryfikacji na każdym urządzeniu',
cta: 'Cofnij dostęp',
noDevices:
'Nie masz żadnych urządzeń zarejestrowanych do weryfikacji twarzą/odciskiem palca ani kluczem dostępu. Jeśli jakieś zarejestrujesz, będziesz mógł/mogła cofnąć ten dostęp tutaj.',
dismiss: 'Rozumiem',
error: 'Żądanie nie powiodło się. Spróbuj ponownie później.',
},
},
validateCodeModal: {
successfulSignInTitle: dedent(`
Expand Down
11 changes: 11 additions & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,17 @@ const translations: TranslationDeepObject<typeof en> = {
enableQuickVerification: {
biometrics: 'Habilite a verificação rápida e segura usando seu rosto ou impressão digital. Sem senhas ou códigos necessários.',
},
revoke: {
revoke: 'Revogar',
title: 'Rosto/digital & chaves de acesso',
explanation:
'Verificação por rosto/digital ou passkey está ativada em um ou mais dispositivos. Revogar o acesso exigirá um código mágico para a próxima verificação em qualquer dispositivo',
confirmationPrompt: 'Tem certeza? Você precisará de um código mágico para a próxima verificação em qualquer dispositivo',
cta: 'Revogar acesso',
noDevices: 'Você não tem nenhum dispositivo registrado para verificação com rosto/digital ou passkey. Se você registrar algum, poderá revogar esse acesso aqui.',
dismiss: 'Entendi',
error: 'A solicitação falhou. Tente novamente mais tarde.',
},
},
validateCodeModal: {
successfulSignInTitle: dedent(`
Expand Down
10 changes: 10 additions & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,16 @@ const translations: TranslationDeepObject<typeof en> = {
enableQuickVerification: {
biometrics: '使用您的脸部或指纹启用快速安全验证。无需密码或代码。',
},
revoke: {
revoke: '撤销',
title: '面容/指纹和通行密钥',
explanation: '在一台或多台设备上已启用面部/指纹或通行密钥验证。撤销访问权限后,下一次在任意设备上进行验证时都需要输入魔法验证码',
confirmationPrompt: '你确定吗?在任何设备上进行下一步验证时,你都需要一个魔法代码',
cta: '撤销访问权限',
noDevices: '您尚未注册任何用于人脸/指纹或通行密钥验证的设备。如果您注册了设备,您将可以在此撤销其访问权限。',
dismiss: '知道了',
error: '请求失败。请稍后再试。',
},
},
validateCodeModal: {
successfulSignInTitle: dedent(`
Expand Down
2 changes: 2 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1307,6 +1307,7 @@ const SIDE_EFFECT_REQUEST_COMMANDS = {
REGISTER_AUTHENTICATION_KEY: 'RegisterAuthenticationKey',
TROUBLESHOOT_MULTIFACTOR_AUTHENTICATION: 'TroubleshootMultifactorAuthentication',
REQUEST_AUTHENTICATION_CHALLENGE: 'RequestAuthenticationChallenge',
REVOKE_MULTIFACTOR_AUTHENTICATION_CREDENTIALS: 'RevokeMultifactorAuthenticationCredentials',
} as const;

type SideEffectRequestCommand = ValueOf<typeof SIDE_EFFECT_REQUEST_COMMANDS>;
Expand Down Expand Up @@ -1339,6 +1340,7 @@ type SideEffectRequestCommandParameters = {
[SIDE_EFFECT_REQUEST_COMMANDS.REGISTER_AUTHENTICATION_KEY]: Parameters.RegisterAuthenticationKeyParams;
[SIDE_EFFECT_REQUEST_COMMANDS.TROUBLESHOOT_MULTIFACTOR_AUTHENTICATION]: Parameters.TroubleshootMultifactorAuthenticationParams;
[SIDE_EFFECT_REQUEST_COMMANDS.REQUEST_AUTHENTICATION_CHALLENGE]: Parameters.RequestAuthenticationChallengeParams;
[SIDE_EFFECT_REQUEST_COMMANDS.REVOKE_MULTIFACTOR_AUTHENTICATION_CREDENTIALS]: EmptyObject;
};

type ApiRequestCommandParameters = WriteCommandParameters & ReadCommandParameters & SideEffectRequestCommandParameters;
Expand Down
6 changes: 6 additions & 0 deletions src/libs/MultifactorAuthentication/Biometrics/VALUES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const REASON = {
SIGNATURE_VERIFICATION_FAILED: 'Signature verification failed',
NO_PENDING_REGISTRATION_CHALLENGE: 'No pending registration challenge',
UNKNOWN_RESPONSE: 'Unknown response',
REVOKE_SUCCESSFUL: 'Revoked successfully',
},
CHALLENGE: {
COULD_NOT_RETRIEVE_A_CHALLENGE: 'Could not retrieve a challenge',
Expand Down Expand Up @@ -112,6 +113,11 @@ const API_RESPONSE_MAP = {
...MULTIFACTOR_AUTHENTICATION_COMMAND_BASE_RESPONSE_MAP,
200: REASON.BACKEND.AUTHORIZATION_SUCCESSFUL,
},

REVOKE_MULTIFACTOR_AUTHENTICATION_SETUP: {
...MULTIFACTOR_AUTHENTICATION_COMMAND_BASE_RESPONSE_MAP,
200: REASON.BACKEND.REVOKE_SUCCESSFUL,
},
} as const;
/* eslint-enable @typescript-eslint/naming-convention */

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,7 @@ const MultifactorAuthenticationStackNavigator = createModalStackNavigator<Multif
[SCREENS.MULTIFACTOR_AUTHENTICATION.BIOMETRICS_TEST]: () => require<ReactComponentModule>('../../../../pages/MultifactorAuthentication/BiometricsTestPage').default,
[SCREENS.MULTIFACTOR_AUTHENTICATION.OUTCOME]: () => require<ReactComponentModule>('@pages/MultifactorAuthentication/OutcomePage').default,
[SCREENS.MULTIFACTOR_AUTHENTICATION.PROMPT]: () => require<ReactComponentModule>('../../../../pages/MultifactorAuthentication/PromptPage').default,
[SCREENS.MULTIFACTOR_AUTHENTICATION.REVOKE]: () => require<ReactComponentModule>('@pages/MultifactorAuthentication/RevokePage').default,
[SCREENS.MULTIFACTOR_AUTHENTICATION.NOT_FOUND]: () => require<ReactComponentModule>('../../../../pages/ErrorPage/NotFoundPage').default,
});

Expand Down
1 change: 1 addition & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1953,6 +1953,7 @@ const config: LinkingOptions<RootNavigatorParamList>['config'] = {
[SCREENS.MULTIFACTOR_AUTHENTICATION.OUTCOME]: ROUTES.MULTIFACTOR_AUTHENTICATION_OUTCOME.route,
[SCREENS.MULTIFACTOR_AUTHENTICATION.PROMPT]: ROUTES.MULTIFACTOR_AUTHENTICATION_PROMPT.route,
[SCREENS.MULTIFACTOR_AUTHENTICATION.NOT_FOUND]: ROUTES.MULTIFACTOR_AUTHENTICATION_NOT_FOUND,
[SCREENS.MULTIFACTOR_AUTHENTICATION.REVOKE]: ROUTES.MULTIFACTOR_AUTHENTICATION_REVOKE,
},
},
},
Expand Down
Loading
Loading