Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: validate new contact method action #48320

Merged
merged 12 commits into from
Sep 2, 2024
4 changes: 4 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ const ONYXKEYS = {
/** Contains metadata (partner, login, validation date) for all of the user's logins */
LOGIN_LIST: 'loginList',

/** Object containing contact method that's going to be added */
PENDING_CONTACT_ACTION: 'pendingContactAction',
getusha marked this conversation as resolved.
Show resolved Hide resolved

/** Information about the current session (authToken, accountID, email, loading, error) */
SESSION: 'session',
STASHED_SESSION: 'stashedSession',
Expand Down Expand Up @@ -811,6 +814,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.USER]: OnyxTypes.User;
[ONYXKEYS.USER_LOCATION]: OnyxTypes.UserLocation;
[ONYXKEYS.LOGIN_LIST]: OnyxTypes.LoginList;
[ONYXKEYS.PENDING_CONTACT_ACTION]: OnyxTypes.PendingContactAction;
[ONYXKEYS.SESSION]: OnyxTypes.Session;
[ONYXKEYS.USER_METADATA]: OnyxTypes.UserMetadata;
[ONYXKEYS.STASHED_SESSION]: OnyxTypes.Session;
Expand Down
1 change: 1 addition & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ const ROUTES = {
route: 'settings/profile/contact-methods/:contactMethod/details',
getRoute: (contactMethod: string, backTo?: string) => getUrlWithBackToParam(`settings/profile/contact-methods/${encodeURIComponent(contactMethod)}/details`, backTo),
},
SETINGS_CONTACT_METHOD_VALIDATE_ACTION: 'settings/profile/contact-methods/validate-action',
SETTINGS_NEW_CONTACT_METHOD: {
route: 'settings/profile/contact-methods/new',
getRoute: (backTo?: string) => getUrlWithBackToParam('settings/profile/contact-methods/new', backTo),
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const SCREENS = {
DISPLAY_NAME: 'Settings_Display_Name',
CONTACT_METHODS: 'Settings_ContactMethods',
CONTACT_METHOD_DETAILS: 'Settings_ContactMethodDetails',
CONTACT_METHOD_VALIDATE_ACTION: 'Settings_ValidateContactMethodAction',
NEW_CONTACT_METHOD: 'Settings_NewContactMethod',
STATUS_CLEAR_AFTER: 'Settings_Status_Clear_After',
STATUS_CLEAR_AFTER_DATE: 'Settings_Status_Clear_After_Date',
Expand Down
2 changes: 1 addition & 1 deletion src/libs/API/parameters/AddNewContactMethodParams.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
type AddNewContactMethodParams = {partnerUserID: string};
type AddNewContactMethodParams = {partnerUserID: string; validateCode: string};

export default AddNewContactMethodParams;
2 changes: 2 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ const WRITE_COMMANDS = {
CONNECT_BANK_ACCOUNT_WITH_PLAID: 'ConnectBankAccountWithPlaid',
ADD_PERSONAL_BANK_ACCOUNT: 'AddPersonalBankAccount',
RESTART_BANK_ACCOUNT_SETUP: 'RestartBankAccountSetup',
RESEND_VALIDATE_CODE: 'ResendValidateCode',
OPT_IN_TO_PUSH_NOTIFICATIONS: 'OptInToPushNotifications',
OPT_OUT_OF_PUSH_NOTIFICATIONS: 'OptOutOfPushNotifications',
READ_NEWEST_ACTION: 'ReadNewestAction',
Expand Down Expand Up @@ -427,6 +428,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.ADD_PERSONAL_BANK_ACCOUNT]: Parameters.AddPersonalBankAccountParams;
[WRITE_COMMANDS.RESTART_BANK_ACCOUNT_SETUP]: Parameters.RestartBankAccountSetupParams;
[WRITE_COMMANDS.OPT_IN_TO_PUSH_NOTIFICATIONS]: Parameters.OptInOutToPushNotificationsParams;
[WRITE_COMMANDS.RESEND_VALIDATE_CODE]: null;
[WRITE_COMMANDS.OPT_OUT_OF_PUSH_NOTIFICATIONS]: Parameters.OptInOutToPushNotificationsParams;
[WRITE_COMMANDS.READ_NEWEST_ACTION]: Parameters.ReadNewestActionParams;
[WRITE_COMMANDS.MARK_AS_UNREAD]: Parameters.MarkAsUnreadParams;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.SETTINGS.PROFILE.ADDRESS_STATE]: () => require<ReactComponentModule>('../../../../pages/settings/Profile/PersonalDetails/StateSelectionPage').default,
[SCREENS.SETTINGS.PROFILE.CONTACT_METHODS]: () => require<ReactComponentModule>('../../../../pages/settings/Profile/Contacts/ContactMethodsPage').default,
[SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_DETAILS]: () => require<ReactComponentModule>('../../../../pages/settings/Profile/Contacts/ContactMethodDetailsPage').default,
[SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_VALIDATE_ACTION]: () => require<ReactComponentModule>('../../../../pages/settings/Profile/Contacts/ValidateContactActionPage').default,
[SCREENS.SETTINGS.PROFILE.NEW_CONTACT_METHOD]: () => require<ReactComponentModule>('../../../../pages/settings/Profile/Contacts/NewContactMethodPage').default,
[SCREENS.SETTINGS.PREFERENCES.PRIORITY_MODE]: () => require<ReactComponentModule>('../../../../pages/settings/Preferences/PriorityModePage').default,
[SCREENS.WORKSPACE.ACCOUNTING.ROOT]: () => require<ReactComponentModule>('../../../../pages/workspace/accounting/PolicyAccountingPage').default,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial<Record<CentralPaneName, string[]>> =
SCREENS.SETTINGS.PROFILE.DISPLAY_NAME,
SCREENS.SETTINGS.PROFILE.CONTACT_METHODS,
SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_DETAILS,
SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_VALIDATE_ACTION,
SCREENS.SETTINGS.PROFILE.NEW_CONTACT_METHOD,
SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER,
SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER_DATE,
Expand Down
3 changes: 3 additions & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,9 @@ const config: LinkingOptions<RootStackParamList>['config'] = {
[SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_DETAILS]: {
path: ROUTES.SETTINGS_CONTACT_METHOD_DETAILS.route,
},
[SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_VALIDATE_ACTION]: {
path: ROUTES.SETINGS_CONTACT_METHOD_VALIDATE_ACTION,
},
[SCREENS.SETTINGS.PROFILE.NEW_CONTACT_METHOD]: {
path: ROUTES.SETTINGS_NEW_CONTACT_METHOD.route,
exact: true,
Expand Down
124 changes: 120 additions & 4 deletions src/libs/actions/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,10 +289,92 @@ function resetContactMethodValidateCodeSentState(contactMethod: string) {
});
}

/**
* Clears unvalidated new contact method action
*/
function clearUnvalidatedNewContactMethodAction() {
Onyx.merge(ONYXKEYS.PENDING_CONTACT_ACTION, {
validateCodeSent: null,
contactMethod: null,
pendingFields: null,
errorFields: null,
});
}

/**
* Validates the action to add secondary contact method
*/
function saveNewContactMethodAndRequestValidationCode(contactMethod: string) {
const optimisticData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.PENDING_CONTACT_ACTION,
value: {
contactMethod,
errorFields: {
actionVerified: null,
},
pendingFields: {
actionVerified: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
},
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.FORMS.NEW_CONTACT_METHOD_FORM,
value: {isLoading: true},
},
];

const successData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.PENDING_CONTACT_ACTION,
value: {
validateCodeSent: true,
errorFields: {
actionVerified: null,
},
pendingFields: {
actionVerified: null,
},
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.FORMS.NEW_CONTACT_METHOD_FORM,
value: {isLoading: false},
},
];

