From 5aee11cc0d91a0f9583d322ba564478f19e105fc Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Fri, 12 Dec 2025 01:20:39 +0700 Subject: [PATCH 01/22] reserve the order flow --- src/ROUTES.ts | 7 +- .../VerifyAddSecondaryLoginCodeParams.ts | 3 + src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + src/libs/actions/User.ts | 67 ++++++++++++++++--- .../Profile/Contacts/ContactMethodsPage.tsx | 7 +- .../NewContactMethodConfirmMagicCodePage.tsx | 45 +++---------- .../Profile/Contacts/NewContactMethodPage.tsx | 39 ++++++++--- src/types/onyx/PendingContactAction.ts | 6 ++ 9 files changed, 115 insertions(+), 62 deletions(-) create mode 100644 src/libs/API/parameters/VerifyAddSecondaryLoginCodeParams.ts diff --git a/src/ROUTES.ts b/src/ROUTES.ts index be5aebf37be8a..330124e0e649a 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -414,13 +414,12 @@ const ROUTES = { getRoute: (backTo?: string) => getUrlWithBackToParam('settings/profile/contact-methods/new', backTo), }, SETTINGS_NEW_CONTACT_METHOD_CONFIRM_MAGIC_CODE: { - route: 'settings/profile/contact-methods/new/:newContactMethod/confirm-magic-code', - getRoute: (newContactMethod: string, backTo?: string) => { - const encodedMethod = encodeURIComponent(newContactMethod); + route: 'settings/profile/contact-methods/new/confirm-magic-code', + getRoute: (backTo?: string) => { // TODO this backTo comes from drilling it through settings screens // should be removed once https://github.com/Expensify/App/pull/72219 is resolved // eslint-disable-next-line no-restricted-syntax -- Legacy route generation - return getUrlWithBackToParam(`settings/profile/contact-methods/new/${encodedMethod}/confirm-magic-code`, backTo); + return getUrlWithBackToParam(`settings/profile/contact-methods/new/confirm-magic-code`, backTo); }, }, SETTINGS_CONTACT_METHOD_VERIFY_ACCOUNT: { diff --git a/src/libs/API/parameters/VerifyAddSecondaryLoginCodeParams.ts b/src/libs/API/parameters/VerifyAddSecondaryLoginCodeParams.ts new file mode 100644 index 0000000000000..7660f403a9c1c --- /dev/null +++ b/src/libs/API/parameters/VerifyAddSecondaryLoginCodeParams.ts @@ -0,0 +1,3 @@ +type VerifyAddSecondaryLoginCodeParams = {validateCode: string}; + +export default VerifyAddSecondaryLoginCodeParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 4e46090c868c3..5ffb83035a221 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -160,6 +160,7 @@ export type {default as OpenWorkspaceInvitePageParams} from './OpenWorkspaceInvi export type {default as OpenWorkspaceMembersPageParams} from './OpenWorkspaceMembersPageParams'; export type {default as OpenPolicyCategoriesPageParams} from './OpenPolicyCategoriesPageParams'; export type {default as OpenPolicyTagsPageParams} from './OpenPolicyTagsPageParams'; +export type {default as VerifyAddSecondaryLoginCodeParams} from './VerifyAddSecondaryLoginCodeParams'; export type {default as OpenDraftWorkspaceRequestParams} from './OpenDraftWorkspaceRequestParams'; export type {default as OpenDraftPerDiemExpenseParams} from './OpenDraftPerDiemExpenseParams'; export type {default as CreateWorkspaceFromIOUPaymentParams} from './CreateWorkspaceFromIOUPaymentParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 0b50f69173f6f..93585cfff2147 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -92,6 +92,7 @@ const WRITE_COMMANDS = { UPDATE_PERSONAL_DETAILS_FOR_WALLET: 'UpdatePersonalDetailsForWallet', VERIFY_IDENTITY: 'VerifyIdentity', ACCEPT_WALLET_TERMS: 'AcceptWalletTerms', + VERIFY_ADD_SECONDARY_LOGIN_CODE: 'VerifyAddSecondaryLoginCode', ANSWER_QUESTIONS_FOR_WALLET: 'AnswerQuestionsForWallet', REQUEST_ACCOUNT_VALIDATION_LINK: 'RequestAccountValidationLink', REQUEST_NEW_VALIDATE_CODE: 'RequestNewValidateCode', @@ -566,6 +567,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_DISPLAY_NAME]: Parameters.UpdateDisplayNameParams; [WRITE_COMMANDS.UPDATE_LEGAL_NAME]: Parameters.UpdateLegalNameParams; [WRITE_COMMANDS.UPDATE_DATE_OF_BIRTH]: Parameters.UpdateDateOfBirthParams; + [WRITE_COMMANDS.VERIFY_ADD_SECONDARY_LOGIN_CODE]: Parameters.VerifyAddSecondaryLoginCodeParams; [WRITE_COMMANDS.UPDATE_PHONE_NUMBER]: Parameters.UpdatePhoneNumberParams; [WRITE_COMMANDS.UPDATE_POLICY_ADDRESS]: Parameters.UpdatePolicyAddressParams; [WRITE_COMMANDS.UPDATE_HOME_ADDRESS]: Parameters.UpdateHomeAddressParams; diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index e88ec32dc2eda..43c34180194c4 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -24,6 +24,7 @@ import type { UpdateStatusParams, UpdateThemeParams, ValidateSecondaryLoginParams, + VerifyAddSecondaryLoginCodeParams, } from '@libs/API/parameters'; import type LockAccountParams from '@libs/API/parameters/LockAccountParams'; import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; @@ -349,16 +350,6 @@ function clearPendingContactActionErrors() { }); } -/** - * 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. - */ -function addPendingContactMethod(contactMethod: string) { - Onyx.merge(ONYXKEYS.PENDING_CONTACT_ACTION, { - contactMethod, - }); -} - /** * Adds a secondary login to a user's account */ @@ -1569,6 +1560,60 @@ function respondToProactiveAppReview(response: 'positive' | 'negative' | 'skip', API.write(WRITE_COMMANDS.RESPOND_TO_PROACTIVE_APP_REVIEW, params, {optimisticData, successData, failureData}); } +/** + * Verify the validation code for adding a secondary login within the contact method flow. + * + * This handles the complete flow for verifying a secondary login: + * 1. Verifies the validation code entered by the user + * 2. On success, stores the validate code to allow adding the new email + * 3. On failure, updates the state to reflect the failed verification + * + * @param validateCode - The validation code entered by the user + */ +function verifyAddSecondaryLoginCode(validateCode: string) { + resetValidateActionCodeSent(); + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PENDING_CONTACT_ACTION, + value: { + validateActionCode: validateCode, + errorFields: { + validateActionCode: null, + }, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PENDING_CONTACT_ACTION, + value: { + validateActionCode: validateCode, + isVerifiedValidateActionCode: true, + errorFields: { + validateActionCode: null, + }, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PENDING_CONTACT_ACTION, + value: { + isVerifiedValidateActionCode: false, + }, + }, + ]; + + const parameters: VerifyAddSecondaryLoginCodeParams = {validateCode}; + + API.write(WRITE_COMMANDS.VERIFY_ADD_SECONDARY_LOGIN_CODE, parameters, {optimisticData, successData, failureData}); +} + export { closeAccount, dismissReferralBanner, @@ -1603,11 +1648,11 @@ export { clearUnvalidatedNewContactMethodAction, clearPendingContactActionErrors, requestValidateCodeAction, - addPendingContactMethod, clearValidateCodeActionError, setIsDebugModeEnabled, resetValidateActionCodeSent, lockAccount, requestUnlockAccount, respondToProactiveAppReview, + verifyAddSecondaryLoginCode, }; diff --git a/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx index 600d3c1663fed..674cd71b80a03 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx +++ b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx @@ -49,11 +49,12 @@ function ContactMethodsPage({route}: ContactMethodsPageProps) { } if (!isUserValidated) { - Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHOD_VERIFY_ACCOUNT.getRoute(Navigation.getActiveRoute(), ROUTES.SETTINGS_NEW_CONTACT_METHOD.getRoute(navigateBackTo))); + Navigation.navigate( + ROUTES.SETTINGS_CONTACT_METHOD_VERIFY_ACCOUNT.getRoute(Navigation.getActiveRoute(), ROUTES.SETTINGS_NEW_CONTACT_METHOD_CONFIRM_MAGIC_CODE.getRoute(navigateBackTo)), + ); return; } - - Navigation.navigate(ROUTES.SETTINGS_NEW_CONTACT_METHOD.getRoute(navigateBackTo)); + Navigation.navigate(ROUTES.SETTINGS_NEW_CONTACT_METHOD_CONFIRM_MAGIC_CODE.getRoute(navigateBackTo)); }, [navigateBackTo, isActingAsDelegate, showDelegateNoAccessModal, isAccountLocked, isUserValidated, showLockedAccountModal]); return ( diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodConfirmMagicCodePage.tsx b/src/pages/settings/Profile/Contacts/NewContactMethodConfirmMagicCodePage.tsx index cac8ece71d47c..b906a155fb639 100644 --- a/src/pages/settings/Profile/Contacts/NewContactMethodConfirmMagicCodePage.tsx +++ b/src/pages/settings/Profile/Contacts/NewContactMethodConfirmMagicCodePage.tsx @@ -1,19 +1,16 @@ -import React, {useCallback, useEffect, useMemo} from 'react'; +import React, {useEffect} from 'react'; import ValidateCodeActionContent from '@components/ValidateCodeActionModal/ValidateCodeActionContent'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import usePrevious from '@hooks/usePrevious'; -import {addNewContactMethod as addNewContactMethodUser, clearContactMethod, clearUnvalidatedNewContactMethodAction, requestValidateCodeAction} from '@libs/actions/User'; +import {clearPendingContactActionErrors, requestValidateCodeAction, verifyAddSecondaryLoginCode} from '@libs/actions/User'; import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; -import {addSMSDomainIfPhoneNumber} from '@libs/PhoneNumber'; import {getContactMethod} from '@libs/UserUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import getDecodedContactMethodFromUriParam from './utils'; type NewContactMethodConfirmMagicCodePageProps = PlatformStackScreenProps; @@ -23,29 +20,15 @@ function NewContactMethodConfirmMagicCodePage({route}: NewContactMethodConfirmMa const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: false}); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); const contactMethod = getContactMethod(account?.primaryLogin, session?.email); - const newContactMethod = useMemo(() => getDecodedContactMethodFromUriParam(route.params.newContactMethod), [route.params.newContactMethod]); - const [pendingContactAction] = useOnyx(ONYXKEYS.PENDING_CONTACT_ACTION, {canBeMissing: false}); - const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST, {canBeMissing: true}); - - const prevPendingContactAction = usePrevious(pendingContactAction); - const loginData = loginList?.[pendingContactAction?.contactMethod ?? newContactMethod]; - const validateLoginError = getLatestErrorField(loginData, 'addedLogin'); - - const addNewContactMethod = useCallback( - (magicCode: string) => { - addNewContactMethodUser(addSMSDomainIfPhoneNumber(newContactMethod), magicCode); - }, - [newContactMethod], - ); + const validateCodeError = getLatestErrorField(pendingContactAction, 'addedLogin'); useEffect(() => { - if (!pendingContactAction?.actionVerified) { + if (!pendingContactAction?.isVerifiedValidateActionCode) { return; } - clearUnvalidatedNewContactMethodAction(); - Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHOD_DETAILS.getRoute(addSMSDomainIfPhoneNumber(newContactMethod), navigateBackTo, true)); - }, [navigateBackTo, newContactMethod, pendingContactAction?.actionVerified, prevPendingContactAction?.contactMethod]); + Navigation.navigate(ROUTES.SETTINGS_NEW_CONTACT_METHOD.getRoute(navigateBackTo)); + }, [navigateBackTo, pendingContactAction?.isVerifiedValidateActionCode]); return ( requestValidateCodeAction()} descriptionPrimary={translate('contacts.enterMagicCode', {contactMethod})} validateCodeActionErrorField="addedLogin" - validateError={validateLoginError} - handleSubmitForm={addNewContactMethod} + validateError={validateCodeError} + handleSubmitForm={verifyAddSecondaryLoginCode} clearError={() => { - if (!pendingContactAction?.contactMethod) { - return; - } - clearContactMethod(newContactMethod); + clearPendingContactActionErrors(); }} onClose={() => { - if (!pendingContactAction?.contactMethod) { - return; - } - clearContactMethod(newContactMethod); - clearUnvalidatedNewContactMethodAction(); - Navigation.goBack(ROUTES.SETTINGS_NEW_CONTACT_METHOD.getRoute(navigateBackTo)); + Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(navigateBackTo)); }} /> ); diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx index c09e700ca454a..ee65082b7c744 100644 --- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx +++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx @@ -1,5 +1,5 @@ import {Str} from 'expensify-common'; -import React, {useCallback, useRef} from 'react'; +import React, {useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; import DelegateNoAccessWrapper from '@components/DelegateNoAccessWrapper'; import FormProvider from '@components/Form/FormProvider'; @@ -18,7 +18,8 @@ import {getPhoneLogin, validateNumber} from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; -import {addPendingContactMethod, resetValidateActionCodeSent} from '@userActions/User'; +import {addSMSDomainIfPhoneNumber} from '@libs/PhoneNumber'; +import {addNewContactMethod, clearUnvalidatedNewContactMethodAction} from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -34,17 +35,15 @@ function NewContactMethodPage({route}: NewContactMethodPageProps) { const loginInputRef = useRef(null); const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST, {canBeMissing: true}); const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); - + const [pendingContactAction] = useOnyx(ONYXKEYS.PENDING_CONTACT_ACTION, {canBeMissing: true}); const navigateBackTo = route?.params?.backTo; - const handleValidateMagicCode = useCallback( + const handleAddSecondaryLogin = useCallback( (values: FormOnyxValues) => { const phoneLogin = getPhoneLogin(values.phoneOrEmail, countryCode); const validateIfNumber = validateNumber(phoneLogin); const submitDetail = (validateIfNumber || values.phoneOrEmail).trim().toLowerCase(); - resetValidateActionCodeSent(); - addPendingContactMethod(submitDetail); - Navigation.navigate(ROUTES.SETTINGS_NEW_CONTACT_METHOD_CONFIRM_MAGIC_CODE.getRoute(submitDetail, navigateBackTo)); + addNewContactMethod(submitDetail, pendingContactAction?.validateActionCode ?? ''); }, [navigateBackTo, countryCode], ); @@ -90,6 +89,29 @@ function NewContactMethodPage({route}: NewContactMethodPageProps) { Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(navigateBackTo)); }, [navigateBackTo]); + const navigateToConfirmMagicCode = useCallback(() => { + clearUnvalidatedNewContactMethodAction(); + Navigation.navigate(ROUTES.SETTINGS_NEW_CONTACT_METHOD_CONFIRM_MAGIC_CODE.getRoute(navigateBackTo ?? '')); + }, [navigateBackTo]); + + useEffect(() => { + if (!pendingContactAction) { + return; + } + if (pendingContactAction?.validateActionCode && pendingContactAction.isVerifiedValidateActionCode) { + return; + } + navigateToConfirmMagicCode(); + }, [navigateToConfirmMagicCode]); + + useEffect(() => { + if (!pendingContactAction?.actionVerified || !pendingContactAction?.contactMethod) { + return; + } + Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHOD_DETAILS.getRoute(addSMSDomainIfPhoneNumber(pendingContactAction?.contactMethod), navigateBackTo, true)); + clearUnvalidatedNewContactMethodAction(); + }, [navigateToConfirmMagicCode, pendingContactAction?.actionVerified, pendingContactAction?.isVerifiedValidateActionCode, pendingContactAction?.validateActionCode]); + return ( loginInputRef.current?.focus()} @@ -106,10 +128,9 @@ function NewContactMethodPage({route}: NewContactMethodPageProps) { {translate('common.pleaseEnterEmailOrPhoneNumber')} diff --git a/src/types/onyx/PendingContactAction.ts b/src/types/onyx/PendingContactAction.ts index c2c75c407c4d5..c5977b694606b 100644 --- a/src/types/onyx/PendingContactAction.ts +++ b/src/types/onyx/PendingContactAction.ts @@ -14,6 +14,12 @@ type ContactAction = OnyxCommon.OnyxValueWithOfflineFeedback< /** Whether the action is validated */ actionVerified?: boolean; + + /** Validation action code for adding secondary login */ + validateActionCode?: string; + + /** Whether the action is verified */ + isVerifiedValidateActionCode?: boolean; }, 'actionVerified' >; From 044806f8b73939c093e03d7b154c848955f165b9 Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Sun, 14 Dec 2025 19:45:25 +0700 Subject: [PATCH 02/22] add loading animation --- src/libs/actions/User.ts | 23 +++++++++++++++---- .../Profile/Contacts/NewContactMethodPage.tsx | 7 +++--- src/types/onyx/PendingContactAction.ts | 3 +++ 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 01bf1d76fe96b..ed18599f6258e 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -373,18 +373,26 @@ function addNewContactMethod(contactMethod: string, validateCode = '') { key: ONYXKEYS.ACCOUNT, value: {isLoading: true}, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PENDING_CONTACT_ACTION, + value: { + contactMethod, + isLoading: true, + errorFields: { + actionVerified: null, + }, + }, + }, ]; const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.PENDING_CONTACT_ACTION, value: { - contactMethod: null, validateCodeSent: null, actionVerified: true, - errorFields: { - actionVerified: null, - }, + isLoading: false, }, }, { @@ -404,6 +412,13 @@ function addNewContactMethod(contactMethod: string, validateCode = '') { key: ONYXKEYS.VALIDATE_ACTION_CODE, value: {validateCodeSent: null}, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PENDING_CONTACT_ACTION, + value: { + isLoading: false, + }, + }, ]; const parameters: AddNewContactMethodParams = {partnerUserID: contactMethod, validateCode}; diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx index ee65082b7c744..a6773e55402b5 100644 --- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx +++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx @@ -45,7 +45,7 @@ function NewContactMethodPage({route}: NewContactMethodPageProps) { const submitDetail = (validateIfNumber || values.phoneOrEmail).trim().toLowerCase(); addNewContactMethod(submitDetail, pendingContactAction?.validateActionCode ?? ''); }, - [navigateBackTo, countryCode], + [navigateBackTo, countryCode, pendingContactAction?.validateActionCode], ); const validate = useCallback( @@ -102,7 +102,7 @@ function NewContactMethodPage({route}: NewContactMethodPageProps) { return; } navigateToConfirmMagicCode(); - }, [navigateToConfirmMagicCode]); + }, [navigateToConfirmMagicCode, pendingContactAction]); useEffect(() => { if (!pendingContactAction?.actionVerified || !pendingContactAction?.contactMethod) { @@ -110,7 +110,7 @@ function NewContactMethodPage({route}: NewContactMethodPageProps) { } Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHOD_DETAILS.getRoute(addSMSDomainIfPhoneNumber(pendingContactAction?.contactMethod), navigateBackTo, true)); clearUnvalidatedNewContactMethodAction(); - }, [navigateToConfirmMagicCode, pendingContactAction?.actionVerified, pendingContactAction?.isVerifiedValidateActionCode, pendingContactAction?.validateActionCode]); + }, [navigateToConfirmMagicCode, pendingContactAction, navigateBackTo]); return ( {translate('common.pleaseEnterEmailOrPhoneNumber')} diff --git a/src/types/onyx/PendingContactAction.ts b/src/types/onyx/PendingContactAction.ts index c5977b694606b..3a3394463f074 100644 --- a/src/types/onyx/PendingContactAction.ts +++ b/src/types/onyx/PendingContactAction.ts @@ -20,6 +20,9 @@ type ContactAction = OnyxCommon.OnyxValueWithOfflineFeedback< /** Whether the action is verified */ isVerifiedValidateActionCode?: boolean; + + /** Whether the action is loading */ + isLoading?: boolean; }, 'actionVerified' >; From 03c84f5578b193cfa4736e12bd7f93952d175362 Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Sun, 14 Dec 2025 19:51:32 +0700 Subject: [PATCH 03/22] add loading animation --- src/libs/actions/User.ts | 7 +++---- .../Contacts/NewContactMethodConfirmMagicCodePage.tsx | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index ed18599f6258e..fd7bbec7984fe 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -1576,6 +1576,7 @@ function verifyAddSecondaryLoginCode(validateCode: string) { key: ONYXKEYS.PENDING_CONTACT_ACTION, value: { validateActionCode: validateCode, + isLoading: true, errorFields: { validateActionCode: null, }, @@ -1588,11 +1589,8 @@ function verifyAddSecondaryLoginCode(validateCode: string) { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.PENDING_CONTACT_ACTION, value: { - validateActionCode: validateCode, isVerifiedValidateActionCode: true, - errorFields: { - validateActionCode: null, - }, + isLoading: false, }, }, ]; @@ -1603,6 +1601,7 @@ function verifyAddSecondaryLoginCode(validateCode: string) { key: ONYXKEYS.PENDING_CONTACT_ACTION, value: { isVerifiedValidateActionCode: false, + isLoading: false, }, }, ]; diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodConfirmMagicCodePage.tsx b/src/pages/settings/Profile/Contacts/NewContactMethodConfirmMagicCodePage.tsx index b906a155fb639..353a7ffa07e12 100644 --- a/src/pages/settings/Profile/Contacts/NewContactMethodConfirmMagicCodePage.tsx +++ b/src/pages/settings/Profile/Contacts/NewContactMethodConfirmMagicCodePage.tsx @@ -44,6 +44,7 @@ function NewContactMethodConfirmMagicCodePage({route}: NewContactMethodConfirmMa onClose={() => { Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(navigateBackTo)); }} + isLoading={pendingContactAction?.isLoading} /> ); } From 1ce25e7c734b861f9169c8ca5fdfe9d2ff1df22e Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Sun, 14 Dec 2025 19:53:29 +0700 Subject: [PATCH 04/22] update dependency list --- src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx index a6773e55402b5..124c19819ef4b 100644 --- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx +++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx @@ -45,7 +45,7 @@ function NewContactMethodPage({route}: NewContactMethodPageProps) { const submitDetail = (validateIfNumber || values.phoneOrEmail).trim().toLowerCase(); addNewContactMethod(submitDetail, pendingContactAction?.validateActionCode ?? ''); }, - [navigateBackTo, countryCode, pendingContactAction?.validateActionCode], + [countryCode, pendingContactAction?.validateActionCode], ); const validate = useCallback( @@ -110,7 +110,7 @@ function NewContactMethodPage({route}: NewContactMethodPageProps) { } Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHOD_DETAILS.getRoute(addSMSDomainIfPhoneNumber(pendingContactAction?.contactMethod), navigateBackTo, true)); clearUnvalidatedNewContactMethodAction(); - }, [navigateToConfirmMagicCode, pendingContactAction, navigateBackTo]); + }, [navigateToConfirmMagicCode, pendingContactAction?.actionVerified, pendingContactAction?.contactMethod, navigateBackTo]); return ( Date: Sun, 14 Dec 2025 19:57:17 +0700 Subject: [PATCH 05/22] remove validateCodeSent --- src/libs/actions/User.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index fd7bbec7984fe..573da8e839a52 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -390,8 +390,8 @@ function addNewContactMethod(contactMethod: string, validateCode = '') { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.PENDING_CONTACT_ACTION, value: { - validateCodeSent: null, actionVerified: true, + contactMethod: null, isLoading: false, }, }, @@ -417,6 +417,8 @@ function addNewContactMethod(contactMethod: string, validateCode = '') { key: ONYXKEYS.PENDING_CONTACT_ACTION, value: { isLoading: false, + contactMethod: null, + actionVerified: false, }, }, ]; From a7d7c713f3c39e6db6a16109ad08dd4c52483479 Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Sun, 14 Dec 2025 20:01:17 +0700 Subject: [PATCH 06/22] keep contactMethod in success data --- src/libs/actions/User.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 573da8e839a52..41be18bd6b1d9 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -391,7 +391,6 @@ function addNewContactMethod(contactMethod: string, validateCode = '') { key: ONYXKEYS.PENDING_CONTACT_ACTION, value: { actionVerified: true, - contactMethod: null, isLoading: false, }, }, @@ -417,7 +416,6 @@ function addNewContactMethod(contactMethod: string, validateCode = '') { key: ONYXKEYS.PENDING_CONTACT_ACTION, value: { isLoading: false, - contactMethod: null, actionVerified: false, }, }, From de07acc64579d8f783b79a5815da24b417d4bac3 Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Sun, 14 Dec 2025 20:06:12 +0700 Subject: [PATCH 07/22] reset isVerifiedValidateActionCode --- src/libs/actions/User.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 41be18bd6b1d9..21f90379ac4f4 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -378,6 +378,7 @@ function addNewContactMethod(contactMethod: string, validateCode = '') { key: ONYXKEYS.PENDING_CONTACT_ACTION, value: { contactMethod, + isVerifiedValidateActionCode: false, isLoading: true, errorFields: { actionVerified: null, From 48c2374144afa65e32d03225fe1a04722ec6e52e Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Sun, 14 Dec 2025 20:35:22 +0700 Subject: [PATCH 08/22] update navigation flow --- src/libs/actions/User.ts | 3 ++- src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 21f90379ac4f4..1e57b2bc8f319 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -378,7 +378,6 @@ function addNewContactMethod(contactMethod: string, validateCode = '') { key: ONYXKEYS.PENDING_CONTACT_ACTION, value: { contactMethod, - isVerifiedValidateActionCode: false, isLoading: true, errorFields: { actionVerified: null, @@ -393,6 +392,8 @@ function addNewContactMethod(contactMethod: string, validateCode = '') { value: { actionVerified: true, isLoading: false, + isVerifiedValidateActionCode: false, + validateActionCode: null, }, }, { diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx index 124c19819ef4b..42f917d36ece0 100644 --- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx +++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx @@ -98,7 +98,7 @@ function NewContactMethodPage({route}: NewContactMethodPageProps) { if (!pendingContactAction) { return; } - if (pendingContactAction?.validateActionCode && pendingContactAction.isVerifiedValidateActionCode) { + if ((pendingContactAction?.validateActionCode && pendingContactAction.isVerifiedValidateActionCode) || pendingContactAction?.actionVerified) { return; } navigateToConfirmMagicCode(); From 8e6516bd9beb972690646bbbdd4ae9755eedda60 Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Tue, 16 Dec 2025 15:59:34 +0700 Subject: [PATCH 09/22] fix API call bug --- .../settings/Profile/Contacts/ContactMethodsPage.tsx | 8 +++++++- .../settings/Profile/Contacts/NewContactMethodPage.tsx | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx index 674cd71b80a03..9fbdd3301d515 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx +++ b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx @@ -30,6 +30,8 @@ function ContactMethodsPage({route}: ContactMethodsPageProps) { const {translate} = useLocalize(); const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST, {canBeMissing: false}); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); + const [pendingContactAction] = useOnyx(ONYXKEYS.PENDING_CONTACT_ACTION, {canBeMissing: false}); + const isVerifiedValidateActionCode = pendingContactAction?.isVerifiedValidateActionCode; const navigateBackTo = route?.params?.backTo; const {isActingAsDelegate, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); @@ -54,8 +56,12 @@ function ContactMethodsPage({route}: ContactMethodsPageProps) { ); return; } + if (isVerifiedValidateActionCode) { + Navigation.navigate(ROUTES.SETTINGS_NEW_CONTACT_METHOD.getRoute(navigateBackTo)); + return; + } Navigation.navigate(ROUTES.SETTINGS_NEW_CONTACT_METHOD_CONFIRM_MAGIC_CODE.getRoute(navigateBackTo)); - }, [navigateBackTo, isActingAsDelegate, showDelegateNoAccessModal, isAccountLocked, isUserValidated, showLockedAccountModal]); + }, [navigateBackTo, isActingAsDelegate, showDelegateNoAccessModal, isAccountLocked, isUserValidated, showLockedAccountModal, isVerifiedValidateActionCode]); return ( Date: Thu, 18 Dec 2025 13:41:51 +0700 Subject: [PATCH 10/22] set server error and clear server error --- src/libs/actions/User.ts | 8 ++++++++ .../Profile/Contacts/NewContactMethodPage.tsx | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 1e57b2bc8f319..ed31a34516e27 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -51,6 +51,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {AppReview, BlockedFromConcierge, CustomStatusDraft, LoginList, Policy} from '@src/types/onyx'; import type Login from '@src/types/onyx/Login'; +import {Errors} from '@src/types/onyx/OnyxCommon'; import type {OnyxServerUpdate, OnyxUpdatesFromServer} from '@src/types/onyx/OnyxUpdatesFromServer'; import type OnyxPersonalDetails from '@src/types/onyx/PersonalDetails'; import type {Status} from '@src/types/onyx/PersonalDetails'; @@ -1613,8 +1614,15 @@ function verifyAddSecondaryLoginCode(validateCode: string) { API.write(WRITE_COMMANDS.VERIFY_ADD_SECONDARY_LOGIN_CODE, parameters, {optimisticData, successData, failureData}); } +function setServerErrorsOnForm(errors: Errors) { + Onyx.set(ONYXKEYS.FORMS.NEW_CONTACT_METHOD_FORM, { + errors, + }); +} + export { closeAccount, + setServerErrorsOnForm, dismissReferralBanner, dismissASAPSubmitExplanation, resendValidateCode, diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx index 524dd54e7c083..16f6d968d952b 100644 --- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx +++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx @@ -13,13 +13,13 @@ import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import {addErrorMessage} from '@libs/ErrorUtils'; +import {addErrorMessage, getLatestErrorField} from '@libs/ErrorUtils'; import {getPhoneLogin, validateNumber} from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import {addSMSDomainIfPhoneNumber} from '@libs/PhoneNumber'; -import {addNewContactMethod, clearUnvalidatedNewContactMethodAction} from '@userActions/User'; +import {addNewContactMethod, clearContactMethod, clearUnvalidatedNewContactMethodAction, setServerErrorsOnForm} from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -37,6 +37,18 @@ function NewContactMethodPage({route}: NewContactMethodPageProps) { const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); const [pendingContactAction] = useOnyx(ONYXKEYS.PENDING_CONTACT_ACTION, {canBeMissing: true}); const navigateBackTo = route?.params?.backTo; + const loginData = pendingContactAction?.contactMethod ? loginList?.[pendingContactAction?.contactMethod] : undefined; + const validateLoginError = getLatestErrorField(loginData, 'addedLogin'); + useEffect(() => { + setServerErrorsOnForm(validateLoginError); + }, [validateLoginError]); + useEffect(() => { + return () => { + if (pendingContactAction?.contactMethod) { + clearContactMethod(pendingContactAction?.contactMethod) + } + } + }, []); const handleAddSecondaryLogin = useCallback( (values: FormOnyxValues) => { From 16f1cbe698d339fe5fae68ba0fdf94084bfe2eee Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Thu, 18 Dec 2025 13:52:35 +0700 Subject: [PATCH 11/22] resolve eslint problem --- src/libs/actions/User.ts | 2 +- .../settings/Profile/Contacts/NewContactMethodPage.tsx | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index ed31a34516e27..5bfc1738354a7 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -51,7 +51,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {AppReview, BlockedFromConcierge, CustomStatusDraft, LoginList, Policy} from '@src/types/onyx'; import type Login from '@src/types/onyx/Login'; -import {Errors} from '@src/types/onyx/OnyxCommon'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; import type {OnyxServerUpdate, OnyxUpdatesFromServer} from '@src/types/onyx/OnyxUpdatesFromServer'; import type OnyxPersonalDetails from '@src/types/onyx/PersonalDetails'; import type {Status} from '@src/types/onyx/PersonalDetails'; diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx index 16f6d968d952b..5298810377136 100644 --- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx +++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx @@ -44,11 +44,12 @@ function NewContactMethodPage({route}: NewContactMethodPageProps) { }, [validateLoginError]); useEffect(() => { return () => { - if (pendingContactAction?.contactMethod) { - clearContactMethod(pendingContactAction?.contactMethod) + if (!pendingContactAction?.contactMethod) { + return; } - } - }, []); + clearContactMethod(pendingContactAction?.contactMethod); + }; + }, [pendingContactAction?.contactMethod]); const handleAddSecondaryLogin = useCallback( (values: FormOnyxValues) => { From c310db0be866d134a3817d01b585789977a475c5 Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Mon, 22 Dec 2025 17:01:18 +0700 Subject: [PATCH 12/22] prevent submitting if server error --- src/components/Form/FormProvider.tsx | 6 +++++- .../settings/Profile/Contacts/NewContactMethodPage.tsx | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 32cb0c0c48a3f..5d66326edf3ba 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -247,6 +247,10 @@ function FormProvider({ touchedInputs.current[inputID] = true; } + if (hasServerError) { + return; + } + // Validate form and return early if any errors are found if (!isEmptyObject(onValidate(trimmedStringValues))) { return; @@ -258,7 +262,7 @@ function FormProvider({ } KeyboardUtils.dismiss().then(() => onSubmit(trimmedStringValues)); - }, [enabledWhenOffline, formState?.isLoading, inputValues, isLoading, network?.isOffline, onSubmit, onValidate, shouldTrimValues]), + }, [enabledWhenOffline, formState?.isLoading, inputValues, isLoading, network?.isOffline, onSubmit, onValidate, shouldTrimValues, hasServerError]), 1000, {leading: true, trailing: false}, ); diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx index bf1eb85c2d647..9b9de1ebb3633 100644 --- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx +++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx @@ -26,6 +26,7 @@ import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/NewContactMethodForm'; import type {Errors} from '@src/types/onyx/OnyxCommon'; +import { isEmptyObject } from '@src/types/utils/EmptyObject'; type NewContactMethodPageProps = PlatformStackScreenProps; @@ -44,7 +45,7 @@ function NewContactMethodPage({route}: NewContactMethodPageProps) { }, [validateLoginError]); useEffect(() => { return () => { - if (!pendingContactAction?.contactMethod) { + if (!pendingContactAction?.contactMethod || isEmptyObject(validateLoginError)) { return; } clearContactMethod(pendingContactAction?.contactMethod); From 203da9693606162d2eb1c4a1ce006b2dd8e2b2d0 Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Mon, 22 Dec 2025 17:28:14 +0700 Subject: [PATCH 13/22] remove failed contact list --- src/libs/actions/User.ts | 17 ++++++++++++----- .../Contacts/ContactMethodDetailsPage.tsx | 2 +- .../Profile/Contacts/NewContactMethodPage.tsx | 17 ++++++++++++----- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 5bfc1738354a7..5f41aedd27ca7 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -281,12 +281,19 @@ function deleteContactMethod(contactMethod: string, loginList: Record>((acc, method) => { + acc[method] = null; + return acc; + }, {}); + + Onyx.merge(ONYXKEYS.LOGIN_LIST, loginsToClear); } /** diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx index 499b4a571e3e9..017775e313dbb 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx +++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx @@ -322,7 +322,7 @@ function ContactMethodDetailsPage({route}: ContactMethodDetailsPageProps) { errors={getLatestErrorField(loginData, 'addedLogin')} errorRowStyles={[themeStyles.mh5, themeStyles.mv3]} onDismiss={() => { - clearContactMethod(contactMethod); + clearContactMethod([contactMethod]); clearUnvalidatedNewContactMethodAction(); Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(backTo)); }} diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx index 9b9de1ebb3633..5f407fd796983 100644 --- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx +++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx @@ -26,7 +26,7 @@ import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/NewContactMethodForm'; import type {Errors} from '@src/types/onyx/OnyxCommon'; -import { isEmptyObject } from '@src/types/utils/EmptyObject'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; type NewContactMethodPageProps = PlatformStackScreenProps; @@ -40,17 +40,24 @@ function NewContactMethodPage({route}: NewContactMethodPageProps) { const navigateBackTo = route?.params?.backTo; const loginData = pendingContactAction?.contactMethod ? loginList?.[pendingContactAction?.contactMethod] : undefined; const validateLoginError = getLatestErrorField(loginData, 'addedLogin'); + useEffect(() => { setServerErrorsOnForm(validateLoginError); }, [validateLoginError]); useEffect(() => { return () => { - if (!pendingContactAction?.contactMethod || isEmptyObject(validateLoginError)) { - return; + if (loginList) { + const removedLogin: string[] = []; + Object.keys(loginList).forEach((login) => { + const error = getLatestErrorField(loginList?.[login], 'addedLogin'); + if (!isEmptyObject(error)) { + removedLogin.push(login); + } + }); + clearContactMethod(removedLogin); } - clearContactMethod(pendingContactAction?.contactMethod); }; - }, [pendingContactAction?.contactMethod]); + }, [loginList]); const handleAddSecondaryLogin = useCallback( (values: FormOnyxValues) => { From 4e865863d04de1fbd8e8a39ce2740b99889598fd Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Mon, 22 Dec 2025 17:35:08 +0700 Subject: [PATCH 14/22] Using for of --- .../Profile/Contacts/NewContactMethodPage.tsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx index 5f407fd796983..ff490837b2842 100644 --- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx +++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx @@ -46,16 +46,17 @@ function NewContactMethodPage({route}: NewContactMethodPageProps) { }, [validateLoginError]); useEffect(() => { return () => { - if (loginList) { - const removedLogin: string[] = []; - Object.keys(loginList).forEach((login) => { - const error = getLatestErrorField(loginList?.[login], 'addedLogin'); - if (!isEmptyObject(error)) { - removedLogin.push(login); - } - }); - clearContactMethod(removedLogin); + if (!loginList) { + return; } + const removedLogin: string[] = []; + for (const login of Object.keys(loginList)) { + const error = getLatestErrorField(loginList?.[login], 'addedLogin'); + if (!isEmptyObject(error)) { + removedLogin.push(login); + } + } + clearContactMethod(removedLogin); }; }, [loginList]); From 62e295cec39ef74fd12736094211fa841dbd834a Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Sat, 27 Dec 2025 16:20:00 +0700 Subject: [PATCH 15/22] Create new file to add Unit test action/Users --- tests/actions/UserTest.ts | 563 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 563 insertions(+) create mode 100644 tests/actions/UserTest.ts diff --git a/tests/actions/UserTest.ts b/tests/actions/UserTest.ts new file mode 100644 index 0000000000000..6e193ebaf4b4f --- /dev/null +++ b/tests/actions/UserTest.ts @@ -0,0 +1,563 @@ +/* eslint-disable no-restricted-syntax */ +import Onyx from 'react-native-onyx'; +import * as API from '@libs/API'; +import {WRITE_COMMANDS} from '@libs/API/types'; +import ONYXKEYS from '@src/ONYXKEYS'; +import * as UserActions from '../../src/libs/actions/User'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +jest.mock('@libs/API'); +const mockAPI = API as jest.Mocked; + +describe('actions/User', () => { + beforeAll(() => { + Onyx.init({ + keys: ONYXKEYS, + }); + }); + + beforeEach(() => { + jest.clearAllMocks(); + return Onyx.clear().then(waitForBatchedUpdates); + }); + + describe('clearContactMethod', () => { + it('should return early when contactMethods array is empty', async () => { + // Given an empty array + const contactMethods: string[] = []; + + // When clearContactMethod is called + UserActions.clearContactMethod(contactMethods); + await waitForBatchedUpdates(); + + // Then LOGIN_LIST should remain unchanged (null/undefined) + const loginList = await new Promise | null>((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.LOGIN_LIST, + callback: (value) => { + Onyx.disconnect(connection); + resolve(value ?? null); + }, + }); + }); + + expect(loginList).toBeNull(); + }); + + it('should clear a single contact method from LOGIN_LIST', async () => { + // Given a login list with a contact method + const contactMethod = 'test@example.com'; + const initialLoginList = { + [contactMethod]: { + partnerUserID: contactMethod, + validatedDate: '2024-01-01', + }, + }; + + await Onyx.merge(ONYXKEYS.LOGIN_LIST, initialLoginList); + await waitForBatchedUpdates(); + + // When clearContactMethod is called with that contact method + UserActions.clearContactMethod([contactMethod]); + await waitForBatchedUpdates(); + + // Then the contact method should be set to null in LOGIN_LIST + const loginList = await new Promise | null>((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.LOGIN_LIST, + callback: (value) => { + Onyx.disconnect(connection); + resolve(value ?? null); + }, + }); + }); + + expect(loginList).toEqual({}); + }); + + it('should clear multiple contact methods from LOGIN_LIST', async () => { + // Given a login list with multiple contact methods + const contactMethod1 = 'test1@example.com'; + const contactMethod2 = 'test2@example.com'; + const contactMethod3 = 'test3@example.com'; + const initialLoginList = { + [contactMethod1]: { + partnerUserID: contactMethod1, + validatedDate: '2024-01-01', + }, + [contactMethod2]: { + partnerUserID: contactMethod2, + validatedDate: '2024-01-02', + }, + [contactMethod3]: { + partnerUserID: contactMethod3, + validatedDate: '2024-01-03', + }, + }; + + await Onyx.merge(ONYXKEYS.LOGIN_LIST, initialLoginList); + await waitForBatchedUpdates(); + + // When clearContactMethod is called with multiple contact methods + UserActions.clearContactMethod([contactMethod1, contactMethod3]); + await waitForBatchedUpdates(); + + // Then the specified contact methods should be set to null, while others remain unchanged + const loginList = await new Promise | null>((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.LOGIN_LIST, + callback: (value) => { + Onyx.disconnect(connection); + resolve(value ?? null); + }, + }); + }); + + expect(loginList).toEqual({ + [contactMethod2]: { + partnerUserID: contactMethod2, + validatedDate: '2024-01-02', + }, + }); + }); + + it('should handle clearing contact methods that do not exist in LOGIN_LIST', async () => { + // Given a login list with some contact methods + const existingContactMethod = 'existing@example.com'; + const nonExistentContactMethod = 'nonexistent@example.com'; + const initialLoginList = { + [existingContactMethod]: { + partnerUserID: existingContactMethod, + validatedDate: '2024-01-01', + }, + }; + + await Onyx.merge(ONYXKEYS.LOGIN_LIST, initialLoginList); + await waitForBatchedUpdates(); + + // When clearContactMethod is called with both existing and non-existent contact methods + UserActions.clearContactMethod([existingContactMethod, nonExistentContactMethod]); + await waitForBatchedUpdates(); + + // Then both should be set to null in LOGIN_LIST + const loginList = await new Promise | null>((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.LOGIN_LIST, + callback: (value) => { + Onyx.disconnect(connection); + resolve(value ?? null); + }, + }); + }); + + expect(loginList).toEqual({}); + }); + }); + + describe('verifyAddSecondaryLoginCode', () => { + it('should call API.write with correct parameters and reset validateCodeSent', async () => { + // Given a validate code + const validateCode = '123456'; + + // Set initial state for VALIDATE_ACTION_CODE + await Onyx.merge(ONYXKEYS.VALIDATE_ACTION_CODE, { + validateCodeSent: true, + }); + await waitForBatchedUpdates(); + + // When verifyAddSecondaryLoginCode is called + UserActions.verifyAddSecondaryLoginCode(validateCode); + await waitForBatchedUpdates(); + + // Then API.write should be called with correct parameters + expect(mockAPI.write).toHaveBeenCalledWith( + WRITE_COMMANDS.VERIFY_ADD_SECONDARY_LOGIN_CODE, + {validateCode}, + expect.objectContaining({ + optimisticData: expect.any(Array), + successData: expect.any(Array), + failureData: expect.any(Array), + }), + ); + + // Verify validateCodeSent is reset to false + const validateActionCode = await new Promise<{validateCodeSent?: boolean} | null>((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.VALIDATE_ACTION_CODE, + callback: (value) => { + Onyx.disconnect(connection); + resolve(value ?? null); + }, + }); + }); + + expect(validateActionCode?.validateCodeSent).toBe(false); + }); + + it('should apply optimisticData correctly', async () => { + // Given a validate code + const validateCode = '123456'; + + // When verifyAddSecondaryLoginCode is called + UserActions.verifyAddSecondaryLoginCode(validateCode); + await waitForBatchedUpdates(); + + // Then verify the optimisticData structure + // eslint-disable-next-line rulesdir/no-multiple-api-calls + const calls = (mockAPI.write as jest.Mock).mock.calls; + const [, , onyxData] = calls[0] as [unknown, unknown, {optimisticData?: Array<{key: string; value: unknown}>}]; + const optimisticData = onyxData.optimisticData ?? []; + + expect(optimisticData).toHaveLength(1); + expect(optimisticData[0]).toEqual({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PENDING_CONTACT_ACTION, + value: { + validateActionCode: validateCode, + isLoading: true, + errorFields: { + validateActionCode: null, + }, + }, + }); + }); + + it('should have correct successData structure', async () => { + // Given a validate code + const validateCode = '123456'; + + // When verifyAddSecondaryLoginCode is called + UserActions.verifyAddSecondaryLoginCode(validateCode); + await waitForBatchedUpdates(); + + // Then verify the successData structure + // eslint-disable-next-line rulesdir/no-multiple-api-calls + const calls = (mockAPI.write as jest.Mock).mock.calls; + const [, , onyxData] = calls[0] as [unknown, unknown, {successData?: Array<{key: string; value: unknown}>}]; + const successData = onyxData.successData ?? []; + + expect(successData).toHaveLength(1); + expect(successData[0]).toEqual({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PENDING_CONTACT_ACTION, + value: { + isVerifiedValidateActionCode: true, + isLoading: false, + }, + }); + }); + + it('should have correct failureData structure', async () => { + // Given a validate code + const validateCode = '123456'; + + // When verifyAddSecondaryLoginCode is called + UserActions.verifyAddSecondaryLoginCode(validateCode); + await waitForBatchedUpdates(); + + // Then verify the failureData structure + // eslint-disable-next-line rulesdir/no-multiple-api-calls + const calls = (mockAPI.write as jest.Mock).mock.calls; + const [, , onyxData] = calls[0] as [unknown, unknown, {failureData?: Array<{key: string; value: unknown}>}]; + const failureData = onyxData.failureData ?? []; + + expect(failureData).toHaveLength(1); + expect(failureData[0]).toEqual({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PENDING_CONTACT_ACTION, + value: { + isVerifiedValidateActionCode: false, + isLoading: false, + }, + }); + }); + + it('should apply optimisticData to Onyx when API.write applies it', async () => { + // Given a validate code and mock API.write that applies optimisticData + const validateCode = '123456'; + + // Mock API.write to apply optimisticData + // eslint-disable-next-line rulesdir/no-multiple-api-calls + (mockAPI.write as jest.Mock).mockImplementation((command, params, options) => { + if (options?.optimisticData) { + for (const update of options.optimisticData) { + if (update.onyxMethod === Onyx.METHOD.MERGE) { + Onyx.merge(update.key, update.value); + } + } + } + return Promise.resolve(); + }); + + // When verifyAddSecondaryLoginCode is called + UserActions.verifyAddSecondaryLoginCode(validateCode); + await waitForBatchedUpdates(); + + // Then PENDING_CONTACT_ACTION should be updated with optimistic data + const pendingContactAction = await new Promise | null>((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.PENDING_CONTACT_ACTION, + callback: (value) => { + Onyx.disconnect(connection); + resolve(value ?? null); + }, + }); + }); + + expect(pendingContactAction).toEqual({ + validateActionCode: validateCode, + isLoading: true, + errorFields: {}, + }); + }); + }); + + describe('addNewContactMethod', () => { + it('should call API.write with correct parameters when validateCode is provided', async () => { + // Given a contact method and validate code + const contactMethod = 'test@example.com'; + const validateCode = '123456'; + + // When addNewContactMethod is called + UserActions.addNewContactMethod(contactMethod, validateCode); + await waitForBatchedUpdates(); + + // Then API.write should be called with correct parameters + expect(mockAPI.write).toHaveBeenCalledWith( + WRITE_COMMANDS.ADD_NEW_CONTACT_METHOD, + {partnerUserID: contactMethod, validateCode}, + expect.objectContaining({ + optimisticData: expect.any(Array), + successData: expect.any(Array), + failureData: expect.any(Array), + }), + ); + }); + + it('should call API.write with empty validateCode when validateCode is not provided', async () => { + // Given a contact method without validate code + const contactMethod = 'test@example.com'; + + // When addNewContactMethod is called without validateCode + UserActions.addNewContactMethod(contactMethod); + await waitForBatchedUpdates(); + + // Then API.write should be called with empty validateCode + expect(mockAPI.write).toHaveBeenCalledWith( + WRITE_COMMANDS.ADD_NEW_CONTACT_METHOD, + {partnerUserID: contactMethod, validateCode: ''}, + expect.objectContaining({ + optimisticData: expect.any(Array), + successData: expect.any(Array), + failureData: expect.any(Array), + }), + ); + }); + + it('should have correct optimisticData structure', async () => { + // Given a contact method + const contactMethod = 'test@example.com'; + + // When addNewContactMethod is called + UserActions.addNewContactMethod(contactMethod); + await waitForBatchedUpdates(); + + // Then verify the optimisticData structure + // eslint-disable-next-line rulesdir/no-multiple-api-calls + const calls = (mockAPI.write as jest.Mock).mock.calls; + const [, , onyxData] = calls[0] as [unknown, unknown, {optimisticData?: Array<{key: string; value: unknown}>}]; + const optimisticData = onyxData.optimisticData ?? []; + + expect(optimisticData).toHaveLength(3); + + // Verify LOGIN_LIST update + const loginListUpdate = optimisticData.find((update) => update.key === ONYXKEYS.LOGIN_LIST); + expect(loginListUpdate).toEqual({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.LOGIN_LIST, + value: { + [contactMethod]: { + partnerUserID: contactMethod, + validatedDate: '', + errorFields: { + "addedLogin": null, + }, + }, + }, + }); + + // Verify ACCOUNT update + const accountUpdate = optimisticData.find((update) => update.key === ONYXKEYS.ACCOUNT); + expect(accountUpdate).toEqual({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.ACCOUNT, + value: {isLoading: true}, + }); + + // Verify PENDING_CONTACT_ACTION update + const pendingContactActionUpdate = optimisticData.find((update) => update.key === ONYXKEYS.PENDING_CONTACT_ACTION); + expect(pendingContactActionUpdate).toEqual({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PENDING_CONTACT_ACTION, + value: { + contactMethod, + isLoading: true, + errorFields: { + actionVerified: null, + }, + }, + }); + }); + + it('should have correct successData structure', async () => { + // Given a contact method + const contactMethod = 'test@example.com'; + + // When addNewContactMethod is called + UserActions.addNewContactMethod(contactMethod); + await waitForBatchedUpdates(); + + // Then verify the successData structure + // eslint-disable-next-line rulesdir/no-multiple-api-calls + const calls = (mockAPI.write as jest.Mock).mock.calls; + const [, , onyxData] = calls[0] as [unknown, unknown, {successData?: Array<{key: string; value: unknown}>}]; + const successData = onyxData.successData ?? []; + + expect(successData).toHaveLength(2); + + // Verify PENDING_CONTACT_ACTION success update + const pendingContactActionUpdate = successData.find((update) => update.key === ONYXKEYS.PENDING_CONTACT_ACTION); + expect(pendingContactActionUpdate).toEqual({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PENDING_CONTACT_ACTION, + value: { + actionVerified: true, + isLoading: false, + isVerifiedValidateActionCode: false, + validateActionCode: null, + }, + }); + + // Verify ACCOUNT success update + const accountUpdate = successData.find((update) => update.key === ONYXKEYS.ACCOUNT); + expect(accountUpdate).toEqual({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.ACCOUNT, + value: {isLoading: false}, + }); + }); + + it('should have correct failureData structure', async () => { + // Given a contact method + const contactMethod = 'test@example.com'; + + // When addNewContactMethod is called + UserActions.addNewContactMethod(contactMethod); + await waitForBatchedUpdates(); + + // Then verify the failureData structure + // eslint-disable-next-line rulesdir/no-multiple-api-calls + const calls = (mockAPI.write as jest.Mock).mock.calls; + const [, , onyxData] = calls[0] as [unknown, unknown, {failureData?: Array<{key: string; value: unknown}>}]; + const failureData = onyxData.failureData ?? []; + + expect(failureData).toHaveLength(3); + + // Verify ACCOUNT failure update + const accountUpdate = failureData.find((update) => update.key === ONYXKEYS.ACCOUNT); + expect(accountUpdate).toEqual({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.ACCOUNT, + value: {isLoading: false}, + }); + + // Verify VALIDATE_ACTION_CODE failure update + const validateActionCodeUpdate = failureData.find((update) => update.key === ONYXKEYS.VALIDATE_ACTION_CODE); + expect(validateActionCodeUpdate).toEqual({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.VALIDATE_ACTION_CODE, + value: {validateCodeSent: null}, + }); + + // Verify PENDING_CONTACT_ACTION failure update + const pendingContactActionUpdate = failureData.find((update) => update.key === ONYXKEYS.PENDING_CONTACT_ACTION); + expect(pendingContactActionUpdate).toEqual({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PENDING_CONTACT_ACTION, + value: { + isLoading: false, + actionVerified: false, + }, + }); + }); + + it('should apply optimisticData to Onyx when API.write applies it', async () => { + // Given a contact method and mock API.write that applies optimisticData + const contactMethod = 'test@example.com'; + + // Mock API.write to apply optimisticData + // eslint-disable-next-line rulesdir/no-multiple-api-calls + (mockAPI.write as jest.Mock).mockImplementation((command, params, options) => { + if (options?.optimisticData) { + for (const update of options.optimisticData) { + if (update.onyxMethod === Onyx.METHOD.MERGE) { + Onyx.merge(update.key, update.value); + } + } + } + return Promise.resolve(); + }); + + // When addNewContactMethod is called + UserActions.addNewContactMethod(contactMethod); + await waitForBatchedUpdates(); + + // Then LOGIN_LIST should be updated with the new contact method + const loginList = await new Promise | null>((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.LOGIN_LIST, + callback: (value) => { + Onyx.disconnect(connection); + resolve(value ?? null); + }, + }); + }); + + expect(loginList?.[contactMethod]).toEqual({ + partnerUserID: contactMethod, + validatedDate: '', + errorFields: {}, + }); + + // Then ACCOUNT should have isLoading: true + const account = await new Promise<{isLoading?: boolean} | null>((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.ACCOUNT, + callback: (value) => { + Onyx.disconnect(connection); + resolve(value ?? null); + }, + }); + }); + + expect(account?.isLoading).toBe(true); + + // Then PENDING_CONTACT_ACTION should be updated + const pendingContactAction = await new Promise | null>((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.PENDING_CONTACT_ACTION, + callback: (value) => { + Onyx.disconnect(connection); + resolve(value ?? null); + }, + }); + }); + + expect(pendingContactAction).toEqual({ + contactMethod, + isLoading: true, + errorFields: {}, + }); + }); + }); +}); From 46df70a718d42239e356e599751f3f2b0365c803 Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Sat, 27 Dec 2025 16:24:00 +0700 Subject: [PATCH 16/22] update Unit test action/Users --- tests/actions/UserTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/actions/UserTest.ts b/tests/actions/UserTest.ts index 6e193ebaf4b4f..59a004abd65b1 100644 --- a/tests/actions/UserTest.ts +++ b/tests/actions/UserTest.ts @@ -380,7 +380,7 @@ describe('actions/User', () => { partnerUserID: contactMethod, validatedDate: '', errorFields: { - "addedLogin": null, + addedLogin: null, }, }, }, From 767472af9f5fbce6565b410a41ff13422ee73c6f Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Sat, 27 Dec 2025 16:39:30 +0700 Subject: [PATCH 17/22] fixing the eslint in new file --- tests/actions/UserTest.ts | 58 ++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/tests/actions/UserTest.ts b/tests/actions/UserTest.ts index 59a004abd65b1..45aa237319bbe 100644 --- a/tests/actions/UserTest.ts +++ b/tests/actions/UserTest.ts @@ -1,7 +1,9 @@ /* eslint-disable no-restricted-syntax */ import Onyx from 'react-native-onyx'; +import type {OnyxMergeInput} from 'react-native-onyx'; import * as API from '@libs/API'; import {WRITE_COMMANDS} from '@libs/API/types'; +import type {OnyxKey} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; import * as UserActions from '../../src/libs/actions/User'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; @@ -174,9 +176,9 @@ describe('actions/User', () => { WRITE_COMMANDS.VERIFY_ADD_SECONDARY_LOGIN_CODE, {validateCode}, expect.objectContaining({ - optimisticData: expect.any(Array), - successData: expect.any(Array), - failureData: expect.any(Array), + optimisticData: expect.any(Array) as Array<{key: string; value: unknown}>, + successData: expect.any(Array) as Array<{key: string; value: unknown}>, + failureData: expect.any(Array) as Array<{key: string; value: unknown}>, }), ); @@ -205,11 +207,11 @@ describe('actions/User', () => { // Then verify the optimisticData structure // eslint-disable-next-line rulesdir/no-multiple-api-calls const calls = (mockAPI.write as jest.Mock).mock.calls; - const [, , onyxData] = calls[0] as [unknown, unknown, {optimisticData?: Array<{key: string; value: unknown}>}]; + const [, , onyxData] = calls.at(0) as [unknown, unknown, {optimisticData?: Array<{key: string; value: unknown}>}]; const optimisticData = onyxData.optimisticData ?? []; expect(optimisticData).toHaveLength(1); - expect(optimisticData[0]).toEqual({ + expect(optimisticData.at(0)).toEqual({ onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.PENDING_CONTACT_ACTION, value: { @@ -233,11 +235,11 @@ describe('actions/User', () => { // Then verify the successData structure // eslint-disable-next-line rulesdir/no-multiple-api-calls const calls = (mockAPI.write as jest.Mock).mock.calls; - const [, , onyxData] = calls[0] as [unknown, unknown, {successData?: Array<{key: string; value: unknown}>}]; + const [, , onyxData] = calls.at(0) as [unknown, unknown, {successData?: Array<{key: string; value: unknown}>}]; const successData = onyxData.successData ?? []; expect(successData).toHaveLength(1); - expect(successData[0]).toEqual({ + expect(successData.at(0)).toEqual({ onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.PENDING_CONTACT_ACTION, value: { @@ -258,11 +260,11 @@ describe('actions/User', () => { // Then verify the failureData structure // eslint-disable-next-line rulesdir/no-multiple-api-calls const calls = (mockAPI.write as jest.Mock).mock.calls; - const [, , onyxData] = calls[0] as [unknown, unknown, {failureData?: Array<{key: string; value: unknown}>}]; + const [, , onyxData] = calls.at(0) as [unknown, unknown, {failureData?: Array<{key: string; value: unknown}>}]; const failureData = onyxData.failureData ?? []; expect(failureData).toHaveLength(1); - expect(failureData[0]).toEqual({ + expect(failureData.at(0)).toEqual({ onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.PENDING_CONTACT_ACTION, value: { @@ -278,11 +280,17 @@ describe('actions/User', () => { // Mock API.write to apply optimisticData // eslint-disable-next-line rulesdir/no-multiple-api-calls - (mockAPI.write as jest.Mock).mockImplementation((command, params, options) => { + (mockAPI.write as jest.Mock).mockImplementation(( + command: unknown, + params: unknown, + options?: { + optimisticData?: Array<{onyxMethod: typeof Onyx.METHOD.MERGE; key: string; value: unknown}>; + }, + ) => { if (options?.optimisticData) { for (const update of options.optimisticData) { if (update.onyxMethod === Onyx.METHOD.MERGE) { - Onyx.merge(update.key, update.value); + Onyx.merge(update.key as OnyxKey, update.value as OnyxMergeInput); } } } @@ -327,9 +335,9 @@ describe('actions/User', () => { WRITE_COMMANDS.ADD_NEW_CONTACT_METHOD, {partnerUserID: contactMethod, validateCode}, expect.objectContaining({ - optimisticData: expect.any(Array), - successData: expect.any(Array), - failureData: expect.any(Array), + optimisticData: expect.any(Array) as Array<{key: string; value: unknown}>, + successData: expect.any(Array) as Array<{key: string; value: unknown}>, + failureData: expect.any(Array) as Array<{key: string; value: unknown}>, }), ); }); @@ -347,9 +355,9 @@ describe('actions/User', () => { WRITE_COMMANDS.ADD_NEW_CONTACT_METHOD, {partnerUserID: contactMethod, validateCode: ''}, expect.objectContaining({ - optimisticData: expect.any(Array), - successData: expect.any(Array), - failureData: expect.any(Array), + optimisticData: expect.any(Array) as Array<{key: string; value: unknown}>, + successData: expect.any(Array) as Array<{key: string; value: unknown}>, + failureData: expect.any(Array) as Array<{key: string; value: unknown}>, }), ); }); @@ -365,7 +373,7 @@ describe('actions/User', () => { // Then verify the optimisticData structure // eslint-disable-next-line rulesdir/no-multiple-api-calls const calls = (mockAPI.write as jest.Mock).mock.calls; - const [, , onyxData] = calls[0] as [unknown, unknown, {optimisticData?: Array<{key: string; value: unknown}>}]; + const [, , onyxData] = calls.at(0) as [unknown, unknown, {optimisticData?: Array<{key: string; value: unknown}>}]; const optimisticData = onyxData.optimisticData ?? []; expect(optimisticData).toHaveLength(3); @@ -420,7 +428,7 @@ describe('actions/User', () => { // Then verify the successData structure // eslint-disable-next-line rulesdir/no-multiple-api-calls const calls = (mockAPI.write as jest.Mock).mock.calls; - const [, , onyxData] = calls[0] as [unknown, unknown, {successData?: Array<{key: string; value: unknown}>}]; + const [, , onyxData] = calls.at(0) as [unknown, unknown, {successData?: Array<{key: string; value: unknown}>}]; const successData = onyxData.successData ?? []; expect(successData).toHaveLength(2); @@ -458,7 +466,7 @@ describe('actions/User', () => { // Then verify the failureData structure // eslint-disable-next-line rulesdir/no-multiple-api-calls const calls = (mockAPI.write as jest.Mock).mock.calls; - const [, , onyxData] = calls[0] as [unknown, unknown, {failureData?: Array<{key: string; value: unknown}>}]; + const [, , onyxData] = calls.at(0) as [unknown, unknown, {failureData?: Array<{key: string; value: unknown}>}]; const failureData = onyxData.failureData ?? []; expect(failureData).toHaveLength(3); @@ -497,11 +505,17 @@ describe('actions/User', () => { // Mock API.write to apply optimisticData // eslint-disable-next-line rulesdir/no-multiple-api-calls - (mockAPI.write as jest.Mock).mockImplementation((command, params, options) => { + (mockAPI.write as jest.Mock).mockImplementation(( + command: unknown, + params: unknown, + options?: { + optimisticData?: Array<{onyxMethod: typeof Onyx.METHOD.MERGE; key: string; value: unknown}>; + }, + ) => { if (options?.optimisticData) { for (const update of options.optimisticData) { if (update.onyxMethod === Onyx.METHOD.MERGE) { - Onyx.merge(update.key, update.value); + Onyx.merge(update.key as OnyxKey, update.value as OnyxMergeInput); } } } From 05c0a38427ac81b2e7e854c3130e78293707b986 Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Sat, 27 Dec 2025 17:07:02 +0700 Subject: [PATCH 18/22] update optimistic data type --- src/libs/actions/User.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index bb04a42b4d7ec..b384c5e870b7a 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -362,7 +362,7 @@ function clearPendingContactActionErrors() { * Adds a secondary login to a user's account */ function addNewContactMethod(contactMethod: string, validateCode = '') { - const optimisticData: Array> = [ + const optimisticData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.LOGIN_LIST, @@ -410,7 +410,7 @@ function addNewContactMethod(contactMethod: string, validateCode = '') { value: {isLoading: false}, }, ]; - const failureData: Array> = [ + const failureData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.ACCOUNT, From dd07a8d1893cbbb26f81eb23529ef9f97238c09a Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Sat, 27 Dec 2025 17:07:48 +0700 Subject: [PATCH 19/22] fixed prettier --- tests/actions/UserTest.ts | 60 +++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/tests/actions/UserTest.ts b/tests/actions/UserTest.ts index 45aa237319bbe..73bc4c694020b 100644 --- a/tests/actions/UserTest.ts +++ b/tests/actions/UserTest.ts @@ -280,22 +280,24 @@ describe('actions/User', () => { // Mock API.write to apply optimisticData // eslint-disable-next-line rulesdir/no-multiple-api-calls - (mockAPI.write as jest.Mock).mockImplementation(( - command: unknown, - params: unknown, - options?: { - optimisticData?: Array<{onyxMethod: typeof Onyx.METHOD.MERGE; key: string; value: unknown}>; - }, - ) => { - if (options?.optimisticData) { - for (const update of options.optimisticData) { - if (update.onyxMethod === Onyx.METHOD.MERGE) { - Onyx.merge(update.key as OnyxKey, update.value as OnyxMergeInput); + (mockAPI.write as jest.Mock).mockImplementation( + ( + command: unknown, + params: unknown, + options?: { + optimisticData?: Array<{onyxMethod: typeof Onyx.METHOD.MERGE; key: string; value: unknown}>; + }, + ) => { + if (options?.optimisticData) { + for (const update of options.optimisticData) { + if (update.onyxMethod === Onyx.METHOD.MERGE) { + Onyx.merge(update.key as OnyxKey, update.value as OnyxMergeInput); + } } } - } - return Promise.resolve(); - }); + return Promise.resolve(); + }, + ); // When verifyAddSecondaryLoginCode is called UserActions.verifyAddSecondaryLoginCode(validateCode); @@ -505,22 +507,24 @@ describe('actions/User', () => { // Mock API.write to apply optimisticData // eslint-disable-next-line rulesdir/no-multiple-api-calls - (mockAPI.write as jest.Mock).mockImplementation(( - command: unknown, - params: unknown, - options?: { - optimisticData?: Array<{onyxMethod: typeof Onyx.METHOD.MERGE; key: string; value: unknown}>; - }, - ) => { - if (options?.optimisticData) { - for (const update of options.optimisticData) { - if (update.onyxMethod === Onyx.METHOD.MERGE) { - Onyx.merge(update.key as OnyxKey, update.value as OnyxMergeInput); + (mockAPI.write as jest.Mock).mockImplementation( + ( + command: unknown, + params: unknown, + options?: { + optimisticData?: Array<{onyxMethod: typeof Onyx.METHOD.MERGE; key: string; value: unknown}>; + }, + ) => { + if (options?.optimisticData) { + for (const update of options.optimisticData) { + if (update.onyxMethod === Onyx.METHOD.MERGE) { + Onyx.merge(update.key as OnyxKey, update.value as OnyxMergeInput); + } } } - } - return Promise.resolve(); - }); + return Promise.resolve(); + }, + ); // When addNewContactMethod is called UserActions.addNewContactMethod(contactMethod); From 2bd065d999975a2c34694dfddfded30b81e1f244 Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Tue, 6 Jan 2026 19:53:02 +0700 Subject: [PATCH 20/22] feat: Add updateIsVerifiedValidateActionCode function and integrate it into NewContactMethodPage for error handling --- src/libs/actions/User.ts | 7 +++++ .../Profile/Contacts/ContactMethodsPage.tsx | 8 +---- .../Profile/Contacts/NewContactMethodPage.tsx | 30 ++++++++----------- 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 47a978dc79c94..e088ab63bd6ef 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -1626,6 +1626,12 @@ function setServerErrorsOnForm(errors: Errors) { }); } +function updateIsVerifiedValidateActionCode(isVerifiedValidateActionCode: boolean) { + Onyx.merge(ONYXKEYS.PENDING_CONTACT_ACTION, { + isVerifiedValidateActionCode, + }); +} + export { closeAccount, setServerErrorsOnForm, @@ -1668,4 +1674,5 @@ export { requestUnlockAccount, respondToProactiveAppReview, verifyAddSecondaryLoginCode, + updateIsVerifiedValidateActionCode, }; diff --git a/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx index a8daa9bc8cb50..0c1b18732b8a1 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx +++ b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx @@ -30,8 +30,6 @@ function ContactMethodsPage({route}: ContactMethodsPageProps) { const {translate} = useLocalize(); const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST, {canBeMissing: false}); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); - const [pendingContactAction] = useOnyx(ONYXKEYS.PENDING_CONTACT_ACTION, {canBeMissing: false}); - const isVerifiedValidateActionCode = pendingContactAction?.isVerifiedValidateActionCode; const navigateBackTo = route?.params?.backTo; const {isActingAsDelegate, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); @@ -56,12 +54,8 @@ function ContactMethodsPage({route}: ContactMethodsPageProps) { ); return; } - if (isVerifiedValidateActionCode) { - Navigation.navigate(ROUTES.SETTINGS_NEW_CONTACT_METHOD.getRoute(navigateBackTo)); - return; - } Navigation.navigate(ROUTES.SETTINGS_NEW_CONTACT_METHOD_CONFIRM_MAGIC_CODE.getRoute(navigateBackTo)); - }, [navigateBackTo, isActingAsDelegate, showDelegateNoAccessModal, isAccountLocked, isUserValidated, showLockedAccountModal, isVerifiedValidateActionCode]); + }, [navigateBackTo, isActingAsDelegate, showDelegateNoAccessModal, isAccountLocked, isUserValidated, showLockedAccountModal]); return ( { - setServerErrorsOnForm(validateLoginError); - }, [validateLoginError]); + updateIsVerifiedValidateActionCode(false); + }, []); + useEffect(() => { + let error = validateLoginError; + if (isEmptyObject(error)) { + error = validateActionCodeError; + } + setServerErrorsOnForm(error); + }, [validateLoginError, validateActionCodeError]); useEffect(() => { return () => { if (!loginList) { @@ -104,21 +113,6 @@ function NewContactMethodPage({route}: NewContactMethodPageProps) { Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(navigateBackTo)); }, [navigateBackTo]); - const navigateToConfirmMagicCode = useCallback(() => { - clearUnvalidatedNewContactMethodAction(); - Navigation.navigate(ROUTES.SETTINGS_NEW_CONTACT_METHOD_CONFIRM_MAGIC_CODE.getRoute(navigateBackTo ?? '')); - }, [navigateBackTo]); - - useEffect(() => { - if (!pendingContactAction) { - return; - } - if ((pendingContactAction?.validateActionCode && pendingContactAction.isVerifiedValidateActionCode) || pendingContactAction?.actionVerified) { - return; - } - navigateToConfirmMagicCode(); - }, [navigateToConfirmMagicCode, pendingContactAction]); - useEffect(() => { if (!pendingContactAction?.actionVerified || !pendingContactAction?.contactMethod) { return; From 6297eaad96466f5924cf798fb6ab377066ed915c Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Tue, 6 Jan 2026 20:17:05 +0700 Subject: [PATCH 21/22] feat: Extend failureData structure to include LOGIN_LIST and update navigation logic in NewContactMethodConfirmMagicCodePage --- src/libs/actions/User.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index e088ab63bd6ef..ad8d5eb74836d 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -398,7 +398,7 @@ function addNewContactMethod(contactMethod: string, validateCode = '') { value: {isLoading: false}, }, ]; - const failureData: Array> = [ + const failureData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.ACCOUNT, @@ -417,6 +417,13 @@ function addNewContactMethod(contactMethod: string, validateCode = '') { actionVerified: false, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.LOGIN_LIST, + value: { + [contactMethod]: null, + }, + }, ]; const parameters: AddNewContactMethodParams = {partnerUserID: contactMethod, validateCode}; From 7b297718a09adb8e27a4ab703c800e1c7bbc1172 Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Tue, 6 Jan 2026 20:27:17 +0700 Subject: [PATCH 22/22] test: Update expected length of failureData in UserTest to reflect recent changes --- tests/actions/UserTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/actions/UserTest.ts b/tests/actions/UserTest.ts index 73bc4c694020b..2070ee8a3fcbf 100644 --- a/tests/actions/UserTest.ts +++ b/tests/actions/UserTest.ts @@ -471,7 +471,7 @@ describe('actions/User', () => { const [, , onyxData] = calls.at(0) as [unknown, unknown, {failureData?: Array<{key: string; value: unknown}>}]; const failureData = onyxData.failureData ?? []; - expect(failureData).toHaveLength(3); + expect(failureData).toHaveLength(4); // Verify ACCOUNT failure update const accountUpdate = failureData.find((update) => update.key === ONYXKEYS.ACCOUNT);