From 13c2f3c6ca31af5d2d88b32e77d26ee2d4ef5be1 Mon Sep 17 00:00:00 2001
From: Jakub Korytko <jakub@korytko.me>
Date: Thu, 15 Jan 2026 18:53:51 +0100
Subject: [PATCH 1/5] feat: add basic multifactor authentication components

---
 .../NoEligibleMethodsDescription.tsx          | 34 ++++++++++
 .../PromptContent.tsx                         | 38 +++++++++++
 .../TriggerCancelConfirmModal.tsx             | 44 +++++++++++++
 .../ValidateCodeResendButton.tsx              | 64 +++++++++++++++++++
 4 files changed, 180 insertions(+)
 create mode 100644 src/components/MultifactorAuthentication/NoEligibleMethodsDescription.tsx
 create mode 100644 src/components/MultifactorAuthentication/PromptContent.tsx
 create mode 100644 src/components/MultifactorAuthentication/TriggerCancelConfirmModal.tsx
 create mode 100644 src/components/MultifactorAuthentication/ValidateCodeResendButton.tsx

diff --git a/src/components/MultifactorAuthentication/NoEligibleMethodsDescription.tsx b/src/components/MultifactorAuthentication/NoEligibleMethodsDescription.tsx
new file mode 100644
index 000000000000..e011160edccd
--- /dev/null
+++ b/src/components/MultifactorAuthentication/NoEligibleMethodsDescription.tsx
@@ -0,0 +1,34 @@
+import Text from '@components/Text';
+import TextLink from '@components/TextLink';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import goToSettings from '@libs/goToSettings/index.native';
+
+const base = 'multifactorAuthentication.pleaseEnableInSystemSettings' as const;
+
+const tPaths = {
+    start: `${base}.start`,
+    link: `${base}.link`,
+    end: `${base}.end`,
+} as const;
+
+function NoEligibleMethodsDescription() {
+    const styles = useThemeStyles();
+    const {translate} = useLocalize();
+
+    const start = translate(tPaths.start);
+    const link = translate(tPaths.link);
+    const end = translate(tPaths.end);
+
+    return (
+        <Text style={[styles.textAlignCenter, styles.textSupporting]}>
+            {start}
+            <TextLink onPress={goToSettings}>{link}</TextLink>
+            {end}
+        </Text>
+    );
+}
+
+NoEligibleMethodsDescription.displayName = 'NoEligibleMethodsDescription';
+
+export default NoEligibleMethodsDescription;
diff --git a/src/components/MultifactorAuthentication/PromptContent.tsx b/src/components/MultifactorAuthentication/PromptContent.tsx
new file mode 100644
index 000000000000..2d87aa920014
--- /dev/null
+++ b/src/components/MultifactorAuthentication/PromptContent.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import {View} from 'react-native';
+import BlockingView from '@components/BlockingViews/BlockingView';
+import type DotLottieAnimation from '@components/LottieAnimations/types';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import type {TranslationPaths} from '@src/languages/types';
+
+type MultifactorAuthenticationPromptContentProps = {
+    animation: DotLottieAnimation;
+    title: TranslationPaths;
+    subtitle: TranslationPaths;
+};
+
+function MultifactorAuthenticationPromptContent({title, subtitle, animation}: MultifactorAuthenticationPromptContentProps) {
+    const styles = useThemeStyles();
+    const {translate} = useLocalize();
+
+    return (
+        <View style={styles.flex1}>
+            <BlockingView
+                animation={animation}
+                animationStyles={styles.emptyLHNAnimation}
+                animationWebStyle={styles.emptyLHNAnimation}
+                title={translate(title)}
+                titleStyles={styles.mb2}
+                subtitle={translate(subtitle)}
+                subtitleStyle={styles.textSupporting}
+                containerStyle={styles.ph5}
+                testID="MultifactorAuthenticationPromptContent"
+            />
+        </View>
+    );
+}
+
+MultifactorAuthenticationPromptContent.displayName = 'MultifactorAuthenticationPromptContent';
+
+export default MultifactorAuthenticationPromptContent;
diff --git a/src/components/MultifactorAuthentication/TriggerCancelConfirmModal.tsx b/src/components/MultifactorAuthentication/TriggerCancelConfirmModal.tsx
new file mode 100644
index 000000000000..510526e82603
--- /dev/null
+++ b/src/components/MultifactorAuthentication/TriggerCancelConfirmModal.tsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import ConfirmModal from '@components/ConfirmModal';
+import useLocalize from '@hooks/useLocalize';
+import type {TranslationPaths} from '@src/languages/types';
+
+type MultifactorAuthenticationTriggerCancelConfirmModalProps = {
+    isVisible: boolean;
+    onConfirm: () => void;
+    onCancel: () => void;
+};
+
+const mockedConfig = {
+    title: 'common.areYouSure',
+    description: 'multifactorAuthentication.biometricsTest.areYouSureToReject',
+    confirmButtonText: 'multifactorAuthentication.biometricsTest.rejectAuthentication',
+    cancelButtonText: 'common.cancel',
+} as const satisfies Record<string, TranslationPaths>;
+
+function MultifactorAuthenticationTriggerCancelConfirmModal({isVisible, onConfirm, onCancel}: MultifactorAuthenticationTriggerCancelConfirmModalProps) {
+    const {translate} = useLocalize();
+
+    const title = translate(mockedConfig.title);
+    const description = translate(mockedConfig.description);
+    const confirmButtonText = translate(mockedConfig.confirmButtonText);
+    const cancelButtonText = translate(mockedConfig.cancelButtonText);
+
+    return (
+        <ConfirmModal
+            danger
+            title={title}
+            onConfirm={onConfirm}
+            onCancel={onCancel}
+            isVisible={isVisible}
+            prompt={description}
+            confirmText={confirmButtonText}
+            cancelText={cancelButtonText}
+            shouldShowCancelButton
+        />
+    );
+}
+
+MultifactorAuthenticationTriggerCancelConfirmModal.displayName = 'MultifactorAuthenticationTriggerCancelConfirmModal';
+
+export default MultifactorAuthenticationTriggerCancelConfirmModal;
diff --git a/src/components/MultifactorAuthentication/ValidateCodeResendButton.tsx b/src/components/MultifactorAuthentication/ValidateCodeResendButton.tsx
new file mode 100644
index 000000000000..254d6945a7ec
--- /dev/null
+++ b/src/components/MultifactorAuthentication/ValidateCodeResendButton.tsx
@@ -0,0 +1,64 @@
+import React from 'react';
+import {View} from 'react-native';
+import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
+import RenderHTML from '@components/RenderHTML';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useThemeStyles from '@hooks/useThemeStyles';
+import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
+
+type MultifactorAuthenticationValidateCodeResendButtonProps = {
+    shouldShowTimer: boolean;
+    timeRemaining: number;
+    shouldDisableResendCode: boolean;
+    hasError: boolean;
+    resendButtonText: TranslationPaths;
+    onResendValidationCode: () => void;
+};
+
+function MultifactorAuthenticationValidateCodeResendButton({
+    shouldShowTimer,
+    timeRemaining,
+    shouldDisableResendCode,
+    hasError,
+    resendButtonText,
+    onResendValidationCode,
+}: MultifactorAuthenticationValidateCodeResendButtonProps) {
+    const styles = useThemeStyles();
+    const StyleUtils = useStyleUtils();
+    const {translate} = useLocalize();
+
+    if (shouldShowTimer) {
+        return (
+            <View style={[styles.mt5, styles.flexRow, styles.renderHTML]}>
+                <RenderHTML
+                    html={translate('validateCodeForm.requestNewCode', {
+                        timeRemaining: `00:${String(timeRemaining).padStart(2, '0')}`,
+                    })}
+                />
+            </View>
+        );
+    }
+
+    return (
+        <PressableWithFeedback
+            style={styles.mt5}
+            onPress={onResendValidationCode}
+            disabled={shouldDisableResendCode}
+            hoverDimmingValue={1}
+            pressDimmingValue={0.2}
+            role={CONST.ROLE.BUTTON}
+            accessibilityLabel={translate(resendButtonText)}
+        >
+            <Text style={[StyleUtils.getDisabledLinkStyles(shouldDisableResendCode)]}>
+                {hasError ? translate('validateCodeForm.requestNewCodeAfterErrorOccurred') : translate(resendButtonText)}
+            </Text>
+        </PressableWithFeedback>
+    );
+}
+
+MultifactorAuthenticationValidateCodeResendButton.displayName = 'MultifactorAuthenticationValidateCodeResendButton';
+
+export default MultifactorAuthenticationValidateCodeResendButton;

From 70eb1a46cf8b9f5f317c871451e885adf4ef92b4 Mon Sep 17 00:00:00 2001
From: Jakub Korytko <jakub@korytko.me>
Date: Thu, 15 Jan 2026 18:55:52 +0100
Subject: [PATCH 2/5] feat: add basic multifactor authentication pages

---
 src/ROUTES.ts                                 |  24 +-
 src/SCREENS.ts                                |   8 +
 .../ModalStackNavigators/index.tsx            |  10 +
 .../Navigators/RightModalNavigator.tsx        |   4 +
 src/libs/Navigation/linkingConfig/config.ts   |   9 +
 src/libs/Navigation/types.ts                  |  13 +
 .../BiometricsTestPage.tsx                    |  58 +++++
 .../NotificationPage.tsx                      |  88 +++++++
 .../MultifactorAuthentication/PromptPage.tsx  |  82 +++++++
 .../ValidateCodePage.tsx                      | 224 ++++++++++++++++++
 tests/unit/NetworkTest.tsx                    |  13 +-
 11 files changed, 531 insertions(+), 2 deletions(-)
 create mode 100644 src/pages/MultifactorAuthentication/BiometricsTestPage.tsx
 create mode 100644 src/pages/MultifactorAuthentication/NotificationPage.tsx
 create mode 100644 src/pages/MultifactorAuthentication/PromptPage.tsx
 create mode 100644 src/pages/MultifactorAuthentication/ValidateCodePage.tsx

diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 1799b9078b2b..4b0bdc639ae1 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -51,6 +51,11 @@ const PUBLIC_SCREENS_ROUTES = {
 // Exported for identifying a url as a verify-account route, associated with a page extending the VerifyAccountPageBase component
 const VERIFY_ACCOUNT = 'verify-account';
 
+const MULTIFACTOR_AUTHENTICATION_PROTECTED_ROUTES = {
+    FACTOR: 'multifactor-authentication/factor',
+    PROMPT: 'multifactor-authentication/prompt',
+} as const;
+
 const ROUTES = {
     ...PUBLIC_SCREENS_ROUTES,
     // This route renders the list of reports.
@@ -3685,6 +3690,23 @@ const ROUTES = {
         route: 'domain/:domainAccountID/admins/:accountID/reset-domain',
         getRoute: (domainAccountID: number, accountID: number) => `domain/${domainAccountID}/admins/${accountID}/reset-domain` as const,
     },
+
+    MULTIFACTOR_AUTHENTICATION_MAGIC_CODE: `${MULTIFACTOR_AUTHENTICATION_PROTECTED_ROUTES.FACTOR}/magic-code`,
+    MULTIFACTOR_AUTHENTICATION_BIOMETRICS_TEST: 'multifactor-authentication/scenario/biometrics-test',
+
+    // The exact notification & prompt type will be added as a part of Multifactor Authentication config in another PR,
+    // for now a string is accepted to avoid blocking this PR.
+    MULTIFACTOR_AUTHENTICATION_NOTIFICATION: {
+        route: 'multifactor-authentication/notification/:notificationType',
+        getRoute: (notificationType: string) => `multifactor-authentication/notification/${notificationType}` as const,
+    },
+
+    MULTIFACTOR_AUTHENTICATION_PROMPT: {
+        route: `${MULTIFACTOR_AUTHENTICATION_PROTECTED_ROUTES.PROMPT}/:promptType`,
+        getRoute: (promptType: string) => `${MULTIFACTOR_AUTHENTICATION_PROTECTED_ROUTES.PROMPT}/${promptType}` as const,
+    },
+
+    MULTIFACTOR_AUTHENTICATION_NOT_FOUND: 'multifactor-authentication/not-found',
 } as const;
 
 /**
@@ -3700,7 +3722,7 @@ const SHARED_ROUTE_PARAMS: Partial<Record<Screen, string[]>> = {
     [SCREENS.WORKSPACE.INITIAL]: ['backTo'],
 } as const;
 
-export {PUBLIC_SCREENS_ROUTES, SHARED_ROUTE_PARAMS, VERIFY_ACCOUNT};
+export {PUBLIC_SCREENS_ROUTES, SHARED_ROUTE_PARAMS, VERIFY_ACCOUNT, MULTIFACTOR_AUTHENTICATION_PROTECTED_ROUTES};
 export default ROUTES;
 
 type ReportAttachmentsRoute = typeof ROUTES.REPORT_ATTACHMENTS.route;
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index c6f861e0666d..9744c56eac72 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -276,6 +276,7 @@ const SCREENS = {
         REPORT_CARD_ACTIVATE: 'Report_Card_Activate',
         DOMAIN: 'Domain',
         EXPENSE_REPORT: 'ExpenseReport',
+        MULTIFACTOR_AUTHENTICATION: 'MultifactorAuthentication',
     },
     REPORT_CARD_ACTIVATE: 'Report_Card_Activate_Root',
     PUBLIC_CONSOLE_DEBUG: 'Console_Debug',
@@ -883,6 +884,13 @@ const SCREENS = {
         MEMBER_DETAILS: 'Member_Details',
         RESET_DOMAIN: 'Domain_Reset',
     },
+    MULTIFACTOR_AUTHENTICATION: {
+        MAGIC_CODE: 'Multifactor_Authentication_Magic_Code',
+        BIOMETRICS_TEST: 'Multifactor_Authentication_Biometrics_Test',
+        NOTIFICATION: 'Multifactor_Authentication_Notification',
+        PROMPT: 'Multifactor_Authentication_Prompt',
+        NOT_FOUND: 'Multifactor_Authentication_Not_Found',
+    },
 } as const;
 
 type Screen = DeepValueOf<typeof SCREENS>;
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index 5041594252a0..7bdfeca882ef 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -17,6 +17,7 @@ import type {
     MergeTransactionNavigatorParamList,
     MissingPersonalDetailsParamList,
     MoneyRequestNavigatorParamList,
+    MultifactorAuthenticationParamList,
     NewChatNavigatorParamList,
     NewReportWorkspaceSelectionNavigatorParamList,
     NewTaskNavigatorParamList,
@@ -984,6 +985,14 @@ const WorkspacesDomainModalStackNavigator = createModalStackNavigator<Workspaces
     [SCREENS.WORKSPACES_DOMAIN_ACCESS_RESTRICTED]: () => require<ReactComponentModule>('../../../../pages/domain/DomainAccessRestrictedPage').default,
 });
 
+const MultifactorAuthenticationStackNavigator = createModalStackNavigator<MultifactorAuthenticationParamList>({
+    [SCREENS.MULTIFACTOR_AUTHENTICATION.MAGIC_CODE]: () => require<ReactComponentModule>('../../../../pages/MultifactorAuthentication/ValidateCodePage').default,
+    [SCREENS.MULTIFACTOR_AUTHENTICATION.BIOMETRICS_TEST]: () => require<ReactComponentModule>('../../../../pages/MultifactorAuthentication/BiometricsTestPage').default,
+    [SCREENS.MULTIFACTOR_AUTHENTICATION.NOTIFICATION]: () => require<ReactComponentModule>('../../../../pages/MultifactorAuthentication/NotificationPage').default,
+    [SCREENS.MULTIFACTOR_AUTHENTICATION.PROMPT]: () => require<ReactComponentModule>('../../../../pages/MultifactorAuthentication/PromptPage').default,
+    [SCREENS.MULTIFACTOR_AUTHENTICATION.NOT_FOUND]: () => require<ReactComponentModule>('../../../../pages/ErrorPage/NotFoundPage').default,
+});
+
 export {
     AddPersonalBankAccountModalStackNavigator,
     AddUnreportedExpenseModalStackNavigator,
@@ -1033,4 +1042,5 @@ export {
     WorkspaceConfirmationModalStackNavigator,
     WorkspaceDuplicateModalStackNavigator,
     WorkspacesDomainModalStackNavigator,
+    MultifactorAuthenticationStackNavigator,
 };
diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
index 92cbd4148cf2..0b1bf503ee63 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
@@ -400,6 +400,10 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) {
                             name={SCREENS.RIGHT_MODAL.SEARCH_COLUMNS}
                             component={ModalStackNavigators.SearchColumnsModalStackNavigator}
                         />
+                        <Stack.Screen
+                            name={SCREENS.RIGHT_MODAL.MULTIFACTOR_AUTHENTICATION}
+                            component={ModalStackNavigators.MultifactorAuthenticationStackNavigator}
+                        />
                     </Stack.Navigator>
                 </Animated.View>
                 {/* The third and second overlays are displayed here to cover RHP screens wider than the currently focused screen. */}
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index caf20777d7be..2b8ac2cef4ec 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -1903,6 +1903,15 @@ const config: LinkingOptions<RootNavigatorParamList>['config'] = {
                         },
                     },
                 },
+                [SCREENS.RIGHT_MODAL.MULTIFACTOR_AUTHENTICATION]: {
+                    screens: {
+                        [SCREENS.MULTIFACTOR_AUTHENTICATION.MAGIC_CODE]: ROUTES.MULTIFACTOR_AUTHENTICATION_MAGIC_CODE,
+                        [SCREENS.MULTIFACTOR_AUTHENTICATION.BIOMETRICS_TEST]: ROUTES.MULTIFACTOR_AUTHENTICATION_BIOMETRICS_TEST,
+                        [SCREENS.MULTIFACTOR_AUTHENTICATION.NOTIFICATION]: ROUTES.MULTIFACTOR_AUTHENTICATION_NOTIFICATION.route,
+                        [SCREENS.MULTIFACTOR_AUTHENTICATION.PROMPT]: ROUTES.MULTIFACTOR_AUTHENTICATION_PROMPT.route,
+                        [SCREENS.MULTIFACTOR_AUTHENTICATION.NOT_FOUND]: ROUTES.MULTIFACTOR_AUTHENTICATION_NOT_FOUND,
+                    },
+                },
             },
         },
 
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 04fa0a403789..eb08b66886b0 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -2289,6 +2289,7 @@ type RightModalNavigatorParamList = {
     };
     [SCREENS.RIGHT_MODAL.DOMAIN]: NavigatorScreenParams<WorkspacesDomainModalNavigatorParamList>;
     [SCREENS.RIGHT_MODAL.SEARCH_COLUMNS]: NavigatorScreenParams<SearchColumnsParamList>;
+    [SCREENS.RIGHT_MODAL.MULTIFACTOR_AUTHENTICATION]: NavigatorScreenParams<MultifactorAuthenticationParamList>;
 };
 
 type TravelNavigatorParamList = {
@@ -2939,6 +2940,17 @@ type TestToolsModalModalNavigatorParamList = {
     };
 };
 
