Skip to content

Commit

Permalink
Merge pull request Expensify#48320 from getusha/validate-contact-meth…
Browse files Browse the repository at this point in the history
…od-action

feat: validate new contact method action
  • Loading branch information
mountiny authored Sep 2, 2024
2 parents 1dbb2af + 0a2c0b2 commit 2cb8354
Show file tree
Hide file tree
Showing 15 changed files with 311 additions and 33 deletions.
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',

/** 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 @@ -197,6 +197,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 @@ -74,6 +74,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 @@ -433,6 +434,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
134 changes: 130 additions & 4 deletions src/libs/actions/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,16 @@ function deleteContactMethod(contactMethod: string, loginList: Record<string, Lo
Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(backTo));
}

/**
* Clears a contact method 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,
});
}

/**
* Clears any possible stored errors for a specific field on a contact method
*/
Expand Down Expand Up @@ -289,10 +299,91 @@ function resetContactMethodValidateCodeSentState(contactMethod: string) {
});
}

/**
* Clears unvalidated new contact method action
*/
function clearUnvalidatedNewContactMethodAction() {
Onyx.merge(ONYXKEYS.PENDING_CONTACT_ACTION, {
validateCodeSent: 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 +401,11 @@ function addNewContactMethodAndNavigate(contactMethod: string, backTo?: string)
},
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.ACCOUNT,
value: {isLoading: true},
},
];
const successData: OnyxUpdate[] = [
{
Expand All @@ -323,6 +419,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 +453,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));
}

/**
Expand Down Expand Up @@ -1123,7 +1246,8 @@ export {
updateNewsletterSubscription,
deleteContactMethod,
clearContactMethodErrors,
addNewContactMethodAndNavigate,
clearContactMethod,
addNewContactMethod,
validateLogin,
validateSecondaryLogin,
isBlockedFromConcierge,
Expand All @@ -1144,4 +1268,6 @@ export {
updateDraftCustomStatus,
clearDraftCustomStatus,
requestRefund,
saveNewContactMethodAndRequestValidationCode,
clearUnvalidatedNewContactMethodAction,
};
10 changes: 6 additions & 4 deletions src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {useOnyx} from 'react-native-onyx';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import ConfirmModal from '@components/ConfirmModal';
import DotIndicatorMessage from '@components/DotIndicatorMessage';
import ErrorMessageRow from '@components/ErrorMessageRow';
import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
Expand Down Expand Up @@ -202,10 +203,11 @@ function ContactMethodDetailsPage({route}: ContactMethodDetailsPageProps) {
/>

{isFailedAddContactMethod && (
<DotIndicatorMessage
style={[themeStyles.mh5, themeStyles.mv3]}
messages={ErrorUtils.getLatestErrorField(loginData, 'addedLogin')}
type="error"
<ErrorMessageRow
errors={ErrorUtils.getLatestErrorField(loginData, 'addedLogin')}
errorRowStyles={[themeStyles.mh5, themeStyles.mv3]}
onClose={() => User.clearContactMethod(contactMethod)}
canDismissError
/>
)}

Expand Down
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]);

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

0 comments on commit 2cb8354

Please sign in to comment.