Skip to content

Commit cc404f8

Browse files
authored
Merge pull request Expensify#86002 from Expensify/chuckdries/3ds-rhp-click-outside-handling
[ECUK in-app 3DS] Enable passkeys for 3DS challenges and show prompt before allowing user to close RHP
2 parents f456efe + 625a691 commit cc404f8

3 files changed

Lines changed: 38 additions & 5 deletions

File tree

src/components/MultifactorAuthentication/config/scenarios/AuthorizeTransaction.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export {
124124

125125
export default {
126126
// Allowed methods are hardcoded here; keep in sync with allowedAuthenticationMethods in useNavigateTo3DSAuthorizationChallenge.
127-
allowedAuthenticationMethods: [CONST.MULTIFACTOR_AUTHENTICATION.TYPE.BIOMETRICS],
127+
allowedAuthenticationMethods: [CONST.MULTIFACTOR_AUTHENTICATION.TYPE.BIOMETRICS, CONST.MULTIFACTOR_AUTHENTICATION.TYPE.PASSKEYS],
128128
action: authorizeTransaction,
129129

130130
// 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

src/libs/actions/MultifactorAuthentication/index.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,21 @@ async function denyTransaction({transactionID}: DenyTransactionParams) {
335335

336336
/** 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 */
337337
async function fireAndForgetDenyTransaction({transactionID}: DenyTransactionParams) {
338-
makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.DENY_TRANSACTION, {transactionID}, {});
338+
makeRequestWithSideEffects(
339+
SIDE_EFFECT_REQUEST_COMMANDS.DENY_TRANSACTION,
340+
{transactionID},
341+
{
342+
optimisticData: [
343+
{
344+
key: ONYXKEYS.LOCALLY_PROCESSED_3DS_TRANSACTION_REVIEWS,
345+
onyxMethod: Onyx.METHOD.MERGE,
346+
value: {
347+
[transactionID]: CONST.MULTIFACTOR_AUTHENTICATION.LOCALLY_PROCESSED_TRANSACTION_ACTION.DENY,
348+
},
349+
},
350+
],
351+
},
352+
);
339353
}
340354

341355
function markHasAcceptedSoftPrompt() {

src/pages/MultifactorAuthentication/AuthorizeTransactionPage/index.tsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type {SeverityLevel} from '@sentry/react-native';
22
import * as Sentry from '@sentry/react-native';
3-
import React, {useState} from 'react';
3+
import React, {useCallback, useRef, useState} from 'react';
44
import {View} from 'react-native';
55
import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView';
66
import HeaderWithBackButton from '@components/HeaderWithBackButton';
@@ -13,6 +13,7 @@ import {
1313
} from '@components/MultifactorAuthentication/config/scenarios/AuthorizeTransaction';
1414
import {useMultifactorAuthentication} from '@components/MultifactorAuthentication/Context';
1515
import ScreenWrapper from '@components/ScreenWrapper';
16+
import useBeforeRemove from '@hooks/useBeforeRemove';
1617
import useLocalize from '@hooks/useLocalize';
1718
import useNetworkWithOfflineStatus from '@hooks/useNetworkWithOfflineStatus';
1819
import useOnyx from '@hooks/useOnyx';
@@ -57,24 +58,40 @@ function MultifactorAuthenticationScenarioAuthorizeTransactionPage({route}: Mult
5758
const {executeScenario} = useMultifactorAuthentication();
5859

5960
const [isConfirmModalVisible, setConfirmModalVisibility] = useState(false);
61+
const allowNavigatingAwayRef = useRef(false);
6062

61-
const showConfirmModal = () => {
63+
const showConfirmModal = useCallback(() => {
6264
// FullPageOfflineBlockingView doesn't wrap HeaderWithBackButton, so we handle navigation manually when offline.
6365
// Offline mode isn't supported in MFA; navigate users away immediately without showing the confirmation modal.
6466
if (isOffline) {
6567
addBreadcrumb('Offline back-navigation (no deny sent)', {transactionID}, 'warning');
68+
allowNavigatingAwayRef.current = true;
6669
Navigation.closeRHPFlow();
6770
return;
6871
}
6972
setConfirmModalVisibility(true);
70-
};
73+
}, [isOffline, transactionID]);
7174

7275
const hideConfirmModal = () => {
7376
setConfirmModalVisibility(false);
7477
};
7578

79+
const onBeforeRemove: Parameters<typeof useBeforeRemove>[0] = useCallback(
80+
(e) => {
81+
if (allowNavigatingAwayRef.current) {
82+
return;
83+
}
84+
e.preventDefault();
85+
showConfirmModal();
86+
},
87+
[showConfirmModal],
88+
);
89+
90+
useBeforeRemove(onBeforeRemove, !!transaction && !denyOutcomeScreen);
91+
7692
const onApproveTransaction = () => {
7793
addBreadcrumb('Approve tapped', {transactionID});
94+
allowNavigatingAwayRef.current = true;
7895
executeScenario(CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.AUTHORIZE_TRANSACTION, {
7996
transactionID,
8097
});
@@ -96,6 +113,8 @@ function MultifactorAuthenticationScenarioAuthorizeTransactionPage({route}: Mult
96113
const onSilentlyDenyTransaction = () => {
97114
addBreadcrumb('Silent deny (user canceled flow)', {transactionID}, 'warning');
98115
fireAndForgetDenyTransaction({transactionID});
116+
setConfirmModalVisibility(false);
117+
allowNavigatingAwayRef.current = true;
99118
Navigation.closeRHPFlow();
100119
};
101120

0 commit comments

Comments
 (0)