+type MultifactorAuthenticationParamList = {
+    [SCREENS.MULTIFACTOR_AUTHENTICATION.MAGIC_CODE]: undefined;
+    [SCREENS.MULTIFACTOR_AUTHENTICATION.BIOMETRICS_TEST]: undefined;
+    [SCREENS.MULTIFACTOR_AUTHENTICATION.NOTIFICATION]: {
+        notificationType: string;
+    };
+    [SCREENS.MULTIFACTOR_AUTHENTICATION.PROMPT]: {
+        promptType: string;
+    };
+};
+
 type RootNavigatorParamList = PublicScreensParamList & AuthScreensParamList & SearchFullscreenNavigatorParamList;
 
 type OnboardingFlowName = keyof OnboardingModalNavigatorParamList;
@@ -3051,4 +3063,5 @@ export type {
     DomainSplitNavigatorParamList,
     DomainScreenName,
     SearchColumnsParamList,
+    MultifactorAuthenticationParamList,
 };
diff --git a/src/pages/MultifactorAuthentication/BiometricsTestPage.tsx b/src/pages/MultifactorAuthentication/BiometricsTestPage.tsx
new file mode 100644
index 000000000000..4e78158c83af
--- /dev/null
+++ b/src/pages/MultifactorAuthentication/BiometricsTestPage.tsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import {View} from 'react-native';
+import BlockingView from '@components/BlockingViews/BlockingView';
+import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView';
+import Button from '@components/Button';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import variables from '@styles/variables';
+import ROUTES from '@src/ROUTES';
+
+function MultifactorAuthenticationBiometricsTestPage() {
+    const {translate} = useLocalize();
+    const styles = useThemeStyles();
+    const onGoBackPress = () => Navigation.dismissModal();
+    const illustrations = useMemoizedLazyIllustrations(['OpenPadlock'] as const);
+
+    return (
+        <ScreenWrapper testID={MultifactorAuthenticationBiometricsTestPage.displayName}>
+            <HeaderWithBackButton
+                title={translate('multifactorAuthentication.biometricsTest.biometricsTest')}
+                onBackButtonPress={onGoBackPress}
+                shouldShowBackButton
+            />
+            <FullPageOfflineBlockingView>
+                <View style={styles.flex1}>
+                    <BlockingView
+                        icon={illustrations.OpenPadlock}
+                        contentFitImage="fill"
+                        iconWidth={variables.openPadlockWidth}
+                        iconHeight={variables.openPadlockHeight}
+                        title={translate('multifactorAuthentication.biometricsTest.biometricsTest')}
+                        containerStyle={styles.p1}
+                        testID={MultifactorAuthenticationBiometricsTestPage.displayName}
+                    />
+                </View>
+                <View style={[styles.flexRow, styles.m5]}>
+                    <Button
+                        success
+                        style={styles.flex1}
+                        onPress={() => {
+                            // Temporary navigation, expected behavior: run the Multifactor Authentication Biometrics Test flow
+                            Navigation.navigate(ROUTES.MULTIFACTOR_AUTHENTICATION_MAGIC_CODE);
+                        }}
+                        text={translate('multifactorAuthentication.biometricsTest.test')}
+                    />
+                </View>
+            </FullPageOfflineBlockingView>
+        </ScreenWrapper>
+    );
+}
+
+MultifactorAuthenticationBiometricsTestPage.displayName = 'MultifactorAuthenticationBiometricsTestPage';
+
+export default MultifactorAuthenticationBiometricsTestPage;
diff --git a/src/pages/MultifactorAuthentication/NotificationPage.tsx b/src/pages/MultifactorAuthentication/NotificationPage.tsx
new file mode 100644
index 000000000000..885fbb4eceb2
--- /dev/null
+++ b/src/pages/MultifactorAuthentication/NotificationPage.tsx
@@ -0,0 +1,88 @@
+import React from 'react';
+import {View} from 'react-native';
+import BlockingView from '@components/BlockingViews/BlockingView';
+import Button from '@components/Button';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import {loadIllustration} from '@components/Icon/IllustrationLoader';
+import ScreenWrapper from '@components/ScreenWrapper';
+import {useMemoizedLazyAsset} from '@hooks/useLazyAsset';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
+import type {MultifactorAuthenticationParamList} from '@libs/Navigation/types';
+import variables from '@styles/variables';
+import type {TranslationPaths} from '@src/languages/types';
+import type SCREENS from '@src/SCREENS';
+
+const mockedConfigSuccess = {
+    headerTitle: 'multifactorAuthentication.biometricsTest.biometricsTest',
+    title: 'multifactorAuthentication.biometricsTest.authenticationSuccessful',
+    description: 'multifactorAuthentication.biometricsTest.successfullyAuthenticatedUsing',
+} as const satisfies Record<string, TranslationPaths>;
+
+const mockedConfigFailure = {
+    headerTitle: 'multifactorAuthentication.biometricsTest.biometricsTest',
+    title: 'multifactorAuthentication.oops',
+    description: 'multifactorAuthentication.biometricsTest.yourAttemptWasUnsuccessful',
+} as const satisfies Record<string, TranslationPaths>;
+
+type MultifactorAuthenticationNotificationPageProps = PlatformStackScreenProps<MultifactorAuthenticationParamList, typeof SCREENS.MULTIFACTOR_AUTHENTICATION.NOTIFICATION>;
+
+function MultifactorAuthenticationNotificationPage({route}: MultifactorAuthenticationNotificationPageProps) {
+    const {translate} = useLocalize();
+    const styles = useThemeStyles();
+    const onGoBackPress = () => {
+        Navigation.dismissModal();
+    };
+
+    const isSuccessNotification = route.params.notificationType === 'success';
+
+    let headerTitle = translate(mockedConfigFailure.headerTitle);
+    let title = translate(mockedConfigFailure.title);
+    let description = translate(mockedConfigFailure.description);
+
+    if (isSuccessNotification) {
+        headerTitle = translate(mockedConfigSuccess.headerTitle);
+        title = translate(mockedConfigSuccess.title);
+        description = translate(mockedConfigSuccess.description, {authType: 'FaceID'});
+    }
+
+    const {asset: icon} = useMemoizedLazyAsset(() => loadIllustration(isSuccessNotification ? 'OpenPadlock' : 'HumptyDumpty'));
+
+    return (
+        <ScreenWrapper testID={MultifactorAuthenticationNotificationPage.displayName}>
+            <HeaderWithBackButton
+                title={headerTitle}
+                onBackButtonPress={onGoBackPress}
+                shouldShowBackButton
+            />
+            <View style={styles.flex1}>
+                <BlockingView
+                    icon={icon}
+                    contentFitImage="fill"
+                    iconWidth={isSuccessNotification ? variables.openPadlockWidth : variables.humptyDumptyWidth}
+                    iconHeight={isSuccessNotification ? variables.openPadlockHeight : variables.humptyDumptyHeight}
+                    title={title}
+                    titleStyles={styles.mb2}
+                    subtitle={description}
+                    subtitleStyle={styles.textSupporting}
+                    containerStyle={styles.ph5}
+                    testID={MultifactorAuthenticationNotificationPage.displayName}
+                />
+            </View>
+            <View style={[styles.flexRow, styles.m5, styles.mt0]}>
+                <Button
+                    success
+                    style={styles.flex1}
+                    onPress={onGoBackPress}
+                    text={translate('common.buttonConfirm')}
+                />
+            </View>
+        </ScreenWrapper>
+    );
+}
+
+MultifactorAuthenticationNotificationPage.displayName = 'MultifactorAuthenticationNotificationPage';
+
+export default MultifactorAuthenticationNotificationPage;
diff --git a/src/pages/MultifactorAuthentication/PromptPage.tsx b/src/pages/MultifactorAuthentication/PromptPage.tsx
new file mode 100644
index 000000000000..6a02d2553f6e
--- /dev/null
+++ b/src/pages/MultifactorAuthentication/PromptPage.tsx
@@ -0,0 +1,82 @@
+import React, {useState} from 'react';
+import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView';
+import Button from '@components/Button';
+import FixedFooter from '@components/FixedFooter';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import LottieAnimations from '@components/LottieAnimations';
+import MultifactorAuthenticationPromptContent from '@components/MultifactorAuthentication/PromptContent';
+import MultifactorAuthenticationTriggerCancelConfirmModal from '@components/MultifactorAuthentication/TriggerCancelConfirmModal';
+import ScreenWrapper from '@components/ScreenWrapper';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@navigation/Navigation';
+import type {TranslationPaths} from '@src/languages/types';
+import ROUTES from '@src/ROUTES';
+
+const mockedConfig = {
+    title: 'multifactorAuthentication.verifyYourself.biometrics',
+    subtitle: 'multifactorAuthentication.enableQuickVerification.biometrics',
+} as const satisfies Record<string, TranslationPaths>;
+
+function MultifactorAuthenticationPromptPage() {
+    const {translate} = useLocalize();
+    const styles = useThemeStyles();
+
+    const [isConfirmModalVisible, setConfirmModalVisibility] = useState(false);
+
+    const onConfirm = () => {
+        // Temporary navigation, expected behavior: let the MultifactorAuthentication know that the soft prompt was accepted
+        Navigation.navigate(ROUTES.MULTIFACTOR_AUTHENTICATION_NOTIFICATION.getRoute('success'));
+    };
+
+    const showConfirmModal = () => {
+        setConfirmModalVisibility(true);
+    };
+
+    const hideConfirmModal = () => {
+        setConfirmModalVisibility(false);
+    };
+
+    const cancelFlow = () => {
+        if (!isConfirmModalVisible) {
+            return;
+        }
+
+        hideConfirmModal();
+        // Temporary navigation, expected behavior: trigger the failure from the MultifactorAuthenticationContext
+        Navigation.navigate(ROUTES.MULTIFACTOR_AUTHENTICATION_NOTIFICATION.getRoute('failure'));
+    };
+
+    return (
+        <ScreenWrapper testID={MultifactorAuthenticationPromptPage.displayName}>
+            <HeaderWithBackButton
+                title={translate('multifactorAuthentication.letsVerifyItsYou')}
+                onBackButtonPress={showConfirmModal}
+                shouldShowBackButton
+            />
+            <FullPageOfflineBlockingView>
+                <MultifactorAuthenticationPromptContent
+                    animation={LottieAnimations.Fingerprint}
+                    title={mockedConfig.title}
+                    subtitle={mockedConfig.subtitle}
+                />
+                <FixedFooter style={[styles.flexColumn, styles.gap3]}>
+                    <Button
+                        success
+                        onPress={onConfirm}
+                        text={translate('common.buttonConfirm')}
+                    />
+                </FixedFooter>
+                <MultifactorAuthenticationTriggerCancelConfirmModal
+                    isVisible={isConfirmModalVisible}
+                    onConfirm={cancelFlow}
+                    onCancel={hideConfirmModal}
+                />
+            </FullPageOfflineBlockingView>
+        </ScreenWrapper>
+    );
+}
+
+MultifactorAuthenticationPromptPage.displayName = 'MultifactorAuthenticationPromptPage';
+
+export default MultifactorAuthenticationPromptPage;
diff --git a/src/pages/MultifactorAuthentication/ValidateCodePage.tsx b/src/pages/MultifactorAuthentication/ValidateCodePage.tsx
new file mode 100644
index 000000000000..a7a3dc3570ed
--- /dev/null
+++ b/src/pages/MultifactorAuthentication/ValidateCodePage.tsx
@@ -0,0 +1,224 @@
+import React, {useEffect, useRef, useState} from 'react';
+import {View} from 'react-native';
+import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView';
+import Button from '@components/Button';
+import FormHelpMessage from '@components/FormHelpMessage';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import MagicCodeInput from '@components/MagicCodeInput';
+import type {MagicCodeInputHandle} from '@components/MagicCodeInput';
+import MultifactorAuthenticationValidateCodeResendButton from '@components/MultifactorAuthentication/ValidateCodeResendButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
+import useOnyx from '@hooks/useOnyx';
+import useThemeStyles from '@hooks/useThemeStyles';
+import AccountUtils from '@libs/AccountUtils';
+import {getLatestErrorMessage} from '@libs/ErrorUtils';
+import {isValidValidateCode} from '@libs/ValidationUtils';
+import Navigation from '@navigation/Navigation';
+import {clearAccountMessages} from '@userActions/Session';
+import {resendValidateCode} from '@userActions/User';
+import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
+
+type FormError = {
+    inputCode?: TranslationPaths;
+};
+
+function MultifactorAuthenticationValidateCodePage() {
+    const {translate} = useLocalize();
+    const {isOffline} = useNetwork();
+    const styles = useThemeStyles();
+
+    // Onyx data
+    const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true});
+    const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false});
+
+    const contactMethod = account?.primaryLogin ?? '';
+
+    // Local state
+    const [inputCode, setInputCode] = useState('');
+    const [formError, setFormError] = useState<FormError>({});
+    const [canShowError, setCanShowError] = useState<boolean>(false);
+    const [timeRemaining, setTimeRemaining] = useState(CONST.REQUEST_CODE_DELAY as number);
+    const [needToClearError, setNeedToClearError] = useState<boolean>(!!account?.errors);
+
+    // Refs
+    const inputRef = useRef<MagicCodeInputHandle>(null);
+    const timerRef = useRef<NodeJS.Timeout | undefined>(undefined);
+
+    // Derived state
+    const hasError = !!account && !isEmptyObject(account?.errors) && !needToClearError;
+    const isValidateCodeFormSubmitting = AccountUtils.isValidateCodeFormSubmitting(account);
+    const shouldDisableResendCode = isOffline ?? account?.isLoading;
+    const shouldShowTimer = timeRemaining > 0 && !isOffline;
+
+    // Timer handling
+    useEffect(() => {
+        if (timeRemaining > 0) {
+            timerRef.current = setTimeout(() => {
+                setTimeRemaining(timeRemaining - 1);
+            }, 1000);
+        }
+        return () => {
+            clearTimeout(timerRef.current);
+        };
+    }, [timeRemaining]);
+
+    // Auto-blur on error
+    useEffect(() => {
+        if (!(inputRef.current && hasError && (session?.autoAuthState === CONST.AUTO_AUTH_STATE.FAILED || account?.isLoading))) {
+            return;
+        }
+        inputRef.current.blur();
+    }, [account?.isLoading, session?.autoAuthState, hasError]);
+
+    // Clear input when code is reset
+    useEffect(() => {
+        if (!inputRef.current || inputCode.length > 0) {
+            return;
+        }
+        inputRef.current.clear();
+    }, [inputCode.length]);
+
+    // Handle needToClearError
+    useEffect(() => {
+        if (!needToClearError) {
+            return;
+        }
+
+        if (account?.errors) {
+            clearAccountMessages();
+            return;
+        }
+        setNeedToClearError(false);
+    }, [account?.errors, needToClearError]);
+
+    // Reset formError when hasError changes
+    useEffect(() => {
+        if (!hasError) {
+            return;
+        }
+        setFormError({});
+    }, [hasError]);
+
+    /**
+     * Handle code input and clear formError upon text change
+     */
+    const onCodeInput = (text: string) => {
+        setFormError({});
+        setCanShowError(false);
+        setInputCode(text);
+
+        // Clear backend errors
+        if (account?.errors) {
+            clearAccountMessages();
+        }
+    };
+
+    const resendValidationCode = () => {
+        resendValidateCode(contactMethod);
+        inputRef.current?.clear();
+        setInputCode('');
+        setFormError({});
+        setCanShowError(false);
+        setTimeRemaining(CONST.REQUEST_CODE_DELAY);
+    };
+
+    /**
+     * Validate and submit form
+     */
+    const validateAndSubmitForm = () => {
+        // Check if already loading
+        if (account?.isLoading) {
+            return;
+        }
+
+        // Clear backend errors before validation
+        if (account?.errors) {
+            clearAccountMessages();
+        }
+
+        setCanShowError(true);
+
+        // Blur input
+        if (inputRef.current) {
+            inputRef.current.blur();
+        }
+
+        // Validate input
+        if (!inputCode.trim()) {
+            setFormError({inputCode: 'validateCodeForm.error.pleaseFillMagicCode'});
+            return;
+        }
+
+        if (!isValidValidateCode(inputCode)) {
+            setFormError({inputCode: 'validateCodeForm.error.incorrectMagicCode'});
+            return;
+        }
+
+        // Clear errors before submit
+        setFormError({});
+
+        // Temporary navigation, expected behavior: trigger submit from the MultifactorAuthenticationContext
+        Navigation.navigate(ROUTES.MULTIFACTOR_AUTHENTICATION_PROMPT.getRoute('enable-biometrics'));
+    };
+
+    const onGoBackPress = () => {
+        // Temporary navigation, expected behavior: trigger onBack from the MultifactorAuthenticationContext
+        Navigation.navigate(ROUTES.MULTIFACTOR_AUTHENTICATION_BIOMETRICS_TEST);
+    };
+
+    return (
+        <ScreenWrapper testID={MultifactorAuthenticationValidateCodePage.displayName}>
+            <HeaderWithBackButton
+                title={translate('multifactorAuthentication.letsVerifyItsYou')}
+                onBackButtonPress={onGoBackPress}
+                shouldShowBackButton
+            />
+            <FullPageOfflineBlockingView>
+                <Text style={[styles.m5, styles.mt3, styles.textNormal]}>{translate('contacts.enterMagicCode', contactMethod)}</Text>
+                <View style={[styles.mh5]}>
+                    <MagicCodeInput
+                        isDisableKeyboard
+                        autoComplete="one-time-code"
+                        name="multifactorAuthenticationValidateCode"
+                        value={inputCode}
+                        onChangeText={onCodeInput}
+                        onFulfill={validateAndSubmitForm}
+                        errorText={canShowError && formError.inputCode ? translate(formError.inputCode) : ''}
+                        hasError={hasError}
+                        ref={inputRef}
+                        maxLength={CONST.MAGIC_CODE_LENGTH}
+                    />
+                    {hasError && <FormHelpMessage message={getLatestErrorMessage(account)} />}
+                    <MultifactorAuthenticationValidateCodeResendButton
+                        shouldShowTimer={shouldShowTimer}
+                        timeRemaining={timeRemaining}
+                        shouldDisableResendCode={shouldDisableResendCode}
+                        hasError={hasError}
+                        resendButtonText="validateCodeForm.magicCodeNotReceived"
+                        onResendValidationCode={resendValidationCode}
+                    />
+                </View>
+                <Button
+                    success
+                    large
+                    style={[styles.w100, styles.p5, styles.mtAuto]}
+                    onPress={validateAndSubmitForm}
+                    text={translate('common.verify')}
+                    isLoading={isValidateCodeFormSubmitting}
+                    isDisabled={isOffline}
+                />
+            </FullPageOfflineBlockingView>
+        </ScreenWrapper>
+    );
+}
+
+MultifactorAuthenticationValidateCodePage.displayName = 'MultifactorAuthenticationValidateCodePage';
+
+export default MultifactorAuthenticationValidateCodePage;
diff --git a/tests/unit/NetworkTest.tsx b/tests/unit/NetworkTest.tsx
index deb12fce73d9..d254e0bf2549 100644
--- a/tests/unit/NetworkTest.tsx
+++ b/tests/unit/NetworkTest.tsx
@@ -1,5 +1,6 @@
 import NetInfo from '@react-native-community/netinfo';
 import type {NetInfoState} from '@react-native-community/netinfo';
