From be280d9347bd7bbc63d2ee1d45598edca5f46b5e Mon Sep 17 00:00:00 2001 From: Chuck Dries Date: Thu, 19 Mar 2026 16:46:42 -0700 Subject: [PATCH 1/3] Use useBeforeRemove in AuthorizeTransactionPage to prompt the user to decline the transaction before navigating away from 3DS challenge --- .../MultifactorAuthentication/index.ts | 16 ++++++++++++- .../AuthorizeTransactionPage/index.tsx | 24 ++++++++++++++++--- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/MultifactorAuthentication/index.ts b/src/libs/actions/MultifactorAuthentication/index.ts index b8ec1c6094b66..043091f3dbf41 100644 --- a/src/libs/actions/MultifactorAuthentication/index.ts +++ b/src/libs/actions/MultifactorAuthentication/index.ts @@ -335,7 +335,21 @@ async function denyTransaction({transactionID}: DenyTransactionParams) { /** Attempt to deny the transaction without handling errors or waiting for a response. We use this to clean up after something unexpected happened trying to authorize or deny a challenge */ async function fireAndForgetDenyTransaction({transactionID}: DenyTransactionParams) { - makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.DENY_TRANSACTION, {transactionID}, {}); + makeRequestWithSideEffects( + SIDE_EFFECT_REQUEST_COMMANDS.DENY_TRANSACTION, + {transactionID}, + { + optimisticData: [ + { + key: ONYXKEYS.LOCALLY_PROCESSED_3DS_TRANSACTION_REVIEWS, + onyxMethod: Onyx.METHOD.MERGE, + value: { + [transactionID]: CONST.MULTIFACTOR_AUTHENTICATION.LOCALLY_PROCESSED_TRANSACTION_ACTION.DENY, + }, + }, + ], + }, + ); } function markHasAcceptedSoftPrompt() { diff --git a/src/pages/MultifactorAuthentication/AuthorizeTransactionPage/index.tsx b/src/pages/MultifactorAuthentication/AuthorizeTransactionPage/index.tsx index ad3491a449043..e4243019464c4 100644 --- a/src/pages/MultifactorAuthentication/AuthorizeTransactionPage/index.tsx +++ b/src/pages/MultifactorAuthentication/AuthorizeTransactionPage/index.tsx @@ -1,6 +1,6 @@ import type {SeverityLevel} from '@sentry/react-native'; import * as Sentry from '@sentry/react-native'; -import React, {useState} from 'react'; +import React, {useCallback, useRef, useState} from 'react'; import {View} from 'react-native'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -13,6 +13,7 @@ import { } from '@components/MultifactorAuthentication/config/scenarios/AuthorizeTransaction'; import {useMultifactorAuthentication} from '@components/MultifactorAuthentication/Context'; import ScreenWrapper from '@components/ScreenWrapper'; +import useBeforeRemove from '@hooks/useBeforeRemove'; import useLocalize from '@hooks/useLocalize'; import useNetworkWithOfflineStatus from '@hooks/useNetworkWithOfflineStatus'; import useOnyx from '@hooks/useOnyx'; @@ -57,22 +58,37 @@ function MultifactorAuthenticationScenarioAuthorizeTransactionPage({route}: Mult const {executeScenario} = useMultifactorAuthentication(); const [isConfirmModalVisible, setConfirmModalVisibility] = useState(false); + const allowNavigatingAwayRef = useRef(false); - const showConfirmModal = () => { + const showConfirmModal = useCallback(() => { // FullPageOfflineBlockingView doesn't wrap HeaderWithBackButton, so we handle navigation manually when offline. // Offline mode isn't supported in MFA; navigate users away immediately without showing the confirmation modal. if (isOffline) { addBreadcrumb('Offline back-navigation (no deny sent)', {transactionID}, 'warning'); + allowNavigatingAwayRef.current = true; Navigation.closeRHPFlow(); return; } setConfirmModalVisibility(true); - }; + }, [isOffline, transactionID]); const hideConfirmModal = () => { setConfirmModalVisibility(false); }; + const onBeforeRemove: Parameters[0] = useCallback( + (e) => { + if (allowNavigatingAwayRef.current) { + return; + } + e.preventDefault(); + showConfirmModal(); + }, + [showConfirmModal], + ); + + useBeforeRemove(onBeforeRemove, !!transaction && !denyOutcomeScreen); + const onApproveTransaction = () => { addBreadcrumb('Approve tapped', {transactionID}); executeScenario(CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.AUTHORIZE_TRANSACTION, { @@ -96,6 +112,8 @@ function MultifactorAuthenticationScenarioAuthorizeTransactionPage({route}: Mult const onSilentlyDenyTransaction = () => { addBreadcrumb('Silent deny (user canceled flow)', {transactionID}, 'warning'); fireAndForgetDenyTransaction({transactionID}); + setConfirmModalVisibility(false); + allowNavigatingAwayRef.current = true; Navigation.closeRHPFlow(); }; From c337390a0492620c164fc4eaed56216444cb74e0 Mon Sep 17 00:00:00 2001 From: Chuck Dries Date: Thu, 19 Mar 2026 16:48:44 -0700 Subject: [PATCH 2/3] Enable passkeys for AuthorizeTransaction scenario --- .../config/scenarios/AuthorizeTransaction.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MultifactorAuthentication/config/scenarios/AuthorizeTransaction.tsx b/src/components/MultifactorAuthentication/config/scenarios/AuthorizeTransaction.tsx index 3c0b40723c541..a078fccd90381 100644 --- a/src/components/MultifactorAuthentication/config/scenarios/AuthorizeTransaction.tsx +++ b/src/components/MultifactorAuthentication/config/scenarios/AuthorizeTransaction.tsx @@ -124,7 +124,7 @@ export { export default { // Allowed methods are hardcoded here; keep in sync with allowedAuthenticationMethods in useNavigateTo3DSAuthorizationChallenge. - allowedAuthenticationMethods: [CONST.MULTIFACTOR_AUTHENTICATION.TYPE.BIOMETRICS], + allowedAuthenticationMethods: [CONST.MULTIFACTOR_AUTHENTICATION.TYPE.BIOMETRICS, CONST.MULTIFACTOR_AUTHENTICATION.TYPE.PASSKEYS], action: authorizeTransaction, // AuthorizeTransaction's callback navigates to the outcome screen, but if it knows the user is going to see an error outcome, we explicitly deny the transaction to make sure the user can't re-approve it on another device From 625a6917d1e7f961126aa0023445437e94ae07bd Mon Sep 17 00:00:00 2001 From: Chuck Dries Date: Fri, 20 Mar 2026 14:39:49 -0700 Subject: [PATCH 3/3] Do not prevent navigation away from AuthorizeTransactionPage after user approves transaction --- .../MultifactorAuthentication/AuthorizeTransactionPage/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/MultifactorAuthentication/AuthorizeTransactionPage/index.tsx b/src/pages/MultifactorAuthentication/AuthorizeTransactionPage/index.tsx index e4243019464c4..c877b622ebef5 100644 --- a/src/pages/MultifactorAuthentication/AuthorizeTransactionPage/index.tsx +++ b/src/pages/MultifactorAuthentication/AuthorizeTransactionPage/index.tsx @@ -91,6 +91,7 @@ function MultifactorAuthenticationScenarioAuthorizeTransactionPage({route}: Mult const onApproveTransaction = () => { addBreadcrumb('Approve tapped', {transactionID}); + allowNavigatingAwayRef.current = true; executeScenario(CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.AUTHORIZE_TRANSACTION, { transactionID, });