Skip to content

Commit

Permalink
Merge pull request #50773 from allgandalf/newPhoneNumberRHPForProfile
Browse files Browse the repository at this point in the history
[Feature]: Add phone number to the private personal details section
  • Loading branch information
mountiny authored Oct 28, 2024
2 parents 75d9877 + c07af88 commit 4d65cc7
Show file tree
Hide file tree
Showing 18 changed files with 214 additions and 3 deletions.
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6017,6 +6017,7 @@ const CONST = {
HAS_WALLET_TERMS_ERRORS: 'hasWalletTermsErrors',
HAS_LOGIN_LIST_INFO: 'hasLoginListInfo',
HAS_SUBSCRIPTION_INFO: 'hasSubscriptionInfo',
HAS_PHONE_NUMBER_ERROR: 'hasPhoneNumberError',
},

DEBUG: {
Expand Down
1 change: 1 addition & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ const ROUTES = {
},
SETTINGS_LEGAL_NAME: 'settings/profile/legal-name',
SETTINGS_DATE_OF_BIRTH: 'settings/profile/date-of-birth',
SETTINGS_PHONE_NUMBER: 'settings/profile/phone',
SETTINGS_ADDRESS: 'settings/profile/address',
SETTINGS_ADDRESS_COUNTRY: {
route: 'settings/profile/address/country',
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ const SCREENS = {
TIMEZONE_SELECT: 'Settings_Timezone_Select',
LEGAL_NAME: 'Settings_LegalName',
DATE_OF_BIRTH: 'Settings_DateOfBirth',
PHONE_NUMBER: 'Settings_PhoneNumber',
ADDRESS: 'Settings_Address',
ADDRESS_COUNTRY: 'Settings_Address_Country',
ADDRESS_STATE: 'Settings_Address_State',
Expand Down
2 changes: 2 additions & 0 deletions src/hooks/useIndicatorStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ function useIndicatorStatus(): IndicatorStatusResult {
const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET);
const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS);
const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST);
const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS);

// If a policy was just deleted from Onyx, then Onyx will pass a null value to the props, and
// those should be cleaned out before doing any error checking
Expand Down Expand Up @@ -57,6 +58,7 @@ function useIndicatorStatus(): IndicatorStatusResult {
[CONST.INDICATOR_STATUS.HAS_LOGIN_LIST_ERROR]: !!loginList && UserUtils.hasLoginListError(loginList),
// Wallet term errors that are not caused by an IOU (we show the red brick indicator for those in the LHN instead)
[CONST.INDICATOR_STATUS.HAS_WALLET_TERMS_ERRORS]: Object.keys(walletTerms?.errors ?? {}).length > 0 && !walletTerms?.chatReportID,
[CONST.INDICATOR_STATUS.HAS_PHONE_NUMBER_ERROR]: !!privatePersonalDetails?.errorFields?.phoneNumber ?? undefined,
};

