Skip to content

Commit

Permalink
Merge pull request #55768 from huult/55337-improve-phone-validation-e…
Browse files Browse the repository at this point in the history
…rror-messages

improve phone validation error messages
  • Loading branch information
NikkiWines authored Feb 20, 2025
2 parents 0daa61d + 22ff035 commit f5df848
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 35 deletions.
6 changes: 6 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,12 @@ const CONST = {
// Regex to read violation value from string given by backend
VIOLATION_LIMIT_REGEX: /[^0-9]+/g,

// Validates phone numbers with digits, '+', '-', '()', '.', and spaces
ACCEPTED_PHONE_CHARACTER_REGEX: /^[0-9+\-().\s]+$/,

// Prevents consecutive special characters or spaces like '--', '..', '((', '))', or ' '.
REPEATED_SPECIAL_CHAR_PATTERN: /([-\s().])\1+/,

MERCHANT_NAME_MAX_LENGTH: 255,

MASKED_PAN_PREFIX: 'XXXXXXXXXXXX',
Expand Down
9 changes: 8 additions & 1 deletion src/libs/LoginUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,11 @@ function areEmailsFromSamePrivateDomain(email1: string, email2: string): boolean
return Str.extractEmailDomain(email1).toLowerCase() === Str.extractEmailDomain(email2).toLowerCase();
}

export {getPhoneNumberWithoutSpecialChars, appendCountryCode, isEmailPublicDomain, validateNumber, getPhoneLogin, areEmailsFromSamePrivateDomain};
function formatE164PhoneNumber(phoneNumber: string) {
const phoneNumberWithCountryCode = appendCountryCode(phoneNumber);
const parsedPhoneNumber = parsePhoneNumber(phoneNumberWithCountryCode);

return parsedPhoneNumber.number?.e164;
}

export {getPhoneNumberWithoutSpecialChars, appendCountryCode, isEmailPublicDomain, validateNumber, getPhoneLogin, areEmailsFromSamePrivateDomain, formatE164PhoneNumber};
9 changes: 9 additions & 0 deletions src/libs/ValidationUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,14 @@ function isValidUSPhone(phoneNumber = '', isCountryCodeOptional?: boolean): bool
return parsedPhoneNumber.possible && parsedPhoneNumber.regionCode === CONST.COUNTRY.US;
}

function isValidPhoneNumber(phoneNumber: string): boolean {
if (!CONST.ACCEPTED_PHONE_CHARACTER_REGEX.test(phoneNumber) || CONST.REPEATED_SPECIAL_CHAR_PATTERN.test(phoneNumber)) {
return false;
}
const parsedPhoneNumber = parsePhoneNumber(phoneNumber);
return parsedPhoneNumber.possible;
}

