Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/components/AddToWalletButton/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import getPlatform from '@libs/getPlatform';
import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
import {handleAddCardToWallet} from '@libs/Wallet/index';
import {walletDebugLog} from '@libs/Wallet/walletDebugLog';

import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
Expand All @@ -32,6 +33,7 @@ function AddToWalletButton({card, cardHolderName, cardDescription, style}: AddTo
const {isInWallet, isLoading: isCardLoading, isCardAvailable, isWalletAvailable} = useIsCardInWallet(card);

const handleOnPress = () => {
walletDebugLog('Add to wallet pressed', false, {cardID: card.cardID, isUkEuCard: isUkEuExpensifyCard(card), isLoading, isCardLoading});
if (isUkEuExpensifyCard(card)) {
executeScenario(CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.ADD_TO_DIGITAL_WALLET, {card, cardHolderName, cardDescription});
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {isHttpSuccess} from '@libs/MultifactorAuthentication/shared/helpers';
import {createLocalMFAError, createMFAErrorFromApiResponse} from '@libs/MultifactorAuthentication/shared/MFAResult';
import type {MultifactorAuthenticationCallbackInput} from '@libs/MultifactorAuthentication/shared/types';
import Navigation from '@libs/Navigation/Navigation';
import {getWalletDebugStack, walletDebugCapture, walletDebugLog, walletDebugState} from '@libs/Wallet/walletDebugLog';

import {clearLocalMFAPublicKeyList, getDeviceBiometricsOnyxKey, requestAuthorizationChallenge, requestRegistrationChallenge} from '@userActions/MultifactorAuthentication';
import {processRegistration, processScenarioAction} from '@userActions/MultifactorAuthentication/processing';
Expand Down Expand Up @@ -67,9 +68,12 @@ function MultifactorAuthenticationContextProvider({children}: MultifactorAuthent
const state = useMultifactorAuthenticationState();
const {dispatch} = useMultifactorAuthenticationActions();
const actionControllerRef = useRef<AbortController | undefined>(undefined);
const debugProcessRunCountRef = useRef(0);
const debugAuthorizeCountRef = useRef(0);

useEffect(
() => () => {
walletDebugLog('MFA scenario-change cleanup', false, {hadActionController: !!actionControllerRef.current});
actionControllerRef.current?.abort();
actionControllerRef.current = undefined;
},
Expand Down Expand Up @@ -193,6 +197,23 @@ function MultifactorAuthenticationContextProvider({children}: MultifactorAuthent
isFlowComplete,
} = state;

debugProcessRunCountRef.current++;
walletDebugLog('MFA process run', false, {
debugProcessRunID: debugProcessRunCountRef.current,
hasScenario: !!scenario,
errorReason: error?.reason,
hasContinuableError: !!continuableError,
hasValidateCode: !!validateCode,
hasRegistrationChallenge: !!registrationChallenge,
hasAuthorizationChallenge: !!authorizationChallenge,
softPromptApproved,
isRegistrationComplete,
isAuthorizationComplete,
isFlowComplete,
isOffline,
hadActionController: !!actionControllerRef.current,
});

// 0. Check if one of the early exit conditions applies:
// - Flow is already complete,
// - User is offline,
Expand All @@ -215,6 +236,9 @@ function MultifactorAuthenticationContextProvider({children}: MultifactorAuthent
}

addMFABreadcrumb('Flow error', error, 'error');
const flowErrorParams = {reason: error.reason, message: error.message, hadActionController: !!actionControllerRef.current};
walletDebugLog('MFA flow error', true, flowErrorParams);
walletDebugCapture(`MFA flow error: ${error.reason}`, flowErrorParams);
handleCallback(false);
return;
}
Expand Down Expand Up @@ -335,11 +359,20 @@ function MultifactorAuthenticationContextProvider({children}: MultifactorAuthent
return;
}

debugAuthorizeCountRef.current++;
const debugAuthorizeID = debugAuthorizeCountRef.current;
walletDebugLog('MFA authorize start', false, {debugAuthorizeID, hadActionController: !!actionControllerRef.current});
await biometrics.authorize(
{
challenge: authorizationChallenge,
},
async (result: AuthorizeResult) => {
walletDebugLog('MFA authorize result', false, {
debugAuthorizeID,
success: result.success,
reason: result.success ? undefined : result.error.reason,
authMethod: result.success ? result.authenticationMethod.code : undefined,
});
addMFABreadcrumb(
'Biometric authorization completed',
result.success ? {success: true, authMethod: result.authenticationMethod.code} : result.error,
Expand Down Expand Up @@ -368,23 +401,43 @@ function MultifactorAuthenticationContextProvider({children}: MultifactorAuthent

// Call backend with signed challenge
const controller = new AbortController();
walletDebugLog('MFA scenario action dispatch', false, {debugAuthorizeID, replacedActionController: !!actionControllerRef.current});
actionControllerRef.current = controller;
const scenarioAPIResponse = await processScenarioAction(
walletDebugState.isDispatchingAction = true;
const scenarioActionPromise = processScenarioAction(
scenario.action,
{
signedChallenge: result.signedChallenge,
authenticationMethod: result.authenticationMethod.marqetaValue,
...payload,
},
{signal: controller.signal},
).catch((scenarioActionError: unknown) => {
);
walletDebugState.isDispatchingAction = false;
walletDebugLog('MFA scenario action dispatch returned', false, {debugAuthorizeID, stack: getWalletDebugStack()});
const scenarioAPIResponse = await scenarioActionPromise.catch((scenarioActionError: unknown) => {
walletDebugLog('MFA scenario action rejected', true, {
debugAuthorizeID,
isAborted: controller.signal.aborted,
isCurrentController: actionControllerRef.current === controller,
errorMessage: getErrorMessage(scenarioActionError),
});
if (controller.signal.aborted || actionControllerRef.current !== controller) {
return undefined;
}
actionControllerRef.current = undefined;
throw scenarioActionError;
});

walletDebugLog('MFA scenario action returned', true, {
debugAuthorizeID,
hasResponse: !!scenarioAPIResponse,
success: scenarioAPIResponse?.success,
reason: scenarioAPIResponse?.success ? scenarioAPIResponse.reason : scenarioAPIResponse?.error.reason,
isAborted: controller.signal.aborted,
isCurrentController: actionControllerRef.current === controller,
});

// A canceled action can finish after the next flow has already started.
if (!scenarioAPIResponse || controller.signal.aborted || actionControllerRef.current !== controller) {
return;
Expand Down Expand Up @@ -493,6 +546,7 @@ function MultifactorAuthenticationContextProvider({children}: MultifactorAuthent
// latest state and drops the duplicate). This check exists only as a perf short-circuit
// so the second tap doesn't pay for an extra captureCredentialsState() native call
// and Sentry breadcrumb on the happy path.
walletDebugLog('MFA executeScenario called', false, {scenario, isAlreadyRunning: !!state.scenario, hadActionController: !!actionControllerRef.current});
if (state.scenario) {
return;
}
Expand Down Expand Up @@ -531,6 +585,7 @@ function MultifactorAuthenticationContextProvider({children}: MultifactorAuthent
* close the modal directly instead to avoid getting stuck.
*/
const cancel = useCallback(async () => {
walletDebugLog('MFA cancel', false, {hasScenario: !!state.scenario, hadActionController: !!actionControllerRef.current});
actionControllerRef.current?.abort();
if (!state.scenario || isOffline) {
addMFABreadcrumb('Flow cancelled - closing directly', {hasScenario: !!state.scenario, isOffline}, 'warning');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {getPaymentMethods} from '@libs/actions/PaymentMethods';
import getPlatform from '@libs/getPlatform';
import Navigation from '@libs/Navigation/Navigation';
import {handleAddCardToWallet} from '@libs/Wallet';
import {getWalletDebugStack, walletDebugLog, walletDebugState} from '@libs/Wallet/walletDebugLog';
import WalletProvisioningError from '@libs/Wallet/WalletProvisioningError';

import CONST from '@src/CONST';
Expand All @@ -19,12 +20,42 @@ type Payload = {

export default {
allowedAuthenticationMethods: [CONST.MULTIFACTOR_AUTHENTICATION.TYPE.BIOMETRICS_HSM],
action: async ({card, cardHolderName, cardDescription, signedChallenge, authenticationMethod}, context) => {
// Hermes V1 does not suspend at await inside async arrows with destructured parameters, so destructure in the body.
action: async (params, context) => {
const {card, cardHolderName, cardDescription, signedChallenge, authenticationMethod} = params;
walletDebugLog('scenario action start', false, {
cardID: card.cardID,
hasSignedChallenge: !!signedChallenge,
authenticationMethod,
hasContext: !!context,
isAborted: !!context?.signal.aborted,
isDispatchingAction: walletDebugState.isDispatchingAction,
});
try {
const status = await handleAddCardToWallet(card, cardHolderName, cardDescription, {
const walletPromise = handleAddCardToWallet(card, cardHolderName, cardDescription, {
authentication: {signedChallenge, authenticationMethod},
signal: context?.signal,
});
walletDebugLog('handleAddCardToWallet returned', false, {
cardID: card.cardID,
valueType: typeof walletPromise,
isPromise: walletPromise instanceof Promise,
hasThen: typeof (walletPromise as Partial<Promise<unknown>> | undefined)?.then === 'function',
isDispatchingAction: walletDebugState.isDispatchingAction,
});
walletPromise.then(
(value) => walletDebugLog('handleAddCardToWallet promise fulfilled', false, {cardID: card.cardID, valueType: typeof value, value: String(value)}),
(error: unknown) => walletDebugLog('handleAddCardToWallet promise rejected', false, {cardID: card.cardID, errorType: error instanceof Error ? error.name : typeof error}),
);
const status = await walletPromise;
walletDebugLog('scenario action got status', false, {
cardID: card.cardID,
status: String(status),
statusType: typeof status,
isAborted: !!context?.signal.aborted,
isDispatchingAction: walletDebugState.isDispatchingAction,
stack: getWalletDebugStack(),
});
if (status === 'success' && !context?.signal.aborted) {
return {httpStatusCode: 200, reason: undefined, message: undefined};
}
Expand All @@ -37,6 +68,13 @@ export default {
message: undefined,
};
} catch (error) {
walletDebugLog('scenario action caught', false, {
cardID: card.cardID,
isProvisioningError: error instanceof WalletProvisioningError,
reason: error instanceof WalletProvisioningError ? error.response.reason : undefined,
errorType: error instanceof Error ? error.name : typeof error,
isAborted: !!context?.signal.aborted,
});
if (error instanceof WalletProvisioningError) {
return error.response;
}
Expand Down
Loading
Loading