const infoChecking: Partial<Record<IndicatorStatus, boolean>> = {
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1783,6 +1783,7 @@ const translations = {
dateShouldBeAfter: ({dateString}: DateShouldBeAfterParams) => `Date should be after ${dateString}.`,
hasInvalidCharacter: 'Name can only include Latin characters.',
incorrectZipFormat: ({zipFormat}: IncorrectZipFormatParams = {}) => `Incorrect zip code format.${zipFormat ? ` Acceptable format: ${zipFormat}` : ''}`,
invalidPhoneNumber: `Please ensure the phone number is valid (e.g. ${CONST.EXAMPLE_PHONE_NUMBER}).`,
},
},
resendValidationForm: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1784,6 +1784,7 @@ const translations = {
dateShouldBeAfter: ({dateString}: DateShouldBeAfterParams) => `La fecha debe ser posterior a ${dateString}.`,
incorrectZipFormat: ({zipFormat}: IncorrectZipFormatParams = {}) => `Formato de código postal incorrecto.${zipFormat ? ` Formato aceptable: ${zipFormat}` : ''}`,
hasInvalidCharacter: 'El nombre sólo puede incluir caracteres latinos.',
invalidPhoneNumber: `Asegúrese de que el número de teléfono sean válidos (p. ej. ${CONST.EXAMPLE_PHONE_NUMBER}).`,
},
},
resendValidationForm: {
Expand Down
5 changes: 5 additions & 0 deletions src/libs/API/parameters/UpdatePhoneNumberParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type UpdatePhoneNumberParams = {
phoneNumber?: string;
};

export default UpdatePhoneNumberParams;
1 change: 1 addition & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export type {default as UpdateGroupChatMemberRolesParams} from './UpdateGroupCha
export type {default as UpdateHomeAddressParams} from './UpdateHomeAddressParams';
export type {default as UpdatePolicyAddressParams} from './UpdatePolicyAddressParams';
export type {default as UpdateLegalNameParams} from './UpdateLegalNameParams';
export type {default as UpdatePhoneNumberParams} from './UpdatePhoneNumberParams';
export type {default as UpdateNewsletterSubscriptionParams} from './UpdateNewsletterSubscriptionParams';
export type {default as UpdatePersonalInformationForBankAccountParams} from './UpdatePersonalInformationForBankAccountParams';
export type {default as UpdatePreferredEmojiSkinToneParams} from './UpdatePreferredEmojiSkinToneParams';
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 @@ -57,6 +57,7 @@ const WRITE_COMMANDS = {
UPDATE_DISPLAY_NAME: 'UpdateDisplayName',
UPDATE_LEGAL_NAME: 'UpdateLegalName',
UPDATE_DATE_OF_BIRTH: 'UpdateDateOfBirth',
UPDATE_PHONE_NUMBER: 'UpdatePhoneNumber',
UPDATE_HOME_ADDRESS: 'UpdateHomeAddress',
UPDATE_POLICY_ADDRESS: 'SetPolicyAddress',
UPDATE_AUTOMATIC_TIMEZONE: 'UpdateAutomaticTimezone',
Expand Down Expand Up @@ -466,6 +467,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.UPDATE_PHONE_NUMBER]: Parameters.UpdatePhoneNumberParams;
[WRITE_COMMANDS.UPDATE_POLICY_ADDRESS]: Parameters.UpdatePolicyAddressParams;
[WRITE_COMMANDS.UPDATE_HOME_ADDRESS]: Parameters.UpdateHomeAddressParams;
[WRITE_COMMANDS.UPDATE_AUTOMATIC_TIMEZONE]: Parameters.UpdateAutomaticTimezoneParams;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.SETTINGS.PROFILE.TIMEZONE_SELECT]: () => require<ReactComponentModule>('../../../../pages/settings/Profile/TimezoneSelectPage').default,
[SCREENS.SETTINGS.PROFILE.LEGAL_NAME]: () => require<ReactComponentModule>('../../../../pages/settings/Profile/PersonalDetails/LegalNamePage').default,
[SCREENS.SETTINGS.PROFILE.DATE_OF_BIRTH]: () => require<ReactComponentModule>('../../../../pages/settings/Profile/PersonalDetails/DateOfBirthPage').default,
[SCREENS.SETTINGS.PROFILE.PHONE_NUMBER]: () => require<ReactComponentModule>('../../../../pages/settings/Profile/PersonalDetails/PhoneNumberPage').default,
[SCREENS.SETTINGS.PROFILE.ADDRESS]: () => require<ReactComponentModule>('../../../../pages/settings/Profile/PersonalDetails/PersonalAddressPage').default,
[SCREENS.SETTINGS.PROFILE.ADDRESS_COUNTRY]: () => require<ReactComponentModule>('../../../../pages/settings/Profile/PersonalDetails/CountrySelectionPage').default,
[SCREENS.SETTINGS.PROFILE.ADDRESS_STATE]: () => require<ReactComponentModule>('../../../../pages/settings/Profile/PersonalDetails/StateSelectionPage').default,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial<Record<CentralPaneName, string[]>> =
SCREENS.SETTINGS.PROFILE.TIMEZONE_SELECT,
SCREENS.SETTINGS.PROFILE.LEGAL_NAME,
SCREENS.SETTINGS.PROFILE.DATE_OF_BIRTH,
SCREENS.SETTINGS.PROFILE.PHONE_NUMBER,
SCREENS.SETTINGS.PROFILE.ADDRESS,
SCREENS.SETTINGS.PROFILE.ADDRESS_COUNTRY,
SCREENS.SETTINGS.SHARE_CODE,
Expand Down
4 changes: 4 additions & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,10 @@ const config: LinkingOptions<RootStackParamList>['config'] = {
path: ROUTES.SETTINGS_LEGAL_NAME,
exact: true,
},
[SCREENS.SETTINGS.PROFILE.PHONE_NUMBER]: {
path: ROUTES.SETTINGS_PHONE_NUMBER,
exact: true,
},
[SCREENS.SETTINGS.PROFILE.DATE_OF_BIRTH]: {
path: ROUTES.SETTINGS_DATE_OF_BIRTH,
exact: true,
Expand Down
20 changes: 19 additions & 1 deletion src/libs/UserUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as defaultAvatars from '@components/Icon/DefaultAvatars';
import {ConciergeAvatar, NotificationsAvatar} from '@components/Icon/Expensicons';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Account, LoginList, Session} from '@src/types/onyx';
import type {Account, LoginList, PrivatePersonalDetails, Session} from '@src/types/onyx';
import type Login from '@src/types/onyx/Login';
import type IconAsset from '@src/types/utils/IconAsset';
import hashCode from './hashCode';
Expand Down Expand Up @@ -78,6 +78,23 @@ function getLoginListBrickRoadIndicator(loginList: OnyxEntry<LoginList>): LoginL
if (hasLoginListInfo(loginList)) {
return CONST.BRICK_ROAD_INDICATOR_STATUS.INFO;
}

