From 3355ed173ada74f60a76ae2d0970c5ad74cdfd67 Mon Sep 17 00:00:00 2001 From: Dariusz Biela Date: Wed, 25 Mar 2026 14:59:53 +0100 Subject: [PATCH 01/10] fix: prevent unwanted validate code resend and display rate limit errors Remove SET_VALIDATE_CODE(undefined) dispatch from onCodeInput so that typing after an invalid code no longer re-triggers process() and sends an unwanted email. Resend now only happens on explicit button press. Switch requestValidateCodeAction to makeRequestWithSideEffects so Main.tsx can detect send failures (e.g. rate limit) and log them. Display backend errors from VALIDATE_ACTION_CODE.errorFields on ValidateCodePage via FormHelpMessage. Unify resend button to use requestValidateCodeAction instead of the session-based resendValidateCode. --- .../MultifactorAuthentication/Context/Main.tsx | 5 ++++- src/libs/API/types.ts | 2 ++ src/libs/actions/User.ts | 3 ++- .../ValidateCodePage.tsx | 15 ++++++++------- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/components/MultifactorAuthentication/Context/Main.tsx b/src/components/MultifactorAuthentication/Context/Main.tsx index d8dda545c203b..a7902f5b41d54 100644 --- a/src/components/MultifactorAuthentication/Context/Main.tsx +++ b/src/components/MultifactorAuthentication/Context/Main.tsx @@ -214,7 +214,10 @@ function MultifactorAuthenticationContextProvider({children}: MultifactorAuthent // Need validate code before registration if (!validateCode) { addMFABreadcrumb('Validate code requested'); - requestValidateCodeAction(); + const response = await requestValidateCodeAction(); + if (response?.jsonCode !== CONST.JSON_CODE.SUCCESS) { + addMFABreadcrumb('Validate code request failed', {jsonCode: response?.jsonCode, message: response?.message}, 'error'); + } Navigation.navigate(ROUTES.MULTIFACTOR_AUTHENTICATION_MAGIC_CODE, {forceReplace: true}); return; } diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index f630ee64c9245..ccb530f36cf99 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -1372,6 +1372,7 @@ const SIDE_EFFECT_REQUEST_COMMANDS = { GET_TRANSACTIONS_PENDING_3DS_REVIEW: 'GetTransactionsPending3DSReview', REVEAL_CARD_PIN: 'RevealCardPIN', CHANGE_CARD_PIN: 'ChangeCardPIN', + RESEND_VALIDATE_CODE: 'ResendValidateCode', } as const; type SideEffectRequestCommand = ValueOf; @@ -1410,6 +1411,7 @@ type SideEffectRequestCommandParameters = { [SIDE_EFFECT_REQUEST_COMMANDS.GET_TRANSACTIONS_PENDING_3DS_REVIEW]: null; [SIDE_EFFECT_REQUEST_COMMANDS.REVEAL_CARD_PIN]: Parameters.RevealCardPINParams; [SIDE_EFFECT_REQUEST_COMMANDS.CHANGE_CARD_PIN]: Parameters.ChangeCardPINParams; + [SIDE_EFFECT_REQUEST_COMMANDS.RESEND_VALIDATE_CODE]: null; }; type ApiRequestCommandParameters = WriteCommandParameters & ReadCommandParameters & SideEffectRequestCommandParameters; diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 5775ba05bbedf..6426c2270e793 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -498,7 +498,8 @@ function requestValidateCodeAction() { }, ]; - API.write(WRITE_COMMANDS.RESEND_VALIDATE_CODE, null, {optimisticData, successData, failureData}); + // eslint-disable-next-line rulesdir/no-api-side-effects-method + return API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.RESEND_VALIDATE_CODE, null, {optimisticData, successData, failureData}); } /** diff --git a/src/pages/MultifactorAuthentication/ValidateCodePage.tsx b/src/pages/MultifactorAuthentication/ValidateCodePage.tsx index 2d2e9b2675a3a..c35d839e1a832 100644 --- a/src/pages/MultifactorAuthentication/ValidateCodePage.tsx +++ b/src/pages/MultifactorAuthentication/ValidateCodePage.tsx @@ -17,12 +17,12 @@ 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 {getLatestErrorField, getLatestErrorMessage} from '@libs/ErrorUtils'; import VALUES from '@libs/MultifactorAuthentication/VALUES'; import {isValidValidateCode} from '@libs/ValidationUtils'; import Navigation from '@navigation/Navigation'; import {clearAccountMessages} from '@userActions/Session'; -import {resendValidateCode} from '@userActions/User'; +import {requestValidateCodeAction} from '@userActions/User'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -40,7 +40,7 @@ function MultifactorAuthenticationValidateCodePage() { // Onyx data const [account] = useOnyx(ONYXKEYS.ACCOUNT); const [session] = useOnyx(ONYXKEYS.SESSION); - + const [validateActionCode] = useOnyx(ONYXKEYS.VALIDATE_ACTION_CODE); const contactMethod = account?.primaryLogin ?? ''; // Local state @@ -65,6 +65,8 @@ function MultifactorAuthenticationValidateCodePage() { const hasError = hasAccountError || hasContinuableError; const isValidateCodeFormSubmitting = AccountUtils.isValidateCodeFormSubmitting(account); const shouldDisableResendCode = isOffline ?? account?.isLoading; + const validateCodeActionError = getLatestErrorField(validateActionCode, 'actionVerified'); + const hasValidateCodeActionError = !isEmptyObject(validateCodeActionError); // Check if this page can handle the continuable error, if not convert to regular error useEffect(() => { @@ -124,16 +126,14 @@ function MultifactorAuthenticationValidateCodePage() { clearAccountMessages(); } - // Clear validateCode and continuable error when user starts typing after an error - // This ensures process() will re-trigger on submit even if user enters the same code + // Clear continuable error when user starts typing after an error if (continuableError) { - dispatch({type: 'SET_VALIDATE_CODE', payload: undefined}); dispatch({type: 'CLEAR_CONTINUABLE_ERROR'}); } }; const resendValidationCode = () => { - resendValidateCode(contactMethod); + requestValidateCodeAction(); inputRef.current?.clear(); setInputCode(''); setFormError({}); @@ -238,6 +238,7 @@ function MultifactorAuthenticationValidateCodePage() { /> {hasContinuableError && } {hasAccountError && } + {hasValidateCodeActionError && } Date: Wed, 25 Mar 2026 16:00:28 +0100 Subject: [PATCH 02/10] fix: use non-blocking .then() for validate code request logging Avoid awaiting requestValidateCodeAction() before navigating to the magic code page. The response is only used for breadcrumb logging, while actual error display relies on Onyx failureData. This removes the unnecessary delay before navigation. --- src/components/MultifactorAuthentication/Context/Main.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/MultifactorAuthentication/Context/Main.tsx b/src/components/MultifactorAuthentication/Context/Main.tsx index 4c14588066632..bbaf38f3b556a 100644 --- a/src/components/MultifactorAuthentication/Context/Main.tsx +++ b/src/components/MultifactorAuthentication/Context/Main.tsx @@ -214,10 +214,12 @@ function MultifactorAuthenticationContextProvider({children}: MultifactorAuthent // Need validate code before registration if (!validateCode) { addMFABreadcrumb('Validate code requested'); - const response = await requestValidateCodeAction(); - if (response?.jsonCode !== CONST.JSON_CODE.SUCCESS) { + requestValidateCodeAction().then((response) => { + if (response?.jsonCode === CONST.JSON_CODE.SUCCESS) { + return; + } addMFABreadcrumb('Validate code request failed', {jsonCode: response?.jsonCode, message: response?.message}, 'error'); - } + }); Navigation.navigate(ROUTES.MULTIFACTOR_AUTHENTICATION_MAGIC_CODE, {forceReplace: true}); return; } From 732b15c67507ba1d25ff43778e0ea576ed023e19 Mon Sep 17 00:00:00 2001 From: Dariusz Biela Date: Wed, 25 Mar 2026 16:05:20 +0100 Subject: [PATCH 03/10] fix: clear validate code action error on resend button press --- src/pages/MultifactorAuthentication/ValidateCodePage.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/MultifactorAuthentication/ValidateCodePage.tsx b/src/pages/MultifactorAuthentication/ValidateCodePage.tsx index c35d839e1a832..1e29181bbe575 100644 --- a/src/pages/MultifactorAuthentication/ValidateCodePage.tsx +++ b/src/pages/MultifactorAuthentication/ValidateCodePage.tsx @@ -22,7 +22,7 @@ import VALUES from '@libs/MultifactorAuthentication/VALUES'; import {isValidValidateCode} from '@libs/ValidationUtils'; import Navigation from '@navigation/Navigation'; import {clearAccountMessages} from '@userActions/Session'; -import {requestValidateCodeAction} from '@userActions/User'; +import {clearValidateCodeActionError, requestValidateCodeAction} from '@userActions/User'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -133,6 +133,9 @@ function MultifactorAuthenticationValidateCodePage() { }; const resendValidationCode = () => { + if (hasValidateCodeActionError) { + clearValidateCodeActionError('actionVerified'); + } requestValidateCodeAction(); inputRef.current?.clear(); setInputCode(''); From 5eae9ae9d3a1bb666e51e57a6e7f151d1b2ac293 Mon Sep 17 00:00:00 2001 From: Dariusz Biela Date: Wed, 25 Mar 2026 21:23:46 +0100 Subject: [PATCH 04/10] fix: display single error above submit button using OfflineWithFeedback Replace multiple FormHelpMessage components with a single OfflineWithFeedback wrapper around the Verify button. Errors now display directly above the submit button with priority: rate limit > invalid code > account error. This matches the pattern used in BaseValidateCodeForm and prevents duplicate error messages from appearing simultaneously. --- .../ValidateCodePage.tsx | 46 +++++++++++++------ 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/src/pages/MultifactorAuthentication/ValidateCodePage.tsx b/src/pages/MultifactorAuthentication/ValidateCodePage.tsx index 1e29181bbe575..d697c5524f1ee 100644 --- a/src/pages/MultifactorAuthentication/ValidateCodePage.tsx +++ b/src/pages/MultifactorAuthentication/ValidateCodePage.tsx @@ -2,7 +2,6 @@ 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'; @@ -10,6 +9,7 @@ import {DefaultCancelConfirmModal} from '@components/MultifactorAuthentication/c import {useMultifactorAuthentication, useMultifactorAuthenticationActions, useMultifactorAuthenticationState} from '@components/MultifactorAuthentication/Context'; import MultifactorAuthenticationValidateCodeResendButton from '@components/MultifactorAuthentication/ValidateCodeResendButton'; import type {MultifactorAuthenticationValidateCodeResendButtonHandle} from '@components/MultifactorAuthentication/ValidateCodeResendButton'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; @@ -62,11 +62,25 @@ function MultifactorAuthenticationValidateCodePage() { // Derived state const hasAccountError = !!account && !isEmptyObject(account?.errors); const hasContinuableError = !!continuableError; - const hasError = hasAccountError || hasContinuableError; const isValidateCodeFormSubmitting = AccountUtils.isValidateCodeFormSubmitting(account); const shouldDisableResendCode = isOffline ?? account?.isLoading; const validateCodeActionError = getLatestErrorField(validateActionCode, 'actionVerified'); const hasValidateCodeActionError = !isEmptyObject(validateCodeActionError); + const hasError = hasAccountError || hasContinuableError || hasValidateCodeActionError; + const errorMessage = getErrorMessage(); + + function getErrorMessage() { + // Rate limit or other backend error when sending/resending the validate code + if (hasValidateCodeActionError) { + return Object.values(validateCodeActionError ?? {}).at(0); + } + // Invalid validate code submitted by the user + if (hasContinuableError) { + return translate('validateCodeForm.error.incorrectMagicCode'); + } + // Generic account/session error (e.g. stale errors from a previous flow) + return getLatestErrorMessage(account); + } // Check if this page can handle the continuable error, if not convert to regular error useEffect(() => { @@ -239,9 +253,6 @@ function MultifactorAuthenticationValidateCodePage() { ref={inputRef} maxLength={CONST.MAGIC_CODE_LENGTH} /> - {hasContinuableError && } - {hasAccountError && } - {hasValidateCodeActionError && } -