Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5aee11c
reserve the order flow
cretadn22 Dec 11, 2025
cc079d2
Merge branch 'main' into reserver-the-order-new-contact-method
cretadn22 Dec 13, 2025
044806f
add loading animation
cretadn22 Dec 14, 2025
03c84f5
add loading animation
cretadn22 Dec 14, 2025
1ce25e7
update dependency list
cretadn22 Dec 14, 2025
40bc1ed
remove validateCodeSent
cretadn22 Dec 14, 2025
a7d7c71
keep contactMethod in success data
cretadn22 Dec 14, 2025
de07acc
reset isVerifiedValidateActionCode
cretadn22 Dec 14, 2025
48c2374
update navigation flow
cretadn22 Dec 14, 2025
a9bb8b8
Merge branch 'main' into reserver-the-order-new-contact-method
cretadn22 Dec 16, 2025
8e6516b
fix API call bug
cretadn22 Dec 16, 2025
ffda251
set server error and clear server error
cretadn22 Dec 18, 2025
16f1cbe
resolve eslint problem
cretadn22 Dec 18, 2025
bfa7cfb
merge the new main to the branch
cretadn22 Dec 22, 2025
c310db0
prevent submitting if server error
cretadn22 Dec 22, 2025
203da96
remove failed contact list
cretadn22 Dec 22, 2025
4e86586
Using for of
cretadn22 Dec 22, 2025
6926b2b
Merge branch 'main' into reserver-the-order-new-contact-method
cretadn22 Dec 27, 2025
62e295c
Create new file to add Unit test action/Users
cretadn22 Dec 27, 2025
46df70a
update Unit test action/Users
cretadn22 Dec 27, 2025
767472a
fixing the eslint in new file
cretadn22 Dec 27, 2025
05c0a38
update optimistic data type
cretadn22 Dec 27, 2025
dd07a8d
fixed prettier
cretadn22 Dec 27, 2025
937b74c
Merge branch 'main' into reserver-the-order-new-contact-method
cretadn22 Jan 6, 2026
2bd065d
feat: Add updateIsVerifiedValidateActionCode function and integrate i…
cretadn22 Jan 6, 2026
6297eaa
feat: Extend failureData structure to include LOGIN_LIST and update n…
cretadn22 Jan 6, 2026
7b29771
test: Update expected length of failureData in UserTest to reflect re…
cretadn22 Jan 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,13 +413,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: {
Expand Down
6 changes: 5 additions & 1 deletion src/components/Form/FormProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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},
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
type VerifyAddSecondaryLoginCodeParams = {validateCode: string};

export default VerifyAddSecondaryLoginCodeParams;
1 change: 1 addition & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,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';
Expand Down
2 changes: 2 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -573,6 +574,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;
Expand Down
136 changes: 113 additions & 23 deletions src/libs/actions/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
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';
Expand All @@ -50,6 +51,7 @@
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 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';
Expand All @@ -63,7 +65,7 @@

