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 9 commits into
base: main
Choose a base branch
from
6 changes: 6 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,12 @@ 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 allowing digits, '+', '-', '()', and '.'.
PHONE_NUMBER_PATTERN: /^[0-9+\-().]+$/,
huult marked this conversation as resolved.
Show resolved Hide resolved

MERCHANT_NAME_MAX_LENGTH: 255,

MASKED_PAN_PREFIX: 'XXXXXXXXXXXX',
Expand Down
2 changes: 1 addition & 1 deletion src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2012,7 +2012,7 @@ const translations = {
taxID: 'Please enter a valid tax ID number.',
website: 'Please enter a valid website.',
zipCode: `Please enter a valid ZIP code using the format: ${CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}.`,
phoneNumber: 'Please enter a valid phone number.',
phoneNumber: `Please enter a valid phone number, with the country code (e.g. ${CONST.EXAMPLE_PHONE_NUMBER})`,
email: 'Please enter a valid email address.',
companyName: 'Please enter a valid business name.',
addressCity: 'Please enter a valid city.',
Expand Down
2 changes: 1 addition & 1 deletion src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2033,7 +2033,7 @@ const translations = {
taxID: 'Por favor, introduce un número de identificación fiscal válido.',
website: 'Por favor, introduce un sitio web válido.',
zipCode: `Formato de código postal incorrecto. Formato aceptable: ${CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}.`,
phoneNumber: 'Por favor, introduce un teléfono válido.',
phoneNumber: `Introduce un teléfono válido, incluyendo el código del país (p. ej. ${CONST.EXAMPLE_PHONE_NUMBER})`,
email: 'Por favor, introduce una dirección de correo electrónico válida.',
companyName: 'Por favor, introduce un nombre comercial legal válido.',
addressCity: 'Por favor, introduce una ciudad válida.',
Expand Down
40 changes: 31 additions & 9 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;
}
const phoneNumber = LoginUtils.appendCountryCode(values[INPUT_IDS.PHONE_NUMBER]);
const parsedPhoneNumber = PhoneNumberUtils.parsePhoneNumber(phoneNumber);
if (!parsedPhoneNumber.possible || !Str.isValidE164Phone(phoneNumber.slice(0))) {

if (!CONST.PHONE_NUMBER_PATTERN.test(phoneNumberValue)) {
errors[INPUT_IDS.PHONE_NUMBER] = translate('bankAccount.error.phoneNumber');
return errors;
}

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

if (!parsedPhoneNumber.possible || !Str.isValidE164Phone(phoneNumberWithCountryCode)) {
errors[INPUT_IDS.PHONE_NUMBER] = translate('bankAccount.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
59 changes: 40 additions & 19 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;
}

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

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

if (!parsedPhoneNumber.possible || !Str.isValidE164Phone(phoneNumberWithCountryCode)) {
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();
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