return undefined;
}

/**
* Gets the appropriate brick road indicator status for the Profile section.
* Error status is higher priority, so we check for that first.
*/
function getProfilePageBrickRoadIndicator(loginList: OnyxEntry<LoginList>, privatePersonalDetails: OnyxEntry<PrivatePersonalDetails>): LoginListIndicator {
const hasPhoneNumberError = !!privatePersonalDetails?.errorFields?.phoneNumber;
if (hasLoginListError(loginList) || hasPhoneNumberError) {
return CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR;
}
if (hasLoginListInfo(loginList)) {
return CONST.BRICK_ROAD_INDICATOR_STATUS.INFO;
}

return undefined;
}

Expand Down Expand Up @@ -240,6 +257,7 @@ export {
getDefaultAvatarURL,
getFullSizeAvatar,
getLoginListBrickRoadIndicator,
getProfilePageBrickRoadIndicator,
getSecondaryPhoneLogin,
getSmallSizeAvatar,
hasLoginListError,
Expand Down
39 changes: 39 additions & 0 deletions src/libs/actions/PersonalDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import type {
UpdateDisplayNameParams,
UpdateHomeAddressParams,
UpdateLegalNameParams,
UpdatePhoneNumberParams,
UpdatePronounsParams,
UpdateSelectedTimezoneParams,
UpdateUserAvatarParams,
} from '@libs/API/parameters';
import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types';
import DateUtils from '@libs/DateUtils';
import * as ErrorUtils from '@libs/ErrorUtils';
import * as LoginUtils from '@libs/LoginUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
Expand Down Expand Up @@ -157,6 +159,41 @@ function updateDateOfBirth({dob}: DateOfBirthForm) {
Navigation.goBack();
}

function updatePhoneNumber(phoneNumber: string, currenPhoneNumber: string) {
const parameters: UpdatePhoneNumberParams = {phoneNumber};
API.write(WRITE_COMMANDS.UPDATE_PHONE_NUMBER, parameters, {
optimisticData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS,
value: {
phoneNumber,
},
},
],
failureData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS,
value: {
phoneNumber: currenPhoneNumber,
errorFields: {
phoneNumber: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('privatePersonalDetails.error.invalidPhoneNumber'),
},
},
},
],
});
}

