Skip to content
Merged
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
63 changes: 44 additions & 19 deletions src/pages/MultifactorAuthentication/ValidateCodePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import MagicCodeInput from '@components/MagicCodeInput';
import type {MagicCodeInputHandle} from '@components/MagicCodeInput';
import {DefaultCancelConfirmModal} from '@components/MultifactorAuthentication/components/Modals';
import {useMultifactorAuthentication, useMultifactorAuthenticationActions, useMultifactorAuthenticationState} from '@components/MultifactorAuthentication/Context';
import addMFABreadcrumb from '@components/MultifactorAuthentication/observability/breadcrumbs';
import MultifactorAuthenticationValidateCodeResendButton from '@components/MultifactorAuthentication/ValidateCodeResendButton';
import type {MultifactorAuthenticationValidateCodeResendButtonHandle} from '@components/MultifactorAuthentication/ValidateCodeResendButton';
import ScreenWrapper from '@components/ScreenWrapper';
Expand All @@ -17,12 +18,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 {clearValidateCodeActionError, requestValidateCodeAction} from '@userActions/User';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
Expand All @@ -40,7 +41,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
Expand All @@ -62,9 +63,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(() => {
Expand Down Expand Up @@ -124,16 +141,18 @@ 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'});
}
Comment on lines +144 to 147
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset validateCode when user starts correcting an invalid code

After an INVALID_VALIDATE_CODE continuable error, this handler now clears only continuableError and leaves state.validateCode unchanged. The MFA engine in Context/Main.tsx re-runs on state.validateCode changes, so if the user retries with the same code value (a common retry path after transient failures), SET_VALIDATE_CODE writes the same value and process() is not triggered, making Verify appear to do nothing. This is a regression from the previous behavior where the stored validate code was cleared before retry.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds plausible, but probably not a big deal?

Comment on lines 145 to 147
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reset stored validate code when retrying after code errors

When the user starts typing after an INVALID_VALIDATE_CODE, this block only clears continuableError but leaves state.validateCode unchanged. The MFA engine effect in Context/Main.tsx is keyed on state.validateCode (not continuableError), so if the user retries with the same code value, SET_VALIDATE_CODE does not change the dependency and process() will not run again. In practice, the Verify action can appear to do nothing until the input differs from the previous submitted code.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once the user enters a valid code, they are immediately redirected; if they enter the same code again, we don't want them to have to confirm twice that it's invalid.

};

const resendValidationCode = () => {
resendValidateCode(contactMethod);
if (hasValidateCodeActionError) {
clearValidateCodeActionError('actionVerified');
}
addMFABreadcrumb('Validate code resend requested');
requestValidateCodeAction();
inputRef.current?.clear();
setInputCode('');
setFormError({});
Expand Down Expand Up @@ -236,8 +255,6 @@ function MultifactorAuthenticationValidateCodePage() {
ref={inputRef}
maxLength={CONST.MAGIC_CODE_LENGTH}
/>
{hasContinuableError && <FormHelpMessage message={translate('validateCodeForm.error.incorrectMagicCode')} />}
{hasAccountError && <FormHelpMessage message={getLatestErrorMessage(account)} />}
<MultifactorAuthenticationValidateCodeResendButton
ref={resendButtonRef}
shouldDisableResendCode={shouldDisableResendCode}
Expand All @@ -246,15 +263,23 @@ function MultifactorAuthenticationValidateCodePage() {
onResendValidationCode={resendValidationCode}
/>
</View>
<Button
success
large
style={[styles.w100, styles.p5, styles.mtAuto]}
onPress={validateAndSubmitForm}
text={translate('common.verify')}
isLoading={isValidateCodeFormSubmitting}
isDisabled={isOffline}
/>
<View style={[styles.w100, styles.mtAuto]}>
{!!errorMessage && (
<FormHelpMessage
style={[styles.mh5]}
message={errorMessage}
/>
)}
<Button
success
large
style={[styles.w100, styles.ph5, styles.pb5, styles.mt4]}
onPress={validateAndSubmitForm}
text={translate('common.verify')}
isLoading={isValidateCodeFormSubmitting}
isDisabled={isOffline}
/>
</View>
<CancelConfirmModal
isVisible={isCancelModalVisible}
onConfirm={cancelFlow}
Expand Down
Loading