+import {NavigationContainer} from '@react-navigation/native';
 import {fireEvent, render, screen, waitFor} from '@testing-library/react-native';
 import {sub as dateSubtract} from 'date-fns/sub';
 import type {Mock} from 'jest-mock';
@@ -8,6 +9,7 @@ import MockedOnyx from 'react-native-onyx';
 import TestToolMenu from '@components/TestToolMenu';
 import {confirmReadyToOpenApp, reconnectApp} from '@libs/actions/App';
 import {resetReauthentication} from '@libs/Middleware/Reauthentication';
+import navigationRef from '@libs/Navigation/navigationRef';
 import CONST from '@src/CONST';
 import * as NetworkActions from '@src/libs/actions/Network';
 import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager';
@@ -480,7 +482,16 @@ describe('NetworkTests', () => {
         const setShouldFailAllRequestsSpy = jest.spyOn(NetworkActions, 'setShouldFailAllRequests');
 
         // Given an opened test tool menu
-        render(<TestToolMenu />);
+        render(
+            <NavigationContainer
+                ref={navigationRef}
+                onReady={() => {
+                    // Navigation is ready, but we still need to handle the timing issue
+                }}
+            >
+                <TestToolMenu />
+            </NavigationContainer>,
+        );
         expect(screen.getByAccessibilityHint('Force offline')).not.toBeDisabled();
         expect(screen.getByAccessibilityHint('Simulate failing network requests')).not.toBeDisabled();
 

From 7fc8d2896185ca1de9ef59608526b5223d3ccc83 Mon Sep 17 00:00:00 2001
From: Jakub Korytko <jakub@korytko.me>
Date: Wed, 14 Jan 2026 15:46:29 +0100
Subject: [PATCH 3/5] feat: add test biometrics button to test tool menu

---
 src/components/TestToolMenu.tsx | 27 +++++++++++++++++++++++++++
 1 file changed, 27 insertions(+)

diff --git a/src/components/TestToolMenu.tsx b/src/components/TestToolMenu.tsx
index d31450a02250..f4f83bfab000 100644
--- a/src/components/TestToolMenu.tsx
+++ b/src/components/TestToolMenu.tsx
@@ -1,15 +1,20 @@
 import React from 'react';
+import {View} from 'react-native';
 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 {isUsingStagingApi} from '@libs/ApiUtils';
+import Navigation from '@libs/Navigation/Navigation';
 import {setShouldFailAllRequests, setShouldForceOffline, setShouldSimulatePoorConnection} from '@userActions/Network';
 import {expireSessionWithDelay, invalidateAuthToken, invalidateCredentials} from '@userActions/Session';
 import {setIsDebugModeEnabled, setShouldUseStagingServer} from '@userActions/User';
 import CONFIG from '@src/CONFIG';
 import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
 import Button from './Button';
 import SoftKillTestToolRow from './SoftKillTestToolRow';
 import Switch from './Switch';
@@ -26,9 +31,20 @@ function TestToolMenu() {
     const {translate} = useLocalize();
     const {clearLHNCache} = useSidebarOrderedReports();
 
+    const {singleExecution} = useSingleExecution();
+    const waitForNavigate = useWaitForNavigation();
+    const navigateToBiometricsTestPage = singleExecution(
+        waitForNavigate(() => {
+            Navigation.navigate(ROUTES.MULTIFACTOR_AUTHENTICATION_BIOMETRICS_TEST);
+        }),
+    );
+
     // Check if the user is authenticated to show options that require authentication
     const isAuthenticated = useIsAuthenticated();
 
+    // Temporary hardcoded false, expected behavior: status fetched from the MultifactorAuthenticationContext
+    const biometricsTitle = translate('multifactorAuthentication.biometricsTest.troubleshootBiometricsStatus', {registered: false});
+
     return (
         <>
             <Text
@@ -83,6 +99,17 @@ function TestToolMenu() {
                             onPress={clearLHNCache}
                         />
                     </TestToolRow>
+
+                    {/* Allows you to test the Biometrics flow */}
+                    <TestToolRow title={biometricsTitle}>
+                        <View style={[styles.flexRow, styles.gap2]}>
+                            <Button
+                                small
+                                text={translate('multifactorAuthentication.biometricsTest.test')}
+                                onPress={() => navigateToBiometricsTestPage()}
+                            />
+                        </View>
+                    </TestToolRow>
                 </>
             )}
 

From 0647a009e728df5b0139114cd9295912341ad24d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jakub=20Kali=C5=84ski?= <kubakalinski2@gmail.com>
Date: Thu, 15 Jan 2026 21:53:55 +0100
Subject: [PATCH 4/5] fix: address PR comments

---
 src/CONST/index.ts                            |  4 +++
 src/ROUTES.ts                                 |  2 +-
 .../NoEligibleMethodsDescription.tsx          |  2 +-
 src/libs/Navigation/types.ts                  |  2 +-
 .../NotificationPage.tsx                      |  5 +++-
 .../MultifactorAuthentication/PromptPage.tsx  |  5 ++--
 .../ValidateCodePage.tsx                      | 30 +++++++++++--------
 7 files changed, 32 insertions(+), 18 deletions(-)

diff --git a/src/CONST/index.ts b/src/CONST/index.ts
index e7f7cff62dd5..78daa0c31e50 100755
--- a/src/CONST/index.ts
+++ b/src/CONST/index.ts
@@ -5469,6 +5469,10 @@ const CONST = {
         DISABLED: 'DISABLED',
         DISABLE: 'DISABLE',
     },
+    MULTIFACTOR_AUTHENTICATION_NOTIFICATION_TYPE: {
+        SUCCESS: 'success',
+        FAILURE: 'failure',
+    },
     MERGE_ACCOUNT_RESULTS: {
         SUCCESS: 'success',
         ERR_2FA: 'err_2fa',
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 4b0bdc639ae1..87990bbe91f8 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -3698,7 +3698,7 @@ const ROUTES = {
     // for now a string is accepted to avoid blocking this PR.
     MULTIFACTOR_AUTHENTICATION_NOTIFICATION: {
         route: 'multifactor-authentication/notification/:notificationType',
-        getRoute: (notificationType: string) => `multifactor-authentication/notification/${notificationType}` as const,
+        getRoute: (notificationType: ValueOf<typeof CONST.MULTIFACTOR_AUTHENTICATION_NOTIFICATION_TYPE>) => `multifactor-authentication/notification/${notificationType}` as const,
     },
 
     MULTIFACTOR_AUTHENTICATION_PROMPT: {
diff --git a/src/components/MultifactorAuthentication/NoEligibleMethodsDescription.tsx b/src/components/MultifactorAuthentication/NoEligibleMethodsDescription.tsx
index e011160edccd..d80ebcc6c3c6 100644
--- a/src/components/MultifactorAuthentication/NoEligibleMethodsDescription.tsx
+++ b/src/components/MultifactorAuthentication/NoEligibleMethodsDescription.tsx
@@ -2,7 +2,7 @@ import Text from '@components/Text';
 import TextLink from '@components/TextLink';
 import useLocalize from '@hooks/useLocalize';
 import useThemeStyles from '@hooks/useThemeStyles';
-import goToSettings from '@libs/goToSettings/index.native';
+import goToSettings from '@libs/goToSettings';
 
 const base = 'multifactorAuthentication.pleaseEnableInSystemSettings' as const;
 
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index eb08b66886b0..3a6f8cec8c1e 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -2944,7 +2944,7 @@ type MultifactorAuthenticationParamList = {
     [SCREENS.MULTIFACTOR_AUTHENTICATION.MAGIC_CODE]: undefined;
     [SCREENS.MULTIFACTOR_AUTHENTICATION.BIOMETRICS_TEST]: undefined;
     [SCREENS.MULTIFACTOR_AUTHENTICATION.NOTIFICATION]: {
-        notificationType: string;
+        notificationType: ValueOf<typeof CONST.MULTIFACTOR_AUTHENTICATION_NOTIFICATION_TYPE>;
     };
     [SCREENS.MULTIFACTOR_AUTHENTICATION.PROMPT]: {
         promptType: string;
diff --git a/src/pages/MultifactorAuthentication/NotificationPage.tsx b/src/pages/MultifactorAuthentication/NotificationPage.tsx
index 885fbb4eceb2..2e64570fd1a4 100644
--- a/src/pages/MultifactorAuthentication/NotificationPage.tsx
+++ b/src/pages/MultifactorAuthentication/NotificationPage.tsx
@@ -12,6 +12,7 @@ import Navigation from '@libs/Navigation/Navigation';
 import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
 import type {MultifactorAuthenticationParamList} from '@libs/Navigation/types';
 import variables from '@styles/variables';
+import CONST from '@src/CONST';
 import type {TranslationPaths} from '@src/languages/types';
 import type SCREENS from '@src/SCREENS';
 
@@ -36,7 +37,7 @@ function MultifactorAuthenticationNotificationPage({route}: MultifactorAuthentic
         Navigation.dismissModal();
     };
 
-    const isSuccessNotification = route.params.notificationType === 'success';
+    const isSuccessNotification = route.params.notificationType === CONST.MULTIFACTOR_AUTHENTICATION_NOTIFICATION_TYPE.SUCCESS;
 
     let headerTitle = translate(mockedConfigFailure.headerTitle);
     let title = translate(mockedConfigFailure.title);
@@ -45,6 +46,8 @@ function MultifactorAuthenticationNotificationPage({route}: MultifactorAuthentic
     if (isSuccessNotification) {
         headerTitle = translate(mockedConfigSuccess.headerTitle);
         title = translate(mockedConfigSuccess.title);
+        // TODO: Replace hardcoded 'FaceID' with the actual authentication type (e.g., 'FaceID', 'TouchID', 'Fingerprint')
+        // once the MFA context provides the auth method used. This will require adding authType to route params and to CONST.
         description = translate(mockedConfigSuccess.description, {authType: 'FaceID'});
     }
 
diff --git a/src/pages/MultifactorAuthentication/PromptPage.tsx b/src/pages/MultifactorAuthentication/PromptPage.tsx
index 6a02d2553f6e..df649a215be6 100644
--- a/src/pages/MultifactorAuthentication/PromptPage.tsx
+++ b/src/pages/MultifactorAuthentication/PromptPage.tsx
@@ -10,6 +10,7 @@ import ScreenWrapper from '@components/ScreenWrapper';
 import useLocalize from '@hooks/useLocalize';
 import useThemeStyles from '@hooks/useThemeStyles';
 import Navigation from '@navigation/Navigation';
+import CONST from '@src/CONST';
 import type {TranslationPaths} from '@src/languages/types';
 import ROUTES from '@src/ROUTES';
 
@@ -26,7 +27,7 @@ function MultifactorAuthenticationPromptPage() {
 
     const onConfirm = () => {
         // Temporary navigation, expected behavior: let the MultifactorAuthentication know that the soft prompt was accepted
-        Navigation.navigate(ROUTES.MULTIFACTOR_AUTHENTICATION_NOTIFICATION.getRoute('success'));
+        Navigation.navigate(ROUTES.MULTIFACTOR_AUTHENTICATION_NOTIFICATION.getRoute(CONST.MULTIFACTOR_AUTHENTICATION_NOTIFICATION_TYPE.SUCCESS));
     };
 
     const showConfirmModal = () => {
@@ -44,7 +45,7 @@ function MultifactorAuthenticationPromptPage() {
 
         hideConfirmModal();
         // Temporary navigation, expected behavior: trigger the failure from the MultifactorAuthenticationContext
-        Navigation.navigate(ROUTES.MULTIFACTOR_AUTHENTICATION_NOTIFICATION.getRoute('failure'));
+        Navigation.navigate(ROUTES.MULTIFACTOR_AUTHENTICATION_NOTIFICATION.getRoute(CONST.MULTIFACTOR_AUTHENTICATION_NOTIFICATION_TYPE.FAILURE));
     };
 
     return (
diff --git a/src/pages/MultifactorAuthentication/ValidateCodePage.tsx b/src/pages/MultifactorAuthentication/ValidateCodePage.tsx
index a7a3dc3570ed..9ada3564f6a2 100644
--- a/src/pages/MultifactorAuthentication/ValidateCodePage.tsx
+++ b/src/pages/MultifactorAuthentication/ValidateCodePage.tsx
@@ -45,26 +45,35 @@ function MultifactorAuthenticationValidateCodePage() {
     const [formError, setFormError] = useState<FormError>({});
     const [canShowError, setCanShowError] = useState<boolean>(false);
     const [timeRemaining, setTimeRemaining] = useState(CONST.REQUEST_CODE_DELAY as number);
-    const [needToClearError, setNeedToClearError] = useState<boolean>(!!account?.errors);
 
     // Refs
     const inputRef = useRef<MagicCodeInputHandle>(null);
     const timerRef = useRef<NodeJS.Timeout | undefined>(undefined);
+    const hasClearedInitialErrorsRef = useRef(false);
 
     // Derived state
-    const hasError = !!account && !isEmptyObject(account?.errors) && !needToClearError;
+    const hasError = !!account && !isEmptyObject(account?.errors);
     const isValidateCodeFormSubmitting = AccountUtils.isValidateCodeFormSubmitting(account);
     const shouldDisableResendCode = isOffline ?? account?.isLoading;
     const shouldShowTimer = timeRemaining > 0 && !isOffline;
 
     // Timer handling
     useEffect(() => {
+        // Clear any existing timeout first
+        if (timerRef.current) {
+            clearTimeout(timerRef.current);
+        }
+
         if (timeRemaining > 0) {
             timerRef.current = setTimeout(() => {
                 setTimeRemaining(timeRemaining - 1);
             }, 1000);
         }
+
         return () => {
+            if (!timerRef.current) {
+                return;
+            }
             clearTimeout(timerRef.current);
         };
     }, [timeRemaining]);
@@ -85,18 +94,14 @@ function MultifactorAuthenticationValidateCodePage() {
         inputRef.current.clear();
     }, [inputCode.length]);
 
-    // Handle needToClearError
+    // Clear any pre-existing errors on mount
     useEffect(() => {
-        if (!needToClearError) {
-            return;
-        }
-
-        if (account?.errors) {
-            clearAccountMessages();
+        if (hasClearedInitialErrorsRef.current || !account?.errors) {
             return;
         }
-        setNeedToClearError(false);
-    }, [account?.errors, needToClearError]);
+        hasClearedInitialErrorsRef.current = true;
+        clearAccountMessages();
+    }, [account?.errors]);
 
     // Reset formError when hasError changes
     useEffect(() => {
@@ -165,6 +170,8 @@ function MultifactorAuthenticationValidateCodePage() {
         setFormError({});
 
         // Temporary navigation, expected behavior: trigger submit from the MultifactorAuthenticationContext
+        // 'enable-biometrics' is a type of the MultifactorAuthentication prompt
+        // will be added as a part of scenario config in another PR to make it complacent with CONSISTENCY-2 (docs)
         Navigation.navigate(ROUTES.MULTIFACTOR_AUTHENTICATION_PROMPT.getRoute('enable-biometrics'));
     };
 
@@ -184,7 +191,6 @@ function MultifactorAuthenticationValidateCodePage() {
                 <Text style={[styles.m5, styles.mt3, styles.textNormal]}>{translate('contacts.enterMagicCode', contactMethod)}</Text>
                 <View style={[styles.mh5]}>
                     <MagicCodeInput
-                        isDisableKeyboard
                         autoComplete="one-time-code"
                         name="multifactorAuthenticationValidateCode"
                         value={inputCode}

From fd892577c66a9a9a1edec09b53a8f6efe1fe0180 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jakub=20Kali=C5=84ski?= <kubakalinski2@gmail.com>
Date: Mon, 19 Jan 2026 12:44:24 +0100
Subject: [PATCH 5/5] fix: address Rafe PR comments

  - Improve MFA component naming and styles
  - Reuse ValidateCodeCountdown in MFA ValidateCodeResendButton
  - Move ValidateCodeCountdown from pages/signin/ to components/ for reusability
  - Refactor MultifactorAuthenticationValidateCodeResendButton to use ValidateCodeCountdown instead of duplicating timer logic
  - Update imports in BaseValidateCodeForm (signin and ValidateCodeActionModal)
  - Adjust BiometricsTestPage
  - Rename constants in NoEligibleMethodsDescription for clarity:
  - base -> baseTranslationPath
  - tPaths -> translationPaths
  - Add dedicated mfaBlockingViewAnimation style instead of reusing emptyLHNAnimation
  - Update PromptContent to use the new MFA-specific animation style
  - Add TODO comments for mocked configs in NotificationPage
  - Add TODO comment for mockedConfig in TriggerCancelConfirmModal
---
 .../NoEligibleMethodsDescription.tsx          | 16 ++--
 .../PromptContent.tsx                         |  4 +-
 .../TriggerCancelConfirmModal.tsx             |  1 +
 .../ValidateCodeResendButton.tsx              | 80 ++++++++++++-------
 .../ValidateCodeForm/BaseValidateCodeForm.tsx |  4 +-
 .../ValidateCodeCountdown/index.tsx           |  0
 .../ValidateCodeCountdown/types.ts            |  0
 .../BiometricsTestPage.tsx                    | 54 +++----------
 .../NotificationPage.tsx                      |  2 +
 .../ValidateCodePage.tsx                      | 34 ++------
 .../ValidateCodeForm/BaseValidateCodeForm.tsx |  4 +-
 src/styles/index.ts                           |  5 ++
 12 files changed, 89 insertions(+), 115 deletions(-)
 rename src/{pages/signin => components}/ValidateCodeCountdown/index.tsx (100%)
 rename src/{pages/signin => components}/ValidateCodeCountdown/types.ts (100%)

diff --git a/src/components/MultifactorAuthentication/NoEligibleMethodsDescription.tsx b/src/components/MultifactorAuthentication/NoEligibleMethodsDescription.tsx
index d80ebcc6c3c6..b1dcab2c7dca 100644
--- a/src/components/MultifactorAuthentication/NoEligibleMethodsDescription.tsx
+++ b/src/components/MultifactorAuthentication/NoEligibleMethodsDescription.tsx
@@ -4,21 +4,21 @@ import useLocalize from '@hooks/useLocalize';
 import useThemeStyles from '@hooks/useThemeStyles';
 import goToSettings from '@libs/goToSettings';
 
-const base = 'multifactorAuthentication.pleaseEnableInSystemSettings' as const;
+const baseTranslationPath = 'multifactorAuthentication.pleaseEnableInSystemSettings' as const;
 
-const tPaths = {
-    start: `${base}.start`,
-    link: `${base}.link`,
-    end: `${base}.end`,
+const translationPaths = {
+    start: `${baseTranslationPath}.start`,
+    link: `${baseTranslationPath}.link`,
+    end: `${baseTranslationPath}.end`,
 } as const;
 
 function NoEligibleMethodsDescription() {
     const styles = useThemeStyles();
     const {translate} = useLocalize();
 
-    const start = translate(tPaths.start);
-    const link = translate(tPaths.link);
-    const end = translate(tPaths.end);
+    const start = translate(translationPaths.start);
+    const link = translate(translationPaths.link);
+    const end = translate(translationPaths.end);
 
     return (
         <Text style={[styles.textAlignCenter, styles.textSupporting]}>
diff --git a/src/components/MultifactorAuthentication/PromptContent.tsx b/src/components/MultifactorAuthentication/PromptContent.tsx
index 2d87aa920014..2d91ff96d1d4 100644
--- a/src/components/MultifactorAuthentication/PromptContent.tsx
+++ b/src/components/MultifactorAuthentication/PromptContent.tsx
@@ -20,8 +20,8 @@ function MultifactorAuthenticationPromptContent({title, subtitle, animation}: Mu
         <View style={styles.flex1}>
             <BlockingView
                 animation={animation}
-                animationStyles={styles.emptyLHNAnimation}
-                animationWebStyle={styles.emptyLHNAnimation}
+                animationStyles={styles.mfaBlockingViewAnimation}
+                animationWebStyle={styles.mfaBlockingViewAnimation}
                 title={translate(title)}
                 titleStyles={styles.mb2}
                 subtitle={translate(subtitle)}
diff --git a/src/components/MultifactorAuthentication/TriggerCancelConfirmModal.tsx b/src/components/MultifactorAuthentication/TriggerCancelConfirmModal.tsx
index 510526e82603..819b38507497 100644
--- a/src/components/MultifactorAuthentication/TriggerCancelConfirmModal.tsx
+++ b/src/components/MultifactorAuthentication/TriggerCancelConfirmModal.tsx
@@ -9,6 +9,7 @@ type MultifactorAuthenticationTriggerCancelConfirmModalProps = {
     onCancel: () => void;
 };
 
+// TODO: this config will be part of the scenario configuration, the current implementation is for testing purposes (https://github.com/Expensify/App/issues/79373)
 const mockedConfig = {
     title: 'common.areYouSure',
     description: 'multifactorAuthentication.biometricsTest.areYouSureToReject',
diff --git a/src/components/MultifactorAuthentication/ValidateCodeResendButton.tsx b/src/components/MultifactorAuthentication/ValidateCodeResendButton.tsx
index 254d6945a7ec..e129d4461e18 100644
--- a/src/components/MultifactorAuthentication/ValidateCodeResendButton.tsx
+++ b/src/components/MultifactorAuthentication/ValidateCodeResendButton.tsx
@@ -1,17 +1,22 @@
-import React from 'react';
+import React, {useCallback, useImperativeHandle, useRef, useState} from 'react';
 import {View} from 'react-native';
 import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
-import RenderHTML from '@components/RenderHTML';
 import Text from '@components/Text';
+import ValidateCodeCountdown from '@components/ValidateCodeCountdown';
+import type {ValidateCodeCountdownHandle} from '@components/ValidateCodeCountdown/types';
 import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
 import useStyleUtils from '@hooks/useStyleUtils';
 import useThemeStyles from '@hooks/useThemeStyles';
 import CONST from '@src/CONST';
 import type {TranslationPaths} from '@src/languages/types';
 
+type MultifactorAuthenticationValidateCodeResendButtonHandle = {
+    resetCountdown: () => void;
+};
+
 type MultifactorAuthenticationValidateCodeResendButtonProps = {
-    shouldShowTimer: boolean;
-    timeRemaining: number;
+    ref?: React.Ref<MultifactorAuthenticationValidateCodeResendButtonHandle>;
     shouldDisableResendCode: boolean;
     hasError: boolean;
     resendButtonText: TranslationPaths;
@@ -19,8 +24,7 @@ type MultifactorAuthenticationValidateCodeResendButtonProps = {
 };
 
 function MultifactorAuthenticationValidateCodeResendButton({
-    shouldShowTimer,
-    timeRemaining,
+    ref,
     shouldDisableResendCode,
     hasError,
     resendButtonText,
@@ -29,36 +33,52 @@ function MultifactorAuthenticationValidateCodeResendButton({
     const styles = useThemeStyles();
     const StyleUtils = useStyleUtils();
     const {translate} = useLocalize();
+    const {isOffline} = useNetwork();
+
+    const [isCountdownRunning, setIsCountdownRunning] = useState(true);
+    const countdownRef = useRef<ValidateCodeCountdownHandle>(null);
 
-    if (shouldShowTimer) {
-        return (
-            <View style={[styles.mt5, styles.flexRow, styles.renderHTML]}>
-                <RenderHTML
-                    html={translate('validateCodeForm.requestNewCode', {
-                        timeRemaining: `00:${String(timeRemaining).padStart(2, '0')}`,
-                    })}
-                />
-            </View>
-        );
-    }
+    const handleCountdownFinish = useCallback(() => {
+        setIsCountdownRunning(false);
+    }, []);
+
+    useImperativeHandle(ref, () => ({
+        resetCountdown: () => {
+            countdownRef.current?.resetCountdown();
+            setIsCountdownRunning(true);
+        },
+    }));
 
     return (
-        <PressableWithFeedback
-            style={styles.mt5}
-            onPress={onResendValidationCode}
-            disabled={shouldDisableResendCode}
-            hoverDimmingValue={1}
-            pressDimmingValue={0.2}
-            role={CONST.ROLE.BUTTON}
-            accessibilityLabel={translate(resendButtonText)}
-        >
-            <Text style={[StyleUtils.getDisabledLinkStyles(shouldDisableResendCode)]}>
-                {hasError ? translate('validateCodeForm.requestNewCodeAfterErrorOccurred') : translate(resendButtonText)}
-            </Text>
-        </PressableWithFeedback>
+        <View style={styles.alignItemsStart}>
+            {isCountdownRunning && !isOffline ? (
+                <View style={[styles.mt5, styles.flexRow, styles.renderHTML]}>
+                    <ValidateCodeCountdown
+                        ref={countdownRef}
+                        onCountdownFinish={handleCountdownFinish}
+                    />
+                </View>
+            ) : (
+                <PressableWithFeedback
+                    style={styles.mt5}
+                    onPress={onResendValidationCode}
+                    disabled={shouldDisableResendCode}
+                    hoverDimmingValue={1}
+                    pressDimmingValue={0.2}
+                    role={CONST.ROLE.BUTTON}
+                    accessibilityLabel={translate(resendButtonText)}
+                >
+                    <Text style={[StyleUtils.getDisabledLinkStyles(shouldDisableResendCode)]}>
+                        {hasError ? translate('validateCodeForm.requestNewCodeAfterErrorOccurred') : translate(resendButtonText)}
+                    </Text>
+                </PressableWithFeedback>
+            )}
+        </View>
     );
 }
 
 MultifactorAuthenticationValidateCodeResendButton.displayName = 'MultifactorAuthenticationValidateCodeResendButton';
 
 export default MultifactorAuthenticationValidateCodeResendButton;
+
+export type {MultifactorAuthenticationValidateCodeResendButtonHandle};
diff --git a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx
index 62fa9a1e17d2..0c8e725f30f0 100644
--- a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx
+++ b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx
@@ -10,6 +10,8 @@ import type {AutoCompleteVariant, MagicCodeInputHandle} from '@components/MagicC
 import OfflineWithFeedback from '@components/OfflineWithFeedback';
 import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
 import Text from '@components/Text';
+import ValidateCodeCountdown from '@components/ValidateCodeCountdown';
+import type {ValidateCodeCountdownHandle} from '@components/ValidateCodeCountdown/types';
 import {WideRHPContext} from '@components/WideRHPContextProvider';
 import useLocalize from '@hooks/useLocalize';
 import useNetwork from '@hooks/useNetwork';
@@ -20,8 +22,6 @@ import useThemeStyles from '@hooks/useThemeStyles';
 import {isMobileSafari} from '@libs/Browser';
 import {getLatestErrorField, getLatestErrorMessage} from '@libs/ErrorUtils';
 import {isValidValidateCode} from '@libs/ValidationUtils';
-import ValidateCodeCountdown from '@pages/signin/ValidateCodeCountdown';
-import type {ValidateCodeCountdownHandle} from '@pages/signin/ValidateCodeCountdown/types';
 import {clearValidateCodeActionError} from '@userActions/User';
 import CONST from '@src/CONST';
 import type {TranslationPaths} from '@src/languages/types';
diff --git a/src/pages/signin/ValidateCodeCountdown/index.tsx b/src/components/ValidateCodeCountdown/index.tsx
similarity index 100%
rename from src/pages/signin/ValidateCodeCountdown/index.tsx
rename to src/components/ValidateCodeCountdown/index.tsx
diff --git a/src/pages/signin/ValidateCodeCountdown/types.ts b/src/components/ValidateCodeCountdown/types.ts
similarity index 100%
rename from src/pages/signin/ValidateCodeCountdown/types.ts
rename to src/components/ValidateCodeCountdown/types.ts
diff --git a/src/pages/MultifactorAuthentication/BiometricsTestPage.tsx b/src/pages/MultifactorAuthentication/BiometricsTestPage.tsx
index 4e78158c83af..048703a98ab6 100644
--- a/src/pages/MultifactorAuthentication/BiometricsTestPage.tsx
+++ b/src/pages/MultifactorAuthentication/BiometricsTestPage.tsx
@@ -1,54 +1,22 @@
-import React from 'react';
-import {View} from 'react-native';
-import BlockingView from '@components/BlockingViews/BlockingView';
-import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView';
-import Button from '@components/Button';
-import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import React, {useEffect} from 'react';
+import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
 import ScreenWrapper from '@components/ScreenWrapper';
-import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
-import useLocalize from '@hooks/useLocalize';
-import useThemeStyles from '@hooks/useThemeStyles';
 import Navigation from '@libs/Navigation/Navigation';
-import variables from '@styles/variables';
 import ROUTES from '@src/ROUTES';
 
+const LOADING_DELAY_MS = 400;
+
 function MultifactorAuthenticationBiometricsTestPage() {
-    const {translate} = useLocalize();
-    const styles = useThemeStyles();
-    const onGoBackPress = () => Navigation.dismissModal();
-    const illustrations = useMemoizedLazyIllustrations(['OpenPadlock'] as const);
+    useEffect(() => {
+        // Show a short loading state so the RHP transition feels smooth, then move to the magic code flow
+        const timeoutId = setTimeout(() => Navigation.navigate(ROUTES.MULTIFACTOR_AUTHENTICATION_MAGIC_CODE), LOADING_DELAY_MS);
+
+        return () => clearTimeout(timeoutId);
+    }, []);
 
     return (
         <ScreenWrapper testID={MultifactorAuthenticationBiometricsTestPage.displayName}>
-            <HeaderWithBackButton
-                title={translate('multifactorAuthentication.biometricsTest.biometricsTest')}
-                onBackButtonPress={onGoBackPress}
-                shouldShowBackButton
-            />
-            <FullPageOfflineBlockingView>
-                <View style={styles.flex1}>
-                    <BlockingView
-                        icon={illustrations.OpenPadlock}
-                        contentFitImage="fill"
-                        iconWidth={variables.openPadlockWidth}
-                        iconHeight={variables.openPadlockHeight}
-                        title={translate('multifactorAuthentication.biometricsTest.biometricsTest')}
-                        containerStyle={styles.p1}
-                        testID={MultifactorAuthenticationBiometricsTestPage.displayName}
-                    />
-                </View>
-                <View style={[styles.flexRow, styles.m5]}>
-                    <Button
-                        success
-                        style={styles.flex1}
-                        onPress={() => {
-                            // Temporary navigation, expected behavior: run the Multifactor Authentication Biometrics Test flow
-                            Navigation.navigate(ROUTES.MULTIFACTOR_AUTHENTICATION_MAGIC_CODE);
-                        }}
-                        text={translate('multifactorAuthentication.biometricsTest.test')}
-                    />
-                </View>
-            </FullPageOfflineBlockingView>
+            <FullScreenLoadingIndicator />
         </ScreenWrapper>
     );
 }
diff --git a/src/pages/MultifactorAuthentication/NotificationPage.tsx b/src/pages/MultifactorAuthentication/NotificationPage.tsx
index 2e64570fd1a4..e961ee56c3c8 100644
--- a/src/pages/MultifactorAuthentication/NotificationPage.tsx
+++ b/src/pages/MultifactorAuthentication/NotificationPage.tsx
@@ -16,12 +16,14 @@ import CONST from '@src/CONST';
 import type {TranslationPaths} from '@src/languages/types';
 import type SCREENS from '@src/SCREENS';
 
+// TODO: this config will be part of the scenario configuration, the current implementation is for testing purposes (https://github.com/Expensify/App/issues/79373)
 const mockedConfigSuccess = {
     headerTitle: 'multifactorAuthentication.biometricsTest.biometricsTest',
     title: 'multifactorAuthentication.biometricsTest.authenticationSuccessful',
     description: 'multifactorAuthentication.biometricsTest.successfullyAuthenticatedUsing',
 } as const satisfies Record<string, TranslationPaths>;
 
+// TODO: this config will be part of the scenario configuration, the current implementation is for testing purposes (https://github.com/Expensify/App/issues/79373)
 const mockedConfigFailure = {
     headerTitle: 'multifactorAuthentication.biometricsTest.biometricsTest',
     title: 'multifactorAuthentication.oops',
diff --git a/src/pages/MultifactorAuthentication/ValidateCodePage.tsx b/src/pages/MultifactorAuthentication/ValidateCodePage.tsx
index 9ada3564f6a2..ddf857df5c39 100644
--- a/src/pages/MultifactorAuthentication/ValidateCodePage.tsx
+++ b/src/pages/MultifactorAuthentication/ValidateCodePage.tsx
@@ -7,6 +7,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton';
 import MagicCodeInput from '@components/MagicCodeInput';
 import type {MagicCodeInputHandle} from '@components/MagicCodeInput';
 import MultifactorAuthenticationValidateCodeResendButton from '@components/MultifactorAuthentication/ValidateCodeResendButton';
+import type {MultifactorAuthenticationValidateCodeResendButtonHandle} from '@components/MultifactorAuthentication/ValidateCodeResendButton';
 import ScreenWrapper from '@components/ScreenWrapper';
 import Text from '@components/Text';
 import useLocalize from '@hooks/useLocalize';
@@ -44,39 +45,16 @@ function MultifactorAuthenticationValidateCodePage() {
     const [inputCode, setInputCode] = useState('');
     const [formError, setFormError] = useState<FormError>({});
     const [canShowError, setCanShowError] = useState<boolean>(false);
-    const [timeRemaining, setTimeRemaining] = useState(CONST.REQUEST_CODE_DELAY as number);
 
     // Refs
     const inputRef = useRef<MagicCodeInputHandle>(null);
-    const timerRef = useRef<NodeJS.Timeout | undefined>(undefined);
+    const resendButtonRef = useRef<MultifactorAuthenticationValidateCodeResendButtonHandle>(null);
     const hasClearedInitialErrorsRef = useRef(false);
 
     // Derived state
     const hasError = !!account && !isEmptyObject(account?.errors);
     const isValidateCodeFormSubmitting = AccountUtils.isValidateCodeFormSubmitting(account);
     const shouldDisableResendCode = isOffline ?? account?.isLoading;
-    const shouldShowTimer = timeRemaining > 0 && !isOffline;
-
-    // Timer handling
-    useEffect(() => {
-        // Clear any existing timeout first
-        if (timerRef.current) {
-            clearTimeout(timerRef.current);
-        }
-
-        if (timeRemaining > 0) {
-            timerRef.current = setTimeout(() => {
-                setTimeRemaining(timeRemaining - 1);
-            }, 1000);
-        }
-
-        return () => {
-            if (!timerRef.current) {
-                return;
-            }
-            clearTimeout(timerRef.current);
-        };
-    }, [timeRemaining]);
 
     // Auto-blur on error
     useEffect(() => {
@@ -131,7 +109,7 @@ function MultifactorAuthenticationValidateCodePage() {
         setInputCode('');
         setFormError({});
         setCanShowError(false);
-        setTimeRemaining(CONST.REQUEST_CODE_DELAY);
+        resendButtonRef.current?.resetCountdown();
     };
 
     /**
@@ -177,7 +155,8 @@ function MultifactorAuthenticationValidateCodePage() {
 
     const onGoBackPress = () => {
         // Temporary navigation, expected behavior: trigger onBack from the MultifactorAuthenticationContext
-        Navigation.navigate(ROUTES.MULTIFACTOR_AUTHENTICATION_BIOMETRICS_TEST);
+        // Close the RHP instead of returning to the invisible biometrics test screen
+        Navigation.dismissModal();
     };
 
     return (
@@ -203,8 +182,7 @@ function MultifactorAuthenticationValidateCodePage() {
                     />
                     {hasError && <FormHelpMessage message={getLatestErrorMessage(account)} />}
                     <MultifactorAuthenticationValidateCodeResendButton
-                        shouldShowTimer={shouldShowTimer}
-                        timeRemaining={timeRemaining}
+                        ref={resendButtonRef}
                         shouldDisableResendCode={shouldDisableResendCode}
                         hasError={hasError}
                         resendButtonText="validateCodeForm.magicCodeNotReceived"
diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx
index 86236e1d7155..6e2d73a0f065 100755
--- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx
+++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx
@@ -9,6 +9,8 @@ import MagicCodeInput from '@components/MagicCodeInput';
 import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
 import Text from '@components/Text';
 import TextInput from '@components/TextInput';
+import ValidateCodeCountdown from '@components/ValidateCodeCountdown';
+import type {ValidateCodeCountdownHandle} from '@components/ValidateCodeCountdown/types';
 import type {WithToggleVisibilityViewProps} from '@components/withToggleVisibilityView';
 import withToggleVisibilityView from '@components/withToggleVisibilityView';
 import useLocalize from '@hooks/useLocalize';
@@ -23,8 +25,6 @@ import {getLatestErrorMessage} from '@libs/ErrorUtils';
 import {isValidRecoveryCode, isValidTwoFactorCode, isValidValidateCode} from '@libs/ValidationUtils';
 import ChangeExpensifyLoginLink from '@pages/signin/ChangeExpensifyLoginLink';
 import Terms from '@pages/signin/Terms';
-import ValidateCodeCountdown from '@pages/signin/ValidateCodeCountdown';
-import type {ValidateCodeCountdownHandle} from '@pages/signin/ValidateCodeCountdown/types';
 import {clearAccountMessages, clearSignInData as sessionActionsClearSignInData, signIn, signInWithValidateCode} from '@userActions/Session';
 import {resendValidateCode as userActionsResendValidateCode} from '@userActions/User';
 import CONST from '@src/CONST';
diff --git a/src/styles/index.ts b/src/styles/index.ts
index 7506658f0865..e8f94330d33d 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -3292,6 +3292,11 @@ const staticStyles = (theme: ThemeColors) =>
             height: 180,
         },
 
+        mfaBlockingViewAnimation: {
+            width: 180,
+            height: 180,
+        },
+
         locationErrorLinkText: {
             textAlignVertical: 'center',
             fontSize: variables.fontSizeLabel,