function clearPhoneNumberError() {
Onyx.merge(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {
errorFields: {
phoneNumber: null,
},
});
}

function updateAddress(street: string, street2: string, city: string, state: string, zip: string, country: Country | '') {
const parameters: UpdateHomeAddressParams = {
homeAddressStreet: street,
Expand Down Expand Up @@ -480,6 +517,8 @@ export {
setDisplayName,
updateDisplayName,
updateLegalName,
updatePhoneNumber,
clearPhoneNumberError,
updatePronouns,
updateSelectedTimezone,
updatePersonalDetailsAndShipExpensifyCards,
Expand Down
5 changes: 3 additions & 2 deletions src/pages/settings/InitialSettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr
const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS);
const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST);
const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS);

const network = useNetwork();
const theme = useTheme();
Expand Down Expand Up @@ -126,7 +127,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr
* @returns object with translationKey, style and items for the account section
*/
const accountMenuItemsData: Menu = useMemo(() => {
const profileBrickRoadIndicator = UserUtils.getLoginListBrickRoadIndicator(loginList);
const profileBrickRoadIndicator = UserUtils.getProfilePageBrickRoadIndicator(loginList, privatePersonalDetails);
const paymentCardList = fundList;
const defaultMenu: Menu = {
sectionStyle: styles.accountSettingsSectionContainer,
Expand Down Expand Up @@ -161,7 +162,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr
};

return defaultMenu;
}, [loginList, fundList, styles.accountSettingsSectionContainer, bankAccountList, userWallet?.errors, walletTerms?.errors]);
}, [loginList, fundList, styles.accountSettingsSectionContainer, bankAccountList, userWallet?.errors, walletTerms?.errors, privatePersonalDetails]);

