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 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..c877b622ebef5 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,24 +58,40 @@ 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}); + allowNavigatingAwayRef.current = true; executeScenario(CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.AUTHORIZE_TRANSACTION, { transactionID, }); @@ -96,6 +113,8 @@ function MultifactorAuthenticationScenarioAuthorizeTransactionPage({route}: Mult const onSilentlyDenyTransaction = () => { addBreadcrumb('Silent deny (user canceled flow)', {transactionID}, 'warning'); fireAndForgetDenyTransaction({transactionID}); + setConfirmModalVisibility(false); + allowNavigatingAwayRef.current = true; Navigation.closeRHPFlow(); };