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

improve phone validation error messages #55768

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
9 changes: 9 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,15 @@ const CONST = {
// Regex to read violation value from string given by backend
VIOLATION_LIMIT_REGEX: /[^0-9]+/g,

// Removes non-digit/non-plus characters for phone sanitization.
SANITIZE_PHONE_REGEX: /[^\d+]/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
42 changes: 32 additions & 10 deletions src/pages/MissingPersonalDetails/substeps/PhoneNumber.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ 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} from '@libs/LoginUtils';
import {parsePhoneNumber} from '@libs/PhoneNumber';
import {isRequiredFulfilled} from '@libs/ValidationUtils';
import type {CustomSubStepProps} from '@pages/MissingPersonalDetails/types';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand All @@ -17,20 +17,40 @@ const STEP_FIELDS = [INPUT_IDS.PHONE_NUMBER];
function PhoneNumberStep({isEditing, onNext, onMove, personalDetailsValues}: CustomSubStepProps) {
const {translate} = useLocalize();

const sanitizePhoneNumber = (num?: string): string => num?.replace(CONST.SANITIZE_PHONE_REGEX, '') ?? '';
const formatPhoneNumber = useCallback((num: string) => {
const phoneNumberWithCountryCode = appendCountryCode(sanitizePhoneNumber(num));
const parsedPhoneNumber = parsePhoneNumber(phoneNumberWithCountryCode);

return parsedPhoneNumber;
}, []);

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

if (!CONST.ACCEPTED_PHONE_CHARACTER_REGEX.test(phoneNumberValue) || CONST.REPEATED_SPECIAL_CHAR_PATTERN.test(phoneNumberValue)) {
errors[INPUT_IDS.PHONE_NUMBER] = translate('common.error.phoneNumber');
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');

const sanitizedPhoneNumber = sanitizePhoneNumber(phoneNumberValue);
const phoneNumberWithCountryCode = appendCountryCode(sanitizedPhoneNumber);
const parsedPhoneNumber = formatPhoneNumber(phoneNumberValue);

if (!parsedPhoneNumber.possible || !Str.isValidE164Phone(phoneNumberWithCountryCode)) {
errors[INPUT_IDS.PHONE_NUMBER] = translate('common.error.phoneNumber');
}

return errors;
},
[translate],
[formatPhoneNumber, translate],
);

const handleSubmit = usePersonalDetailsFormSubmit({
Expand All @@ -47,7 +67,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: formatPhoneNumber(values[INPUT_IDS.PHONE_NUMBER]).number?.e164 ?? ''});
}}
inputId={INPUT_IDS.PHONE_NUMBER}
inputLabel={translate('common.phoneNumber')}
inputMode={CONST.INPUT_MODE.TEL}
Expand Down
61 changes: 41 additions & 20 deletions src/pages/settings/Profile/PersonalDetails/PhoneNumberPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ 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} 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 {parsePhoneNumber} from '@libs/PhoneNumber';
import {isRequiredFulfilled} 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 +32,28 @@ 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 sanitizePhoneNumber = (num?: string): string => num?.replace(CONST.SANITIZE_PHONE_REGEX, '') ?? '';
const formatPhoneNumber = useCallback((num: string) => {
const phoneNumberWithCountryCode = appendCountryCode(sanitizePhoneNumber(num));
const parsedPhoneNumber = parsePhoneNumber(phoneNumberWithCountryCode);

return parsedPhoneNumber;
}, []);

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 (phoneNumber !== values?.phoneNumber && values?.phoneNumber) {
const formattedPhone = formatPhoneNumber(values.phoneNumber);

updatePhone(formattedPhone.number?.e164 ?? '', currenPhoneNumber);
}

Navigation.goBack();
Expand All @@ -52,22 +62,33 @@ 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');

if (!CONST.ACCEPTED_PHONE_CHARACTER_REGEX.test(phoneNumberValue) || CONST.REPEATED_SPECIAL_CHAR_PATTERN.test(phoneNumberValue)) {
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();
const sanitizedPhoneNumber = sanitizePhoneNumber(phoneNumberValue);
const phoneNumberWithCountryCode = appendCountryCode(sanitizedPhoneNumber);
const parsedPhoneNumber = formatPhoneNumber(phoneNumberValue);

if (!parsedPhoneNumber.possible || !Str.isValidE164Phone(phoneNumberWithCountryCode)) {
errors[INPUT_IDS.PHONE_NUMBER] = translate('common.error.phoneNumber');
}

if (validateLoginError && Object.keys(errors).length > 0) {
clearPhoneNumberError();
}

return errors;
},
[translate, validateLoginError],
[formatPhoneNumber, translate, validateLoginError],
);

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