diff --git a/knip.ts b/knip.ts index f96b81b8c41..604973003d2 100644 --- a/knip.ts +++ b/knip.ts @@ -37,6 +37,7 @@ const config: KnipConfig = { 'husky', 'react-native-randombytes', // not sure we need this; only referenced in iOS Podfile.lock ], + ignore: ['src/utils/inputValidation.ts', 'src/utils/country.json'], } export default config diff --git a/package.json b/package.json index 6b1886cd623..7933c859d50 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,6 @@ "@celo/contractkit": "^3.2.0", "@celo/cryptographic-utils": "^3.2.0", "@celo/identity": "^3.2.0", - "@celo/phone-utils": "^3.2.0", "@celo/utils": "^3.2.0", "@celo/wallet-rpc": "^3.2.0", "@coinbase/cbpay-js": "^2.2.1", @@ -130,6 +129,7 @@ "fp-ts": "2.16.9", "futoin-hkdf": "^1.5.3", "fuzzysort": "^2.0.4", + "google-libphonenumber": "^3.2.38", "i18next": "^23.14.0", "ibantools": "^4.5.1", "intl-pluralrules": "^2.0.1", @@ -202,7 +202,8 @@ "victory-native": "^36.9.2", "viem": "^2.20.1", "vm-browserify": "^1.1.2", - "web3": "1.10.4" + "web3": "1.10.4", + "web3-utils": "^4.3.1" }, "devDependencies": { "@actions/github": "^5.1.1", @@ -219,6 +220,7 @@ "@sentry/types": "^7.111.0", "@testing-library/jest-native": "^5.4.3", "@testing-library/react-native": "^12.4.5", + "@types/country-data": "^0.0.5", "@types/crypto-js": "^4.1.1", "@types/fast-levenshtein": "^0.0.2", "@types/fs-extra": "^9.0.13", diff --git a/src/account/reducer.ts b/src/account/reducer.ts index b892481d27d..452a2d670f7 100644 --- a/src/account/reducer.ts +++ b/src/account/reducer.ts @@ -1,10 +1,10 @@ -import { isE164NumberStrict } from '@celo/phone-utils' import { Actions, ActionTypes } from 'src/account/actions' import { Actions as AppActions, ActionTypes as AppActionTypes } from 'src/app/actions' import { DEV_SETTINGS_ACTIVE_INITIALLY } from 'src/config' import { deleteKeylessBackupCompleted, keylessBackupCompleted } from 'src/keylessBackup/slice' import { getRehydratePayload, REHYDRATE, RehydrateAction } from 'src/redux/persist-helper' import Logger from 'src/utils/Logger' +import { isE164NumberStrict } from 'src/utils/phoneNumbers' import { Actions as Web3Actions, ActionTypes as Web3ActionTypes } from 'src/web3/actions' interface State { diff --git a/src/account/saga.ts b/src/account/saga.ts index 551dc7da41d..bdfa12f4097 100644 --- a/src/account/saga.ts +++ b/src/account/saga.ts @@ -1,4 +1,3 @@ -import { parsePhoneNumber } from '@celo/phone-utils' import { UnlockableWallet } from '@celo/wallet-base' import firebase from '@react-native-firebase/app' import { Platform } from 'react-native' @@ -39,6 +38,7 @@ import { patchUpdateStatsigUser } from 'src/statsig' import { restartApp } from 'src/utils/AppRestart' import Logger from 'src/utils/Logger' import { ensureError } from 'src/utils/ensureError' +import { parsePhoneNumber } from 'src/utils/phoneNumbers' import { safely } from 'src/utils/safely' import { clearStoredAccounts } from 'src/web3/KeychainAccounts' import { getWallet } from 'src/web3/contracts' diff --git a/src/account/selectors.ts b/src/account/selectors.ts index 0ea9457d3b5..6bf31ffe64c 100644 --- a/src/account/selectors.ts +++ b/src/account/selectors.ts @@ -1,9 +1,9 @@ -import { Countries } from '@celo/phone-utils' import * as RNLocalize from 'react-native-localize' import { createSelector } from 'reselect' import i18n from 'src/i18n' import { RecipientType } from 'src/recipients/recipient' import { RootState } from 'src/redux/reducers' +import { Countries } from 'src/utils/Countries' import { getCountryFeatures } from 'src/utils/countryFeatures' import { currentAccountSelector } from 'src/web3/selectors' diff --git a/src/account/utils.ts b/src/account/utils.ts index 275772dab55..8cee01ded4b 100644 --- a/src/account/utils.ts +++ b/src/account/utils.ts @@ -1,4 +1,4 @@ -import { parsePhoneNumber } from '@celo/phone-utils' +import { parsePhoneNumber } from 'src/utils/phoneNumbers' const ADDRESS_LENGTH = 42 // TODO(ACT-1173): see if this can be replaced with a viem helper diff --git a/src/analytics/selectors.ts b/src/analytics/selectors.ts index 9c3be6e1eed..82c553daaa9 100644 --- a/src/analytics/selectors.ts +++ b/src/analytics/selectors.ts @@ -1,12 +1,14 @@ -import { getRegionCodeFromCountryCode } from '@celo/phone-utils' import BigNumber from 'bignumber.js' import { camelCase } from 'lodash' import DeviceInfo from 'react-native-device-info' import * as RNLocalize from 'react-native-localize' import { createSelector } from 'reselect' -import { defaultCountryCodeSelector, pincodeTypeSelector } from 'src/account/selectors' +import { + backupCompletedSelector, + defaultCountryCodeSelector, + pincodeTypeSelector, +} from 'src/account/selectors' import { phoneVerificationStatusSelector } from 'src/app/selectors' -import { backupCompletedSelector } from 'src/account/selectors' import { currentLanguageSelector } from 'src/i18n/selectors' import { getLocalCurrencyCode } from 'src/localCurrency/selectors' import { userLocationDataSelector } from 'src/networkInfo/selectors' @@ -21,6 +23,7 @@ import { RootState } from 'src/redux/reducers' import { tokensListSelector, tokensWithTokenBalanceSelector } from 'src/tokens/selectors' import { sortByUsdBalance } from 'src/tokens/utils' import { NetworkId } from 'src/transactions/types' +import { getRegionCodeFromCountryCode } from 'src/utils/phoneNumbers' import { mtwAddressSelector, rawWalletAddressSelector } from 'src/web3/selectors' function toPascalCase(str: string) { diff --git a/src/app/saga.test.ts b/src/app/saga.test.ts index 11ad32aa766..0a7d53a52cb 100644 --- a/src/app/saga.test.ts +++ b/src/app/saga.test.ts @@ -1,5 +1,4 @@ import * as DEK from '@celo/cryptographic-utils/lib/dataEncryptionKey' -import getPhoneHash from '@celo/phone-utils/lib/getPhoneHash' import { FetchMock } from 'jest-fetch-mock/types' import { BIOMETRY_TYPE } from 'react-native-keychain' import * as RNLocalize from 'react-native-localize' @@ -8,9 +7,9 @@ import * as matchers from 'redux-saga-test-plan/matchers' import { EffectProviders, StaticProvider } from 'redux-saga-test-plan/providers' import { call, select } from 'redux-saga/effects' import { e164NumberSelector } from 'src/account/selectors' +import AppAnalytics from 'src/analytics/AppAnalytics' import { AppEvents, InviteEvents } from 'src/analytics/Events' import { HooksEnablePreviewOrigin, WalletConnectPairingOrigin } from 'src/analytics/types' -import AppAnalytics from 'src/analytics/AppAnalytics' import { appLock, inAppReviewRequested, @@ -37,6 +36,7 @@ import { sentryNetworkErrorsSelector, shouldRunVerificationMigrationSelector, } from 'src/app/selectors' +import { DEEPLINK_PREFIX } from 'src/config' import { activeDappSelector } from 'src/dapps/selectors' import { FiatExchangeFlow } from 'src/fiatExchanges/utils' import { initI18n } from 'src/i18n' @@ -56,6 +56,7 @@ import { handlePaymentDeeplink } from 'src/send/utils' import { initializeSentry } from 'src/sentry/Sentry' import { getDynamicConfigParams, getFeatureGate, patchUpdateStatsigUser } from 'src/statsig' import { NetworkId } from 'src/transactions/types' +import getPhoneHash from 'src/utils/getPhoneHash' import { navigateToURI } from 'src/utils/linking' import Logger from 'src/utils/Logger' import { ONE_DAY_IN_MILLIS } from 'src/utils/time' @@ -71,7 +72,6 @@ import { } from 'src/web3/selectors' import { createMockStore } from 'test/utils' import { mockAccount, mockTokenBalances } from 'test/values' -import { DEEPLINK_PREFIX } from 'src/config' jest.mock('src/analytics/AppAnalytics') jest.mock('src/sentry/Sentry') diff --git a/src/app/saga.ts b/src/app/saga.ts index f1474c8ff8f..ca75ce09986 100644 --- a/src/app/saga.ts +++ b/src/app/saga.ts @@ -1,5 +1,4 @@ import { compressedPubKey } from '@celo/cryptographic-utils' -import getPhoneHash from '@celo/phone-utils/lib/getPhoneHash' import { hexToBuffer } from '@celo/utils/lib/address' import locales from 'locales' import { AppState, Platform } from 'react-native' @@ -69,6 +68,7 @@ import { swapSuccess } from 'src/swap/slice' import { NetworkId } from 'src/transactions/types' import Logger from 'src/utils/Logger' import { ensureError } from 'src/utils/ensureError' +import getPhoneHash from 'src/utils/getPhoneHash' import { isDeepLink, navigateToURI } from 'src/utils/linking' import { safely } from 'src/utils/safely' import { ONE_DAY_IN_MILLIS } from 'src/utils/time' diff --git a/src/components/InviteOptionsModal.tsx b/src/components/InviteOptionsModal.tsx index 84f22ddd7fb..688b018770a 100644 --- a/src/components/InviteOptionsModal.tsx +++ b/src/components/InviteOptionsModal.tsx @@ -1,9 +1,8 @@ -import getPhoneHash from '@celo/phone-utils/lib/getPhoneHash' import * as React from 'react' import { useTranslation } from 'react-i18next' import { Share } from 'react-native' -import { InviteEvents } from 'src/analytics/Events' import AppAnalytics from 'src/analytics/AppAnalytics' +import { InviteEvents } from 'src/analytics/Events' import { INVITE_REWARDS_NFTS_LEARN_MORE, INVITE_REWARDS_STABLETOKEN_LEARN_MORE } from 'src/config' import InviteModal from 'src/invite/InviteModal' import { useShareUrl } from 'src/invite/hooks' @@ -11,6 +10,7 @@ import { Recipient, getDisplayName } from 'src/recipients/recipient' import { useSelector } from 'src/redux/hooks' import { inviteRewardsActiveSelector, inviteRewardsTypeSelector } from 'src/send/selectors' import { InviteRewardsType } from 'src/send/types' +import getPhoneHash from 'src/utils/getPhoneHash' interface Props { recipient: Recipient diff --git a/src/components/PhoneNumberInput.test.tsx b/src/components/PhoneNumberInput.test.tsx index 265a6f72e7f..221f86fd6a0 100644 --- a/src/components/PhoneNumberInput.test.tsx +++ b/src/components/PhoneNumberInput.test.tsx @@ -1,9 +1,9 @@ -import { Countries } from '@celo/phone-utils' import { act, fireEvent, render } from '@testing-library/react-native' import * as React from 'react' import { Platform } from 'react-native' import SmsRetriever from 'react-native-sms-retriever' import PhoneNumberInput from 'src/components/PhoneNumberInput' +import { Countries } from 'src/utils/Countries' jest.mock('react-native-sms-retriever', () => { return { diff --git a/src/components/PhoneNumberInput.tsx b/src/components/PhoneNumberInput.tsx index be3f9a3241d..f7fd1138ecc 100644 --- a/src/components/PhoneNumberInput.tsx +++ b/src/components/PhoneNumberInput.tsx @@ -1,4 +1,3 @@ -import { LocalizedCountry, parsePhoneNumber } from '@celo/phone-utils' import { ValidatorKind } from '@celo/utils/lib/inputValidation' import React, { useEffect, useRef } from 'react' import { Platform, StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-native' @@ -10,6 +9,8 @@ import Touchable from 'src/components/Touchable' import ValidatedTextInput from 'src/components/ValidatedTextInput' import colors from 'src/styles/colors' import { Spacing } from 'src/styles/styles' +import { type LocalizedCountry } from 'src/utils/Countries' +import { parsePhoneNumber } from 'src/utils/phoneNumbers' const TAG = 'PhoneNumberInput' diff --git a/src/components/PhoneNumberWithFlag.tsx b/src/components/PhoneNumberWithFlag.tsx index 2a26d64f79f..0f33c2f5e69 100644 --- a/src/components/PhoneNumberWithFlag.tsx +++ b/src/components/PhoneNumberWithFlag.tsx @@ -1,8 +1,9 @@ -import { getCountryEmoji, parsePhoneNumber } from '@celo/phone-utils' import * as React from 'react' import { StyleSheet, Text, View } from 'react-native' import Colors from 'src/styles/colors' import { typeScale } from 'src/styles/fonts' +import { getCountryEmoji } from 'src/utils/getCountryEmoji' +import { parsePhoneNumber } from 'src/utils/phoneNumbers' interface Props { e164PhoneNumber: string diff --git a/src/components/ValidatedTextInput.tsx b/src/components/ValidatedTextInput.tsx index 755087a4747..81d3213f493 100644 --- a/src/components/ValidatedTextInput.tsx +++ b/src/components/ValidatedTextInput.tsx @@ -2,11 +2,11 @@ * TextInput with input validation, interchangeable with `./TextInput.tsx` */ -import { validateInput } from '@celo/phone-utils' import { ValidatorKind } from '@celo/utils/lib/inputValidation' import * as React from 'react' import { KeyboardType } from 'react-native' import TextInput, { TextInputProps } from 'src/components/TextInput' +import { validateInput } from 'src/utils/inputValidation' interface OwnProps { InputComponent: React.ComponentType diff --git a/src/identity/actions.ts b/src/identity/actions.ts index 5cff1d019ea..5d8b2c200ed 100644 --- a/src/identity/actions.ts +++ b/src/identity/actions.ts @@ -1,5 +1,4 @@ import { normalizeAddressWith0x } from '@celo/base' -import { E164Number } from '@celo/phone-utils' import { AddressToDisplayNameType, AddressToE164NumberType, @@ -10,6 +9,7 @@ import { } from 'src/identity/reducer' import { ImportContactsStatus } from 'src/identity/types' import { Recipient } from 'src/recipients/recipient' +import { type E164Number } from 'src/utils/E164Number' export enum Actions { SET_SEEN_VERIFICATION_NUX = 'IDENTITY/SET_SEEN_VERIFICATION_NUX', diff --git a/src/identity/commentEncryption.ts b/src/identity/commentEncryption.ts index 2309c851924..33380e10056 100644 --- a/src/identity/commentEncryption.ts +++ b/src/identity/commentEncryption.ts @@ -9,7 +9,6 @@ import { encryptComment as encryptCommentRaw, } from '@celo/cryptographic-utils' import { PhoneNumberHashDetails } from '@celo/identity/lib/odis/phone-number-identifier' -import getPhoneHash from '@celo/phone-utils/lib/getPhoneHash' import { eqAddress, hexToBuffer } from '@celo/utils/lib/address' import { memoize, values } from 'lodash' import { MAX_COMMENT_LENGTH } from 'src/config' @@ -29,6 +28,7 @@ import { import { e164NumberToAddressSelector, e164NumberToSaltSelector } from 'src/identity/selectors' import { UpdateTransactionsAction } from 'src/transactions/actions' import { Network, TokenTransaction, TokenTransactionTypeV2 } from 'src/transactions/types' +import getPhoneHash from 'src/utils/getPhoneHash' import Logger from 'src/utils/Logger' import { getContractKit } from 'src/web3/contracts' import { doFetchDataEncryptionKey } from 'src/web3/dataEncryptionKey' diff --git a/src/identity/privateHashing.ts b/src/identity/privateHashing.ts index e3b4e574542..b0536bde927 100644 --- a/src/identity/privateHashing.ts +++ b/src/identity/privateHashing.ts @@ -1,8 +1,8 @@ import { PhoneNumberHashDetails } from '@celo/identity/lib/odis/phone-number-identifier' -import { PhoneNumberUtils } from '@celo/phone-utils' import { e164NumberSelector } from 'src/account/selectors' import { E164NumberToSaltType } from 'src/identity/reducer' import { e164NumberToSaltSelector } from 'src/identity/selectors' +import getPhoneHash from 'src/utils/getPhoneHash' import { select } from 'typed-redux-saga' // Get the wallet user's own phone hash details if they're cached @@ -23,7 +23,7 @@ export function* getUserSelfPhoneHashDetails() { const details: PhoneNumberHashDetails = { e164Number, pepper: salt, - phoneHash: PhoneNumberUtils.getPhoneHash(e164Number, salt), + phoneHash: getPhoneHash(e164Number, salt), } return details diff --git a/src/keylessBackup/KeylessBackupPhoneInput.tsx b/src/keylessBackup/KeylessBackupPhoneInput.tsx index 45bbd8bac0a..15bbe59d3fb 100644 --- a/src/keylessBackup/KeylessBackupPhoneInput.tsx +++ b/src/keylessBackup/KeylessBackupPhoneInput.tsx @@ -1,4 +1,3 @@ -import { Countries } from '@celo/phone-utils' import { NativeStackScreenProps } from '@react-navigation/native-stack' import React, { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -7,8 +6,8 @@ import * as RNLocalize from 'react-native-localize' import { SafeAreaView } from 'react-native-safe-area-context' import { defaultCountryCodeSelector, e164NumberSelector } from 'src/account/selectors' import { getPhoneNumberDetails } from 'src/account/utils' -import { KeylessBackupEvents } from 'src/analytics/Events' import AppAnalytics from 'src/analytics/AppAnalytics' +import { KeylessBackupEvents } from 'src/analytics/Events' import BackButton from 'src/components/BackButton' import Button, { BtnSizes, BtnTypes } from 'src/components/Button' import KeyboardAwareScrollView from 'src/components/KeyboardAwareScrollView' @@ -28,6 +27,7 @@ import Colors from 'src/styles/colors' import { typeScale } from 'src/styles/fonts' import { Spacing } from 'src/styles/styles' import variables from 'src/styles/variables' +import { Countries } from 'src/utils/Countries' type Props = NativeStackScreenProps diff --git a/src/localCurrency/selectors.ts b/src/localCurrency/selectors.ts index bb6f73f9146..a32850a0b59 100644 --- a/src/localCurrency/selectors.ts +++ b/src/localCurrency/selectors.ts @@ -1,4 +1,3 @@ -import { getRegionCode } from '@celo/phone-utils' import CountryData from 'country-data' import { getCurrencies } from 'react-native-localize' import { createSelector } from 'reselect' @@ -10,6 +9,7 @@ import { LocalCurrencySymbol, } from 'src/localCurrency/consts' import { RootState } from 'src/redux/reducers' +import { getRegionCode } from 'src/utils/phoneNumbers' function getCountryCurrencies(e164PhoneNumber: string) { const regionCode = getRegionCode(e164PhoneNumber) diff --git a/src/navigator/SettingsMenu.tsx b/src/navigator/SettingsMenu.tsx index a87db9a86fd..48b0b317539 100644 --- a/src/navigator/SettingsMenu.tsx +++ b/src/navigator/SettingsMenu.tsx @@ -6,11 +6,17 @@ import deviceInfoModule from 'react-native-device-info' import { ScrollView } from 'react-native-gesture-handler' import { useSelector } from 'react-redux' import { defaultCountryCodeSelector, e164NumberSelector, nameSelector } from 'src/account/selectors' -import { phoneNumberVerifiedSelector } from 'src/app/selectors' +import { phoneNumberVerifiedSelector, walletConnectEnabledSelector } from 'src/app/selectors' import ContactCircleSelf from 'src/components/ContactCircleSelf' +import { SettingsItemTextValue } from 'src/components/SettingsItem' import Touchable from 'src/components/Touchable' -import Help from 'src/icons/navigator/Help' import Envelope from 'src/icons/Envelope' +import ForwardChevron from 'src/icons/ForwardChevron' +import Lock from 'src/icons/Lock' +import Help from 'src/icons/navigator/Help' +import Wallet from 'src/icons/navigator/Wallet' +import Preferences from 'src/icons/Preferences' +import Stack from 'src/icons/Stack' import { headerWithCloseButton } from 'src/navigator/Headers' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' @@ -18,16 +24,9 @@ import { StackParamList } from 'src/navigator/types' import colors, { Colors } from 'src/styles/colors' import { typeScale } from 'src/styles/fonts' import { Spacing } from 'src/styles/styles' -import { parsePhoneNumber } from '@celo/phone-utils' -import ForwardChevron from 'src/icons/ForwardChevron' -import Wallet from 'src/icons/navigator/Wallet' -import Preferences from 'src/icons/Preferences' -import Lock from 'src/icons/Lock' -import Stack from 'src/icons/Stack' -import { selectSessions } from 'src/walletConnect/selectors' -import { walletConnectEnabledSelector } from 'src/app/selectors' import variables from 'src/styles/variables' -import { SettingsItemTextValue } from 'src/components/SettingsItem' +import { parsePhoneNumber } from 'src/utils/phoneNumbers' +import { selectSessions } from 'src/walletConnect/selectors' type Props = NativeStackScreenProps diff --git a/src/navigator/types.tsx b/src/navigator/types.tsx index 8308c2f2c7a..1558c0f8a17 100644 --- a/src/navigator/types.tsx +++ b/src/navigator/types.tsx @@ -1,4 +1,3 @@ -import { Countries } from '@celo/phone-utils' import { KycSchema } from '@fiatconnect/fiatconnect-types' import { SendOrigin, WalletConnectPairingOrigin } from 'src/analytics/types' import { EarnTabType } from 'src/earn/types' @@ -15,6 +14,7 @@ import { Recipient } from 'src/recipients/recipient' import { QrCode, TransactionDataInput } from 'src/send/types' import { AssetTabType } from 'src/tokens/types' import { NetworkId, TokenTransaction, TokenTransfer } from 'src/transactions/types' +import { Countries } from 'src/utils/Countries' import { Currency } from 'src/utils/currencies' import { SerializableTransactionRequest } from 'src/viem/preparedTransactionSerialization' import { ActionRequestProps } from 'src/walletConnect/screens/ActionRequest' diff --git a/src/networkInfo/saga.ts b/src/networkInfo/saga.ts index 755d0318e72..4204256c4d8 100644 --- a/src/networkInfo/saga.ts +++ b/src/networkInfo/saga.ts @@ -1,4 +1,3 @@ -import { getRegionCodeFromCountryCode } from '@celo/phone-utils' import NetInfo, { NetInfoState } from '@react-native-community/netinfo' import { getIpAddress } from 'react-native-device-info' import { eventChannel } from 'redux-saga' @@ -7,6 +6,7 @@ import { isE2EEnv } from 'src/config' import { setNetworkConnectivity, updateUserLocationData } from 'src/networkInfo/actions' import Logger from 'src/utils/Logger' import { fetchWithTimeout } from 'src/utils/fetchWithTimeout' +import { getRegionCodeFromCountryCode } from 'src/utils/phoneNumbers' import networkConfig from 'src/web3/networkConfig' import { call, cancelled, put, select, spawn, take } from 'typed-redux-saga' diff --git a/src/onboarding/registration/SelectCountry.test.tsx b/src/onboarding/registration/SelectCountry.test.tsx index 9d4480c13f1..3c914f0ea85 100644 --- a/src/onboarding/registration/SelectCountry.test.tsx +++ b/src/onboarding/registration/SelectCountry.test.tsx @@ -1,4 +1,3 @@ -import { Countries } from '@celo/phone-utils' import { fireEvent, render } from '@testing-library/react-native' import * as React from 'react' import 'react-native' @@ -6,6 +5,7 @@ import { Provider } from 'react-redux' import i18n from 'src/i18n' import { Screens } from 'src/navigator/Screens' import SelectCountry from 'src/onboarding/registration/SelectCountry' +import { Countries } from 'src/utils/Countries' import { createMockStore, getMockStackScreenProps } from 'test/utils' const onSelectCountry = jest.fn() diff --git a/src/onboarding/registration/SelectCountry.tsx b/src/onboarding/registration/SelectCountry.tsx index fa448dd246a..0d448a3a81b 100644 --- a/src/onboarding/registration/SelectCountry.tsx +++ b/src/onboarding/registration/SelectCountry.tsx @@ -1,4 +1,3 @@ -import { LocalizedCountry } from '@celo/phone-utils' import { NativeStackScreenProps } from '@react-navigation/native-stack' import React, { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -12,6 +11,7 @@ import { Screens } from 'src/navigator/Screens' import { StackParamList } from 'src/navigator/types' import SelectCountryItem from 'src/onboarding/registration/SelectCountryItem' import colors from 'src/styles/colors' +import { LocalizedCountry } from 'src/utils/Countries' import { getCountryFeatures } from 'src/utils/countryFeatures' const keyExtractor = (item: LocalizedCountry) => item.alpha2 diff --git a/src/onboarding/registration/SelectCountryItem.tsx b/src/onboarding/registration/SelectCountryItem.tsx index 525879bcdce..42f050e78f8 100644 --- a/src/onboarding/registration/SelectCountryItem.tsx +++ b/src/onboarding/registration/SelectCountryItem.tsx @@ -1,9 +1,9 @@ -import { LocalizedCountry } from '@celo/phone-utils' import * as React from 'react' import { StyleSheet, Text, View } from 'react-native' import Touchable from 'src/components/Touchable' import colors from 'src/styles/colors' import { typeScale } from 'src/styles/fonts' +import { LocalizedCountry } from 'src/utils/Countries' interface Props { country: LocalizedCountry diff --git a/src/qrcode/schema.ts b/src/qrcode/schema.ts index 5d09d61c563..d15f61fb2cc 100644 --- a/src/qrcode/schema.ts +++ b/src/qrcode/schema.ts @@ -1,4 +1,3 @@ -import { E164PhoneNumberType } from '@celo/phone-utils' import { AddressType } from '@celo/utils/lib/io' import { isLeft } from 'fp-ts/lib/Either' import { @@ -10,9 +9,10 @@ import { union as ioUnion, } from 'io-ts' import { PathReporter } from 'io-ts/lib/PathReporter' +import { DEEPLINK_PREFIX } from 'src/config' import { LocalCurrencyCode } from 'src/localCurrency/consts' +import { E164PhoneNumberType } from 'src/utils/E164Number' import { parse } from 'url' -import { DEEPLINK_PREFIX } from 'src/config' export const UriDataType = ioType({ address: AddressType, diff --git a/src/recipients/recipient.ts b/src/recipients/recipient.ts index 3f2ddbcc7ae..e0f1054def4 100644 --- a/src/recipients/recipient.ts +++ b/src/recipients/recipient.ts @@ -1,4 +1,3 @@ -import { parsePhoneNumber } from '@celo/phone-utils' import * as fuzzysort from 'fuzzysort' import { TFunction } from 'i18next' import { MinimalContact } from 'react-native-contacts' @@ -11,6 +10,7 @@ import { } from 'src/identity/reducer' import { RecipientVerificationStatus } from 'src/identity/types' import Logger from 'src/utils/Logger' +import { parsePhoneNumber } from 'src/utils/phoneNumbers' const TAG = 'recipients/recipient' diff --git a/src/send/hooks.ts b/src/send/hooks.ts index ccece3d35bf..c08c5cbe400 100644 --- a/src/send/hooks.ts +++ b/src/send/hooks.ts @@ -1,4 +1,3 @@ -import { parsePhoneNumber } from '@celo/phone-utils' import { isValidAddress } from '@celo/utils/lib/address' import { NameResolution, ResolutionKind } from '@valora/resolve-kit' import { debounce, throttle } from 'lodash' @@ -17,6 +16,7 @@ import { import { phoneRecipientCacheSelector, recipientInfoSelector } from 'src/recipients/reducer' import { resolveId } from 'src/recipients/resolve-id' import { useSelector } from 'src/redux/hooks' +import { parsePhoneNumber } from 'src/utils/phoneNumbers' const TYPING_DEBOUNCE_MILLSECONDS = 300 const SEARCH_THROTTLE_TIME = 100 diff --git a/src/transactions/UserSection.tsx b/src/transactions/UserSection.tsx index 928bc1219c3..13e69d41d1c 100644 --- a/src/transactions/UserSection.tsx +++ b/src/transactions/UserSection.tsx @@ -1,4 +1,3 @@ -import { getDisplayNumberInternational } from '@celo/phone-utils' import React, { useState } from 'react' import { useTranslation } from 'react-i18next' import { LayoutAnimation, StyleSheet, Text, View } from 'react-native' @@ -9,6 +8,7 @@ import { Screens } from 'src/navigator/Screens' import { getDisplayName, Recipient, recipientHasNumber } from 'src/recipients/recipient' import colors from 'src/styles/colors' import { typeScale } from 'src/styles/fonts' +import { getDisplayNumberInternational } from 'src/utils/phoneNumbers' interface Props { type: 'sent' | 'received' | 'withdrawn' diff --git a/src/utils/Countries.test.ts b/src/utils/Countries.test.ts new file mode 100644 index 00000000000..2a4b52fce14 --- /dev/null +++ b/src/utils/Countries.test.ts @@ -0,0 +1,180 @@ +/** + * Reference file: + * https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/phone-utils/src/countries.test.ts + */ + +import { Countries } from 'src/utils/Countries' + +const countries = new Countries('en-us') + +describe('countries', () => { + describe('getCountryMap', () => { + test('Valid Country', () => { + const country = countries.getCountryByCodeAlpha2('US') + + expect(country).toBeDefined() + + // check these to make tsc happy + if (country && country.names) { + // eslint-disable-next-line jest/no-conditional-expect + expect(country.names['en-us']).toEqual('United States') + } + }) + + test('Invalid Country', () => { + // canary islands, no calling code + const invalidCountry = countries.getCountryByCodeAlpha2('IC') + + expect(invalidCountry).toBeUndefined() + }) + }) + describe('getCountry', () => { + test('has all country data', () => { + const country = countries.getCountry('taiwan') + + expect(country).toMatchInlineSnapshot(` + { + "alpha2": "TW", + "alpha3": "TWN", + "countryCallingCode": "+886", + "countryCallingCodes": [ + "+886", + ], + "countryPhonePlaceholder": { + "national": "00 0000 0000", + }, + "currencies": [ + "TWD", + ], + "displayName": "Taiwan", + "displayNameNoDiacritics": "taiwan", + "emoji": "🇹🇼", + "ioc": "TPE", + "languages": [ + "zho", + ], + "name": "Taiwan", + "names": { + "en-us": "Taiwan", + "es-419": "Taiwán", + }, + "status": "assigned", + } + `) + }) + }) + + describe('Country Search', () => { + test('finds an exact match', () => { + const results = countries.getFilteredCountries('taiwan') + + expect(results).toMatchInlineSnapshot(` + [ + { + "alpha2": "TW", + "alpha3": "TWN", + "countryCallingCode": "+886", + "countryCallingCodes": [ + "+886", + ], + "countryPhonePlaceholder": { + "national": "00 0000 0000", + }, + "currencies": [ + "TWD", + ], + "displayName": "Taiwan", + "displayNameNoDiacritics": "taiwan", + "emoji": "🇹🇼", + "ioc": "TPE", + "languages": [ + "zho", + ], + "name": "Taiwan", + "names": { + "en-us": "Taiwan", + "es-419": "Taiwán", + }, + "status": "assigned", + }, + ] + `) + }) + + test('finds countries by calling code', () => { + const results = countries.getFilteredCountries('49') + + expect(results).toMatchInlineSnapshot(` + [ + { + "alpha2": "DE", + "alpha3": "DEU", + "countryCallingCode": "+49", + "countryCallingCodes": [ + "+49", + ], + "countryPhonePlaceholder": { + "national": "000 000000", + }, + "currencies": [ + "EUR", + ], + "displayName": "Germany", + "displayNameNoDiacritics": "germany", + "emoji": "🇩🇪", + "ioc": "GER", + "languages": [ + "deu", + ], + "name": "Germany", + "names": { + "en-us": "Germany", + "es-419": "Alemania", + }, + "status": "assigned", + }, + ] + `) + }) + + test('finds countries by ISO code', () => { + const results = countries.getFilteredCountries('gb') + + expect(results).toMatchInlineSnapshot(` + [ + { + "alpha2": "GB", + "alpha3": "GBR", + "countryCallingCode": "+44", + "countryCallingCodes": [ + "+44", + ], + "countryPhonePlaceholder": { + "national": "0000 000 0000", + }, + "currencies": [ + "GBP", + ], + "displayName": "United Kingdom", + "displayNameNoDiacritics": "united kingdom", + "emoji": "🇬🇧", + "ioc": "GBR", + "languages": [ + "eng", + "cor", + "gle", + "gla", + "cym", + ], + "name": "United Kingdom", + "names": { + "en-us": "United Kingdom", + "es-419": "Reino Unido", + }, + "status": "assigned", + }, + ] + `) + }) + }) +}) diff --git a/src/utils/Countries.ts b/src/utils/Countries.ts new file mode 100644 index 00000000000..24aa74566ac --- /dev/null +++ b/src/utils/Countries.ts @@ -0,0 +1,121 @@ +/** + * Reference file: + * https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/phone-utils/src/countries.ts + */ + +// more countries @ https://github.com/umpirsky/country-list +import countryData from 'country-data' +import { getExampleNumber } from 'src/utils/phoneNumbers' +import esData from './country.json' + +interface CountryNames { + [name: string]: string +} + +export interface LocalizedCountry extends Omit { + displayName: string + displayNameNoDiacritics: string + names: CountryNames + countryPhonePlaceholder: { + national?: string | undefined + international?: string | undefined + } + countryCallingCode: string +} + +const removeDiacritics = (word: string) => + word && + word + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase() + .trim() + +const matchCountry = (country: LocalizedCountry, query: string) => { + return ( + country.displayNameNoDiacritics.startsWith(query) || + country.countryCallingCode.startsWith('+' + query) || + country.alpha3.startsWith(query.toUpperCase()) + ) +} + +export class Countries { + language: string + countryMap: Map + localizedCountries: LocalizedCountry[] + + constructor(language?: string) { + // fallback to 'en-us' + this.language = language ? language.toLocaleLowerCase() : 'en-us' + this.countryMap = new Map() + this.localizedCountries = [] + this.assignCountries() + } + + getCountry(countryName?: string | null): LocalizedCountry | undefined { + if (!countryName) { + return undefined + } + + const query = removeDiacritics(countryName) + + return this.localizedCountries.find((country) => country.displayNameNoDiacritics === query) + } + + getCountryByCodeAlpha2(countryCode: string): LocalizedCountry | undefined { + return this.countryMap.get(countryCode) + } + + getFilteredCountries(query: string): LocalizedCountry[] { + query = removeDiacritics(query) + // Return full list if the query is empty + if (!query || !query.length) { + return this.localizedCountries + } + + return this.localizedCountries.filter((country) => matchCountry(country, query)) + } + + private assignCountries() { + // add other languages to country data + this.localizedCountries = countryData.callingCountries.all + .map((country: countryData.Country) => { + // this is assuming these two are the only cases, in i18n.ts seems like there + // are fallback languages 'es-US' and 'es-LA' that are not covered + const names: CountryNames = { + 'en-us': country.name, + // @ts-ignore + 'es-419': esData[country.alpha2], + } + + const displayName = names[this.language] || country.name + + // We only use the first calling code, others are irrelevant in the current dataset. + // Also some of them have a non standard calling code + // for instance: 'Antigua And Barbuda' has '+1 268', where only '+1' is expected + // so we fix this here + const countryCallingCode = country.countryCallingCodes[0].split(' ')[0] + + const localizedCountry = { + names, + displayName, + displayNameNoDiacritics: removeDiacritics(displayName), + countryPhonePlaceholder: { + national: getExampleNumber(countryCallingCode), + // Not needed right now + // international: getExampleNumber(countryCallingCode, true, true), + }, + countryCallingCode, + ...country, + // Use default emoji when flag emoji is missing + emoji: country.emoji || '🏳', + } + + // use ISO 3166-1 alpha2 code as country id + this.countryMap.set(country.alpha2.toUpperCase(), localizedCountry) + + return localizedCountry + }) + .sort((a, b) => a.displayName.localeCompare(b.displayName)) + } +} diff --git a/src/utils/E164Number.ts b/src/utils/E164Number.ts new file mode 100644 index 00000000000..c829c2d6585 --- /dev/null +++ b/src/utils/E164Number.ts @@ -0,0 +1,22 @@ +/** + * Reference file: + * https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/phone-utils/src/io.ts + */ + +import { either } from 'fp-ts/lib/Either' +import * as t from 'io-ts' +import { isE164NumberStrict } from 'src/utils/phoneNumbers' + +export const E164PhoneNumberType = new t.Type( + 'E164Number', + t.string.is, + (input, context) => + either.chain(t.string.validate(input, context), (stringValue) => + isE164NumberStrict(stringValue) + ? t.success(stringValue) + : t.failure(stringValue, context, 'is not a valid e164 number') + ), + String +) + +export type E164Number = t.TypeOf diff --git a/src/utils/country.json b/src/utils/country.json new file mode 100644 index 00000000000..6f9b785b9d2 --- /dev/null +++ b/src/utils/country.json @@ -0,0 +1,257 @@ +{ + "AF": "Afganist\u00e1n", + "AL": "Albania", + "DE": "Alemania", + "AD": "Andorra", + "AO": "Angola", + "AI": "Anguila", + "AQ": "Ant\u00e1rtida", + "AG": "Antigua y Barbuda", + "SA": "Arabia Saud\u00ed", + "DZ": "Argelia", + "AR": "Argentina", + "AM": "Armenia", + "AW": "Aruba", + "AU": "Australia", + "AT": "Austria", + "AZ": "Azerbaiy\u00e1n", + "BS": "Bahamas", + "BD": "Banglad\u00e9s", + "BB": "Barbados", + "BH": "Bar\u00e9in", + "BE": "B\u00e9lgica", + "BZ": "Belice", + "BJ": "Ben\u00edn", + "BM": "Bermudas", + "BY": "Bielorrusia", + "BO": "Bolivia", + "BA": "Bosnia y Herzegovina", + "BW": "Botsuana", + "BR": "Brasil", + "BN": "Brun\u00e9i", + "BG": "Bulgaria", + "BF": "Burkina Faso", + "BI": "Burundi", + "BT": "But\u00e1n", + "CV": "Cabo Verde", + "KH": "Camboya", + "CM": "Camer\u00fan", + "CA": "Canad\u00e1", + "IC": "Canarias", + "BQ": "Caribe neerland\u00e9s", + "QA": "Catar", + "EA": "Ceuta y Melilla", + "TD": "Chad", + "CZ": "Chequia", + "CL": "Chile", + "CN": "China", + "CY": "Chipre", + "VA": "Ciudad del Vaticano", + "CO": "Colombia", + "KM": "Comoras", + "CG": "Congo", + "KP": "Corea del Norte", + "KR": "Corea del Sur", + "CR": "Costa Rica", + "CI": "C\u00f4te d\u2019Ivoire", + "HR": "Croacia", + "CU": "Cuba", + "CW": "Curazao", + "DG": "Diego Garc\u00eda", + "DK": "Dinamarca", + "DM": "Dominica", + "EC": "Ecuador", + "EG": "Egipto", + "SV": "El Salvador", + "AE": "Emiratos \u00c1rabes Unidos", + "ER": "Eritrea", + "SK": "Eslovaquia", + "SI": "Eslovenia", + "ES": "Espa\u00f1a", + "US": "Estados Unidos", + "EE": "Estonia", + "SZ": "Esuatini", + "ET": "Etiop\u00eda", + "PH": "Filipinas", + "FI": "Finlandia", + "FJ": "Fiyi", + "FR": "Francia", + "GA": "Gab\u00f3n", + "GM": "Gambia", + "GE": "Georgia", + "GH": "Ghana", + "GI": "Gibraltar", + "GD": "Granada", + "GR": "Grecia", + "GL": "Groenlandia", + "GP": "Guadalupe", + "GU": "Guam", + "GT": "Guatemala", + "GF": "Guayana Francesa", + "GG": "Guernsey", + "GN": "Guinea", + "GQ": "Guinea Ecuatorial", + "GW": "Guinea-Bis\u00e1u", + "GY": "Guyana", + "HT": "Hait\u00ed", + "HN": "Honduras", + "HU": "Hungr\u00eda", + "IN": "India", + "ID": "Indonesia", + "IQ": "Irak", + "IR": "Ir\u00e1n", + "IE": "Irlanda", + "AC": "Isla de la Ascensi\u00f3n", + "IM": "Isla de Man", + "CX": "Isla de Navidad", + "NF": "Isla Norfolk", + "IS": "Islandia", + "AX": "Islas \u00c5land", + "KY": "Islas Caim\u00e1n", + "CC": "Islas Cocos", + "CK": "Islas Cook", + "FO": "Islas Feroe", + "GS": "Islas Georgia del Sur y Sandwich del Sur", + "FK": "Islas Malvinas", + "MP": "Islas Marianas del Norte", + "MH": "Islas Marshall", + "UM": "Islas menores alejadas de EE. UU.", + "PN": "Islas Pitcairn", + "SB": "Islas Salom\u00f3n", + "TC": "Islas Turcas y Caicos", + "VG": "Islas V\u00edrgenes Brit\u00e1nicas", + "VI": "Islas V\u00edrgenes de EE. UU.", + "IL": "Israel", + "IT": "Italia", + "JM": "Jamaica", + "JP": "Jap\u00f3n", + "JE": "Jersey", + "JO": "Jordania", + "KZ": "Kazajist\u00e1n", + "KE": "Kenia", + "KG": "Kirguist\u00e1n", + "KI": "Kiribati", + "XK": "Kosovo", + "KW": "Kuwait", + "LA": "Laos", + "LS": "Lesoto", + "LV": "Letonia", + "LB": "L\u00edbano", + "LR": "Liberia", + "LY": "Libia", + "LI": "Liechtenstein", + "LT": "Lituania", + "LU": "Luxemburgo", + "MK": "Macedonia", + "MG": "Madagascar", + "MY": "Malasia", + "MW": "Malaui", + "MV": "Maldivas", + "ML": "Mali", + "MT": "Malta", + "MA": "Marruecos", + "MQ": "Martinica", + "MU": "Mauricio", + "MR": "Mauritania", + "YT": "Mayotte", + "MX": "M\u00e9xico", + "FM": "Micronesia", + "MD": "Moldavia", + "MC": "M\u00f3naco", + "MN": "Mongolia", + "ME": "Montenegro", + "MS": "Montserrat", + "MZ": "Mozambique", + "MM": "Myanmar (Birmania)", + "NA": "Namibia", + "NR": "Nauru", + "NP": "Nepal", + "NI": "Nicaragua", + "NE": "N\u00edger", + "NG": "Nigeria", + "NU": "Niue", + "NO": "Noruega", + "NC": "Nueva Caledonia", + "NZ": "Nueva Zelanda", + "OM": "Om\u00e1n", + "NL": "Pa\u00edses Bajos", + "PK": "Pakist\u00e1n", + "PW": "Palaos", + "PA": "Panam\u00e1", + "PG": "Pap\u00faa Nueva Guinea", + "PY": "Paraguay", + "PE": "Per\u00fa", + "PF": "Polinesia Francesa", + "PL": "Polonia", + "PT": "Portugal", + "XA": "Pseudo-Accents", + "XB": "Pseudo-Bidi", + "PR": "Puerto Rico", + "HK": "RAE de Hong Kong (China)", + "MO": "RAE de Macao (China)", + "GB": "Reino Unido", + "CF": "Rep\u00fablica Centroafricana", + "CD": "Rep\u00fablica Democr\u00e1tica del Congo", + "DO": "Rep\u00fablica Dominicana", + "RE": "Reuni\u00f3n", + "RW": "Ruanda", + "RO": "Ruman\u00eda", + "RU": "Rusia", + "EH": "S\u00e1hara Occidental", + "WS": "Samoa", + "AS": "Samoa Americana", + "BL": "San Bartolom\u00e9", + "KN": "San Crist\u00f3bal y Nieves", + "SM": "San Marino", + "MF": "San Mart\u00edn", + "PM": "San Pedro y Miquel\u00f3n", + "VC": "San Vicente y las Granadinas", + "SH": "Santa Elena", + "LC": "Santa Luc\u00eda", + "ST": "Santo Tom\u00e9 y Pr\u00edncipe", + "SN": "Senegal", + "RS": "Serbia", + "SC": "Seychelles", + "SL": "Sierra Leona", + "SG": "Singapur", + "SX": "Sint Maarten", + "SY": "Siria", + "SO": "Somalia", + "LK": "Sri Lanka", + "ZA": "Sud\u00e1frica", + "SD": "Sud\u00e1n", + "SS": "Sud\u00e1n del Sur", + "SE": "Suecia", + "CH": "Suiza", + "SR": "Surinam", + "SJ": "Svalbard y Jan Mayen", + "TH": "Tailandia", + "TW": "Taiw\u00e1n", + "TZ": "Tanzania", + "TJ": "Tayikist\u00e1n", + "IO": "Territorio Brit\u00e1nico del Oc\u00e9ano \u00cdndico", + "TF": "Territorios Australes Franceses", + "PS": "Territorios Palestinos", + "TL": "Timor-Leste", + "TG": "Togo", + "TK": "Tokelau", + "TO": "Tonga", + "TT": "Trinidad y Tobago", + "TA": "Trist\u00e1n de Acu\u00f1a", + "TN": "T\u00fanez", + "TM": "Turkmenist\u00e1n", + "TR": "Turqu\u00eda", + "TV": "Tuvalu", + "UA": "Ucrania", + "UG": "Uganda", + "UY": "Uruguay", + "UZ": "Uzbekist\u00e1n", + "VU": "Vanuatu", + "VE": "Venezuela", + "VN": "Vietnam", + "WF": "Wallis y Futuna", + "YE": "Yemen", + "DJ": "Yibuti", + "ZM": "Zambia", + "ZW": "Zimbabue" +} diff --git a/src/utils/getCountryEmoji.ts b/src/utils/getCountryEmoji.ts new file mode 100644 index 00000000000..81dfaca0663 --- /dev/null +++ b/src/utils/getCountryEmoji.ts @@ -0,0 +1,28 @@ +/** + * Reference file: + * https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/phone-utils/src/getCountryEmoji.ts + */ + +import CountryData from 'country-data' +import { getCountryCode, getRegionCode } from 'src/utils/phoneNumbers' + +export function getCountryEmoji( + e164PhoneNumber: string, + countryCodePossible?: number, + regionCodePossible?: string +) { + // The country code and region code can both be passed in, or it can be inferred from the e164PhoneNumber + let countryCode: any + let regionCode: any + countryCode = countryCodePossible + regionCode = regionCodePossible + if (!countryCode || !regionCode) { + countryCode = getCountryCode(e164PhoneNumber) + regionCode = getRegionCode(e164PhoneNumber) + } + const countries = CountryData.lookup.countries({ countryCallingCodes: `+${countryCode}` }) + const userCountryArray = countries.filter((c: any) => c.alpha2 === regionCode) + const country = userCountryArray.length > 0 ? userCountryArray[0] : undefined + + return country ? country.emoji : '' +} diff --git a/src/utils/getPhoneHash.ts b/src/utils/getPhoneHash.ts new file mode 100644 index 00000000000..fbdd0f45c41 --- /dev/null +++ b/src/utils/getPhoneHash.ts @@ -0,0 +1,29 @@ +/** + * Reference file: + * https://github.com/celo-org/developer-tooling/blob/8572a0f978d1aa01a36775ef4be48c3eafdbb204/packages/sdk/phone-utils/src/getPhoneHash.ts + * + * The file was removed in later versions of @celo/phone-utils so the function + * is taken in its final form before removal. + */ + +import { getIdentifierHash, getPrefixedIdentifier, IdentifierPrefix } from 'src/utils/identifier' +import { soliditySha3 } from 'web3-utils' + +/** + * Rerefence implementation uses "web3-utils" version 1.3.6: + * https://github.com/celo-org/developer-tooling/blob/8572a0f978d1aa01a36775ef4be48c3eafdbb204/packages/sdk/base/package.json#L30 + * + * In that version the function "soliditySha3" returns "string | null" while the latest version of "web3-utils" + * returns "string | undefined". For the sake of compatibility with the original implementation while + * also using the latest version of "web3-utils" "?? null" was added to replace possible undefined with null. + */ +const sha3 = (v: string): string | null => soliditySha3({ type: 'string', value: v }) ?? null +const getPhoneHash = (phoneNumber: string, salt?: string): string => { + if (salt) { + return getIdentifierHash(sha3, phoneNumber, IdentifierPrefix.PHONE_NUMBER, salt) + } + // backwards compatibility for old phoneUtils getPhoneHash + return sha3(getPrefixedIdentifier(phoneNumber, IdentifierPrefix.PHONE_NUMBER)) as string +} + +export default getPhoneHash diff --git a/src/utils/identifier.ts b/src/utils/identifier.ts new file mode 100644 index 00000000000..b50ab97466a --- /dev/null +++ b/src/utils/identifier.ts @@ -0,0 +1,81 @@ +/** + * Reference file: + * https://github.com/celo-org/developer-tooling/blob/8572a0f978d1aa01a36775ef4be48c3eafdbb204/packages/sdk/base/src/identifier.ts + */ + +// These functions were moved from the identity SDK because the protocol package +// and @celo/phone-utils both need these core identifier generation functions as well. +// The protocol package cannot depend on the identity SDK as is since this creates +// a non-trivial dependency cycle (currently, if A->B means "A depends on B", +// identity -> phone-number-privacy-common -> contractkit -> protocol). + +const PEPPER_SEPARATOR = '__' + +// Docstring is duplicated in @celo/identity; make sure to update in both places. +/** + * Standardized prefixes for ODIS identifiers. + * + * @remarks These prefixes prevent collisions between off-chain identifiers. + * i.e. if a user's instagram and twitter handles are the same, + * these prefixes prevent the ODIS identifers from being the same. + * + * If you would like to use a prefix that isn't included, please put up a PR + * adding it to @celo/base (in celo-monorepo/packages/sdk/base/src/identifier.ts) + * to ensure interoperability with other projects. When adding new prefixes, + * please use either the full platform name in all lowercase (e.g. 'facebook') + * or DID methods https://w3c.github.io/did-spec-registries/#did-methods. + * Make sure to add the expected value for the unit test case in + * `celo-monorepo/packages/sdk/base/src/identifier.test.ts`, + * otherwise the test will fail. + * + * The NULL prefix is included to allow projects to use the sdk without selecting + * a predefined prefix or adding their own. Production use of the NULL prefix is + * discouraged since identifiers will not be interoperable with other projects. + * Please think carefully before using the NULL prefix. + */ +export enum IdentifierPrefix { + PHONE_NUMBER = 'tel', +} + +// Docstring is duplicated in @celo/identity; make sure to update in both places. +/** + * Concatenates the identifierPrefix and plaintextIdentifier with the separator '://' + * + * @param plaintextIdentifier Off-chain identifier, ex: phone number, twitter handle, email, etc. + * @param identifierPrefix Standardized prefix used to prevent collisions between identifiers + */ +export const getPrefixedIdentifier = ( + plaintextIdentifier: string, + identifierPrefix: IdentifierPrefix +): string => identifierPrefix + '://' + plaintextIdentifier + +/** + * Helper function for getIdentifierHash in @celo/identity, so that this can + * be used in protocol tests without dependency issues. + * + * @remarks + * Concatenates the plaintext prefixed identifier with the pepper derived by hashing the unblinded + * signature returned by ODIS. + * + * @param sha3 Hash function (i.e. soliditySha3) to use to generate the identifier + * @param plaintextIdentifier Off-chain identifier, ex: phone number, twitter handle, email, etc. + * @param identifierPrefix Standardized prefix used to prevent collisions between identifiers + * @param pepper Hash of the unblinded signature returned by ODIS + */ +export const getIdentifierHash = ( + sha3: (a: string) => string | null, + plaintextIdentifier: string, + identifierPrefix: IdentifierPrefix, + pepper: string +): string => { + // hashing the identifier before appending the pepper to avoid domain collisions where the + // identifier may contain underscores + // not doing this for phone numbers to maintain backwards compatibility + const value = + identifierPrefix === IdentifierPrefix.PHONE_NUMBER + ? getPrefixedIdentifier(plaintextIdentifier, identifierPrefix) + PEPPER_SEPARATOR + pepper + : (sha3(getPrefixedIdentifier(plaintextIdentifier, identifierPrefix)) as string) + + PEPPER_SEPARATOR + + pepper + return sha3(value) as string +} diff --git a/src/utils/inputValidation.test.ts b/src/utils/inputValidation.test.ts new file mode 100644 index 00000000000..614023d64f2 --- /dev/null +++ b/src/utils/inputValidation.test.ts @@ -0,0 +1,53 @@ +/** + * Reference file: + * https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/phone-utils/src/inputValidation.test.ts + */ + +import { validateInput, ValidatorKind, type BaseProps } from 'src/utils/inputValidation' + +describe('inputValidation', () => { + const validateFunction = ( + itStr: string, + inputs: string[], + validator: ValidatorKind, + expected: string, + props?: BaseProps + ) => { + // eslint-disable-next-line jest/valid-title + it(itStr, () => + inputs.forEach((input) => { + const result = validateInput(input, { validator, countryCallingCode: '1', ...props }) + expect(result).toEqual(expected) + }) + ) + } + + const numbers = ['bu1.23n', '1.2.3', '1.23', '1.2.-_[`/,zx3.....', '1.b.23'] + + validateFunction('validates integers', numbers, ValidatorKind.Integer, '123') + + validateFunction('validates decimals', numbers, ValidatorKind.Decimal, '1.23') + + validateFunction( + 'allows comma decimals', + numbers.map((val) => val.replace('.', ',')), + ValidatorKind.Decimal, + '1,23', + { decimalSeparator: ',' } + ) + + validateFunction( + 'validates phone numbers', + [ + '4023939889', + '(402)3939889', + '(402)393-9889', + '402bun393._=988-9', + '402 393 9889', + '(4023) 9-39-88-9', + '4-0-2-3-9-3-9-8-8-9', // phone-kebab + ], + ValidatorKind.Phone, + '(402) 393-9889' + ) +}) diff --git a/src/utils/inputValidation.ts b/src/utils/inputValidation.ts new file mode 100644 index 00000000000..1184235faf2 --- /dev/null +++ b/src/utils/inputValidation.ts @@ -0,0 +1,76 @@ +/** + * Reference files (this file is a combination of both): + * https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/base/src/inputValidation.ts + * https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/phone-utils/src/inputValidation.ts + */ + +import { getDisplayPhoneNumber } from 'src/utils/phoneNumbers' + +export enum ValidatorKind { + Custom = 'custom', + Decimal = 'decimal', + Integer = 'integer', + Phone = 'phone', +} + +export interface BaseProps { + validator?: ValidatorKind + customValidator?: (input: string) => string + countryCallingCode?: string + decimalSeparator?: string +} + +function validateInteger(input: string): string { + return input.replace(/[^0-9]/g, '') +} + +function validateDecimal(input: string, decimalSeparator: string = '.'): string { + const regex = decimalSeparator === ',' ? /[^0-9,]/g : /[^0-9.]/g + + const cleanedArray = input.replace(regex, '').split(decimalSeparator) + + if (cleanedArray.length <= 1) { + // Empty string or no decimals + return cleanedArray.join('') + } else { + return cleanedArray.shift() + decimalSeparator + cleanedArray.join('') + } +} + +function validatePhone(input: string, countryCallingCode?: string): string { + input = input.replace(/[^0-9()\- ]/g, '') + + if (!countryCallingCode) { + return input + } + + const displayNumber = getDisplayPhoneNumber(input, countryCallingCode) + + if (!displayNumber) { + return input + } + + return displayNumber +} + +export function validateInput(input: string, props: BaseProps): string { + if (!props.validator && !props.customValidator) { + return input + } + + switch (props.validator) { + case 'decimal': + return validateDecimal(input, props.decimalSeparator) + case 'integer': + return validateInteger(input) + case 'phone': + return validatePhone(input, props.countryCallingCode) + case 'custom': { + if (props.customValidator) { + return props.customValidator(input) + } + } + } + + throw new Error('Unhandled input validator') +} diff --git a/src/utils/phoneNumber.test.ts b/src/utils/phoneNumber.test.ts new file mode 100644 index 00000000000..e959013b4c0 --- /dev/null +++ b/src/utils/phoneNumber.test.ts @@ -0,0 +1,231 @@ +import { + getCountryCode, + getDisplayPhoneNumber, + getExampleNumber, + getRegionCode, + getRegionCodeFromCountryCode, + parsePhoneNumber, +} from 'src/utils/phoneNumbers' + +const COUNTRY_CODES = { + US: '+1', + DE: '+49', + AR: '+54', + MX: '+52', + LR: '+231', + CI: '+225', +} + +const TEST_PHONE_NUMBERS = { + VALID_US_1: '6282287826', + VALID_US_2: '(628) 228-7826', + VALID_US_3: '+16282287826', + VALID_US_4: '16282287826', + VALID_DE_1: '015229355106', + VALID_DE_2: '01522 (935)-5106', + VALID_DE_3: '+49 01522 935 5106', + VALID_AR_1: '091126431111', + VALID_AR_2: '(911) 2643-1111', + VALID_AR_3: '+5411 2643-1111', + VALID_AR_4: '9 11 2643 1111', + VALID_MX_1: '33 1234-5678', + VALID_MX_2: '1 33 1234-5678', + VALID_MX_3: '+52 1 33 1234-5678', + VALID_LR: '881551952', + VALID_CI: '+225 2122003801', + FORMATTED_AR: '+5491126431111', + FORMATTED_MX: '+523312345678', + FORMATTED_LR: '+231881551952', + FORMATTED_CI: '+2252122003801', + DISPLAY_AR: '9 11 2643-1111', + DISPLAY_MX: '33 1234 5678', + DISPLAY_MX_3: '+52 1 33 1234-5678', + DISPLAY_LR: '88 155 1952', + DISPLAY_CI: '21 22 0 03801', + INVALID_EMPTY: '', + TOO_SHORT: '123', + VALID_E164: '+141555544444', +} + +describe('Phone number formatting and utilities', () => { + describe('Display formatting', () => { + it('Invalid empty', () => { + expect(getDisplayPhoneNumber(TEST_PHONE_NUMBERS.INVALID_EMPTY, COUNTRY_CODES.US)).toBe('') + }) + it('Format US phone simple, no country code', () => { + expect(getDisplayPhoneNumber(TEST_PHONE_NUMBERS.VALID_US_1, COUNTRY_CODES.US)).toBe( + '(628) 228-7826' + ) + }) + it('Format US phone messy, no country code', () => { + expect(getDisplayPhoneNumber(TEST_PHONE_NUMBERS.VALID_US_2, COUNTRY_CODES.US)).toBe( + '(628) 228-7826' + ) + }) + it('Format US phone simple, with country code and wrong region', () => { + expect(getDisplayPhoneNumber(TEST_PHONE_NUMBERS.VALID_US_3, COUNTRY_CODES.AR)).toBe( + '(628) 228-7826' + ) + }) + it('Format US phone simple, with country code but no param', () => { + expect(getDisplayPhoneNumber(TEST_PHONE_NUMBERS.VALID_US_3, COUNTRY_CODES.US)).toBe( + '(628) 228-7826' + ) + }) + it('Format US phone simple, with country code no plus', () => { + expect(getDisplayPhoneNumber(TEST_PHONE_NUMBERS.VALID_US_4, COUNTRY_CODES.US)).toBe( + '(628) 228-7826' + ) + }) + it('Format DE phone simple, no country code', () => { + expect(getDisplayPhoneNumber(TEST_PHONE_NUMBERS.VALID_DE_1, COUNTRY_CODES.DE)).toBe( + '01522 9355106' + ) + }) + it('Format DE phone messy, no country code', () => { + expect(getDisplayPhoneNumber(TEST_PHONE_NUMBERS.VALID_DE_2, COUNTRY_CODES.DE)).toBe( + '01522 9355106' + ) + }) + it('Format DE phone messy, wrong country code', () => { + expect(getDisplayPhoneNumber(TEST_PHONE_NUMBERS.VALID_DE_2, COUNTRY_CODES.US)).toBe( + TEST_PHONE_NUMBERS.VALID_DE_2 + ) + }) + it('Format DE phone with country code', () => { + expect(getDisplayPhoneNumber(TEST_PHONE_NUMBERS.VALID_DE_3, COUNTRY_CODES.DE)).toBe( + '01522 9355106' + ) + }) + it('Format AR phone simple, no country code', () => { + expect(getDisplayPhoneNumber(TEST_PHONE_NUMBERS.VALID_AR_1, COUNTRY_CODES.AR)).toBe( + TEST_PHONE_NUMBERS.DISPLAY_AR + ) + }) + it('Format AR phone messy, no country code', () => { + expect(getDisplayPhoneNumber(TEST_PHONE_NUMBERS.VALID_AR_2, COUNTRY_CODES.AR)).toBe( + TEST_PHONE_NUMBERS.DISPLAY_AR + ) + }) + it('Format AR phone with country code', () => { + expect(getDisplayPhoneNumber(TEST_PHONE_NUMBERS.VALID_AR_3, COUNTRY_CODES.AR)).toBe( + TEST_PHONE_NUMBERS.DISPLAY_AR + ) + }) + + it('Format MX phone with country code', () => { + expect(getDisplayPhoneNumber(TEST_PHONE_NUMBERS.VALID_MX_3, COUNTRY_CODES.MX)).toBe( + TEST_PHONE_NUMBERS.DISPLAY_MX_3 + ) + }) + + it('Format LR phone with no country code', () => { + expect(getDisplayPhoneNumber(TEST_PHONE_NUMBERS.VALID_LR, COUNTRY_CODES.LR)).toBe( + TEST_PHONE_NUMBERS.DISPLAY_LR + ) + }) + + it('Format CI phone with no country code', () => { + expect(getDisplayPhoneNumber(TEST_PHONE_NUMBERS.VALID_CI, COUNTRY_CODES.CI)).toBe( + TEST_PHONE_NUMBERS.DISPLAY_CI + ) + }) + }) + + describe('Number Parsing', () => { + it('Invalid empty', () => { + expect(parsePhoneNumber(TEST_PHONE_NUMBERS.INVALID_EMPTY, COUNTRY_CODES.US)).toBe(null) + }) + it('Too short', () => { + expect(parsePhoneNumber(TEST_PHONE_NUMBERS.TOO_SHORT, COUNTRY_CODES.US)).toBe(null) + }) + it('Format US messy phone #', () => { + expect(parsePhoneNumber(TEST_PHONE_NUMBERS.VALID_US_2, COUNTRY_CODES.US)).toMatchObject({ + e164Number: '+16282287826', + displayNumber: '(628) 228-7826', + countryCode: 1, + regionCode: 'US', + }) + }) + it('Format DE messy phone #', () => { + expect(parsePhoneNumber(TEST_PHONE_NUMBERS.VALID_DE_2, COUNTRY_CODES.DE)).toMatchObject({ + e164Number: '+4915229355106', + displayNumber: '01522 9355106', + countryCode: 49, + regionCode: 'DE', + }) + }) + it('Format AR messy phone # 1', () => { + expect(parsePhoneNumber(TEST_PHONE_NUMBERS.VALID_AR_4, COUNTRY_CODES.AR)).toMatchObject({ + e164Number: TEST_PHONE_NUMBERS.FORMATTED_AR, + displayNumber: TEST_PHONE_NUMBERS.DISPLAY_AR, + countryCode: 54, + regionCode: 'AR', + }) + }) + + it('Format MX phone # 1', () => { + expect(parsePhoneNumber(TEST_PHONE_NUMBERS.VALID_MX_1, COUNTRY_CODES.MX)).toMatchObject({ + e164Number: TEST_PHONE_NUMBERS.FORMATTED_MX, + displayNumber: TEST_PHONE_NUMBERS.DISPLAY_MX, + countryCode: 52, + regionCode: 'MX', + }) + }) + + it('Format CI phone #', () => { + expect(parsePhoneNumber(TEST_PHONE_NUMBERS.VALID_CI, COUNTRY_CODES.CI)).toMatchObject({ + e164Number: TEST_PHONE_NUMBERS.FORMATTED_CI, + displayNumber: TEST_PHONE_NUMBERS.DISPLAY_CI, + countryCode: 225, + regionCode: 'CI', + }) + }) + }) + + describe('Other phone helper methods', () => { + it('gets country code', () => { + expect(getCountryCode(TEST_PHONE_NUMBERS.VALID_US_3)).toBe(1) + expect(getCountryCode(TEST_PHONE_NUMBERS.VALID_DE_3)).toBe(49) + expect(getCountryCode(TEST_PHONE_NUMBERS.VALID_AR_3)).toBe(54) + expect(getCountryCode(TEST_PHONE_NUMBERS.VALID_CI)).toBe(225) + }) + + it('gets region code', () => { + expect(getRegionCode(TEST_PHONE_NUMBERS.VALID_US_3)).toBe('US') + expect(getRegionCode(TEST_PHONE_NUMBERS.VALID_DE_3)).toBe('DE') + expect(getRegionCode(TEST_PHONE_NUMBERS.VALID_AR_3)).toBe('AR') + expect(getRegionCode(TEST_PHONE_NUMBERS.VALID_CI)).toBe('CI') + }) + + it('gets region code from country code', () => { + expect(getRegionCodeFromCountryCode(COUNTRY_CODES.US)).toBe('US') + expect(getRegionCodeFromCountryCode(COUNTRY_CODES.DE)).toBe('DE') + expect(getRegionCodeFromCountryCode(COUNTRY_CODES.AR)).toBe('AR') + expect(getRegionCodeFromCountryCode(COUNTRY_CODES.CI)).toBe('CI') + }) + }) + + describe('Example phones', () => { + it('gets example by country showing zeros', () => { + expect(getExampleNumber(COUNTRY_CODES.AR)).toBe('000 0000-0000') + expect(getExampleNumber(COUNTRY_CODES.DE)).toBe('000 000000') + expect(getExampleNumber(COUNTRY_CODES.US)).toBe('(000) 000-0000') + expect(getExampleNumber(COUNTRY_CODES.CI)).toBe('00 00 0 00000') + }) + + it('gets example by country', () => { + expect(getExampleNumber(COUNTRY_CODES.AR, false)).toBe('011 2345-6789') + expect(getExampleNumber(COUNTRY_CODES.DE, false)).toBe('030 123456') + expect(getExampleNumber(COUNTRY_CODES.US, false)).toBe('(201) 555-0123') + expect(getExampleNumber(COUNTRY_CODES.CI, false)).toBe('21 23 4 56789') + }) + + it('gets example by country showing zeros in international way', () => { + expect(getExampleNumber(COUNTRY_CODES.AR, true, true)).toBe('+54 00 0000-0000') + expect(getExampleNumber(COUNTRY_CODES.DE, true, true)).toBe('+49 00 000000') + expect(getExampleNumber(COUNTRY_CODES.US, true, true)).toBe('+1 000-000-0000') + expect(getExampleNumber(COUNTRY_CODES.CI, true, true)).toBe('+225 00 00 0 00000') + }) + }) +}) diff --git a/src/utils/phoneNumbers.ts b/src/utils/phoneNumbers.ts new file mode 100644 index 00000000000..21da423aa5a --- /dev/null +++ b/src/utils/phoneNumbers.ts @@ -0,0 +1,236 @@ +/** + * Reference file: + * https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/phone-utils/src/phoneNumbers.ts + */ + +import { + PhoneNumberFormat, + PhoneNumberType, + PhoneNumberUtil, + type PhoneNumber, +} from 'google-libphonenumber' +import Logger from 'src/utils/Logger' + +const phoneUtil = PhoneNumberUtil.getInstance() + +interface ParsedPhoneNumber { + e164Number: string + displayNumber: string + displayNumberInternational: string + countryCode?: number + regionCode?: string +} + +const MIN_PHONE_LENGTH = 4 + +export function getCountryCode(e164PhoneNumber: string) { + if (!e164PhoneNumber) { + return null + } + try { + return phoneUtil.parse(e164PhoneNumber).getCountryCode() + } catch (error) { + Logger.debug(`getCountryCode, number: ${e164PhoneNumber}, error: ${error}`) + return null + } +} + +export function getRegionCode(e164PhoneNumber: string) { + if (!e164PhoneNumber) { + return null + } + try { + return phoneUtil.getRegionCodeForNumber(phoneUtil.parse(e164PhoneNumber)) + } catch (error) { + Logger.debug(`getRegionCodeForNumber, number: ${e164PhoneNumber}, error: ${error}`) + return null + } +} + +export function getRegionCodeFromCountryCode(countryCode: string) { + if (!countryCode) { + return null + } + try { + return phoneUtil.getRegionCodeForCountryCode(parseInt(countryCode, 10)) + } catch (error) { + Logger.debug(`getRegionCodeFromCountryCode, countrycode: ${countryCode}, error: ${error}`) + return null + } +} + +export function getDisplayPhoneNumber(phoneNumber: string, defaultCountryCode: string) { + const phoneDetails = parsePhoneNumber(phoneNumber, defaultCountryCode) + if (phoneDetails) { + return phoneDetails.displayNumber + } else { + // Fallback to input instead of showing nothing for invalid numbers + return phoneNumber + } +} + +export function getDisplayNumberInternational(e164PhoneNumber: string) { + const countryCode = getCountryCode(e164PhoneNumber) + const phoneDetails = parsePhoneNumber(e164PhoneNumber, (countryCode || '').toString()) + if (phoneDetails) { + return phoneDetails.displayNumberInternational + } else { + // Fallback to input instead of showing nothing for invalid numbers + return e164PhoneNumber + } +} + +export function isE164NumberStrict(phoneNumber: string) { + try { + const parsedPhoneNumber = phoneUtil.parse(phoneNumber) + if (!phoneUtil.isValidNumber(parsedPhoneNumber)) { + return false + } + return phoneUtil.format(parsedPhoneNumber, PhoneNumberFormat.E164) === phoneNumber + } catch { + return false + } +} + +export function parsePhoneNumber( + phoneNumberRaw: string, + defaultCountryCode?: string +): ParsedPhoneNumber | null { + try { + if (!phoneNumberRaw || phoneNumberRaw.length < MIN_PHONE_LENGTH) { + return null + } + + const defaultRegionCode = defaultCountryCode + ? getRegionCodeFromCountryCode(defaultCountryCode) + : null + const parsedNumberUnfixed = phoneUtil.parse(phoneNumberRaw, defaultRegionCode || undefined) + const parsedCountryCode = parsedNumberUnfixed.getCountryCode() + const parsedRegionCode = phoneUtil.getRegionCodeForNumber(parsedNumberUnfixed) + const parsedNumber = handleSpecialCasesForParsing( + parsedNumberUnfixed, + parsedCountryCode, + parsedRegionCode + ) + + if (!parsedNumber) { + return null + } + + const isValid = phoneUtil.isValidNumberForRegion(parsedNumber, parsedRegionCode) + + return isValid + ? { + e164Number: phoneUtil.format(parsedNumber, PhoneNumberFormat.E164), + displayNumber: handleSpecialCasesForDisplay(parsedNumber, parsedCountryCode), + displayNumberInternational: phoneUtil.format( + parsedNumber, + PhoneNumberFormat.INTERNATIONAL + ), + countryCode: parsedCountryCode, + regionCode: parsedRegionCode, + } + : null + } catch (error) { + Logger.debug(`phoneNumbers/parsePhoneNumber/Failed to parse phone number, error: ${error}`) + return null + } +} + +/** + * Some countries require a prefix before the area code depending on if the number is + * mobile vs landline and international vs national + */ + +function prependToFormMobilePhoneNumber( + parsedNumber: PhoneNumber, + regionCode: string, + prefix: string +) { + if (phoneUtil.getNumberType(parsedNumber) === PhoneNumberType.MOBILE) { + return parsedNumber + } + + let nationalNumber = phoneUtil.format(parsedNumber, PhoneNumberFormat.NATIONAL) + // Nationally formatted numbers sometimes contain leading 0 + if (nationalNumber.charAt(0) === '0') { + nationalNumber = nationalNumber.slice(1) + } + // If the number already starts with prefix, don't prepend it again + if (nationalNumber.startsWith(prefix)) { + return null + } + + const adjustedNumber = phoneUtil.parse(prefix + nationalNumber, regionCode) + return phoneUtil.getNumberType(adjustedNumber) === PhoneNumberType.MOBILE ? adjustedNumber : null +} + +function handleSpecialCasesForParsing( + parsedNumber: PhoneNumber, + countryCode?: number, + regionCode?: string +) { + if (!countryCode || !regionCode) { + return parsedNumber + } + + switch (countryCode) { + // Argentina + // https://github.com/googlei18n/libphonenumber/blob/master/FAQ.md#why-is-this-number-from-argentina-ar-or-mexico-mx-not-identified-as-the-right-number-type + // https://en.wikipedia.org/wiki/Telephone_numbers_in_Argentina + case 54: + return prependToFormMobilePhoneNumber(parsedNumber, regionCode, '9') + + default: + return parsedNumber + } +} + +// TODO(Rossy) Given the inconsistencies of numbers around the world, we should +// display e164 everywhere to ensure users knows exactly who their sending money to +function handleSpecialCasesForDisplay(parsedNumber: PhoneNumber, countryCode?: number) { + switch (countryCode) { + // Argentina + // The Google lib formatter incorretly adds '15' to the nationally formatted number for Argentina + // However '15' is only needed when calling a mobile from a landline + case 54: + return phoneUtil + .format(parsedNumber, PhoneNumberFormat.INTERNATIONAL) + .replace(/\+54(\s)?/, '') + + case 231: + const formatted = phoneUtil.format(parsedNumber, PhoneNumberFormat.NATIONAL) + return formatted && formatted[0] === '0' ? formatted.slice(1) : formatted + + default: + return phoneUtil.format(parsedNumber, PhoneNumberFormat.NATIONAL) + } +} + +export function getExampleNumber( + regionCode: string, + useOnlyZeroes = true, + isInternational = false +) { + const examplePhone = phoneUtil.getExampleNumber( + getRegionCodeFromCountryCode(regionCode) as string + ) + + if (!examplePhone) { + return + } + + const formatedExample = phoneUtil.format( + examplePhone, + isInternational ? PhoneNumberFormat.INTERNATIONAL : PhoneNumberFormat.NATIONAL + ) + + if (useOnlyZeroes) { + if (isInternational) { + return formatedExample.replace(/(^\+[0-9]{1,3} |[0-9])/g, (value, _, i) => (i ? '0' : value)) + } + return formatedExample.replace(/[0-9]/g, '0') + } + + return formatedExample +} diff --git a/src/verify/VerificationStartScreen.tsx b/src/verify/VerificationStartScreen.tsx index a3c2b70bccc..d38241cc2ac 100644 --- a/src/verify/VerificationStartScreen.tsx +++ b/src/verify/VerificationStartScreen.tsx @@ -1,4 +1,3 @@ -import { Countries } from '@celo/phone-utils' import { useHeaderHeight } from '@react-navigation/elements' import { NativeStackScreenProps } from '@react-navigation/native-stack' import React, { useEffect, useLayoutEffect, useMemo, useState } from 'react' @@ -14,8 +13,8 @@ import { e164NumberSelector, } from 'src/account/selectors' import { getPhoneNumberDetails } from 'src/account/utils' -import { PhoneVerificationEvents } from 'src/analytics/Events' import AppAnalytics from 'src/analytics/AppAnalytics' +import { PhoneVerificationEvents } from 'src/analytics/Events' import BackButton from 'src/components/BackButton' import Button, { BtnSizes, BtnTypes } from 'src/components/Button' import InfoBottomSheet from 'src/components/InfoBottomSheet' @@ -40,6 +39,7 @@ import { useDispatch, useSelector } from 'src/redux/hooks' import colors from 'src/styles/colors' import { typeScale } from 'src/styles/fonts' import { Spacing } from 'src/styles/styles' +import { Countries } from 'src/utils/Countries' import { walletAddressSelector } from 'src/web3/selectors' function VerificationStartScreen({ diff --git a/src/verify/hooks.ts b/src/verify/hooks.ts index 0949cb61a86..5793dab9f47 100644 --- a/src/verify/hooks.ts +++ b/src/verify/hooks.ts @@ -1,5 +1,4 @@ import { compressedPubKey } from '@celo/cryptographic-utils/lib/dataEncryptionKey' -import getPhoneHash from '@celo/phone-utils/lib/getPhoneHash' import { hexToBuffer } from '@celo/utils/lib/address' import { useEffect, useRef, useState } from 'react' import { useAsync, useAsyncCallback } from 'react-async-hook' @@ -7,8 +6,8 @@ import { Platform } from 'react-native' import DeviceInfo from 'react-native-device-info' import { e164NumberSelector } from 'src/account/selectors' import { showError } from 'src/alert/actions' -import { PhoneVerificationEvents } from 'src/analytics/Events' import AppAnalytics from 'src/analytics/AppAnalytics' +import { PhoneVerificationEvents } from 'src/analytics/Events' import { ErrorMessages } from 'src/app/ErrorMessages' import { phoneNumberRevoked, phoneNumberVerificationCompleted } from 'src/app/actions' import { inviterAddressSelector } from 'src/app/selectors' @@ -22,6 +21,7 @@ import { import { retrieveSignedMessage } from 'src/pincode/authentication' import { useDispatch, useSelector } from 'src/redux/hooks' import Logger from 'src/utils/Logger' +import getPhoneHash from 'src/utils/getPhoneHash' import networkConfig from 'src/web3/networkConfig' import { dataEncryptionKeySelector, walletAddressSelector } from 'src/web3/selectors' diff --git a/yarn.lock b/yarn.lock index 4d4fa7e3849..8404a63daab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1213,7 +1213,7 @@ io-ts "2.0.1" is-base64 "^1.1.0" -"@celo/phone-utils@^3.1.0", "@celo/phone-utils@^3.2.0": +"@celo/phone-utils@^3.1.0": version "3.2.0" resolved "https://registry.yarnpkg.com/@celo/phone-utils/-/phone-utils-3.2.0.tgz#e8e2716225eb1732f9e0eeae18812666445627e0" integrity sha512-TbEKN7emsQY3nac7Lv5XkPA7HWXjrhuRpbtVDsJ8uB3Pbu9zAYCs0J8QlikEUJLPGoRA3oCZLri54GhWFG84BQ== @@ -4822,6 +4822,11 @@ resolved "https://registry.yarnpkg.com/@types/country-data/-/country-data-0.0.0.tgz#6f5563cae3d148780c5b6539803a29bd93f8f1a1" integrity sha512-lIxCk6G7AwmUagQ4gIQGxUBnvAq664prFD9nSAz6dgd1XmBXBtZABV/op+QsJsIyaP1GZsf/iXhYKHX3azSRCw== +"@types/country-data@^0.0.5": + version "0.0.5" + resolved "https://registry.yarnpkg.com/@types/country-data/-/country-data-0.0.5.tgz#75d27ccb80f40901d0c537fd95a3a6e3db90a843" + integrity sha512-IHr8m91tilfp32svmvAIPs9zuuMKZcUk/HRsZPN7y4jafQdpXHLqiMJtlP1a+W4eE0AkUz1Y42/iMxxXOSNpTQ== + "@types/crypto-js@^4.1.1": version "4.1.1" resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.1.1.tgz#602859584cecc91894eb23a4892f38cfa927890d" @@ -7604,7 +7609,7 @@ cosmiconfig@^7.0.0: country-data@^0.0.31: version "0.0.31" resolved "https://registry.yarnpkg.com/country-data/-/country-data-0.0.31.tgz#80966b8e1d147fa6d6a589d32933f8793774956d" - integrity sha1-gJZrjh0Uf6bWpYnTKTP4eTd0lW0= + integrity sha512-YqlY/i6ikZwoBFfdjK+hJTGaBdTgDpXLI15MCj2UsXZ2cPBb+Kx86AXmDH7PRGt0LUleck0cCgNdWeIhfbcxkQ== dependencies: currency-symbol-map "~2" underscore ">1.4.4" @@ -10170,6 +10175,11 @@ google-libphonenumber@^3.2.15, google-libphonenumber@^3.2.27: resolved "https://registry.yarnpkg.com/google-libphonenumber/-/google-libphonenumber-3.2.32.tgz#63c48a9c247b64a3bc2eec21bdf3fcfbf2e148c0" integrity sha512-mcNgakausov/B/eTgVeX8qc8IwWjRrupk9UzZZ/QDEvdh5fAjE7Aa211bkZpZj42zKkeS6MTT8avHUwjcLxuGQ== +google-libphonenumber@^3.2.38: + version "3.2.38" + resolved "https://registry.yarnpkg.com/google-libphonenumber/-/google-libphonenumber-3.2.38.tgz#5adb0c8ff807d84d4d46b9536d0116d00d85f137" + integrity sha512-t/K0dsVmA0gMMVLJgcMeB9g1Ar4ANVWfkY+AJGSdfyJ2Ay7Bu8ceLYpUlC6FZSilZgaF1qbkM9tZydGBEBHqAg== + gopd@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -17041,7 +17051,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -17076,6 +17086,15 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -17147,7 +17166,14 @@ string_decoder@^1.0.0, string_decoder@^1.0.3, string_decoder@^1.1.1, string_deco dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -18861,6 +18887,13 @@ web3-core@1.10.4: web3-core-requestmanager "1.10.4" web3-utils "1.10.4" +web3-errors@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/web3-errors/-/web3-errors-1.3.0.tgz#504e4d3218899df108856940087a8022d6688d74" + integrity sha512-j5JkAKCtuVMbY3F5PYXBqg1vWrtF4jcyyMY1rlw8a4PV67AkqlepjGgpzWJZd56Mt+TvHy6DA1F/3Id8LatDSQ== + dependencies: + web3-types "^1.7.0" + web3-eth-abi@1.10.4: version "1.10.4" resolved "https://registry.yarnpkg.com/web3-eth-abi/-/web3-eth-abi-1.10.4.tgz#16c19d0bde0aaf8c1a56cb7743a83156d148d798" @@ -19006,6 +19039,11 @@ web3-shh@1.10.4: web3-core-subscriptions "1.10.4" web3-net "1.10.4" +web3-types@^1.6.0, web3-types@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/web3-types/-/web3-types-1.7.0.tgz#9945fa644af96b20b1db18564aff9ab8db00df59" + integrity sha512-nhXxDJ7a5FesRw9UG5SZdP/C/3Q2EzHGnB39hkAV+YGXDMgwxBXFWebQLfEzZzuArfHnvC0sQqkIHNwSKcVjdA== + web3-utils@1.10.4, web3-utils@^1.0.0-beta.31: version "1.10.4" resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-1.10.4.tgz#0daee7d6841641655d8b3726baf33b08eda1cbec" @@ -19034,6 +19072,28 @@ web3-utils@1.3.6: underscore "1.12.1" utf8 "3.0.0" +web3-utils@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-4.3.1.tgz#3dcd75e3c683c26f0ba824bf27d7bc0a68b111de" + integrity sha512-kGwOk8FxOLJ9DQC68yqNQc7AzN+k9YDLaW+ZjlAXs3qORhf8zXk5SxWAAGLbLykMs3vTeB0FTb1Exut4JEYfFA== + dependencies: + ethereum-cryptography "^2.0.0" + eventemitter3 "^5.0.1" + web3-errors "^1.2.0" + web3-types "^1.7.0" + web3-validator "^2.0.6" + +web3-validator@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/web3-validator/-/web3-validator-2.0.6.tgz#a0cdaa39e1d1708ece5fae155b034e29d6a19248" + integrity sha512-qn9id0/l1bWmvH4XfnG/JtGKKwut2Vokl6YXP5Kfg424npysmtRLe9DgiNBM9Op7QL/aSiaA0TVXibuIuWcizg== + dependencies: + ethereum-cryptography "^2.0.0" + util "^0.12.5" + web3-errors "^1.2.0" + web3-types "^1.6.0" + zod "^3.21.4" + web3@1.10.4, web3@1.3.6: version "1.10.4" resolved "https://registry.yarnpkg.com/web3/-/web3-1.10.4.tgz#5d5e59b976eaf758b060fe1a296da5fe87bdc79c" @@ -19264,7 +19324,7 @@ wordwrap@0.0.2: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" integrity sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8= -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -19299,6 +19359,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" @@ -19697,7 +19766,7 @@ zod@3.22.4: resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg== -zod@^3.23.8: +zod@^3.21.4, zod@^3.23.8: version "3.23.8" resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==