function isValidValidateCode(validateCode: string): boolean {
return !!validateCode.match(CONST.VALIDATE_CODE_REGEX_STRING);
}
Expand Down Expand Up @@ -660,6 +668,7 @@ export {
isRequiredFulfilled,
getFieldRequiredErrors,
isValidUSPhone,
isValidPhoneNumber,
isValidWebsite,
validateIdentity,
isValidTwoFactorCode,
Expand Down
17 changes: 13 additions & 4 deletions src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumberStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import SingleFieldStep from '@components/SubStepForms/SingleFieldStep';
import useLocalize from '@hooks/useLocalize';
import type {SubStepProps} from '@hooks/useSubStep/types';
import useWalletAdditionalDetailsStepFormSubmit from '@hooks/useWalletAdditionalDetailsStepFormSubmit';
import {getFieldRequiredErrors, isValidUSPhone} from '@libs/ValidationUtils';
import {appendCountryCode, formatE164PhoneNumber} from '@libs/LoginUtils';
import {getFieldRequiredErrors, isValidPhoneNumber, isValidUSPhone} from '@libs/ValidationUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import INPUT_IDS from '@src/types/form/WalletAdditionalDetailsForm';
Expand All @@ -23,9 +24,15 @@ function PhoneNumberStep({onNext, onMove, isEditing}: SubStepProps) {
(values: FormOnyxValues<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS>): FormInputErrors<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS> => {
const errors = getFieldRequiredErrors(values, STEP_FIELDS);

if (values.phoneNumber && !isValidUSPhone(values.phoneNumber, true)) {
errors.phoneNumber = translate('bankAccount.error.phoneNumber');
if (values.phoneNumber) {
const phoneNumberWithCountryCode = appendCountryCode(values.phoneNumber);
const e164FormattedPhoneNumber = formatE164PhoneNumber(values.phoneNumber);

if (!isValidPhoneNumber(phoneNumberWithCountryCode) || !isValidUSPhone(e164FormattedPhoneNumber)) {
errors.phoneNumber = translate('common.error.phoneNumber');
}
}

return errors;
},
[translate],
Expand All @@ -46,7 +53,9 @@ function PhoneNumberStep({onNext, onMove, isEditing}: SubStepProps) {
formTitle={translate('personalInfoStep.whatsYourPhoneNumber')}
formDisclaimer={translate('personalInfoStep.weNeedThisToVerify')}
validate={validate}
onSubmit={handleSubmit}
onSubmit={(values) => {
handleSubmit({...values, phoneNumber: formatE164PhoneNumber(values.phoneNumber) ?? ''});
}}
inputId={PERSONAL_INFO_STEP_KEY.PHONE_NUMBER}
inputLabel={translate('common.phoneNumber')}
inputMode={CONST.INPUT_MODE.TEL}
Expand Down
24 changes: 14 additions & 10 deletions src/pages/MissingPersonalDetails/substeps/PhoneNumber.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import {Str} from 'expensify-common';
import React, {useCallback} from 'react';
import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import SingleFieldStep from '@components/SubStepForms/SingleFieldStep';
import useLocalize from '@hooks/useLocalize';
import usePersonalDetailsFormSubmit from '@hooks/usePersonalDetailsFormSubmit';
import * as LoginUtils from '@libs/LoginUtils';
import * as PhoneNumberUtils from '@libs/PhoneNumber';
import * as ValidationUtils from '@libs/ValidationUtils';
import {appendCountryCode, formatE164PhoneNumber} from '@libs/LoginUtils';
import {isRequiredFulfilled, isValidPhoneNumber} from '@libs/ValidationUtils';
import type {CustomSubStepProps} from '@pages/MissingPersonalDetails/types';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand All @@ -20,14 +18,18 @@ function PhoneNumberStep({isEditing, onNext, onMove, personalDetailsValues}: Cus
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])) {
const phoneNumber = values[INPUT_IDS.PHONE_NUMBER];
const phoneNumberWithCountryCode = appendCountryCode(phoneNumber);

if (!isRequiredFulfilled(phoneNumber)) {
errors[INPUT_IDS.PHONE_NUMBER] = translate('common.error.fieldRequired');
return errors;
}
const phoneNumber = LoginUtils.appendCountryCode(values[INPUT_IDS.PHONE_NUMBER]);
const parsedPhoneNumber = PhoneNumberUtils.parsePhoneNumber(phoneNumber);
if (!parsedPhoneNumber.possible || !Str.isValidE164Phone(phoneNumber.slice(0))) {
errors[INPUT_IDS.PHONE_NUMBER] = translate('bankAccount.error.phoneNumber');

if (!isValidPhoneNumber(phoneNumberWithCountryCode)) {
errors[INPUT_IDS.PHONE_NUMBER] = translate('common.error.phoneNumber');
}

return errors;
},
[translate],
Expand All @@ -47,7 +49,9 @@ function PhoneNumberStep({isEditing, onNext, onMove, personalDetailsValues}: Cus
formID={ONYXKEYS.FORMS.PERSONAL_DETAILS_FORM}
formTitle={translate('privatePersonalDetails.enterPhoneNumber')}
validate={validate}
onSubmit={handleSubmit}
onSubmit={(values) => {
handleSubmit({...values, phoneNumber: formatE164PhoneNumber(values[INPUT_IDS.PHONE_NUMBER]) ?? ''});
}}
inputId={INPUT_IDS.PHONE_NUMBER}
inputLabel={translate('common.phoneNumber')}
inputMode={CONST.INPUT_MODE.TEL}
Expand Down
43 changes: 23 additions & 20 deletions src/pages/settings/Profile/PersonalDetails/PhoneNumberPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import {Str} from 'expensify-common';
import React, {useCallback} from 'react';
import {useOnyx} from 'react-native-onyx';
import DelegateNoAccessWrapper from '@components/DelegateNoAccessWrapper';
Expand All @@ -13,12 +12,11 @@ import TextInput from '@components/TextInput';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ErrorUtils from '@libs/ErrorUtils';
import * as LoginUtils from '@libs/LoginUtils';
import {getEarliestErrorField} from '@libs/ErrorUtils';
import {appendCountryCode, formatE164PhoneNumber} 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 {isRequiredFulfilled, isValidPhoneNumber} from '@libs/ValidationUtils';
import {clearPhoneNumberError, updatePhoneNumber as updatePhone} from '@userActions/PersonalDetails';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import INPUT_IDS from '@src/types/form/PersonalDetailsForm';
Expand All @@ -32,18 +30,18 @@ function PhoneNumberPage() {
const {inputCallbackRef} = useAutoFocusInput();
const phoneNumber = privatePersonalDetails?.phoneNumber ?? '';

const validateLoginError = ErrorUtils.getEarliestErrorField(privatePersonalDetails, 'phoneNumber');
const validateLoginError = 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();
clearPhoneNumberError();
}

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

Navigation.goBack();
Expand All @@ -52,19 +50,24 @@ function PhoneNumberPage() {
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])) {
const phoneNumberValue = values[INPUT_IDS.PHONE_NUMBER];

if (!isRequiredFulfilled(phoneNumberValue)) {
errors[INPUT_IDS.PHONE_NUMBER] = translate('common.error.fieldRequired');
return errors;
}
const phoneNumberWithCountryCode = LoginUtils.appendCountryCode(values[INPUT_IDS.PHONE_NUMBER]);
const parsedPhoneNumber = PhoneNumberUtils.parsePhoneNumber(values[INPUT_IDS.PHONE_NUMBER]);
if (!parsedPhoneNumber.possible || !Str.isValidE164Phone(phoneNumberWithCountryCode.slice(0))) {
errors[INPUT_IDS.PHONE_NUMBER] = translate('bankAccount.error.phoneNumber');

const phoneNumberWithCountryCode = appendCountryCode(phoneNumberValue);

if (!isValidPhoneNumber(phoneNumberWithCountryCode)) {
errors[INPUT_IDS.PHONE_NUMBER] = translate('common.error.phoneNumber');
return errors;
}

// Clear the error when the user tries to validate the form and there are errors
if (validateLoginError && !!errors) {
PersonalDetails.clearPhoneNumberError();
if (validateLoginError && Object.keys(errors).length > 0) {
clearPhoneNumberError();
}

return errors;
},
[translate, validateLoginError],
Expand Down Expand Up @@ -95,7 +98,7 @@ function PhoneNumberPage() {
<OfflineWithFeedback
errors={validateLoginError}
errorRowStyles={styles.mt2}
onClose={() => PersonalDetails.clearPhoneNumberError()}
onClose={() => clearPhoneNumberError()}
>
<InputWrapper
InputComponent={TextInput}
Expand All @@ -111,7 +114,7 @@ function PhoneNumberPage() {
if (!validateLoginError) {
return;
}
PersonalDetails.clearPhoneNumberError();
clearPhoneNumberError();
}}
/>
</OfflineWithFeedback>
Expand Down

0 comments on commit f5df848

Please sign in to comment.