diff --git a/package-lock.json b/package-lock.json index 17bb2937fbbe..7f85ce9f4c50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "@react-navigation/native": "6.0.13", "@react-navigation/stack": "6.3.1", "@ua/react-native-airship": "^15.2.0", + "awesome-phonenumber": "^5.4.0", "babel-plugin-transform-remove-console": "^6.9.4", "babel-polyfill": "^6.26.0", "dom-serializer": "^0.2.2", @@ -16727,6 +16728,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/awesome-phonenumber": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/awesome-phonenumber/-/awesome-phonenumber-5.4.0.tgz", + "integrity": "sha512-jf6E+GHKRIMobCKygQhZ9kHmdxZ8hvXUlVhLyesP/k8JVpmWAyNa5TzWDS0hKe480tmT419yRtGLmTpD0k8pUQ==", + "engines": { + "node": ">=14" + } + }, "node_modules/axe-core": { "version": "4.4.3", "dev": true, @@ -52437,6 +52446,11 @@ "version": "1.0.5", "dev": true }, + "awesome-phonenumber": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/awesome-phonenumber/-/awesome-phonenumber-5.4.0.tgz", + "integrity": "sha512-jf6E+GHKRIMobCKygQhZ9kHmdxZ8hvXUlVhLyesP/k8JVpmWAyNa5TzWDS0hKe480tmT419yRtGLmTpD0k8pUQ==" + }, "axe-core": { "version": "4.4.3", "dev": true diff --git a/package.json b/package.json index bcf5453792c8..12700aa68a36 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "@react-navigation/native": "6.0.13", "@react-navigation/stack": "6.3.1", "@ua/react-native-airship": "^15.2.0", + "awesome-phonenumber": "^5.4.0", "babel-plugin-transform-remove-console": "^6.9.4", "babel-polyfill": "^6.26.0", "dom-serializer": "^0.2.2", diff --git a/src/components/withLocalize.js b/src/components/withLocalize.js index eae8796efa56..422bff30c87d 100755 --- a/src/components/withLocalize.js +++ b/src/components/withLocalize.js @@ -7,12 +7,12 @@ import getComponentDisplayName from '../libs/getComponentDisplayName'; import ONYXKEYS from '../ONYXKEYS'; import * as Localize from '../libs/Localize'; import DateUtils from '../libs/DateUtils'; -import * as LocalePhoneNumber from '../libs/LocalePhoneNumber'; import * as NumberFormatUtils from '../libs/NumberFormatUtils'; import * as LocaleDigitUtils from '../libs/LocaleDigitUtils'; import CONST from '../CONST'; import compose from '../libs/compose'; import withCurrentUserPersonalDetails from './withCurrentUserPersonalDetails'; +import * as LocalePhoneNumber from '../libs/LocalePhoneNumber'; const LocaleContext = createContext(null); @@ -29,11 +29,9 @@ const withLocalizePropTypes = { /** Formats a datetime to local date and time string */ datetimeToCalendarTime: PropTypes.func.isRequired, - /** Returns a locally converted phone number without the country code */ - toLocalPhone: PropTypes.func.isRequired, - - /** Returns an internationally converted phone number with the country code */ - fromLocalPhone: PropTypes.func.isRequired, + /** Returns a locally converted phone number for numbers from the same region + * and an internationally converted phone number with the country code for numbers from other regions */ + formatPhoneNumber: PropTypes.func.isRequired, /** Gets the standard digit corresponding to a locale digit */ fromLocaleDigit: PropTypes.func.isRequired, @@ -77,8 +75,7 @@ class LocaleContextProvider extends React.Component { numberFormat: this.numberFormat.bind(this), datetimeToRelative: this.datetimeToRelative.bind(this), datetimeToCalendarTime: this.datetimeToCalendarTime.bind(this), - fromLocalPhone: this.fromLocalPhone.bind(this), - toLocalPhone: this.toLocalPhone.bind(this), + formatPhoneNumber: this.formatPhoneNumber.bind(this), fromLocaleDigit: this.fromLocaleDigit.bind(this), toLocaleDigit: this.toLocaleDigit.bind(this), preferredLocale: this.props.preferredLocale, @@ -126,19 +123,11 @@ class LocaleContextProvider extends React.Component { } /** - * @param {Number} number - * @returns {String} - */ - toLocalPhone(number) { - return LocalePhoneNumber.toLocalPhone(this.props.preferredLocale, number); - } - - /** - * @param {Number} number + * @param {String} phoneNumber * @returns {String} */ - fromLocalPhone(number) { - return LocalePhoneNumber.fromLocalPhone(this.props.preferredLocale, number); + formatPhoneNumber(phoneNumber) { + return LocalePhoneNumber.formatPhoneNumber(phoneNumber); } /** diff --git a/src/libs/LocalePhoneNumber.js b/src/libs/LocalePhoneNumber.js index 3d47e7c6a5a9..dbe7e39f5941 100644 --- a/src/libs/LocalePhoneNumber.js +++ b/src/libs/LocalePhoneNumber.js @@ -1,53 +1,73 @@ import lodashGet from 'lodash/get'; -import lodashTrim from 'lodash/trim'; -import lodashIncludes from 'lodash/includes'; -import lodashStartsWith from 'lodash/startsWith'; +import Onyx from 'react-native-onyx'; import Str from 'expensify-common/lib/str'; -import translations from '../languages/translations'; +import {parsePhoneNumber} from 'awesome-phonenumber'; +import ONYXKEYS from '../ONYXKEYS'; -/** - * Returns a locally converted phone number without the country code - * - * @param {String} locale eg 'en', 'es-ES' - * @param {String} number - * @returns {String} - */ -function toLocalPhone(locale, number) { - const numString = lodashTrim(number); - const withoutPlusNum = lodashIncludes(numString, '+') ? Str.cutBefore(numString, '+') : numString; - const country = lodashGet(translations, [locale, 'phoneCountryCode']); - - if (country) { - if (lodashStartsWith(withoutPlusNum, country)) { - return Str.cutBefore(withoutPlusNum, country); +let currentUserEmail; +Onyx.connect({ + key: ONYXKEYS.SESSION, + callback: (val) => { + // When signed out, val is undefined + if (!val) { + return; } - return numString; - } - return number; -} + + currentUserEmail = val.email; + }, +}); + +let currentUserPersonalDetails; +Onyx.connect({ + key: ONYXKEYS.PERSONAL_DETAILS, + callback: (val) => { + currentUserPersonalDetails = lodashGet(val, currentUserEmail, {}); + }, +}); + +let countryCodeByIP; +Onyx.connect({ + key: ONYXKEYS.COUNTRY_CODE, + callback: val => countryCodeByIP = val || 1, +}); /** - * Returns an internationally converted phone number with the country code + * Returns a locally converted phone number for numbers from the same region + * and an internationally converted phone number with the country code for numbers from other regions * - * @param {String} locale eg 'en', 'es-ES' * @param {String} number * @returns {String} */ -function fromLocalPhone(locale, number) { - const numString = lodashTrim(number); - const withoutPlusNum = lodashIncludes(numString, '+') ? Str.cutBefore(numString, '+') : numString; - const country = lodashGet(translations, [locale, 'phoneCountryCode']); - - if (country) { - if (lodashStartsWith(withoutPlusNum, country)) { - return `+${withoutPlusNum}`; - } - return `+${country}${withoutPlusNum}`; +function formatPhoneNumber(number) { + const parsedPhoneNumber = parsePhoneNumber(Str.removeSMSDomain(number)); + + // return the string untouched if it's not a phone number + if (!parsedPhoneNumber.valid) { + return number; + } + + let signedInUserCountryCode; + + /** + * if there is a phone number saved in the user's personal details we format the other numbers depending on + * the phone number's country code, otherwise we use country code based on the user's IP + */ + if (currentUserPersonalDetails.phoneNumber) { + signedInUserCountryCode = parsePhoneNumber(currentUserPersonalDetails.phoneNumber).countryCode; + } else { + signedInUserCountryCode = countryCodeByIP; } - return number; + + const regionCode = parsedPhoneNumber.countryCode; + + if (regionCode === signedInUserCountryCode) { + return parsedPhoneNumber.number.national; + } + + return parsedPhoneNumber.number.international; } export { - toLocalPhone, - fromLocalPhone, + // eslint-disable-next-line import/prefer-default-export + formatPhoneNumber, }; diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 617b84b83f6f..e5a5d22bec6e 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -12,6 +12,7 @@ import Permissions from './Permissions'; import * as CollectionUtils from './CollectionUtils'; import Navigation from './Navigation/Navigation'; import * as LoginUtils from './LoginUtils'; +import * as LocalePhoneNumber from './LocalePhoneNumber'; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can @@ -161,7 +162,7 @@ function getPersonalDetailsForLogins(logins, personalDetails) { if (!personalDetail) { personalDetail = { login, - displayName: Str.removeSMSDomain(login), + displayName: LocalePhoneNumber.formatPhoneNumber(login), avatar: ReportUtils.getDefaultAvatar(login), }; } @@ -189,7 +190,7 @@ function getParticipantsOptions(report, personalDetails) { text: details.displayName, firstName: lodashGet(details, 'firstName', ''), lastName: lodashGet(details, 'lastName', ''), - alternateText: Str.isSMSLogin(details.login) ? Str.removeSMSDomain(details.login) : details.login, + alternateText: Str.isSMSLogin(details.login) ? LocalePhoneNumber.formatPhoneNumber(details.login) : details.login, icons: [{ source: ReportUtils.getAvatar(details.avatar, details.login), name: details.login, @@ -431,13 +432,13 @@ function createOption(logins, personalDetails, report, reportActions = {}, { } else { result.alternateText = (showChatPreviewLine && lastMessageText) ? lastMessageText - : Str.removeSMSDomain(personalDetail.login); + : LocalePhoneNumber.formatPhoneNumber(personalDetail.login); } reportName = ReportUtils.getReportName(report, policies); } else { reportName = ReportUtils.getDisplayNameForParticipant(logins[0]); result.keyForList = personalDetail.login; - result.alternateText = Str.removeSMSDomain(personalDetail.login); + result.alternateText = LocalePhoneNumber.formatPhoneNumber(personalDetail.login); } result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result, iouReports); diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index f35f2ceb7784..93844f5ba886 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -7,7 +7,6 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import ONYXKEYS from '../ONYXKEYS'; import CONST from '../CONST'; import * as Localize from './Localize'; -import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as Expensicons from '../components/Icon/Expensicons'; import hashCode from './hashCode'; import Navigation from './Navigation/Navigation'; @@ -21,6 +20,7 @@ import linkingConfig from './Navigation/linkingConfig'; import * as defaultAvatars from '../components/Icon/DefaultAvatars'; import isReportMessageAttachment from './isReportMessageAttachment'; import * as defaultWorkspaceAvatars from '../components/Icon/WorkspaceDefaultAvatars'; +import * as LocalePhoneNumber from './LocalePhoneNumber'; let sessionEmail; Onyx.connect({ @@ -766,7 +766,7 @@ function getDisplayNameForParticipant(login, shouldUseShortForm = false) { const loginWithoutSMSDomain = Str.removeSMSDomain(personalDetails.login); let longName = personalDetails.displayName || loginWithoutSMSDomain; if (longName === loginWithoutSMSDomain && Str.isSMSLogin(longName)) { - longName = LocalePhoneNumber.toLocalPhone(preferredLocale, longName); + longName = LocalePhoneNumber.formatPhoneNumber(longName); } const shortName = personalDetails.firstName || longName; diff --git a/src/libs/SidebarUtils.js b/src/libs/SidebarUtils.js index 7ca56320904b..50dc46c8d4b5 100644 --- a/src/libs/SidebarUtils.js +++ b/src/libs/SidebarUtils.js @@ -10,6 +10,7 @@ import * as Localize from './Localize'; import CONST from '../CONST'; import * as OptionsListUtils from './OptionsListUtils'; import * as CollectionUtils from './CollectionUtils'; +import * as LocalePhoneNumber from './LocalePhoneNumber'; // Note: It is very important that the keys subscribed to here are the same // keys that are connected to SidebarLinks withOnyx(). If there was a key missing from SidebarLinks and it's data was updated @@ -251,6 +252,9 @@ function getOptionData(reportID) { const hasMultipleParticipants = participantPersonalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat; const subtitle = ReportUtils.getChatRoomSubtitle(report, policies); + const login = Str.removeSMSDomain(personalDetail.login); + const formattedLogin = Str.isSMSLogin(login) ? LocalePhoneNumber.formatPhoneNumber(login) : login; + // We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade. const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips((participantPersonalDetailList || []).slice(0, 10), hasMultipleParticipants); @@ -302,7 +306,7 @@ function getOptionData(reportID) { }).join(' '); } - result.alternateText = lastMessageText || Str.removeSMSDomain(personalDetail.login); + result.alternateText = lastMessageText || formattedLogin; } result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result, iouReports); diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index f23b246280e1..f8ed8a839929 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -6,6 +6,7 @@ import ONYXKEYS from '../../ONYXKEYS'; import CONST from '../../CONST'; import * as API from '../API'; import * as ReportUtils from '../ReportUtils'; +import * as LocalePhoneNumber from '../LocalePhoneNumber'; import ROUTES from '../../ROUTES'; import Navigation from '../Navigation/Navigation'; @@ -29,9 +30,9 @@ Onyx.connect({ * @returns {String} */ function getDisplayName(login, personalDetail) { - // If we have a number like +15857527441@expensify.sms then let's remove @expensify.sms + // If we have a number like +15857527441@expensify.sms then let's remove @expensify.sms and format it // so that the option looks cleaner in our UI. - const userLogin = Str.removeSMSDomain(login); + const userLogin = LocalePhoneNumber.formatPhoneNumber(login); const userDetails = personalDetail || lodashGet(personalDetails, login); if (!userDetails) { diff --git a/src/pages/DetailsPage.js b/src/pages/DetailsPage.js index fd8b5f961d41..df926a1ae8e1 100755 --- a/src/pages/DetailsPage.js +++ b/src/pages/DetailsPage.js @@ -108,6 +108,10 @@ class DetailsPage extends React.PureComponent { pronouns = this.props.translate(`pronouns.${localeKey}`); } + const phoneNumber = getPhoneNumber(details); + const displayName = isSMSLogin ? this.props.formatPhoneNumber(phoneNumber) : details.displayName; + const phoneOrEmail = isSMSLogin ? getPhoneNumber(details) : details.login; + return ( @@ -127,7 +131,7 @@ class DetailsPage extends React.PureComponent { @@ -151,7 +155,7 @@ class DetailsPage extends React.PureComponent { {Boolean(details.displayName) && ( - {isSMSLogin ? this.props.toLocalPhone(details.displayName) : details.displayName} + {displayName} )} {details.login ? ( @@ -161,11 +165,13 @@ class DetailsPage extends React.PureComponent { ? 'common.phoneNumber' : 'common.email')} - - + + {isSMSLogin - ? this.props.toLocalPhone(getPhoneNumber(details)) + ? this.props.formatPhoneNumber(phoneNumber) : details.login} @@ -186,7 +192,7 @@ class DetailsPage extends React.PureComponent { {details.login !== this.props.session.email && ( Report.navigateToAndOpenReport([details.login])} wrapperStyle={styles.breakAll} diff --git a/src/pages/home/report/ParticipantLocalTime.js b/src/pages/home/report/ParticipantLocalTime.js index ab1b3dcc4b5d..fb494afb05e9 100644 --- a/src/pages/home/report/ParticipantLocalTime.js +++ b/src/pages/home/report/ParticipantLocalTime.js @@ -56,7 +56,7 @@ class ParticipantLocalTime extends PureComponent { render() { const reportRecipientDisplayName = this.props.participant.firstName || (Str.isSMSLogin(this.props.participant.login) - ? this.props.toLocalPhone(this.props.participant.displayName) + ? this.props.formatPhoneNumber(this.props.participant.displayName) : this.props.participant.displayName); return ( diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js index 19d2cc05f58c..ed208a7db999 100644 --- a/src/pages/home/report/ReportActionItemSingle.js +++ b/src/pages/home/report/ReportActionItemSingle.js @@ -65,7 +65,10 @@ const ReportActionItemSingle = (props) => { // we'll need to take the displayName from personal details and have it be in the same format for now. Eventually, // we should stop referring to the report history items entirely for this information. const personArray = displayName - ? [{type: 'TEXT', text: Str.isSMSLogin(login) ? props.toLocalPhone(displayName) : displayName}] + ? [{ + type: 'TEXT', + text: Str.isSMSLogin(login) ? props.formatPhoneNumber(displayName) : displayName, + }] : props.action.person; return ( diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index ef001593a7c0..597d8c4beb0c 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -4,7 +4,6 @@ import {View, ScrollView, Pressable} from 'react-native'; import PropTypes from 'prop-types'; import _ from 'underscore'; import {withOnyx} from 'react-native-onyx'; -import Str from 'expensify-common/lib/str'; import {withNetwork} from '../../components/OnyxProvider'; import styles from '../../styles/styles'; import Text from '../../components/Text'; @@ -304,7 +303,7 @@ class InitialSettingsPage extends React.Component { {this.props.currentUserPersonalDetails.displayName ? this.props.currentUserPersonalDetails.displayName - : Str.removeSMSDomain(this.props.session.email)} + : this.props.formatPhoneNumber(this.props.session.email)} @@ -313,7 +312,7 @@ class InitialSettingsPage extends React.Component { style={[styles.textLabelSupporting, styles.mt1]} numberOfLines={1} > - {Str.removeSMSDomain(this.props.session.email)} + {this.props.formatPhoneNumber(this.props.session.email)} )} diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js index ba5c0e5fbb5b..a2aa35f6beff 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js +++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js @@ -156,6 +156,12 @@ class ContactMethodDetailsPage extends Component { render() { const contactMethod = this.getContactMethod(); + + // replacing spaces with "hard spaces" to prevent breaking the number + const formattedContactMethod = Str.isSMSLogin(contactMethod) + ? this.props.formatPhoneNumber(contactMethod).replace(/ /g, '\u00A0') + : contactMethod; + const loginData = this.props.loginList[contactMethod]; if (!contactMethod || !loginData) { return ; @@ -169,7 +175,7 @@ class ContactMethodDetailsPage extends Component { return ( Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS)} onCloseButtonPress={() => Navigation.dismissModal(true)} @@ -191,7 +197,7 @@ class ContactMethodDetailsPage extends Component { - {this.props.translate('contacts.enterMagicCode', {contactMethod})} + {this.props.translate('contacts.enterMagicCode', {contactMethod: formattedContactMethod})} diff --git a/src/pages/settings/Profile/Contacts/ContactMethodsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodsPage.js index 7d8e362a438e..ce7711e58de2 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodsPage.js +++ b/src/pages/settings/Profile/Contacts/ContactMethodsPage.js @@ -83,13 +83,15 @@ const ContactMethodsPage = (props) => { // Default to using login key if we deleted login.partnerUserID optimistically // but still need to show the pending login being deleted while offline. const partnerUserID = login.partnerUserID || loginName; + const menuItemTitle = Str.isSMSLogin(partnerUserID) ? props.formatPhoneNumber(partnerUserID) : partnerUserID; + return ( Navigation.navigate(ROUTES.getEditContactMethodRoute(partnerUserID))} brickRoadIndicator={indicator} diff --git a/src/pages/settings/Profile/ProfilePage.js b/src/pages/settings/Profile/ProfilePage.js index 3ea4f9abb9f7..88890bcd0f99 100755 --- a/src/pages/settings/Profile/ProfilePage.js +++ b/src/pages/settings/Profile/ProfilePage.js @@ -1,4 +1,3 @@ -import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; import React from 'react'; import {View} from 'react-native'; @@ -65,7 +64,7 @@ const ProfilePage = (props) => { }, { description: props.translate('contacts.contactMethod'), - title: Str.removeSMSDomain(lodashGet(currentUserDetails, 'login', '')), + title: props.formatPhoneNumber(lodashGet(currentUserDetails, 'login', '')), pageRoute: ROUTES.SETTINGS_CONTACT_METHODS, brickRoadIndicator: contactMethodBrickRoadIndicator, }, diff --git a/src/pages/settings/Security/CloseAccountPage.js b/src/pages/settings/Security/CloseAccountPage.js index 56a5c27e12a0..b80ab2687bac 100644 --- a/src/pages/settings/Security/CloseAccountPage.js +++ b/src/pages/settings/Security/CloseAccountPage.js @@ -85,7 +85,7 @@ class CloseAccountPage extends Component { } render() { - const userEmailOrPhone = Str.removeSMSDomain(this.props.session.email); + const userEmailOrPhone = this.props.formatPhoneNumber(this.props.session.email); return ( ( {!_.isEmpty(props.credentials.login) && ( - {props.translate('loginForm.notYou', {user: Str.removeSMSDomain(props.credentials.login)})} + {props.translate('loginForm.notYou', {user: props.formatPhoneNumber(props.credentials.login)})} )} { const isSMSLogin = Str.isSMSLogin(props.credentials.login); - const login = isSMSLogin ? props.toLocalPhone(Str.removeSMSDomain(props.credentials.login)) : props.credentials.login; + + // replacing spaces with "hard spaces" to prevent breaking the number + const login = isSMSLogin + ? props.formatPhoneNumber(props.credentials.login).replace(/ /g, '\u00A0') + : props.credentials.login; + const loginType = (isSMSLogin ? props.translate('common.phone') : props.translate('common.email')).toLowerCase(); return ( diff --git a/src/pages/signin/SignInPage.js b/src/pages/signin/SignInPage.js index f83f467106ee..3282e16d50b2 100644 --- a/src/pages/signin/SignInPage.js +++ b/src/pages/signin/SignInPage.js @@ -102,16 +102,18 @@ class SignInPage extends Component { } else { const userLogin = Str.removeSMSDomain(lodashGet(this.props, 'credentials.login', '')); + // replacing spaces with "hard spaces" to prevent breaking the number + const userLoginToDisplay = Str.isSMSLogin(userLogin) ? this.props.formatPhoneNumber(userLogin).replace(/ /g, '\u00A0') : userLogin; if (this.props.account.validated) { welcomeHeader = this.props.isSmallScreenWidth ? '' : this.props.translate('welcomeText.welcomeBack'); welcomeText = this.props.isSmallScreenWidth - ? `${this.props.translate('welcomeText.welcomeBack')} ${this.props.translate('welcomeText.welcomeEnterMagicCode', {login: userLogin})}` - : this.props.translate('welcomeText.welcomeEnterMagicCode', {login: userLogin}); + ? `${this.props.translate('welcomeText.welcomeBack')} ${this.props.translate('welcomeText.welcomeEnterMagicCode', {login: userLoginToDisplay})}` + : this.props.translate('welcomeText.welcomeEnterMagicCode', {login: userLoginToDisplay}); } else { welcomeHeader = this.props.isSmallScreenWidth ? '' : this.props.translate('welcomeText.welcome'); welcomeText = this.props.isSmallScreenWidth - ? `${this.props.translate('welcomeText.welcome')} ${this.props.translate('welcomeText.newFaceEnterMagicCode', {login: userLogin})}` - : this.props.translate('welcomeText.newFaceEnterMagicCode', {login: userLogin}); + ? `${this.props.translate('welcomeText.welcome')} ${this.props.translate('welcomeText.newFaceEnterMagicCode', {login: userLoginToDisplay})}` + : this.props.translate('welcomeText.newFaceEnterMagicCode', {login: userLoginToDisplay}); } } } else if (showPasswordForm) { diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js index a10e39b03293..0daa5fd747c3 100644 --- a/src/pages/workspace/WorkspaceMembersPage.js +++ b/src/pages/workspace/WorkspaceMembersPage.js @@ -6,7 +6,6 @@ import { } from 'react-native'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; -import Str from 'expensify-common/lib/str'; import styles from '../../styles/styles'; import ONYXKEYS from '../../ONYXKEYS'; import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; @@ -289,8 +288,8 @@ class WorkspaceMembersPage extends React.Component { onSelectRow={() => this.toggleUser(item.login, item.pendingAction)} boldStyle option={{ - text: Str.removeSMSDomain(item.displayName), - alternateText: Str.removeSMSDomain(item.login), + text: this.props.formatPhoneNumber(item.displayName), + alternateText: this.props.formatPhoneNumber(item.login), participantsList: [item], icons: [{ source: ReportUtils.getAvatar(item.avatar, item.login), diff --git a/tests/unit/LocalePhoneNumberTest.js b/tests/unit/LocalePhoneNumberTest.js index bb2c250205ad..40d6bcc7bf69 100644 --- a/tests/unit/LocalePhoneNumberTest.js +++ b/tests/unit/LocalePhoneNumberTest.js @@ -1,89 +1,49 @@ -const localePhoneNumber = require('../../src/libs/LocalePhoneNumber'); -const CONST = require('../../src/CONST').default; +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '../../src/ONYXKEYS'; +import * as LocalePhoneNumber from '../../src/libs/LocalePhoneNumber'; +import waitForPromisesToResolve from '../utils/waitForPromisesToResolve'; -describe('localePhoneNumber', () => { - it('Test to local Number Conversion by locale', () => { - expect(localePhoneNumber.toLocalPhone(CONST.LOCALES.ES_ES, '34547474747474')).toBe('547474747474'); - }); - - // Failing due to the use of Trimstart. - it('Test to local Number Conversion by locale', () => { - expect(localePhoneNumber.toLocalPhone(CONST.LOCALES.ES_ES, '343434343434')).toBe('3434343434'); - }); - - it('Test to local Number Conversion by locale', () => { - expect(localePhoneNumber.toLocalPhone(CONST.LOCALES.ES_ES, '+34547474747474')).toBe('547474747474'); - }); - - it('Test to local Number Conversion by locale', () => { - expect(localePhoneNumber.toLocalPhone(CONST.LOCALES.ES_ES, '547474747474')).toBe('547474747474'); - }); - - it('Test to local Number Conversion by locale', () => { - expect(localePhoneNumber.toLocalPhone(CONST.LOCALES.ES_ES, '+17474747474')).toBe('+17474747474'); - }); - - it('Test to local Number Conversion by locale', () => { - expect(localePhoneNumber.toLocalPhone(CONST.LOCALES.EN, '+1547474747474')).toBe('547474747474'); - }); - - it('Test to local Number Conversion by locale', () => { - expect(localePhoneNumber.toLocalPhone(CONST.LOCALES.EN, '1547474747474')).toBe('547474747474'); - }); - - it('Test to local Number Conversion by locale', () => { - expect(localePhoneNumber.toLocalPhone(CONST.LOCALES.EN, '547474747474')).toBe('547474747474'); - }); +const ES_NUMBER = '+34702474537'; +const US_NUMBER = '+18332403627'; - it('Test to local Number Conversion by locale', () => { - expect(localePhoneNumber.toLocalPhone(CONST.LOCALES.EN, '+347474747474')).toBe('+347474747474'); - }); - - it('Test to local Number Conversion by locale', () => { - expect(localePhoneNumber.toLocalPhone(CONST.LOCALES.EN, '+34 747 474 7474')).toBe('+34 747 474 7474'); - }); +describe('LocalePhoneNumber utils', () => { + beforeAll(() => Onyx.init({ + keys: ONYXKEYS, + })); - it('Test to local Number Conversion by locale', () => { - expect(localePhoneNumber.toLocalPhone('en-EN', '+17474747474')).toBe('+17474747474'); - }); + describe('formatPhoneNumber function - when the current user has a phone number', () => { + beforeEach(() => Onyx.multiSet({ + [ONYXKEYS.SESSION]: {email: 'current@user.com'}, + [ONYXKEYS.COUNTRY_CODE]: 34, + [ONYXKEYS.PERSONAL_DETAILS]: {'current@user.com': {phoneNumber: US_NUMBER}}, + }).then(waitForPromisesToResolve)); - it('Test to international Number Conversion by locale', () => { - expect(localePhoneNumber.fromLocalPhone(CONST.LOCALES.ES_ES, '34547474747474')).toBe('+34547474747474'); - }); + afterEach(() => Onyx.clear()); - it('Test to international Number Conversion by locale', () => { - expect(localePhoneNumber.fromLocalPhone(CONST.LOCALES.ES_ES, '+34547474747474')).toBe('+34547474747474'); - }); + it('should display a number from the same region formatted locally', () => { + expect(LocalePhoneNumber.formatPhoneNumber(US_NUMBER)).toBe('(833) 240-3627'); + }); - it('Test to international Number Conversion by locale', () => { - expect(localePhoneNumber.fromLocalPhone(CONST.LOCALES.ES_ES, '547474747474')).toBe('+34547474747474'); + it('should display a number from another region formatted internationally', () => { + expect(LocalePhoneNumber.formatPhoneNumber(ES_NUMBER)).toBe('+34 702 47 45 37'); + }); }); - it('Test to international Number Conversion by locale', () => { - expect(localePhoneNumber.fromLocalPhone(CONST.LOCALES.ES_ES, '+17474747474')).toBe('+3417474747474'); - }); + describe('formatPhoneNumber function - when the current user does not have a phone number', () => { + beforeEach(() => Onyx.multiSet({ + [ONYXKEYS.SESSION]: {email: 'current@user.com'}, + [ONYXKEYS.COUNTRY_CODE]: 34, + [ONYXKEYS.PERSONAL_DETAILS]: {'current@user.com': {phoneNumber: ''}}, + }).then(waitForPromisesToResolve)); - it('Test to international Number Conversion by locale', () => { - expect(localePhoneNumber.fromLocalPhone(CONST.LOCALES.EN, '+1547474747474')).toBe('+1547474747474'); - }); + afterEach(() => Onyx.clear()); - it('Test to international Number Conversion by locale', () => { - expect(localePhoneNumber.fromLocalPhone(CONST.LOCALES.EN, '1547474747474')).toBe('+1547474747474'); - }); - - it('Test to international Number Conversion by locale', () => { - expect(localePhoneNumber.fromLocalPhone(CONST.LOCALES.EN, '547474747474')).toBe('+1547474747474'); - }); - - it('Test to international Number Conversion by locale', () => { - expect(localePhoneNumber.fromLocalPhone(CONST.LOCALES.EN, '+347474747474')).toBe('+1347474747474'); - }); - - it('Test to international Number Conversion by locale', () => { - expect(localePhoneNumber.fromLocalPhone(CONST.LOCALES.EN, ' + 34 747 474 7474 ')).toBe('+1 34 747 474 7474'); - }); + it('should display a number from the same region formatted locally', () => { + expect(LocalePhoneNumber.formatPhoneNumber(ES_NUMBER)).toBe('702 47 45 37'); + }); - it('Test to international Number Conversion by locale', () => { - expect(localePhoneNumber.fromLocalPhone('en-EN', '+17474747474')).toBe('+17474747474'); + it('should display a number from another region formatted internationally', () => { + expect(LocalePhoneNumber.formatPhoneNumber(US_NUMBER)).toBe('+1 833-240-3627'); + }); }); }); diff --git a/tests/unit/ReportUtilsTest.js b/tests/unit/ReportUtilsTest.js index c9fcca5a6a7d..d610b740135f 100644 --- a/tests/unit/ReportUtilsTest.js +++ b/tests/unit/ReportUtilsTest.js @@ -25,8 +25,8 @@ const participantsPersonalDetails = { login: 'lagertha@vikings.net', pronouns: 'She/her', }, - '+12223334444@expensify.sms': { - login: '+12223334444@expensify.sms', + '+18332403627@expensify.sms': { + login: '+18332403627@expensify.sms', }, }; const policy = { @@ -64,8 +64,8 @@ describe('ReportUtils', () => { pronouns: 'She/her', }, { - displayName: '2223334444', - tooltip: '+12223334444', + displayName: '(833) 240-3627', + tooltip: '+18332403627', pronouns: undefined, }, ]); @@ -89,8 +89,8 @@ describe('ReportUtils', () => { pronouns: 'She/her', }, { - displayName: '2223334444', - tooltip: '+12223334444', + displayName: '(833) 240-3627', + tooltip: '+18332403627', pronouns: undefined, }, ]); @@ -113,15 +113,15 @@ describe('ReportUtils', () => { test('SMS', () => { expect(ReportUtils.getReportName({ - participants: [currentUserEmail, '+12223334444@expensify.sms'], - })).toBe('2223334444'); + participants: [currentUserEmail, '+18332403627@expensify.sms'], + })).toBe('(833) 240-3627'); }); }); test('Group DM', () => { expect(ReportUtils.getReportName({ - participants: [currentUserEmail, 'ragnar@vikings.net', 'floki@vikings.net', 'lagertha@vikings.net', '+12223334444@expensify.sms'], - })).toBe('Ragnar, floki@vikings.net, Lagertha, 2223334444'); + participants: [currentUserEmail, 'ragnar@vikings.net', 'floki@vikings.net', 'lagertha@vikings.net', '+18332403627@expensify.sms'], + })).toBe('Ragnar, floki@vikings.net, Lagertha, (833) 240-3627'); }); describe('Default Policy Room', () => {