const failureData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.PENDING_CONTACT_ACTION,
value: {
validateCodeSent: null,
errorFields: {
actionVerified: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('contacts.genericFailureMessages.requestContactMethodValidateCode'),
},
pendingFields: {
actionVerified: null,
},
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.FORMS.NEW_CONTACT_METHOD_FORM,
value: {isLoading: false},
},
];

API.write(WRITE_COMMANDS.RESEND_VALIDATE_CODE, null, {optimisticData, successData, failureData});
}

/**
* Adds a secondary login to a user's account
*/
function addNewContactMethodAndNavigate(contactMethod: string, backTo?: string) {
function addNewContactMethod(contactMethod: string, validateCode = '') {
const optimisticData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
Expand All @@ -310,6 +392,11 @@ function addNewContactMethodAndNavigate(contactMethod: string, backTo?: string)
},
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.ACCOUNT,
value: {isLoading: true},
},
];
const successData: OnyxUpdate[] = [
{
Expand All @@ -323,6 +410,24 @@ function addNewContactMethodAndNavigate(contactMethod: string, backTo?: string)
},
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.PENDING_CONTACT_ACTION,
value: {
validateCodeSent: null,
errorFields: {
actionVerified: null,
},
pendingFields: {
actionVerified: null,
},
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.ACCOUNT,
value: {isLoading: false},
},
];
const failureData: OnyxUpdate[] = [
{
Expand All @@ -339,12 +444,21 @@ function addNewContactMethodAndNavigate(contactMethod: string, backTo?: string)
},
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.ACCOUNT,
value: {isLoading: false},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.PENDING_CONTACT_ACTION,
value: {validateCodeSent: null},
},
];

