From c71114b7d1a045ae4689e018340e98d39bec44d0 Mon Sep 17 00:00:00 2001 From: Prince Mendiratta Date: Fri, 28 Apr 2023 01:28:29 +0530 Subject: [PATCH 1/4] feat: contactMethodPage submit e164 values to api Signed-off-by: Prince Mendiratta --- src/pages/settings/Profile/Contacts/NewContactMethodPage.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.js b/src/pages/settings/Profile/Contacts/NewContactMethodPage.js index 7e15c606404c..e879e2c4856d 100644 --- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.js +++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.js @@ -81,7 +81,10 @@ function NewContactMethodPage(props) { Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS); return; } - User.addNewContactMethodAndNavigate(login, password); + const phoneLogin = LoginUtils.appendCountryCode(LoginUtils.getPhoneNumberWithoutSpecialChars(login)); + const parsedPhoneNumber = parsePhoneNumber(phoneLogin); + + User.addNewContactMethodAndNavigate(parsedPhoneNumber.possible ? parsedPhoneNumber.number.e164 : login, password); }, [login, props.loginList, password]); return ( From c23e1a8c5e33751d591b7d32c451ddb25b8ba138 Mon Sep 17 00:00:00 2001 From: Prince Mendiratta Date: Fri, 28 Apr 2023 01:36:14 +0530 Subject: [PATCH 2/4] Revert "Revert "[PhoneNumber Verification] Display only valid numbers in user search."" This reverts commit fcf63a9329726516d7f91090a6dcba229e86e6aa. --- src/CONST.js | 6 --- src/libs/LoginUtils.js | 16 +------- src/libs/OptionsListUtils.js | 41 ++++++++----------- src/libs/ValidationUtils.js | 10 ++--- src/pages/DetailsPage.js | 6 ++- .../EnablePayments/AdditionalDetailsStep.js | 4 +- src/pages/ReimbursementAccount/CompanyStep.js | 4 +- .../Profile/Contacts/NewContactMethodPage.js | 5 ++- src/pages/signin/LoginForm.js | 9 ++-- 9 files changed, 38 insertions(+), 63 deletions(-) diff --git a/src/CONST.js b/src/CONST.js index 20e293011f1c..edd5e3b61fa8 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -963,15 +963,10 @@ const CONST = { }, REGEX: { SPECIAL_CHARS_WITHOUT_NEWLINE: /((?!\n)[()-\s\t])/g, - US_PHONE: /^\+1\d{10}$/, - US_PHONE_WITH_OPTIONAL_COUNTRY_CODE: /^(\+1)?\d{10}$/, DIGITS_AND_PLUS: /^\+?[0-9]*$/, - PHONE_E164_PLUS: /^\+?[1-9]\d{1,14}$/, - PHONE_WITH_SPECIAL_CHARS: /^\s*(?:\+?(\d{1,3}))?[-. (]*(\d{3})[-. )]*(\d{3})[-. ]*(\d{4})(?: *x(\d+))?\s*$/, ALPHABETIC_CHARS: /[a-zA-Z]+/, ALPHABETIC_CHARS_WITH_NUMBER: /^[a-zA-Z0-9 ]*$/, POSITIVE_INTEGER: /^\d+$/, - NON_ALPHA_NUMERIC: /[^A-Za-z0-9+]/g, PO_BOX: /\b[P|p]?(OST|ost)?\.?\s*[O|o|0]?(ffice|FFICE)?\.?\s*[B|b][O|o|0]?[X|x]?\.?\s+[#]?(\d+)\b/, ANY_VALUE: /^.+$/, ZIP_CODE: /[0-9]{5}(?:[- ][0-9]{4})?/, @@ -993,7 +988,6 @@ const CONST = { // Extract attachment's source from the data's html string ATTACHMENT_DATA: /(data-expensify-source|data-name)="([^"]+)"/g, - NON_NUMERIC_WITH_PLUS: /[^0-9+]/g, EMOJI_NAME: /:[\w+-]+:/g, EMOJI_SUGGESTIONS: /:[a-zA-Z0-9_+-]{1,40}$/, AFTER_FIRST_LINE_BREAK: /\n.*/g, diff --git a/src/libs/LoginUtils.js b/src/libs/LoginUtils.js index c4f160f8cf40..6da0f1617e38 100644 --- a/src/libs/LoginUtils.js +++ b/src/libs/LoginUtils.js @@ -1,4 +1,3 @@ -import Str from 'expensify-common/lib/str'; import Onyx from 'react-native-onyx'; import CONST from '../CONST'; import ONYXKEYS from '../ONYXKEYS'; @@ -19,16 +18,6 @@ function getPhoneNumberWithoutSpecialChars(phone) { return phone.replace(CONST.REGEX.SPECIAL_CHARS_WITHOUT_NEWLINE, ''); } -/** - * Remove +1 and special chars from the phone number - * - * @param {String} phone - * @return {String} - */ -function getPhoneNumberWithoutUSCountryCodeAndSpecialChars(phone) { - return getPhoneNumberWithoutSpecialChars(phone.replace(/^\+1/, '')); -} - /** * Append user country code to the phone number * @@ -36,13 +25,10 @@ function getPhoneNumberWithoutUSCountryCodeAndSpecialChars(phone) { * @return {String} */ function appendCountryCode(phone) { - return (Str.isValidPhone(phone) && !phone.includes('+')) - ? `+${countryCodeByIP}${phone}` - : phone; + return phone.startsWith('+') ? phone : `+${countryCodeByIP}${phone}`; } export { getPhoneNumberWithoutSpecialChars, - getPhoneNumberWithoutUSCountryCodeAndSpecialChars, appendCountryCode, }; diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index e05c3613207d..ffed87332ec2 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -4,6 +4,7 @@ import Onyx from 'react-native-onyx'; import lodashOrderBy from 'lodash/orderBy'; import lodashGet from 'lodash/get'; import Str from 'expensify-common/lib/str'; +import {parsePhoneNumber} from 'awesome-phonenumber'; import ONYXKEYS from '../ONYXKEYS'; import CONST from '../CONST'; import * as ReportUtils from './ReportUtils'; @@ -32,12 +33,6 @@ Onyx.connect({ callback: val => loginList = _.isEmpty(val) ? {} : val, }); -let countryCodeByIP; -Onyx.connect({ - key: ONYXKEYS.COUNTRY_CODE, - callback: val => countryCodeByIP = val || 1, -}); - let preferredLocale; Onyx.connect({ key: ONYXKEYS.NVP_PREFERRED_LOCALE, @@ -134,9 +129,9 @@ function getPolicyExpenseReportOptions(report) { * @return {String} */ function addSMSDomainIfPhoneNumber(login) { - if (Str.isValidPhone(login) && !Str.isValidEmail(login)) { - const smsLogin = login + CONST.SMS.DOMAIN; - return smsLogin.includes('+') ? smsLogin : `+${countryCodeByIP}${smsLogin}`; + const parsedPhoneNumber = parsePhoneNumber(login); + if (parsedPhoneNumber.possible && !Str.isValidEmail(login)) { + return parsedPhoneNumber.number.e164 + CONST.SMS.DOMAIN; } return login; } @@ -532,8 +527,8 @@ function getOptions(reports, personalDetails, { let recentReportOptions = []; let personalDetailsOptions = []; const reportMapForLogins = {}; - const isPhoneNumber = CONST.REGEX.PHONE_WITH_SPECIAL_CHARS.test(searchInputValue); - const searchValue = isPhoneNumber ? searchInputValue.replace(CONST.REGEX.NON_NUMERIC_WITH_PLUS, '') : searchInputValue; + const parsedPhoneNumber = parsePhoneNumber(LoginUtils.appendCountryCode(searchInputValue)); + const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number.e164 : searchInputValue; // Filter out all the reports that shouldn't be displayed const filteredReports = _.filter(reports, report => ReportUtils.shouldReportBeInOptionList( @@ -676,25 +671,21 @@ function getOptions(reports, personalDetails, { const noOptions = (recentReportOptions.length + personalDetailsOptions.length) === 0; const noOptionsMatchExactly = !_.find(personalDetailsOptions.concat(recentReportOptions), option => option.login === searchValue.toLowerCase()); - // If the phone number doesn't have an international code then let's prefix it with the - // current user's international code based on their IP address. - const login = LoginUtils.appendCountryCode(searchValue); - - if (login && (noOptions || noOptionsMatchExactly) - && !isCurrentUser({login}) - && _.every(selectedOptions, option => option.login !== login) - && ((Str.isValidEmail(login) && !Str.isDomainEmail(login)) || Str.isValidPhone(login)) - && (!_.find(loginOptionsToExclude, loginOptionToExclude => loginOptionToExclude.login === addSMSDomainIfPhoneNumber(login).toLowerCase())) - && (login !== CONST.EMAIL.CHRONOS || Permissions.canUseChronos(betas)) + if (searchValue && (noOptions || noOptionsMatchExactly) + && !isCurrentUser({login: searchValue}) + && _.every(selectedOptions, option => option.login !== searchValue) + && ((Str.isValidEmail(searchValue) && !Str.isDomainEmail(searchValue)) || parsedPhoneNumber.possible) + && (!_.find(loginOptionsToExclude, loginOptionToExclude => loginOptionToExclude.login === addSMSDomainIfPhoneNumber(searchValue).toLowerCase())) + && (searchValue !== CONST.EMAIL.CHRONOS || Permissions.canUseChronos(betas)) ) { - userToInvite = createOption([login], personalDetails, null, reportActions, { + userToInvite = createOption([searchValue], personalDetails, null, reportActions, { showChatPreviewLine, }); // If user doesn't exist, use a default avatar userToInvite.icons = [{ - source: ReportUtils.getAvatar('', login), - name: login, + source: ReportUtils.getAvatar('', searchValue), + name: searchValue, type: CONST.ICON_TYPE_AVATAR, }]; } @@ -864,7 +855,7 @@ function getHeaderMessage(hasSelectableOptions, hasUserToInvite, searchValue, ma return Localize.translate(preferredLocale, 'common.maxParticipantsReached', {count: CONST.REPORT.MAXIMUM_PARTICIPANTS}); } - const isValidPhone = Str.isValidPhone(LoginUtils.appendCountryCode(searchValue)); + const isValidPhone = parsePhoneNumber(LoginUtils.appendCountryCode(searchValue)).possible; const isValidEmail = Str.isValidEmail(searchValue); diff --git a/src/libs/ValidationUtils.js b/src/libs/ValidationUtils.js index 90eccdd7e94b..7e0f65f0a91d 100644 --- a/src/libs/ValidationUtils.js +++ b/src/libs/ValidationUtils.js @@ -1,6 +1,7 @@ import moment from 'moment'; import _ from 'underscore'; import {URL_REGEX_WITH_REQUIRED_PROTOCOL} from 'expensify-common/lib/Url'; +import {parsePhoneNumber} from 'awesome-phonenumber'; import CONST from '../CONST'; import * as CardUtils from './CardUtils'; import * as LoginUtils from './LoginUtils'; @@ -297,12 +298,11 @@ function validateIdentity(identity) { * @returns {Boolean} */ function isValidUSPhone(phoneNumber = '', isCountryCodeOptional) { - // Remove non alphanumeric characters from the phone number - const sanitizedPhone = (phoneNumber || '').replace(CONST.REGEX.NON_ALPHA_NUMERIC, ''); - const isUsPhone = isCountryCodeOptional - ? CONST.REGEX.US_PHONE_WITH_OPTIONAL_COUNTRY_CODE.test(sanitizedPhone) : CONST.REGEX.US_PHONE.test(sanitizedPhone); + const phone = phoneNumber || ''; + const regionCode = isCountryCodeOptional ? CONST.COUNTRY.US : null; - return CONST.REGEX.PHONE_E164_PLUS.test(sanitizedPhone) && isUsPhone; + const parsedPhoneNumber = parsePhoneNumber(phone, {regionCode}); + return parsedPhoneNumber.possible && parsedPhoneNumber.regionCode === CONST.COUNTRY.US; } /** diff --git a/src/pages/DetailsPage.js b/src/pages/DetailsPage.js index 84c36ea945d0..003823bc3ca8 100755 --- a/src/pages/DetailsPage.js +++ b/src/pages/DetailsPage.js @@ -5,6 +5,7 @@ import _ from 'underscore'; import {withOnyx} from 'react-native-onyx'; import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; +import {parsePhoneNumber} from 'awesome-phonenumber'; import styles from '../styles/styles'; import Text from '../components/Text'; import ONYXKEYS from '../ONYXKEYS'; @@ -73,8 +74,9 @@ const defaultProps = { */ const getPhoneNumber = (details) => { // If the user hasn't set a displayName, it is set to their phone number, so use that - if (Str.isValidPhone(details.displayName)) { - return details.displayName; + const parsedPhoneNumber = parsePhoneNumber(details.displayName); + if (parsedPhoneNumber.possible) { + return parsedPhoneNumber.number.e164; } // If the user has set a displayName, get the phone number from the SMS login diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.js b/src/pages/EnablePayments/AdditionalDetailsStep.js index bbc9a991d14d..dc5bbd6edd3e 100644 --- a/src/pages/EnablePayments/AdditionalDetailsStep.js +++ b/src/pages/EnablePayments/AdditionalDetailsStep.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; import {View} from 'react-native'; import moment from 'moment/moment'; +import {parsePhoneNumber} from 'awesome-phonenumber'; import IdologyQuestions from './IdologyQuestions'; import ScreenWrapper from '../../components/ScreenWrapper'; import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; @@ -18,7 +19,6 @@ import TextLink from '../../components/TextLink'; import TextInput from '../../components/TextInput'; import * as Wallet from '../../libs/actions/Wallet'; import * as ValidationUtils from '../../libs/ValidationUtils'; -import * as LoginUtils from '../../libs/LoginUtils'; import * as ErrorUtils from '../../libs/ErrorUtils'; import AddressForm from '../ReimbursementAccount/AddressForm'; import DatePicker from '../../components/DatePicker'; @@ -173,7 +173,7 @@ class AdditionalDetailsStep extends React.Component { */ activateWallet(values) { const personalDetails = { - phoneNumber: LoginUtils.getPhoneNumberWithoutUSCountryCodeAndSpecialChars(values[INPUT_IDS.PHONE_NUMBER]), + phoneNumber: parsePhoneNumber(values[INPUT_IDS.PHONE_NUMBER], {regionCode: CONST.COUNTRY.US}).number.significant, legalFirstName: values[INPUT_IDS.LEGAL_FIRST_NAME], legalLastName: values[INPUT_IDS.LEGAL_LAST_NAME], addressStreet: values[INPUT_IDS.ADDRESS.street], diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index e8480b4b9436..9acc6296bb40 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -5,6 +5,7 @@ import {View} from 'react-native'; import Str from 'expensify-common/lib/str'; import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; +import {parsePhoneNumber} from 'awesome-phonenumber'; import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; import CONST from '../../CONST'; import * as BankAccounts from '../../libs/actions/BankAccounts'; @@ -18,7 +19,6 @@ import TextLink from '../../components/TextLink'; import StatePicker from '../../components/StatePicker'; import withLocalize from '../../components/withLocalize'; import * as ValidationUtils from '../../libs/ValidationUtils'; -import * as LoginUtils from '../../libs/LoginUtils'; import compose from '../../libs/compose'; import ONYXKEYS from '../../ONYXKEYS'; import Picker from '../../components/Picker'; @@ -148,7 +148,7 @@ class CompanyStep extends React.Component { // Fields from Company step ...values, companyTaxID: values.companyTaxID.replace(CONST.REGEX.NON_NUMERIC, ''), - companyPhone: LoginUtils.getPhoneNumberWithoutUSCountryCodeAndSpecialChars(values.companyPhone), + companyPhone: parsePhoneNumber(values.companyPhone, {regionCode: CONST.COUNTRY.US}).number.significant, }; BankAccounts.updateCompanyInformationForBankAccount(bankAccount); diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.js b/src/pages/settings/Profile/Contacts/NewContactMethodPage.js index a9f4006a9676..e879e2c4856d 100644 --- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.js +++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.js @@ -8,6 +8,7 @@ import {withOnyx} from 'react-native-onyx'; import {compose} from 'underscore'; import lodashGet from 'lodash/get'; import Str from 'expensify-common/lib/str'; +import {parsePhoneNumber} from 'awesome-phonenumber'; import Button from '../../../../components/Button'; import FixedFooter from '../../../../components/FixedFooter'; import HeaderWithCloseButton from '../../../../components/HeaderWithCloseButton'; @@ -68,10 +69,10 @@ function NewContactMethodPage(props) { }, []); const isFormValid = useMemo(() => { - const phoneLogin = LoginUtils.getPhoneNumberWithoutSpecialChars(login); + const phoneLogin = LoginUtils.appendCountryCode(LoginUtils.getPhoneNumberWithoutSpecialChars(login)); return (Permissions.canUsePasswordlessLogins(props.betas) || password) - && (Str.isValidEmail(login) || Str.isValidPhone(phoneLogin)); + && (Str.isValidEmail(login) || parsePhoneNumber(phoneLogin).possible); }, [login, password, props.betas]); const submitForm = useCallback(() => { diff --git a/src/pages/signin/LoginForm.js b/src/pages/signin/LoginForm.js index 78633db86ac0..e9ead825a3da 100755 --- a/src/pages/signin/LoginForm.js +++ b/src/pages/signin/LoginForm.js @@ -4,6 +4,7 @@ import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; import _ from 'underscore'; import Str from 'expensify-common/lib/str'; +import {parsePhoneNumber} from 'awesome-phonenumber'; import styles from '../../styles/styles'; import Text from '../../components/Text'; import * as Session from '../../libs/actions/Session'; @@ -143,10 +144,10 @@ class LoginForm extends React.Component { return; } - const phoneLogin = LoginUtils.getPhoneNumberWithoutSpecialChars(login); - const isValidPhoneLogin = Str.isValidPhone(phoneLogin); + const phoneLogin = LoginUtils.appendCountryCode(LoginUtils.getPhoneNumberWithoutSpecialChars(login)); + const parsedPhoneNumber = parsePhoneNumber(phoneLogin); - if (!Str.isValidEmail(login) && !isValidPhoneLogin) { + if (!Str.isValidEmail(login) && !parsedPhoneNumber.possible) { if (ValidationUtils.isNumericWithSpecialChars(login)) { this.setState({formError: 'common.error.phoneNumber'}); } else { @@ -160,7 +161,7 @@ class LoginForm extends React.Component { }); // Check if this login has an account associated with it or not - Session.beginSignIn(isValidPhoneLogin ? phoneLogin : login); + Session.beginSignIn(parsedPhoneNumber.possible ? parsedPhoneNumber.number.e164 : login); } render() { From 09f489147cf2c3ce9379e2b45328c4c50f1e05a3 Mon Sep 17 00:00:00 2001 From: Prince Mendiratta Date: Fri, 28 Apr 2023 03:43:45 +0530 Subject: [PATCH 3/4] fix: reorder const declaration Signed-off-by: Prince Mendiratta --- src/pages/settings/Profile/Contacts/NewContactMethodPage.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.js b/src/pages/settings/Profile/Contacts/NewContactMethodPage.js index e879e2c4856d..d3d38a917ab0 100644 --- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.js +++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.js @@ -76,13 +76,14 @@ function NewContactMethodPage(props) { }, [login, password, props.betas]); const submitForm = useCallback(() => { + const phoneLogin = LoginUtils.appendCountryCode(LoginUtils.getPhoneNumberWithoutSpecialChars(login)); + const parsedPhoneNumber = parsePhoneNumber(phoneLogin); + // If this login already exists, just go back. if (lodashGet(props.loginList, login)) { Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS); return; } - const phoneLogin = LoginUtils.appendCountryCode(LoginUtils.getPhoneNumberWithoutSpecialChars(login)); - const parsedPhoneNumber = parsePhoneNumber(phoneLogin); User.addNewContactMethodAndNavigate(parsedPhoneNumber.possible ? parsedPhoneNumber.number.e164 : login, password); }, [login, props.loginList, password]); From 5d2e75b077f211e55180fb34d9401a96da8a4c96 Mon Sep 17 00:00:00 2001 From: Prince Mendiratta Date: Sat, 29 Apr 2023 14:21:23 +0530 Subject: [PATCH 4/4] fix: minor refactoring Signed-off-by: Prince Mendiratta --- src/pages/settings/Profile/Contacts/NewContactMethodPage.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.js b/src/pages/settings/Profile/Contacts/NewContactMethodPage.js index d3d38a917ab0..dd3845c26149 100644 --- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.js +++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.js @@ -78,14 +78,15 @@ function NewContactMethodPage(props) { const submitForm = useCallback(() => { const phoneLogin = LoginUtils.appendCountryCode(LoginUtils.getPhoneNumberWithoutSpecialChars(login)); const parsedPhoneNumber = parsePhoneNumber(phoneLogin); + const userLogin = parsedPhoneNumber.possible ? parsedPhoneNumber.number.e164 : login; // If this login already exists, just go back. - if (lodashGet(props.loginList, login)) { + if (lodashGet(props.loginList, userLogin)) { Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS); return; } - User.addNewContactMethodAndNavigate(parsedPhoneNumber.possible ? parsedPhoneNumber.number.e164 : login, password); + User.addNewContactMethodAndNavigate(userLogin, password); }, [login, props.loginList, password]); return (