/**
* Retuns a list of menu items data for workspace section
Expand Down
121 changes: 121 additions & 0 deletions src/pages/settings/Profile/PersonalDetails/PhoneNumberPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import {Str} from 'expensify-common';
import React, {useCallback} from 'react';
import {useOnyx} from 'react-native-onyx';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
import TextInput from '@components/TextInput';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ErrorUtils from '@libs/ErrorUtils';
import * as LoginUtils from '@libs/LoginUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as PhoneNumberUtils from '@libs/PhoneNumber';
import * as ValidationUtils from '@libs/ValidationUtils';
import * as PersonalDetails from '@userActions/PersonalDetails';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import INPUT_IDS from '@src/types/form/PersonalDetailsForm';
import type {PrivatePersonalDetails} from '@src/types/onyx';

function PhoneNumberPage() {
const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS);
const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {initialValue: true});
const styles = useThemeStyles();
const {translate} = useLocalize();
const phoneNumber = privatePersonalDetails?.phoneNumber ?? '';

const validateLoginError = ErrorUtils.getEarliestErrorField(privatePersonalDetails, 'phoneNumber');
const currenPhoneNumber = privatePersonalDetails?.phoneNumber ?? '';

const updatePhoneNumber = (values: PrivatePersonalDetails) => {
// Clear the error when the user tries to submit the form
if (validateLoginError) {
PersonalDetails.clearPhoneNumberError();
}

// Only call the API if the user has changed their phone number
if (phoneNumber !== values?.phoneNumber) {
PersonalDetails.updatePhoneNumber(values?.phoneNumber ?? '', currenPhoneNumber);
}

Navigation.goBack();
};

const validate = useCallback(
(values: FormOnyxValues<typeof ONYXKEYS.FORMS.PERSONAL_DETAILS_FORM>): FormInputErrors<typeof ONYXKEYS.FORMS.PERSONAL_DETAILS_FORM> => {
const errors: FormInputErrors<typeof ONYXKEYS.FORMS.PERSONAL_DETAILS_FORM> = {};
if (!ValidationUtils.isRequiredFulfilled(values[INPUT_IDS.PHONE_NUMBER])) {
errors[INPUT_IDS.PHONE_NUMBER] = translate('common.error.fieldRequired');
}
const phoneNumberWithCountryCode = LoginUtils.appendCountryCode(values[INPUT_IDS.PHONE_NUMBER]);
const parsedPhoneNumber = PhoneNumberUtils.parsePhoneNumber(phoneNumberWithCountryCode);
if (!parsedPhoneNumber.possible || !Str.isValidE164Phone(phoneNumberWithCountryCode.slice(0))) {
errors[INPUT_IDS.PHONE_NUMBER] = translate('bankAccount.error.phoneNumber');
}

// Clear the error when the user tries to validate the form and there are errors
if (validateLoginError && !!errors) {
PersonalDetails.clearPhoneNumberError();
}
return errors;
},
[translate, validateLoginError],
);

return (
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
shouldEnableMaxHeight
testID={PhoneNumberPage.displayName}
>
<HeaderWithBackButton
title={translate('common.phoneNumber')}
onBackButtonPress={() => Navigation.goBack()}
/>
{isLoadingApp ? (
<FullscreenLoadingIndicator style={[styles.flex1, styles.pRelative]} />
) : (
<FormProvider
style={[styles.flexGrow1, styles.ph5]}
formID={ONYXKEYS.FORMS.PERSONAL_DETAILS_FORM}
validate={validate}
onSubmit={updatePhoneNumber}
submitButtonText={translate('common.save')}
enabledWhenOffline
>
<OfflineWithFeedback
errors={validateLoginError}
errorRowStyles={styles.mt2}
onClose={() => PersonalDetails.clearPhoneNumberError()}
>
<InputWrapper
InputComponent={TextInput}
inputID={INPUT_IDS.PHONE_NUMBER}
name="lfname"
label={translate('common.phoneNumber')}
aria-label={translate('common.phoneNumber')}
role={CONST.ROLE.PRESENTATION}
defaultValue={phoneNumber}
spellCheck={false}
onBlur={() => {
if (!validateLoginError) {
return;
}
PersonalDetails.clearPhoneNumberError();
}}
/>
</OfflineWithFeedback>
</FormProvider>
)}
</ScreenWrapper>
);
}

PhoneNumberPage.displayName = 'PhoneNumberPage';

export default PhoneNumberPage;
7 changes: 7 additions & 0 deletions src/pages/settings/Profile/ProfilePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ function ProfilePage() {
title: privateDetails.dob ?? '',
pageRoute: ROUTES.SETTINGS_DATE_OF_BIRTH,
},
{
description: translate('common.phoneNumber'),
title: privateDetails.phoneNumber ?? '',
pageRoute: ROUTES.SETTINGS_PHONE_NUMBER,
brickRoadIndicator: privatePersonalDetails?.errorFields?.phoneNumber ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
},
{
description: translate('privatePersonalDetails.address'),
title: PersonalDetailsUtils.getFormattedAddress(privateDetails),
Expand Down Expand Up @@ -195,6 +201,7 @@ function ProfilePage() {
description={detail.description}
wrapperStyle={styles.sectionMenuItemTopDescription}
onPress={() => Navigation.navigate(detail.pageRoute)}
brickRoadIndicator={detail.brickRoadIndicator}
/>
))}
</>
Expand Down
Loading

0 comments on commit 4d65cc7

Please sign in to comment.