diff --git a/.eslintrc.js b/.eslintrc.js index 6194ccd39d3f..f852c970f85c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -14,6 +14,11 @@ const restrictedImportPaths = [ importNames: ['TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight'], message: "Please use 'PressableWithFeedback' and/or 'PressableWithoutFeedback' from 'src/components/Pressable' instead.", }, + { + name: 'awesome-phonenumber', + importNames: ['parsePhoneNumber'], + message: "Please use '@libs/PhoneNumber' instead.", + }, { name: 'react-native-safe-area-context', importNames: ['useSafeAreaInsets', 'SafeAreaConsumer', 'SafeAreaInsetsContext'], diff --git a/src/libs/LocalePhoneNumber.ts b/src/libs/LocalePhoneNumber.ts index e50f3be87c84..933aa7937560 100644 --- a/src/libs/LocalePhoneNumber.ts +++ b/src/libs/LocalePhoneNumber.ts @@ -1,7 +1,7 @@ -import {parsePhoneNumber} from 'awesome-phonenumber'; import Str from 'expensify-common/lib/str'; import Onyx from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; +import {parsePhoneNumber} from './PhoneNumber'; let countryCodeByIP: number; Onyx.connect({ diff --git a/src/libs/LoginUtils.ts b/src/libs/LoginUtils.ts index 742f9bfe16ce..dca84b9b11e0 100644 --- a/src/libs/LoginUtils.ts +++ b/src/libs/LoginUtils.ts @@ -1,9 +1,9 @@ -import {parsePhoneNumber} from 'awesome-phonenumber'; import {PUBLIC_DOMAINS} from 'expensify-common/lib/CONST'; import Str from 'expensify-common/lib/str'; import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import {parsePhoneNumber} from './PhoneNumber'; let countryCodeByIP: number; Onyx.connect({ diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 0abdfdd02224..7793e388a3bd 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -1,5 +1,4 @@ /* eslint-disable no-continue */ -import {parsePhoneNumber} from 'awesome-phonenumber'; import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; import lodashOrderBy from 'lodash/orderBy'; @@ -17,6 +16,7 @@ import ModifiedExpenseMessage from './ModifiedExpenseMessage'; import Navigation from './Navigation/Navigation'; import Permissions from './Permissions'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; +import * as PhoneNumber from './PhoneNumber'; import * as ReportActionUtils from './ReportActionsUtils'; import * as ReportUtils from './ReportUtils'; import * as TaskUtils from './TaskUtils'; @@ -129,7 +129,7 @@ Onyx.connect({ * @return {String} */ function addSMSDomainIfPhoneNumber(login) { - const parsedPhoneNumber = parsePhoneNumber(login); + const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(login); if (parsedPhoneNumber.possible && !Str.isValidEmail(login)) { return parsedPhoneNumber.number.e164 + CONST.SMS.DOMAIN; } @@ -1333,7 +1333,7 @@ function getOptions( let recentReportOptions = []; let personalDetailsOptions = []; const reportMapForAccountIDs = {}; - const parsedPhoneNumber = parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue))); + const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue))); const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number.e164 : searchInputValue.toLowerCase(); // Filter out all the reports that shouldn't be displayed @@ -1843,7 +1843,7 @@ function getHeaderMessage(hasSelectableOptions, hasUserToInvite, searchValue, ma return Localize.translate(preferredLocale, 'common.maxParticipantsReached', {count: CONST.REPORT.MAXIMUM_PARTICIPANTS}); } - const isValidPhone = parsePhoneNumber(LoginUtils.appendCountryCode(searchValue)).possible; + const isValidPhone = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(searchValue)).possible; const isValidEmail = Str.isValidEmail(searchValue); diff --git a/src/libs/PhoneNumber.ts b/src/libs/PhoneNumber.ts new file mode 100644 index 000000000000..f92aade2c892 --- /dev/null +++ b/src/libs/PhoneNumber.ts @@ -0,0 +1,43 @@ +// eslint-disable-next-line no-restricted-imports +import {parsePhoneNumber as originalParsePhoneNumber} from 'awesome-phonenumber'; +import type {ParsedPhoneNumber, ParsedPhoneNumberInvalid, PhoneNumberParseOptions} from 'awesome-phonenumber'; +import CONST from '@src/CONST'; + +/** + * Wraps awesome-phonenumber's parsePhoneNumber function to handle the case where we want to treat + * a US phone number that's technically valid as invalid. eg: +115005550009. + * See https://github.com/Expensify/App/issues/28492 + */ +function parsePhoneNumber(phoneNumber: string, options?: PhoneNumberParseOptions): ParsedPhoneNumber { + const parsedPhoneNumber = originalParsePhoneNumber(phoneNumber, options); + if (!parsedPhoneNumber.possible) { + return parsedPhoneNumber; + } + + const phoneNumberWithoutSpecialChars = phoneNumber.replace(CONST.REGEX.SPECIAL_CHARS_WITHOUT_NEWLINE, ''); + if (!/^\+11[0-9]{10}$/.test(phoneNumberWithoutSpecialChars)) { + return parsedPhoneNumber; + } + + const countryCode = phoneNumberWithoutSpecialChars.substring(0, 2); + const phoneNumberWithoutCountryCode = phoneNumberWithoutSpecialChars.substring(2); + + return { + ...parsedPhoneNumber, + valid: false, + possible: false, + number: { + ...parsedPhoneNumber.number, + + // mimic the behavior of awesome-phonenumber + e164: phoneNumberWithoutSpecialChars, + international: `${countryCode} ${phoneNumberWithoutCountryCode}`, + national: phoneNumberWithoutCountryCode, + rfc3966: `tel:${countryCode}-${phoneNumberWithoutCountryCode}`, + significant: phoneNumberWithoutCountryCode, + }, + } as ParsedPhoneNumberInvalid; +} + +// eslint-disable-next-line import/prefer-default-export +export {parsePhoneNumber}; diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index d2d9efa8ae4a..9ba11fb16d6a 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -1,4 +1,3 @@ -import {parsePhoneNumber} from 'awesome-phonenumber'; import {addYears, endOfMonth, format, isAfter, isBefore, isSameDay, isValid, isWithinInterval, parse, parseISO, startOfDay, subYears} from 'date-fns'; import {URL_REGEX_WITH_REQUIRED_PROTOCOL} from 'expensify-common/lib/Url'; import isDate from 'lodash/isDate'; @@ -10,6 +9,7 @@ import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import * as CardUtils from './CardUtils'; import DateUtils from './DateUtils'; import * as LoginUtils from './LoginUtils'; +import {parsePhoneNumber} from './PhoneNumber'; import StringUtils from './StringUtils'; /** diff --git a/src/pages/DetailsPage.js b/src/pages/DetailsPage.js index 2fdab08e6675..f215b4167ab6 100755 --- a/src/pages/DetailsPage.js +++ b/src/pages/DetailsPage.js @@ -1,4 +1,3 @@ -import {parsePhoneNumber} from 'awesome-phonenumber'; import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; @@ -23,6 +22,7 @@ import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; +import {parsePhoneNumber} from '@libs/PhoneNumber'; import * as ReportUtils from '@libs/ReportUtils'; import * as UserUtils from '@libs/UserUtils'; import * as Report from '@userActions/Report'; diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.js b/src/pages/EnablePayments/AdditionalDetailsStep.js index 18da2c11a0e6..faa525a318ab 100644 --- a/src/pages/EnablePayments/AdditionalDetailsStep.js +++ b/src/pages/EnablePayments/AdditionalDetailsStep.js @@ -1,4 +1,3 @@ -import {parsePhoneNumber} from 'awesome-phonenumber'; import {subYears} from 'date-fns'; import PropTypes from 'prop-types'; import React from 'react'; @@ -17,6 +16,7 @@ import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultPro import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; +import {parsePhoneNumber} from '@libs/PhoneNumber'; import * as ValidationUtils from '@libs/ValidationUtils'; import AddressForm from '@pages/ReimbursementAccount/AddressForm'; import * as PersonalDetails from '@userActions/PersonalDetails'; diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index 8432d25b6ad7..c0c782f176ca 100755 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -1,4 +1,3 @@ -import {parsePhoneNumber} from 'awesome-phonenumber'; import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; @@ -27,6 +26,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; +import {parsePhoneNumber} from '@libs/PhoneNumber'; import * as ReportUtils from '@libs/ReportUtils'; import * as UserUtils from '@libs/UserUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index 9f985e15a95e..8828cce5cc74 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -1,4 +1,3 @@ -import {parsePhoneNumber} from 'awesome-phonenumber'; import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; @@ -20,6 +19,7 @@ import TextLink from '@components/TextLink'; import withLocalize from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; +import {parsePhoneNumber} from '@libs/PhoneNumber'; import * as ValidationUtils from '@libs/ValidationUtils'; import * as BankAccounts from '@userActions/BankAccounts'; import CONST from '@src/CONST'; diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.js b/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.js index ef0eb3e4eddc..5e4feac83d96 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.js +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.js @@ -1,4 +1,3 @@ -import {parsePhoneNumber} from 'awesome-phonenumber'; import Str from 'expensify-common/lib/str'; import PropTypes from 'prop-types'; import React from 'react'; @@ -9,6 +8,7 @@ import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import FormUtils from '@libs/FormUtils'; +import {parsePhoneNumber} from '@libs/PhoneNumber'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; diff --git a/src/pages/signin/LoginForm/BaseLoginForm.js b/src/pages/signin/LoginForm/BaseLoginForm.js index 1ac12dca0a09..8fcea461eacd 100644 --- a/src/pages/signin/LoginForm/BaseLoginForm.js +++ b/src/pages/signin/LoginForm/BaseLoginForm.js @@ -1,5 +1,4 @@ import {useIsFocused} from '@react-navigation/native'; -import {parsePhoneNumber} from 'awesome-phonenumber'; import Str from 'expensify-common/lib/str'; import PropTypes from 'prop-types'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; @@ -25,6 +24,7 @@ import * as ErrorUtils from '@libs/ErrorUtils'; import isInputAutoFilled from '@libs/isInputAutoFilled'; import Log from '@libs/Log'; import * as LoginUtils from '@libs/LoginUtils'; +import {parsePhoneNumber} from '@libs/PhoneNumber'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; import Visibility from '@libs/Visibility'; diff --git a/tests/unit/PhoneNumberTest.js b/tests/unit/PhoneNumberTest.js new file mode 100644 index 000000000000..f720dc6a88e1 --- /dev/null +++ b/tests/unit/PhoneNumberTest.js @@ -0,0 +1,43 @@ +import {parsePhoneNumber} from '@libs/PhoneNumber'; + +describe('PhoneNumber', () => { + describe('parsePhoneNumber', () => { + it('Should return valid phone number', () => { + const validNumbers = [ + '+1 (234) 567-8901', + '+12345678901', + '+54 11 8765-4321', + '+49 30 123456', + '+44 20 8759 9036', + '+34 606 49 95 99', + ' + 1 2 3 4 5 6 7 8 9 0 1', + '+ 4 4 2 0 8 7 5 9 9 0 3 6', + '+1 ( 2 3 4 ) 5 6 7 - 8 9 0 1', + ]; + + validNumbers.forEach((givenPhone) => { + const parsedPhone = parsePhoneNumber(givenPhone); + expect(parsedPhone.valid).toBe(true); + expect(parsedPhone.possible).toBe(true); + }); + }); + it('Should return invalid phone number if US number has extra 1 after country code', () => { + const validNumbers = ['+1 1 (234) 567-8901', '+112345678901', '+115550123355', '+ 1 1 5 5 5 0 1 2 3 3 5 5']; + + validNumbers.forEach((givenPhone) => { + const parsedPhone = parsePhoneNumber(givenPhone); + expect(parsedPhone.valid).toBe(false); + expect(parsedPhone.possible).toBe(false); + }); + }); + it('Should return invalid phone number', () => { + const invalidNumbers = ['+165025300001', 'John Doe', '123', 'email@domain.com']; + + invalidNumbers.forEach((givenPhone) => { + const parsedPhone = parsePhoneNumber(givenPhone); + expect(parsedPhone.valid).toBe(false); + expect(parsedPhone.possible).toBe(false); + }); + }); + }); +});