diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 38e1b25c6a9f..ae424230a191 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -105,7 +105,7 @@ const ONYXKEYS = { PENDING_CONTACT_ACTION: 'pendingContactAction', /** Store the information of magic code */ - VALIDATE_ACTION_CODE: 'validate_action_code', + VALIDATE_ACTION_CODE: 'validateActionCode', /** A list of policies that a user can join */ JOINABLE_POLICIES: 'joinablePolicies', diff --git a/src/components/ValidateCodeActionForm/index.tsx b/src/components/ValidateCodeActionForm/index.tsx index 75aa127f26dd..88a5ba5d614d 100644 --- a/src/components/ValidateCodeActionForm/index.tsx +++ b/src/components/ValidateCodeActionForm/index.tsx @@ -1,11 +1,9 @@ import React, {forwardRef, useEffect, useRef} from 'react'; import {View} from 'react-native'; -import {useOnyx} from 'react-native-onyx'; import Text from '@components/Text'; import ValidateCodeForm from '@components/ValidateCodeActionModal/ValidateCodeForm'; import type {ValidateCodeFormHandle} from '@components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm'; import useThemeStyles from '@hooks/useThemeStyles'; -import ONYXKEYS from '@src/ONYXKEYS'; import type {ValidateCodeActionFormProps} from './type'; function ValidateCodeActionForm({ @@ -15,19 +13,16 @@ function ValidateCodeActionForm({ descriptionSecondaryStyles, validatePendingAction, validateError, + hasMagicCodeBeenSent, handleSubmitForm, clearError, sendValidateCode, - hasMagicCodeBeenSent, isLoading, submitButtonText, forwardedRef, shouldSkipInitialValidation, }: ValidateCodeActionFormProps) { const themeStyles = useThemeStyles(); - - const [validateCodeAction] = useOnyx(ONYXKEYS.VALIDATE_ACTION_CODE, {canBeMissing: true}); - const isUnmounted = useRef(false); useEffect(() => { @@ -56,15 +51,15 @@ function ValidateCodeActionForm({ {!!descriptionSecondary && {descriptionSecondary}} diff --git a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx index cc75dd9e6440..f5046898d9a8 100644 --- a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -23,7 +23,6 @@ import {clearValidateCodeActionError} from '@userActions/User'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {ValidateMagicCodeAction} from '@src/types/onyx'; import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -37,20 +36,25 @@ type ValidateCodeFormError = { }; type ValidateCodeFormProps = { - /** If the magic code has been resent previously */ - hasMagicCodeBeenSent?: boolean; - /** Specifies autocomplete hints for the system, so it can provide autofill */ autoComplete?: AutoCompleteVariant; /** Forwarded inner ref */ innerRef?: ForwardedRef; - /** The state of magic code that being sent */ - validateCodeAction?: ValidateMagicCodeAction; + hasMagicCodeBeenSent?: boolean; - /** The pending action for submitting form */ - validatePendingAction?: PendingAction | null; + /** The pending action of magic code being sent + * if not supplied, we will retrieve it from the validateCodeAction above: `validateCodeAction.pendingFields.validateCodeSent` + */ + validatePendingAction?: PendingAction; + + /** The field where any magic code error will be stored. e.g. if replacing a card and magic code fails, it'll be stored in: + * {"errorFields": {"replaceLostCard": {}}} + * If replacing a virtual card, the errorField wil be 'reportVirtualCard', etc. + * These values are set in the backend, please reach out to an internal engineer if you're adding a validate code modal to a flow. + */ + validateCodeActionErrorField: string; /** The error of submitting */ validateError?: Errors; @@ -87,10 +91,10 @@ type ValidateCodeFormProps = { }; function BaseValidateCodeForm({ - hasMagicCodeBeenSent, autoComplete = 'one-time-code', innerRef = () => {}, - validateCodeAction, + hasMagicCodeBeenSent, + validateCodeActionErrorField, validatePendingAction, validateError, handleSubmitForm, @@ -115,13 +119,15 @@ function BaseValidateCodeForm({ const [account = {}] = useOnyx(ONYXKEYS.ACCOUNT, { canBeMissing: true, }); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- nullish coalescing doesn't achieve the same result in this case const shouldDisableResendValidateCode = !!isOffline || account?.isLoading; const focusTimeoutRef = useRef(null); const [timeRemaining, setTimeRemaining] = useState(CONST.REQUEST_CODE_DELAY as number); const [canShowError, setCanShowError] = useState(false); - const latestActionVerifiedError = getLatestErrorField(validateCodeAction, 'actionVerified'); - + const [validateCodeAction] = useOnyx(ONYXKEYS.VALIDATE_ACTION_CODE, {canBeMissing: true}); + const validateCodeSent = useMemo(() => hasMagicCodeBeenSent ?? validateCodeAction?.validateCodeSent, [hasMagicCodeBeenSent, validateCodeAction?.validateCodeSent]); + const latestValidateCodeError = getLatestErrorField(validateCodeAction, validateCodeActionErrorField); const timerRef = useRef(); useImperativeHandle(innerRef, () => ({ @@ -169,11 +175,11 @@ function BaseValidateCodeForm({ ); useEffect(() => { - if (!hasMagicCodeBeenSent) { + if (!validateCodeSent) { return; } inputValidateCodeRef.current?.clear(); - }, [hasMagicCodeBeenSent]); + }, [validateCodeSent]); useEffect(() => { if (timeRemaining > 0) { @@ -203,18 +209,26 @@ function BaseValidateCodeForm({ setValidateCode(text); setFormError({}); - if (!isEmptyObject(validateError) || !isEmptyObject(latestActionVerifiedError)) { + if (!isEmptyObject(validateError) || !isEmptyObject(latestValidateCodeError)) { + // Clear flow specific error clearError(); - clearValidateCodeActionError('actionVerified'); + + // Clear "incorrect magic code" error + clearValidateCodeActionError(validateCodeActionErrorField); } }, - [validateError, clearError, latestActionVerifiedError], + [validateError, clearError, latestValidateCodeError, validateCodeActionErrorField], ); /** * Check that all the form fields are valid, then trigger the submit callback */ const validateAndSubmitForm = useCallback(() => { + // Clear flow specific error + clearError(); + + // Clear "incorrect magic" code error + clearValidateCodeActionError(validateCodeActionErrorField); setCanShowError(true); if (!validateCode.trim()) { setFormError({validateCode: 'validateCodeForm.error.pleaseFillMagicCode'}); @@ -228,7 +242,7 @@ function BaseValidateCodeForm({ setFormError({}); handleSubmitForm(validateCode); - }, [validateCode, handleSubmitForm]); + }, [validateCode, handleSubmitForm, validateCodeActionErrorField, clearError]); const errorText = useMemo(() => { if (!canShowError) { @@ -241,6 +255,10 @@ function BaseValidateCodeForm({ }, [canShowError, formError, account, translate]); const shouldShowTimer = timeRemaining > 0 && !isOffline; + + // latestValidateCodeError only holds an error related to bad magic code + // while validateError holds flow-specific errors + const finalValidateError = !isEmptyObject(latestValidateCodeError) ? latestValidateCodeError : validateError; return ( <> clearValidateCodeActionError('actionVerified')} + onClose={() => clearValidateCodeActionError(validateCodeActionErrorField)} > {!shouldShowTimer && ( @@ -284,7 +301,7 @@ function BaseValidateCodeForm({ )} - {!!hasMagicCodeBeenSent && ( + {!!validateCodeSent && ( clearError()} + onClose={() => { + clearError(); + if (!isEmptyObject(validateCodeAction?.errorFields) && validateCodeActionErrorField) { + clearValidateCodeActionError(validateCodeActionErrorField); + } + }} style={buttonStyles} > {shouldShowSkipButton && ( diff --git a/src/components/ValidateCodeActionModal/index.tsx b/src/components/ValidateCodeActionModal/index.tsx index 4dfa2099ebbc..13c8418ecb35 100644 --- a/src/components/ValidateCodeActionModal/index.tsx +++ b/src/components/ValidateCodeActionModal/index.tsx @@ -22,13 +22,13 @@ function ValidateCodeActionModal({ descriptionSecondary, onClose, onModalHide, - validatePendingAction, validateError, + validatePendingAction, + validateCodeActionErrorField, handleSubmitForm, clearError, footer, sendValidateCode, - hasMagicCodeBeenSent, isLoading, shouldHandleNavigationBack, disableAnimation, @@ -40,8 +40,7 @@ function ValidateCodeActionModal({ const validateCodeFormRef = useRef(null); const styles = useThemeStyles(); const threeDotsAnchorPosition = useThreeDotsAnchorPosition(styles.threeDotsPopoverOffset); - - const [validateCodeAction] = useOnyx(ONYXKEYS.VALIDATE_ACTION_CODE); + const [validateCodeAction] = useOnyx(ONYXKEYS.VALIDATE_ACTION_CODE, {canBeMissing: true}); const hide = useCallback(() => { clearError(); @@ -50,13 +49,13 @@ function ValidateCodeActionModal({ }, [onClose, clearError]); useEffect(() => { - if (!firstRenderRef.current || !isVisible || hasMagicCodeBeenSent) { + if (!firstRenderRef.current || !isVisible || validateCodeAction?.validateCodeSent) { return; } firstRenderRef.current = false; sendValidateCode(); - // We only want to send validate code on first render not on change of hasMagicCodeBeenSent, so we don't add it as a dependency. + // We only want to send validate code on first render not on change of validateCodeSent, so we don't add it as a dependency. // eslint-disable-next-line react-compiler/react-compiler // eslint-disable-next-line react-hooks/exhaustive-deps }, [isVisible, sendValidateCode]); @@ -102,15 +101,14 @@ function ValidateCodeActionModal({ {!!descriptionSecondary && {descriptionSecondary}} diff --git a/src/components/ValidateCodeActionModal/type.ts b/src/components/ValidateCodeActionModal/type.ts index b0ff887130cd..9aa4aa0c0427 100644 --- a/src/components/ValidateCodeActionModal/type.ts +++ b/src/components/ValidateCodeActionModal/type.ts @@ -21,12 +21,15 @@ type ValidateCodeActionModalProps = { /** Function to be called when the modal is closed */ onModalHide?: () => void; - /** The pending action for submitting form */ - validatePendingAction?: PendingAction | null; + /** The pending action we're trying to validate */ + validatePendingAction?: PendingAction; - /** The error of submitting */ + /** The error of submitting, this holds any error specific to the flow (e.g invalid reason when replacing a card) but NOT an incorrect magic code */ validateError?: Errors; + /** The errorField name of validateCodeAction.errorFields, e.g. "addLogin" to store the magic code error when adding a new contact method */ + validateCodeActionErrorField: string; + /** Function is called when submitting form */ handleSubmitForm: (validateCode: string) => void; @@ -39,9 +42,6 @@ type ValidateCodeActionModalProps = { /** Function is called when validate code modal is mounted and on magic code resend */ sendValidateCode: () => void; - /** If the magic code has been resent previously */ - hasMagicCodeBeenSent?: boolean; - /** Whether the form is loading or not */ isLoading?: boolean; diff --git a/src/languages/en.ts b/src/languages/en.ts index f64a3b67fa26..c9075b647114 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1950,7 +1950,7 @@ const translations = { requestNewCodeAfterErrorOccurred: 'Request a new code', error: { pleaseFillMagicCode: 'Please enter your magic code', - incorrectMagicCode: 'Incorrect magic code', + incorrectMagicCode: 'Incorrect or invalid magic code. Please try again or request a new code.', pleaseFillTwoFactorAuth: 'Please enter your two-factor authentication code', }, }, diff --git a/src/languages/es.ts b/src/languages/es.ts index b40eafc5281c..ad25134186fd 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1952,9 +1952,9 @@ const translations = { requestNewCode: 'Pedir un código nuevo en ', requestNewCodeAfterErrorOccurred: 'Solicitar un nuevo código', error: { - pleaseFillMagicCode: 'Por favor, introduce el código mágico', - incorrectMagicCode: 'Código mágico incorrecto', - pleaseFillTwoFactorAuth: 'Por favor, introduce tu código de autenticación de dos factores', + pleaseFillMagicCode: 'Por favor, introduce el código mágico.', + incorrectMagicCode: 'Código mágico incorrecto o no válido. Inténtalo de nuevo o solicita otro código.', + pleaseFillTwoFactorAuth: 'Por favor, introduce tu código de autenticación de dos factores.', }, }, passwordForm: { diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 8d6d327f6d28..7c340a042e2f 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -52,20 +52,6 @@ function reportVirtualExpensifyCardFraud(card: Card, validateCode: string) { errors: null, }, }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.CARD_LIST, - value: { - [cardID]: null, - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${card?.fundID}_${CONST.EXPENSIFY_CARD.BANK}`, - value: { - [cardID]: null, - }, - }, ]; const successData: OnyxUpdate[] = [ @@ -86,26 +72,6 @@ function reportVirtualExpensifyCardFraud(card: Card, validateCode: string) { isLoading: false, }, }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.CARD_LIST, - value: { - [cardID]: { - ...card, - errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), - }, - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${card?.fundID}_${CONST.EXPENSIFY_CARD.BANK}`, - value: { - [cardID]: { - ...card, - errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), - }, - }, - }, ]; const parameters: ReportVirtualExpensifyCardFraudParams = { diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index a4ac11e727bb..bf924a03b707 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -1350,7 +1350,7 @@ function MergeIntoAccountAndLogin(workEmail: string | undefined, validateCode: s ).then((response) => { if (response?.jsonCode === CONST.JSON_CODE.EXP_ERROR) { // If the error other than invalid code, we show a blocking screen - if (response?.message === CONST.MERGE_ACCOUNT_INVALID_CODE_ERROR) { + if (response?.message === CONST.MERGE_ACCOUNT_INVALID_CODE_ERROR || response?.title === CONST.MERGE_ACCOUNT_INVALID_CODE_ERROR) { Onyx.merge(ONYXKEYS.ONBOARDING_ERROR_MESSAGE, translateLocal('contacts.genericFailureMessages.validateSecondaryLogin')); } else { Onyx.merge(ONYXKEYS.NVP_ONBOARDING, {isMergingAccountBlocked: true}); diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 59ab12b44d6f..8ea01da4572f 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -335,6 +335,12 @@ function clearUnvalidatedNewContactMethodAction() { Onyx.merge(ONYXKEYS.PENDING_CONTACT_ACTION, null); } +function clearPendingContactActionErrors() { + Onyx.merge(ONYXKEYS.PENDING_CONTACT_ACTION, { + errorFields: null, + }); +} + /** * When user adds a new contact method, they need to verify the magic code first * So we add the temporary contact method to Onyx to use it later, after user verified magic code. @@ -345,76 +351,6 @@ function addPendingContactMethod(contactMethod: string) { }); } -/** - * Validates the action to add secondary contact method - */ -function saveNewContactMethodAndRequestValidationCode(contactMethod: string) { - const optimisticData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PENDING_CONTACT_ACTION, - value: { - contactMethod, - errorFields: { - actionVerified: null, - }, - pendingFields: { - actionVerified: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }, - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.NEW_CONTACT_METHOD_FORM, - value: {isLoading: true}, - }, - ]; - - const successData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PENDING_CONTACT_ACTION, - value: { - validateCodeSent: true, - errorFields: { - actionVerified: null, - }, - pendingFields: { - actionVerified: null, - }, - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.NEW_CONTACT_METHOD_FORM, - value: {isLoading: false}, - }, - ]; - - const failureData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PENDING_CONTACT_ACTION, - value: { - validateCodeSent: null, - errorFields: { - actionVerified: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('contacts.genericFailureMessages.requestContactMethodValidateCode'), - }, - pendingFields: { - actionVerified: null, - }, - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.NEW_CONTACT_METHOD_FORM, - value: {isLoading: false}, - }, - ]; - - API.write(WRITE_COMMANDS.RESEND_VALIDATE_CODE, null, {optimisticData, successData, failureData}); -} - /** * Adds a secondary login to a user's account */ @@ -430,9 +366,6 @@ function addNewContactMethod(contactMethod: string, validateCode = '') { errorFields: { addedLogin: null, }, - pendingFields: { - addedLogin: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }, }, }, }, @@ -443,17 +376,6 @@ function addNewContactMethod(contactMethod: string, validateCode = '') { }, ]; const successData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.LOGIN_LIST, - value: { - [contactMethod]: { - pendingFields: { - addedLogin: null, - }, - }, - }, - }, { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.PENDING_CONTACT_ACTION, @@ -464,9 +386,6 @@ function addNewContactMethod(contactMethod: string, validateCode = '') { errorFields: { actionVerified: null, }, - pendingFields: { - actionVerified: null, - }, }, }, { @@ -476,20 +395,6 @@ function addNewContactMethod(contactMethod: string, validateCode = '') { }, ]; const failureData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.LOGIN_LIST, - value: { - [contactMethod]: { - errorFields: { - addedLogin: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('contacts.genericFailureMessages.addContactMethod'), - }, - pendingFields: { - addedLogin: null, - }, - }, - }, - }, { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.ACCOUNT, @@ -497,7 +402,7 @@ function addNewContactMethod(contactMethod: string, validateCode = '') { }, { onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PENDING_CONTACT_ACTION, + key: ONYXKEYS.VALIDATE_ACTION_CODE, value: {validateCodeSent: null}, }, ]; @@ -1512,8 +1417,8 @@ export { updateDraftCustomStatus, clearDraftCustomStatus, requestRefund, - saveNewContactMethodAndRequestValidationCode, clearUnvalidatedNewContactMethodAction, + clearPendingContactActionErrors, requestValidateCodeAction, addPendingContactMethod, clearValidateCodeActionError, diff --git a/src/pages/MissingPersonalDetails/MissingPersonalDetailsMagicCodeModal.tsx b/src/pages/MissingPersonalDetails/MissingPersonalDetailsMagicCodeModal.tsx index 057f2ff3cba9..ba5c727c4701 100644 --- a/src/pages/MissingPersonalDetails/MissingPersonalDetailsMagicCodeModal.tsx +++ b/src/pages/MissingPersonalDetails/MissingPersonalDetailsMagicCodeModal.tsx @@ -19,10 +19,10 @@ type MissingPersonalDetailsMagicCodeModalProps = { function MissingPersonalDetailsMagicCodeModal({onClose, isValidateCodeActionModalVisible, handleSubmitForm}: MissingPersonalDetailsMagicCodeModalProps) { const {translate} = useLocalize(); - const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS); - const [cardList] = useOnyx(ONYXKEYS.CARD_LIST); - const [account] = useOnyx(ONYXKEYS.ACCOUNT); - const [validateCodeAction] = useOnyx(ONYXKEYS.VALIDATE_ACTION_CODE); + const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true}); + const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {canBeMissing: true}); + const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); + const [validateCodeAction] = useOnyx(ONYXKEYS.VALIDATE_ACTION_CODE, {canBeMissing: true}); const privateDetailsErrors = privatePersonalDetails?.errors ?? undefined; const validateLoginError = getLatestError(privateDetailsErrors); const primaryLogin = account?.primaryLogin ?? ''; @@ -50,7 +50,7 @@ function MissingPersonalDetailsMagicCodeModal({onClose, isValidateCodeActionModa }; const clearError = () => { - if (!validateLoginError) { + if (isEmptyObject(validateLoginError) && isEmptyObject(validateCodeAction?.errorFields)) { return; } clearPersonalDetailsErrors(); @@ -60,12 +60,12 @@ function MissingPersonalDetailsMagicCodeModal({onClose, isValidateCodeActionModa requestValidateCodeAction()} - hasMagicCodeBeenSent={validateCodeAction?.validateCodeSent} handleSubmitForm={handleSubmitForm} isLoading={privatePersonalDetails?.isLoading} /> diff --git a/src/pages/OnboardingPrivateDomain/BaseOnboardingPrivateDomain.tsx b/src/pages/OnboardingPrivateDomain/BaseOnboardingPrivateDomain.tsx index f0e0828999dd..8fc5b5f2db05 100644 --- a/src/pages/OnboardingPrivateDomain/BaseOnboardingPrivateDomain.tsx +++ b/src/pages/OnboardingPrivateDomain/BaseOnboardingPrivateDomain.tsx @@ -27,7 +27,6 @@ function BaseOnboardingPrivateDomain({shouldUseNativeStyles, route}: BaseOnboard const [credentials] = useOnyx(ONYXKEYS.CREDENTIALS, {canBeMissing: true}); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: true}); - const [validateCodeAction] = useOnyx(ONYXKEYS.VALIDATE_ACTION_CODE, {canBeMissing: true}); const [getAccessiblePoliciesAction] = useOnyx(ONYXKEYS.VALIDATE_USER_AND_GET_ACCESSIBLE_POLICIES, {canBeMissing: true}); const [joinablePolicies] = useOnyx(ONYXKEYS.JOINABLE_POLICIES, {canBeMissing: true}); const joinablePoliciesLength = Object.keys(joinablePolicies ?? {}).length; @@ -79,7 +78,7 @@ function BaseOnboardingPrivateDomain({shouldUseNativeStyles, route}: BaseOnboard {translate('onboarding.peopleYouMayKnow')} {translate('onboarding.workspaceYouMayJoin', {domain, email})} { getAccessiblePolicies(code); setHasMagicCodeBeenSent(false); diff --git a/src/pages/OnboardingWorkEmailValidation/BaseOnboardingWorkEmailValidation.tsx b/src/pages/OnboardingWorkEmailValidation/BaseOnboardingWorkEmailValidation.tsx index dbed150a3ae7..dbaed73ee5d5 100644 --- a/src/pages/OnboardingWorkEmailValidation/BaseOnboardingWorkEmailValidation.tsx +++ b/src/pages/OnboardingWorkEmailValidation/BaseOnboardingWorkEmailValidation.tsx @@ -33,7 +33,6 @@ function BaseOnboardingWorkEmailValidation({shouldUseNativeStyles}: BaseOnboardi const [onboardingEmail] = useOnyx(ONYXKEYS.FORMS.ONBOARDING_WORK_EMAIL_FORM, {canBeMissing: true}); const workEmail = onboardingEmail?.onboardingWorkEmail; - const [validateCodeAction] = useOnyx(ONYXKEYS.VALIDATE_ACTION_CODE, {canBeMissing: true}); const {onboardingIsMediumOrLargerScreenWidth} = useResponsiveLayout(); const [onboardingValues] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {canBeMissing: true}); const isVsb = onboardingValues && 'signupQualifier' in onboardingValues && onboardingValues.signupQualifier === CONST.ONBOARDING_SIGNUP_QUALIFIERS.VSB; @@ -118,9 +117,9 @@ function BaseOnboardingWorkEmailValidation({shouldUseNativeStyles}: BaseOnboardi {translate('onboarding.workEmailValidation.title')} {translate('onboarding.workEmailValidation.magicCodeSent', {workEmail})} setOnboardingErrorMessage('')} buttonStyles={[styles.flex2, styles.justifyContentEnd, styles.mb5]} shouldShowSkipButton diff --git a/src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx b/src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx index ff1b6ea6bd23..1c4adf11ab5e 100644 --- a/src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx +++ b/src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx @@ -91,19 +91,18 @@ function VerifiedBankAccountFlowEntryPoint({ const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); - const [account] = useOnyx(ONYXKEYS.ACCOUNT); - const [isPlaidDisabled] = useOnyx(ONYXKEYS.IS_PLAID_DISABLED); - const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); + const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); + const [isPlaidDisabled] = useOnyx(ONYXKEYS.IS_PLAID_DISABLED, {canBeMissing: true}); + const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true}); const errors = reimbursementAccount?.errors ?? {}; const pendingAction = reimbursementAccount?.pendingAction ?? null; - const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST); + const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST, {canBeMissing: true}); const optionPressed = useRef(''); const isAccountValidated = useAccountValidation(); const contactMethod = account?.primaryLogin ?? ''; const loginData = useMemo(() => loginList?.[contactMethod], [loginList, contactMethod]); const validateLoginError = getEarliestErrorField(loginData, 'validateLogin'); - const hasMagicCodeBeenSent = !!loginData?.validateCodeSent; const plaidDesktopMessage = getPlaidDesktopMessage(); const bankAccountRoute = `${ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(policyID, REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW, ROUTES.WORKSPACE_INITIAL.getRoute(policyID))}`; const personalBankAccounts = bankAccountList ? Object.keys(bankAccountList).filter((key) => bankAccountList[key].accountType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) : []; @@ -304,7 +303,7 @@ function VerifiedBankAccountFlowEntryPoint({ descriptionPrimary={translate('contacts.featureRequiresValidate')} descriptionSecondary={translate('contacts.enterMagicCode', {contactMethod})} isVisible={!!isValidateCodeActionModalVisible} - hasMagicCodeBeenSent={hasMagicCodeBeenSent} + validateCodeActionErrorField="validateLogin" validatePendingAction={loginData?.pendingFields?.validateCodeSent} sendValidateCode={() => requestValidateCodeAction()} handleSubmitForm={(validateCode) => validateSecondaryLogin(loginList, contactMethod, validateCode)} diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx index 37e23eb81f3e..341fc9ece40d 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx +++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx @@ -15,6 +15,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import ValidateCodeActionForm from '@components/ValidateCodeActionForm'; +import type {ValidateCodeFormHandle} from '@components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useTheme from '@hooks/useTheme'; @@ -46,18 +47,17 @@ import type SCREENS from '@src/SCREENS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import KeyboardUtils from '@src/utils/keyboard'; -import type {ValidateCodeFormHandle} from './ValidateCodeForm/BaseValidateCodeForm'; type ContactMethodDetailsPageProps = PlatformStackScreenProps; function ContactMethodDetailsPage({route}: ContactMethodDetailsPageProps) { - const [loginList, loginListResult] = useOnyx(ONYXKEYS.LOGIN_LIST); - const [session, sessionResult] = useOnyx(ONYXKEYS.SESSION); - const [myDomainSecurityGroups, myDomainSecurityGroupsResult] = useOnyx(ONYXKEYS.MY_DOMAIN_SECURITY_GROUPS); - const [securityGroups, securityGroupsResult] = useOnyx(ONYXKEYS.COLLECTION.SECURITY_GROUP); - const [isLoadingReportData, isLoadingReportDataResult] = useOnyx(ONYXKEYS.IS_LOADING_REPORT_DATA, {initialValue: true}); + const [loginList, loginListResult] = useOnyx(ONYXKEYS.LOGIN_LIST, {canBeMissing: true}); + const [session, sessionResult] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: true}); + const [myDomainSecurityGroups, myDomainSecurityGroupsResult] = useOnyx(ONYXKEYS.MY_DOMAIN_SECURITY_GROUPS, {canBeMissing: true}); + const [securityGroups, securityGroupsResult] = useOnyx(ONYXKEYS.COLLECTION.SECURITY_GROUP, {canBeMissing: true}); + const [isLoadingReportData, isLoadingReportDataResult] = useOnyx(ONYXKEYS.IS_LOADING_REPORT_DATA, {initialValue: true, canBeMissing: true}); const [isValidateCodeFormVisible, setIsValidateCodeFormVisible] = useState(true); - const [isActingAsDelegate] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => !!account?.delegatedAccess?.delegate}); + const [isActingAsDelegate] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => !!account?.delegatedAccess?.delegate, canBeMissing: true}); const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); const isLoadingOnyxValues = isLoadingOnyxValue(loginListResult, sessionResult, myDomainSecurityGroupsResult, securityGroupsResult, isLoadingReportDataResult); @@ -341,7 +341,6 @@ function ContactMethodDetailsPage({route}: ContactMethodDetailsPageProps) { {isValidateCodeFormVisible && !!loginData && !loginData.validatedDate && ( validateSecondaryLogin(loginList, contactMethod, validateCode)} validateError={!isEmptyObject(validateLoginError) ? validateLoginError : getLatestErrorField(loginData, 'validateCodeSent')} clearError={() => clearContactMethodErrors(contactMethod, !isEmptyObject(validateLoginError) ? 'validateLogin' : 'validateCodeSent')} diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx index 2cf44004238a..7a916666b46e 100644 --- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx +++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx @@ -30,6 +30,7 @@ import { addPendingContactMethod, clearContactMethod, clearContactMethodErrors, + clearPendingContactActionErrors, clearUnvalidatedNewContactMethodAction, requestValidateCodeAction, } from '@userActions/User'; @@ -48,8 +49,8 @@ function NewContactMethodPage({route, navigation}: NewContactMethodPageProps) { const {translate} = useLocalize(); const loginInputRef = useRef(null); const [isValidateCodeActionModalVisible, setIsValidateCodeActionModalVisible] = useState(false); - const [pendingContactAction] = useOnyx(ONYXKEYS.PENDING_CONTACT_ACTION); - const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST); + const [pendingContactAction] = useOnyx(ONYXKEYS.PENDING_CONTACT_ACTION, {canBeMissing: true}); + const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST, {canBeMissing: true}); const loginData = loginList?.[pendingContactAction?.contactMethod ?? contactMethod]; const validateLoginError = getLatestErrorField(loginData, 'addedLogin'); const isUserValidated = useAccountValidation(); @@ -180,7 +181,7 @@ function NewContactMethodPage({route, navigation}: NewContactMethodPageProps) { )} { @@ -188,6 +189,7 @@ function NewContactMethodPage({route, navigation}: NewContactMethodPageProps) { return; } clearContactMethodErrors(addSMSDomainIfPhoneNumber(pendingContactAction?.contactMethod ?? contactMethod), 'addedLogin'); + clearPendingContactActionErrors(); }} onClose={() => { if (pendingContactAction?.contactMethod) { @@ -197,7 +199,6 @@ function NewContactMethodPage({route, navigation}: NewContactMethodPageProps) { setIsValidateCodeActionModalVisible(false); }} isVisible={isValidateCodeActionModalVisible} - hasMagicCodeBeenSent={!!loginData?.validateCodeSent} title={translate('delegate.makeSureItIsYou')} sendValidateCode={() => requestValidateCodeAction()} descriptionPrimary={translate('contacts.enterMagicCode', {contactMethod})} diff --git a/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.tsx deleted file mode 100644 index 6ec1dbc5d67e..000000000000 --- a/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.tsx +++ /dev/null @@ -1,281 +0,0 @@ -import {useFocusEffect} from '@react-navigation/native'; -import type {ForwardedRef} from 'react'; -import React, {useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; -import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; -import Button from '@components/Button'; -import DotIndicatorMessage from '@components/DotIndicatorMessage'; -import MagicCodeInput from '@components/MagicCodeInput'; -import type {AutoCompleteVariant, MagicCodeInputHandle} from '@components/MagicCodeInput'; -import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; -import Text from '@components/Text'; -import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; -import * as ErrorUtils from '@libs/ErrorUtils'; -import * as ValidationUtils from '@libs/ValidationUtils'; -import * as Session from '@userActions/Session'; -import * as User from '@userActions/User'; -import CONST from '@src/CONST'; -import type {TranslationPaths} from '@src/languages/types'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {Account, LoginList, PendingContactAction} from '@src/types/onyx'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; - -type ValidateCodeFormHandle = { - focus: () => void; - focusLastSelected: () => void; -}; - -type ValidateCodeFormError = { - validateCode?: TranslationPaths; -}; - -type BaseValidateCodeFormOnyxProps = { - /** The details about the account that the user is signing in with */ - account: OnyxEntry; -}; - -type ValidateCodeFormProps = { - /** The contact method being valdiated */ - contactMethod: string; - - /** If the magic code has been resent previously */ - hasMagicCodeBeenSent?: boolean; - - /** Login list for the user that is signed in */ - loginList?: LoginList; - - /** Specifies autocomplete hints for the system, so it can provide autofill */ - autoComplete?: AutoCompleteVariant; - - /** Forwarded inner ref */ - innerRef?: ForwardedRef; - - /** Whether we are validating the action taken to add the magic code */ - isValidatingAction?: boolean; - - /** The contact that's going to be added after successful validation */ - pendingContact?: PendingContactAction; -}; - -type BaseValidateCodeFormProps = BaseValidateCodeFormOnyxProps & ValidateCodeFormProps; - -function BaseValidateCodeForm({ - account = {}, - contactMethod, - hasMagicCodeBeenSent, - loginList, - autoComplete = 'one-time-code', - innerRef = () => {}, - isValidatingAction = false, - pendingContact, -}: BaseValidateCodeFormProps) { - const {translate} = useLocalize(); - const {isOffline} = useNetwork(); - const theme = useTheme(); - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - const [formError, setFormError] = useState({}); - const [validateCode, setValidateCode] = useState(''); - const loginData = loginList?.[pendingContact?.contactMethod ?? contactMethod]; - const inputValidateCodeRef = useRef(null); - const validateLoginError = ErrorUtils.getEarliestErrorField(loginData, 'validateLogin'); - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- nullish coalescing doesn't achieve the same result in this case - const shouldDisableResendValidateCode = !!isOffline || account?.isLoading; - const focusTimeoutRef = useRef(null); - const validateCodeSentIsPressedRef = useRef(false); - const [showDotIndicator, setShowDotIndicator] = useState(false); - - useImperativeHandle(innerRef, () => ({ - focus() { - inputValidateCodeRef.current?.focus(); - }, - focusLastSelected() { - if (!inputValidateCodeRef.current) { - return; - } - if (focusTimeoutRef.current) { - clearTimeout(focusTimeoutRef.current); - } - focusTimeoutRef.current = setTimeout(() => { - inputValidateCodeRef.current?.focusLastSelected(); - }, CONST.ANIMATED_TRANSITION); - }, - })); - - useFocusEffect( - useCallback(() => { - if (!inputValidateCodeRef.current) { - return; - } - if (focusTimeoutRef.current) { - clearTimeout(focusTimeoutRef.current); - } - focusTimeoutRef.current = setTimeout(() => { - inputValidateCodeRef.current?.focusLastSelected(); - }, CONST.ANIMATED_TRANSITION); - return () => { - if (!focusTimeoutRef.current) { - return; - } - clearTimeout(focusTimeoutRef.current); - }; - }, []), - ); - - useEffect(() => { - Session.clearAccountMessages(); - if (!validateLoginError) { - return; - } - User.clearContactMethodErrors(contactMethod, 'validateLogin'); - // contactMethod is not added as a dependency since it does not change between renders - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - if (!hasMagicCodeBeenSent) { - return; - } - inputValidateCodeRef.current?.clear(); - }, [hasMagicCodeBeenSent]); - - useEffect(() => { - // `validateCodeSent` is updated asynchronously, - // and `validateCodeSentIsPressedRef.current` is updated faster than `hasMagicCodeBeenSent`. - // This can cause the component to hide and show the `DotIndicatorMessage` multiple times - // in quick succession, leading to a flickering effect. - if ((hasMagicCodeBeenSent ?? !!pendingContact?.validateCodeSent) && validateCodeSentIsPressedRef.current) { - setShowDotIndicator(true); - } else { - setShowDotIndicator(false); - } - }, [hasMagicCodeBeenSent, pendingContact, validateCodeSentIsPressedRef]); - - /** - * Request a validate code / magic code be sent to verify this contact method - */ - const resendValidateCode = () => { - if (!!pendingContact?.contactMethod && isValidatingAction) { - User.saveNewContactMethodAndRequestValidationCode(pendingContact?.contactMethod); - } else { - User.requestContactMethodValidateCode(contactMethod); - } - - inputValidateCodeRef.current?.clear(); - validateCodeSentIsPressedRef.current = true; - }; - /** - * Handle text input and clear formError upon text change - */ - const onTextInput = useCallback( - (text: string) => { - setValidateCode(text); - setFormError({}); - - if (validateLoginError) { - User.clearContactMethodErrors(contactMethod, 'validateLogin'); - } - }, - [validateLoginError, contactMethod], - ); - - /** - * Check that all the form fields are valid, then trigger the submit callback - */ - const validateAndSubmitForm = useCallback(() => { - if (!validateCode.trim()) { - setFormError({validateCode: 'validateCodeForm.error.pleaseFillMagicCode'}); - return; - } - - if (!ValidationUtils.isValidValidateCode(validateCode)) { - setFormError({validateCode: 'validateCodeForm.error.incorrectMagicCode'}); - return; - } - - setFormError({}); - - if (!!pendingContact?.contactMethod && isValidatingAction) { - User.addNewContactMethod(pendingContact?.contactMethod, validateCode); - return; - } - - User.validateSecondaryLogin(loginList, contactMethod, validateCode); - }, [loginList, validateCode, contactMethod, isValidatingAction, pendingContact?.contactMethod]); - - return ( - <> - - User.clearContactMethodErrors(contactMethod, 'validateCodeSent')} - > - - - {translate('validateCodeForm.magicCodeNotReceived')} - - {showDotIndicator && ( - - )} - - - User.clearContactMethodErrors(contactMethod, 'validateLogin')} - > -