let currentUserAccountID = -1;
let currentEmail = '';
Onyx.connect({

Check warning on line 68 in src/libs/actions/User.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.SESSION,
callback: (value) => {
currentUserAccountID = value?.accountID ?? CONST.DEFAULT_NUMBER_ID;
Expand All @@ -72,7 +74,7 @@
});

let allPolicies: OnyxCollection<Policy>;
Onyx.connect({

Check warning on line 77 in src/libs/actions/User.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.POLICY,
waitForCollectionCallback: true,
callback: (value) => (allPolicies = value),
Expand Down Expand Up @@ -267,12 +269,19 @@
}

/**
* Clears a contact method optimistically. this is used when the contact method fails to be added to the backend
* Clears a list of contact methods optimistically. This is used when the contact method fails to be added to the backend.
*/
function clearContactMethod(contactMethod: string) {
Onyx.merge(ONYXKEYS.LOGIN_LIST, {
[contactMethod]: null,
});
function clearContactMethod(contactMethods: string[]) {
if (contactMethods.length === 0) {
return;
}

const loginsToClear = contactMethods.reduce<Record<string, null>>((acc, method) => {
acc[method] = null;
return acc;
}, {});

Onyx.merge(ONYXKEYS.LOGIN_LIST, loginsToClear);
}

/**
Expand Down Expand Up @@ -337,21 +346,11 @@
});
}

/**
* 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.set(ONYXKEYS.PENDING_CONTACT_ACTION, {
contactMethod,
});
}

/**
* Adds a secondary login to a user's account
*/
function addNewContactMethod(contactMethod: string, validateCode = '') {
const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.LOGIN_LIST | typeof ONYXKEYS.ACCOUNT>> = [
const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.LOGIN_LIST | typeof ONYXKEYS.ACCOUNT | typeof ONYXKEYS.PENDING_CONTACT_ACTION>> = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.LOGIN_LIST,
Expand All @@ -370,27 +369,36 @@
key: ONYXKEYS.ACCOUNT,
value: {isLoading: true},
},
];
const successData: Array<OnyxUpdate<typeof ONYXKEYS.PENDING_CONTACT_ACTION | typeof ONYXKEYS.ACCOUNT>> = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.PENDING_CONTACT_ACTION,
value: {
contactMethod: null,
validateCodeSent: null,
actionVerified: true,
contactMethod,
isLoading: true,
errorFields: {
actionVerified: null,
},
},
},
];
const successData: Array<OnyxUpdate<typeof ONYXKEYS.PENDING_CONTACT_ACTION | typeof ONYXKEYS.ACCOUNT>> = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.PENDING_CONTACT_ACTION,
value: {
actionVerified: true,
isLoading: false,
isVerifiedValidateActionCode: false,
validateActionCode: null,
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.ACCOUNT,
value: {isLoading: false},
},
];
const failureData: Array<OnyxUpdate<typeof ONYXKEYS.ACCOUNT | typeof ONYXKEYS.VALIDATE_ACTION_CODE>> = [
const failureData: Array<OnyxUpdate<typeof ONYXKEYS.ACCOUNT | typeof ONYXKEYS.VALIDATE_ACTION_CODE | typeof ONYXKEYS.PENDING_CONTACT_ACTION | typeof ONYXKEYS.LOGIN_LIST>> = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.ACCOUNT,
Expand All @@ -401,6 +409,21 @@
key: ONYXKEYS.VALIDATE_ACTION_CODE,
value: {validateCodeSent: null},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.PENDING_CONTACT_ACTION,
value: {
isLoading: false,
actionVerified: false,
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.LOGIN_LIST,
value: {
[contactMethod]: null,
},
},
];

const parameters: AddNewContactMethodParams = {partnerUserID: contactMethod, validateCode};
Expand Down Expand Up @@ -1551,8 +1574,74 @@
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,
isLoading: true,
errorFields: {
validateActionCode: null,
},
},
},
];

const successData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.PENDING_CONTACT_ACTION,
value: {
isVerifiedValidateActionCode: true,
isLoading: false,
},
},
];

const failureData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.PENDING_CONTACT_ACTION,
value: {
isVerifiedValidateActionCode: false,
isLoading: false,
},
},
];

const parameters: VerifyAddSecondaryLoginCodeParams = {validateCode};

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,
});
}

function updateIsVerifiedValidateActionCode(isVerifiedValidateActionCode: boolean) {
Onyx.merge(ONYXKEYS.PENDING_CONTACT_ACTION, {
isVerifiedValidateActionCode,
});
}

export {
closeAccount,
setServerErrorsOnForm,
dismissReferralBanner,
dismissASAPSubmitExplanation,
resendValidateCode,
Expand Down Expand Up @@ -1585,11 +1674,12 @@
clearUnvalidatedNewContactMethodAction,
clearPendingContactActionErrors,
requestValidateCodeAction,
addPendingContactMethod,
clearValidateCodeActionError,
setIsDebugModeEnabled,
resetValidateActionCodeSent,
lockAccount,
requestUnlockAccount,
respondToProactiveAppReview,
verifyAddSecondaryLoginCode,
updateIsVerifiedValidateActionCode,
};
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,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));
}}
Expand Down
7 changes: 4 additions & 3 deletions src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SettingsNavigatorParamList, typeof SCREENS.SETTINGS.PROFILE.NEW_CONTACT_METHOD_CONFIRM_MAGIC_CODE>;

Expand All @@ -23,52 +20,31 @@ 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 (
<ValidateCodeActionContent
title={translate('delegate.makeSureItIsYou')}
sendValidateCode={() => 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));
}}
isLoading={pendingContactAction?.isLoading}
/>
);
}
Expand Down
Loading
Loading