diff --git a/src/hooks/useContactImport.ts b/src/hooks/useContactImport.ts index 3590429b3e24..67f1d5ed9456 100644 --- a/src/hooks/useContactImport.ts +++ b/src/hooks/useContactImport.ts @@ -6,8 +6,11 @@ import type {ContactImportResult} from '@libs/ContactImport/types'; import useContactPermissions from '@libs/ContactPermission/useContactPermissions'; import getContacts from '@libs/ContactUtils'; import type {SearchOption} from '@libs/OptionsListUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetails} from '@src/types/onyx'; import useLocalize from './useLocalize'; +import useOnyx from './useOnyx'; /** * Return type of the useContactImport hook. @@ -28,14 +31,15 @@ function useContactImport(): UseContactImportResult { const [contactPermissionState, setContactPermissionState] = useState(RESULTS.UNAVAILABLE); const [contacts, setContacts] = useState>>([]); const {localeCompare} = useLocalize(); + const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); const importAndSaveContacts = useCallback(() => { contactImport().then(({contactList, permissionStatus}: ContactImportResult) => { setContactPermissionState(permissionStatus); - const usersFromContact = getContacts(contactList, localeCompare); + const usersFromContact = getContacts(contactList, localeCompare, countryCode); setContacts(usersFromContact); }); - }, [localeCompare]); + }, [localeCompare, countryCode]); useContactPermissions({ importAndSaveContacts, diff --git a/src/libs/ContactUtils.ts b/src/libs/ContactUtils.ts index 34576062b426..f5945491a4ce 100644 --- a/src/libs/ContactUtils.ts +++ b/src/libs/ContactUtils.ts @@ -28,7 +28,7 @@ function sortEmailObjects(emails: StringHolder[], localeCompare: LocaleContextPr }); } -const getContacts = (deviceContacts: DeviceContact[] | [], localeCompare: LocaleContextProps['localeCompare']): Array> => { +const getContacts = (deviceContacts: DeviceContact[] | [], localeCompare: LocaleContextProps['localeCompare'], countryCode: number): Array> => { return deviceContacts .map((contact) => { const email = sortEmailObjects(contact?.emailAddresses ?? [], localeCompare)?.at(0) ?? ''; @@ -47,6 +47,7 @@ const getContacts = (deviceContacts: DeviceContact[] | [], localeCompare: Locale email, phone: phoneNumber, avatar: avatarSource, + countryCode, }); }) .filter((contact): contact is SearchOption => contact !== null); diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index a5f7b4ab2c55..fd17aca798a1 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -13,7 +13,7 @@ import filterArrayByMatch from '@libs/filterArrayByMatch'; import {isReportMessageAttachment} from '@libs/isReportMessageAttachment'; import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; import {translateLocal} from '@libs/Localize'; -import {appendCountryCode, appendCountryCodeWithCountryCode, getPhoneNumberWithoutSpecialChars} from '@libs/LoginUtils'; +import {appendCountryCodeWithCountryCode, getPhoneNumberWithoutSpecialChars} from '@libs/LoginUtils'; import {MaxHeap} from '@libs/MaxHeap'; import {MinHeap} from '@libs/MinHeap'; import {getForReportAction} from '@libs/ModifiedExpenseMessage'; @@ -1471,6 +1471,7 @@ function getUserToInviteContactOption({ email = '', phone = '', avatar = '', + countryCode = CONST.DEFAULT_COUNTRY_CODE, }: GetUserToInviteConfig): SearchOption | null { // If email is provided, use it as the primary identifier // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing @@ -1479,7 +1480,7 @@ function getUserToInviteContactOption({ // Handle phone number parsing for either provided phone or searchValue // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const phoneToCheck = phone || searchValue; - const parsedPhoneNumber = parsePhoneNumber(appendCountryCode(Str.removeSMSDomain(phoneToCheck))); + const parsedPhoneNumber = parsePhoneNumber(appendCountryCodeWithCountryCode(Str.removeSMSDomain(phoneToCheck), countryCode)); const isCurrentUserLogin = isCurrentUser({login: effectiveSearchValue} as PersonalDetails); const isInSelectedOption = selectedOptions.some((option) => 'login' in option && option.login === effectiveSearchValue); diff --git a/src/pages/MissingPersonalDetails/substeps/PhoneNumber.tsx b/src/pages/MissingPersonalDetails/substeps/PhoneNumber.tsx index 33b7f125f5ba..bcd86c791452 100644 --- a/src/pages/MissingPersonalDetails/substeps/PhoneNumber.tsx +++ b/src/pages/MissingPersonalDetails/substeps/PhoneNumber.tsx @@ -2,8 +2,9 @@ import React, {useCallback} from 'react'; import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import SingleFieldStep from '@components/SubStepForms/SingleFieldStep'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; import usePersonalDetailsFormSubmit from '@hooks/usePersonalDetailsFormSubmit'; -import {appendCountryCode, formatE164PhoneNumber} from '@libs/LoginUtils'; +import {appendCountryCodeWithCountryCode, formatE164PhoneNumber} from '@libs/LoginUtils'; import {isRequiredFulfilled, isValidPhoneNumber} from '@libs/ValidationUtils'; import type {CustomSubStepProps} from '@pages/MissingPersonalDetails/types'; import CONST from '@src/CONST'; @@ -14,12 +15,13 @@ const STEP_FIELDS = [INPUT_IDS.PHONE_NUMBER]; function PhoneNumberStep({isEditing, onNext, onMove, personalDetailsValues}: CustomSubStepProps) { const {translate} = useLocalize(); + const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: true}); const validate = useCallback( (values: FormOnyxValues): FormInputErrors => { const errors: FormInputErrors = {}; const phoneNumber = values[INPUT_IDS.PHONE_NUMBER]; - const phoneNumberWithCountryCode = appendCountryCode(phoneNumber); + const phoneNumberWithCountryCode = appendCountryCodeWithCountryCode(phoneNumber, countryCode); if (!isRequiredFulfilled(phoneNumber)) { errors[INPUT_IDS.PHONE_NUMBER] = translate('common.error.fieldRequired'); @@ -32,7 +34,7 @@ function PhoneNumberStep({isEditing, onNext, onMove, personalDetailsValues}: Cus return errors; }, - [translate], + [translate, countryCode], ); const handleSubmit = usePersonalDetailsFormSubmit({ diff --git a/src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx b/src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx index 45e3f1681508..71b68adddacb 100644 --- a/src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx +++ b/src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx @@ -24,7 +24,7 @@ import {searchInServer} from '@libs/actions/Report'; import {READ_COMMANDS} from '@libs/API/types'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import HttpUtils from '@libs/HttpUtils'; -import {appendCountryCode} from '@libs/LoginUtils'; +import {appendCountryCodeWithCountryCode} from '@libs/LoginUtils'; import {navigateAfterOnboardingWithMicrotaskQueue} from '@libs/navigateAfterOnboarding'; import type {MemberForList} from '@libs/OptionsListUtils'; import {filterAndOrderOptions, formatMemberForList, getHeaderMessage, getMemberInviteOptions, getSearchValueForPhoneOrEmail} from '@libs/OptionsListUtils'; @@ -58,13 +58,13 @@ function BaseOnboardingWorkspaceInvite({shouldUseNativeStyles}: BaseOnboardingWo const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {canBeMissing: true, initWithStoredValues: false}); const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: false}); - const [countryCode] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const session = useSession(); const {isBetaEnabled} = usePermissions(); const {options, areOptionsInitialized} = useOptionsList({ shouldInitialize: didScreenTransitionEnd, }); + const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); const welcomeNoteSubject = useMemo( () => `# ${currentUserPersonalDetails?.displayName ?? ''} invited you to ${policy?.name ?? 'a workspace'}`, @@ -295,7 +295,11 @@ function BaseOnboardingWorkspaceInvite({shouldUseNativeStyles}: BaseOnboardingWo } if ( usersToInvite.length === 0 && - excludedUsers[parsePhoneNumber(appendCountryCode(searchValue)).possible ? addSMSDomainIfPhoneNumber(appendCountryCode(searchValue)) : searchValue] + excludedUsers[ + parsePhoneNumber(appendCountryCodeWithCountryCode(searchValue, countryCode)).possible + ? addSMSDomainIfPhoneNumber(appendCountryCodeWithCountryCode(searchValue, countryCode)) + : searchValue + ] ) { return translate('messages.userIsAlreadyMember', {login: searchValue, name: policy?.name ?? ''}); } diff --git a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx index a7fc6772f40e..c33aa29b4eab 100644 --- a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx +++ b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx @@ -124,6 +124,7 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde action, isPaidGroupPolicy, searchTerm, + countryCode, ]); const chatOptions = useMemo(() => { diff --git a/src/pages/settings/Profile/PersonalDetails/PhoneNumberPage.tsx b/src/pages/settings/Profile/PersonalDetails/PhoneNumberPage.tsx index 744d2255d857..b19c47d56d8f 100644 --- a/src/pages/settings/Profile/PersonalDetails/PhoneNumberPage.tsx +++ b/src/pages/settings/Profile/PersonalDetails/PhoneNumberPage.tsx @@ -13,7 +13,7 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import {getEarliestErrorField} from '@libs/ErrorUtils'; -import {appendCountryCode, formatE164PhoneNumber} from '@libs/LoginUtils'; +import {appendCountryCodeWithCountryCode, formatE164PhoneNumber} from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; import {isRequiredFulfilled, isValidPhoneNumber} from '@libs/ValidationUtils'; import {clearPhoneNumberError, updatePhoneNumber as updatePhone} from '@userActions/PersonalDetails'; @@ -25,6 +25,7 @@ import type {PrivatePersonalDetails} from '@src/types/onyx'; function PhoneNumberPage() { const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true}); const [isLoadingApp = true] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true}); + const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); const styles = useThemeStyles(); const {translate} = useLocalize(); const {inputCallbackRef} = useAutoFocusInput(); @@ -57,7 +58,7 @@ function PhoneNumberPage() { return errors; } - const phoneNumberWithCountryCode = appendCountryCode(phoneNumberValue); + const phoneNumberWithCountryCode = appendCountryCodeWithCountryCode(phoneNumberValue, countryCode); if (!isValidPhoneNumber(phoneNumberWithCountryCode)) { errors[INPUT_IDS.PHONE_NUMBER] = translate('common.error.phoneNumber'); @@ -70,7 +71,7 @@ function PhoneNumberPage() { return errors; }, - [translate, validateLoginError], + [translate, validateLoginError, countryCode], ); return ( diff --git a/src/pages/signin/LoginForm/BaseLoginForm.tsx b/src/pages/signin/LoginForm/BaseLoginForm.tsx index 1e19a16aa86c..12f90a32ae9c 100644 --- a/src/pages/signin/LoginForm/BaseLoginForm.tsx +++ b/src/pages/signin/LoginForm/BaseLoginForm.tsx @@ -22,7 +22,7 @@ import {isMobileWebKit} from '@libs/Browser'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; import {getLatestErrorMessage} from '@libs/ErrorUtils'; import isInputAutoFilled from '@libs/isInputAutoFilled'; -import {appendCountryCode, getPhoneNumberWithoutSpecialChars} from '@libs/LoginUtils'; +import {appendCountryCodeWithCountryCode, getPhoneNumberWithoutSpecialChars} from '@libs/LoginUtils'; import {parsePhoneNumber} from '@libs/PhoneNumber'; import StringUtils from '@libs/StringUtils'; import {isNumericWithSpecialChars} from '@libs/ValidationUtils'; @@ -44,6 +44,7 @@ function BaseLoginForm({blurOnSubmit = false, isVisible, ref}: BaseLoginFormProp const {login, setLogin} = useLogin(); const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); const [closeAccount] = useOnyx(ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM, {canBeMissing: true}); + const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); const styles = useThemeStyles(); const {isOffline} = useNetwork(); const {translate} = useLocalize(); @@ -67,7 +68,7 @@ function BaseLoginForm({blurOnSubmit = false, isVisible, ref}: BaseLoginFormProp return false; } - const phoneLogin = appendCountryCode(getPhoneNumberWithoutSpecialChars(loginTrim)); + const phoneLogin = appendCountryCodeWithCountryCode(getPhoneNumberWithoutSpecialChars(loginTrim), countryCode); const parsedPhoneNumber = parsePhoneNumber(phoneLogin); if (!Str.isValidEmail(loginTrim) && !parsedPhoneNumber.possible) { @@ -82,7 +83,7 @@ function BaseLoginForm({blurOnSubmit = false, isVisible, ref}: BaseLoginFormProp setFormError(undefined); return true; }, - [setFormError], + [setFormError, countryCode], ); /** @@ -138,12 +139,12 @@ function BaseLoginForm({blurOnSubmit = false, isVisible, ref}: BaseLoginFormProp const loginTrim = StringUtils.removeInvisibleCharacters(login.trim()); - const phoneLogin = appendCountryCode(getPhoneNumberWithoutSpecialChars(loginTrim)); + const phoneLogin = appendCountryCodeWithCountryCode(getPhoneNumberWithoutSpecialChars(loginTrim), countryCode); const parsedPhoneNumber = parsePhoneNumber(phoneLogin); // Check if this login has an account associated with it or not beginSignIn(parsedPhoneNumber.possible && parsedPhoneNumber.number?.e164 ? parsedPhoneNumber.number.e164 : loginTrim); - }, [login, account, closeAccount, isOffline, validate]); + }, [login, account, closeAccount, isOffline, validate, countryCode]); useEffect(() => { // Call clearAccountMessages on the login page (home route). diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index a2387ef1c719..531e6c12634d 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -20,7 +20,7 @@ import {searchInServer} from '@libs/actions/Report'; import {READ_COMMANDS} from '@libs/API/types'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import HttpUtils from '@libs/HttpUtils'; -import {appendCountryCode} from '@libs/LoginUtils'; +import {appendCountryCodeWithCountryCode} from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import {filterAndOrderOptions, formatMemberForList, getHeaderMessage, getMemberInviteOptions, getSearchValueForPhoneOrEmail, getUserToInviteOption} from '@libs/OptionsListUtils'; @@ -55,7 +55,7 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { const [usersToInvite, setUsersToInvite] = useState([]); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false, canBeMissing: true}); - const [countryCode] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); + const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); const firstRenderRef = useRef(true); const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: false}); const [invitedEmailsToAccountIDsDraft] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${route.params.policyID.toString()}`, {canBeMissing: true}); @@ -283,7 +283,11 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { } if ( usersToInvite.length === 0 && - excludedUsers[parsePhoneNumber(appendCountryCode(searchValue)).possible ? addSMSDomainIfPhoneNumber(appendCountryCode(searchValue)) : searchValue] + excludedUsers[ + parsePhoneNumber(appendCountryCodeWithCountryCode(searchValue, countryCode)).possible + ? addSMSDomainIfPhoneNumber(appendCountryCodeWithCountryCode(searchValue, countryCode)) + : searchValue + ] ) { return translate('messages.userIsAlreadyMember', {login: searchValue, name: policyName}); } diff --git a/tests/unit/LoginUtilsTest.ts b/tests/unit/LoginUtilsTest.ts index 3b0d788d6591..8115989cb352 100644 --- a/tests/unit/LoginUtilsTest.ts +++ b/tests/unit/LoginUtilsTest.ts @@ -1,5 +1,6 @@ import Onyx from 'react-native-onyx'; -import * as LoginUtils from '@libs/LoginUtils'; +import {appendCountryCodeWithCountryCode, getPhoneLogin, getPhoneNumberWithoutSpecialChars, isEmailPublicDomain, validateNumber} from '@libs/LoginUtils'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; @@ -21,75 +22,77 @@ describe('LoginUtils', () => { describe('getPhoneNumberWithoutSpecialChars', () => { it('Should return valid phone number', () => { const givenPhone = '+12345678901'; - const parsedPhone = LoginUtils.getPhoneNumberWithoutSpecialChars(givenPhone); + const parsedPhone = getPhoneNumberWithoutSpecialChars(givenPhone); expect(parsedPhone).toBe('+12345678901'); }); it('Should return valid phone number even if received special chars', () => { const givenPhone = '+1(234) 56-7\t8-9 01'; - const parsedPhone = LoginUtils.getPhoneNumberWithoutSpecialChars(givenPhone); + const parsedPhone = getPhoneNumberWithoutSpecialChars(givenPhone); expect(parsedPhone).toBe('+12345678901'); }); }); describe('appendCountryCode', () => { it('Should return valid phone number with country code when received a phone with country code', () => { const givenPhone = '+12345678901'; - const parsedPhone = LoginUtils.appendCountryCode(givenPhone); + const countryCode = CONST.DEFAULT_COUNTRY_CODE; + const parsedPhone = appendCountryCodeWithCountryCode(givenPhone, countryCode); expect(parsedPhone).toBe('+12345678901'); }); it('Should return valid phone number with country code when received a phone without country code', () => { const givenPhone = '2345678901'; - const parsedPhone = LoginUtils.appendCountryCode(givenPhone); + const countryCode = CONST.DEFAULT_COUNTRY_CODE; + const parsedPhone = appendCountryCodeWithCountryCode(givenPhone, countryCode); expect(parsedPhone).toBe('+12345678901'); }); }); describe('isEmailPublicDomain', () => { it('Should return true if email is from public domain', () => { const givenEmail = 'test@gmail.com'; - const parsedEmail = LoginUtils.isEmailPublicDomain(givenEmail); + const parsedEmail = isEmailPublicDomain(givenEmail); expect(parsedEmail).toBe(true); }); it('Should return false if email is not from public domain', () => { const givenEmail = 'test@test.com'; - const parsedEmail = LoginUtils.isEmailPublicDomain(givenEmail); + const parsedEmail = isEmailPublicDomain(givenEmail); expect(parsedEmail).toBe(false); }); it("Should return false if provided string isn't email", () => { const givenEmail = 'test'; - const parsedEmail = LoginUtils.isEmailPublicDomain(givenEmail); + const parsedEmail = isEmailPublicDomain(givenEmail); expect(parsedEmail).toBe(false); }); }); describe('validateNumber', () => { it("Should return valid phone number with '@expensify.sms' suffix if provided phone number is valid", () => { const givenPhone = '+12345678901'; - const parsedPhone = LoginUtils.validateNumber(givenPhone); + const parsedPhone = validateNumber(givenPhone); expect(parsedPhone).toBe('+12345678901@expensify.sms'); }); it('Should return empty string if provided phone number is not valid', () => { const givenPhone = '786'; - const parsedPhone = LoginUtils.validateNumber(givenPhone); + const parsedPhone = validateNumber(givenPhone); expect(parsedPhone).toBe(''); }); it('Should return empty string if provided phone number is empty', () => { const givenPhone = ''; - const parsedPhone = LoginUtils.validateNumber(givenPhone); + const parsedPhone = validateNumber(givenPhone); expect(parsedPhone).toBe(''); }); }); describe('getPhoneLogin', () => { it('Should return valid phone number with country code if provided phone number is valid and with country code', () => { const givenPhone = '+12345678901'; - const parsedPhone = LoginUtils.getPhoneLogin(givenPhone); + const parsedPhone = getPhoneLogin(givenPhone); expect(parsedPhone).toBe('+12345678901'); }); it('Should return valid phone number with country code if provided phone number is valid and without country code', () => { const givenPhone = '2345678901'; - const parsedPhone = LoginUtils.getPhoneLogin(givenPhone); + const parsedPhone = getPhoneLogin(givenPhone); expect(parsedPhone).toBe('+12345678901'); }); it('Should return empty string if provided phone number is empty', () => { const givenPhone = ''; - const parsedPhone = LoginUtils.getPhoneLogin(givenPhone); + const parsedPhone = getPhoneLogin(givenPhone); expect(parsedPhone).toBe(''); }); });