const parameters: AddNewContactMethodParams = {partnerUserID: contactMethod};
const parameters: AddNewContactMethodParams = {partnerUserID: contactMethod, validateCode};

API.write(WRITE_COMMANDS.ADD_NEW_CONTACT_METHOD, parameters, {optimisticData, successData, failureData});
Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(backTo));
getusha marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down Expand Up @@ -1123,7 +1237,7 @@ export {
updateNewsletterSubscription,
deleteContactMethod,
clearContactMethodErrors,
addNewContactMethodAndNavigate,
addNewContactMethod,
validateLogin,
validateSecondaryLogin,
isBlockedFromConcierge,
Expand All @@ -1144,4 +1258,6 @@ export {
updateDraftCustomStatus,
clearDraftCustomStatus,
requestRefund,
saveNewContactMethodAndRequestValidationCode,
clearUnvalidatedNewContactMethodAction,
};
40 changes: 28 additions & 12 deletions src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type {StackScreenProps} from '@react-navigation/stack';
import {Str} from 'expensify-common';
import React, {useCallback, useRef} from 'react';
import React, {useCallback, useEffect, useRef} from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import {useOnyx, withOnyx} from 'react-native-onyx';
import DotIndicatorMessage from '@components/DotIndicatorMessage';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
import type {FormOnyxValues} from '@components/Form/types';
Expand Down Expand Up @@ -38,19 +39,29 @@ function NewContactMethodPage({loginList, route}: NewContactMethodPageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const loginInputRef = useRef<AnimatedTextInputRef>(null);
const [pendingContactAction] = useOnyx(ONYXKEYS.PENDING_CONTACT_ACTION);

const navigateBackTo = route?.params?.backTo ?? ROUTES.SETTINGS_PROFILE;

const addNewContactMethod = useCallback(
(values: FormOnyxValues<typeof ONYXKEYS.FORMS.NEW_CONTACT_METHOD_FORM>) => {
const phoneLogin = LoginUtils.getPhoneLogin(values.phoneOrEmail);
const validateIfnumber = LoginUtils.validateNumber(phoneLogin);
const submitDetail = (validateIfnumber || values.phoneOrEmail).trim().toLowerCase();
const hasFailedToSendVerificationCode = !!pendingContactAction?.errorFields?.actionVerified;

User.addNewContactMethodAndNavigate(submitDetail, route.params?.backTo);
},
[route.params?.backTo],
);
const addNewContactMethod = useCallback((values: FormOnyxValues<typeof ONYXKEYS.FORMS.NEW_CONTACT_METHOD_FORM>) => {
const phoneLogin = LoginUtils.getPhoneLogin(values.phoneOrEmail);
const validateIfnumber = LoginUtils.validateNumber(phoneLogin);
const submitDetail = (validateIfnumber || values.phoneOrEmail).trim().toLowerCase();

User.saveNewContactMethodAndRequestValidationCode(submitDetail);
}, []);

useEffect(() => {
if (!pendingContactAction?.validateCodeSent) {
return;
}

Navigation.navigate(ROUTES.SETINGS_CONTACT_METHOD_VALIDATE_ACTION);
}, [pendingContactAction]);
Comment on lines +56 to +62
Copy link
Member

Choose a reason for hiding this comment

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

@getusha What is the purpose for this logic?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@parasharrajat We started requiring verification before adding a contact method. the new flow is temporarily save new contact > verify action > add the new contact method.

saveNewContactMethodAndRequestValidationCode just saves the contact method temporarily and requests a magic code, so this logic just navigates youValidateContactActionPage after the magic code is sent

validateCodeSent: true,


useEffect(() => () => User.clearUnvalidatedNewContactMethodAction(), []);

const validate = React.useCallback(
(values: FormOnyxValues<typeof ONYXKEYS.FORMS.NEW_CONTACT_METHOD_FORM>): Errors => {
Expand Down Expand Up @@ -105,7 +116,6 @@ function NewContactMethodPage({loginList, route}: NewContactMethodPageProps) {
onSubmit={addNewContactMethod}
submitButtonText={translate('common.add')}
style={[styles.flexGrow1, styles.mh5]}
enabledWhenOffline
>
<Text style={styles.mb5}>{translate('common.pleaseEnterEmailOrPhoneNumber')}</Text>
<View style={styles.mb6}>
Expand All @@ -122,6 +132,12 @@ function NewContactMethodPage({loginList, route}: NewContactMethodPageProps) {
maxLength={CONST.LOGIN_CHARACTER_LIMIT}
/>
</View>
{hasFailedToSendVerificationCode && (
<DotIndicatorMessage
messages={ErrorUtils.getLatestErrorField(pendingContactAction, 'actionVerified')}
type="error"
/>
)}
</FormProvider>
</ScreenWrapper>
);
Expand Down
Loading
Loading