Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Phone Utils Package #8997

Merged
merged 6 commits into from
Dec 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions dependency-graph.json
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,15 @@
"@celo/flake-tracker"
]
},
"@celo/phone-utils": {
"location": "packages/sdk/phone-utils",
"dependencies": [
"@celo/base",
"@celo/flake-tracker",
"@celo/typescript",
"@celo/utils"
]
},
"@celo/transactions-uri": {
"location": "packages/sdk/transactions-uri",
"dependencies": [
Expand Down
3 changes: 3 additions & 0 deletions packages/sdk/phone-utils/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.js
!jest.config.js
lib
7 changes: 7 additions & 0 deletions packages/sdk/phone-utils/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/node_modules
/coverage
/tslint.json
/tsconfig.json
/test
/src
**.test.js
7 changes: 7 additions & 0 deletions packages/sdk/phone-utils/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const { nodeFlakeTracking } = require('@celo/flake-tracker/src/jest/config.js')

module.exports = {
preset: 'ts-jest',
testMatch: ['<rootDir>/src/**/?(*.)+(spec|test).ts?(x)'],
...nodeFlakeTracking,
}
38 changes: 38 additions & 0 deletions packages/sdk/phone-utils/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "@celo/phone-utils",
"version": "1.3.3-dev",
"description": "Celo phone utils",
"author": "Celo",
"license": "Apache-2.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"sideEffects": false,
"scripts": {
"prepublishOnly": "yarn build",
"build": "tsc -b .",
"clean": "tsc -b . --clean",
"docs": "typedoc && ts-node scripts/linkdocs.ts utils",
"test": "jest --runInBand --ci",
"test:verbose": "jest --verbose",
"lint": "tslint -c tslint.json --project ."
},
"files": [
"lib/**/*"
],
"dependencies": {
"@celo/base": "1.3.3-dev",
"@celo/utils": "1.3.3-dev",
"@types/country-data": "^0.0.0",
"@types/ethereumjs-util": "^5.2.0",
"@types/google-libphonenumber": "^7.4.17",
"@types/node": "^10.12.18",
"country-data": "^0.0.31",
"fp-ts": "2.1.1",
"io-ts": "2.0.1",
"google-libphonenumber": "^3.2.15"
},
"devDependencies": {
"@celo/flake-tracker": "0.0.1-dev",
"@celo/typescript": "0.0.1"
}
}
174 changes: 174 additions & 0 deletions packages/sdk/phone-utils/src/countries.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { Countries } from './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) {
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(`
Object {
"alpha2": "TW",
"alpha3": "TWN",
"countryCallingCode": "+886",
"countryCallingCodes": Array [
"+886",
],
"countryPhonePlaceholder": Object {
"national": "00 0000 0000",
},
"currencies": Array [
"TWD",
],
"displayName": "Taiwan",
"displayNameNoDiacritics": "taiwan",
"emoji": "🇹🇼",
"ioc": "TPE",
"languages": Array [
"zho",
],
"name": "Taiwan",
"names": Object {
"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(`
Array [
Object {
"alpha2": "TW",
"alpha3": "TWN",
"countryCallingCode": "+886",
"countryCallingCodes": Array [
"+886",
],
"countryPhonePlaceholder": Object {
"national": "00 0000 0000",
},
"currencies": Array [
"TWD",
],
"displayName": "Taiwan",
"displayNameNoDiacritics": "taiwan",
"emoji": "🇹🇼",
"ioc": "TPE",
"languages": Array [
"zho",
],
"name": "Taiwan",
"names": Object {
"en-us": "Taiwan",
"es-419": "Taiwán",
},
"status": "assigned",
},
]
`)
})

test('finds countries by calling code', () => {
const results = countries.getFilteredCountries('49')

expect(results).toMatchInlineSnapshot(`
Array [
Object {
"alpha2": "DE",
"alpha3": "DEU",
"countryCallingCode": "+49",
"countryCallingCodes": Array [
"+49",
],
"countryPhonePlaceholder": Object {
"national": "000 000000",
},
"currencies": Array [
"EUR",
],
"displayName": "Germany",
"displayNameNoDiacritics": "germany",
"emoji": "🇩🇪",
"ioc": "GER",
"languages": Array [
"deu",
],
"name": "Germany",
"names": Object {
"en-us": "Germany",
"es-419": "Alemania",
},
"status": "assigned",
},
]
`)
})

test('finds countries by ISO code', () => {
const results = countries.getFilteredCountries('gb')

expect(results).toMatchInlineSnapshot(`
Array [
Object {
"alpha2": "GB",
"alpha3": "GBR",
"countryCallingCode": "+44",
"countryCallingCodes": Array [
"+44",
],
"countryPhonePlaceholder": Object {
"national": "0000 000 0000",
},
"currencies": Array [
"GBP",
],
"displayName": "United Kingdom",
"displayNameNoDiacritics": "united kingdom",
"emoji": "🇬🇧",
"ioc": "GBR",
"languages": Array [
"eng",
"cor",
"gle",
"gla",
"cym",
],
"name": "United Kingdom",
"names": Object {
"en-us": "United Kingdom",
"es-419": "Reino Unido",
},
"status": "assigned",
},
]
`)
})
})
})
116 changes: 116 additions & 0 deletions packages/sdk/phone-utils/src/countries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import countryData from 'country-data'
// more countries @ https://github.com/umpirsky/country-list
import esData from './data/countries/es/country.json'
import { getExampleNumber } from './phoneNumbers'

interface CountryNames {
[name: string]: string
}

export interface LocalizedCountry extends Omit<countryData.Country, 'countryCallingCodes'> {
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<string, LocalizedCountry>
localizedCountries: LocalizedCountry[]

constructor(language?: string) {
// fallback to 'en-us'
this.language = language ? language.toLocaleLowerCase() : 'en-us'
this.countryMap = new Map()
this.localizedCountries = Array()
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))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"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"}
23 changes: 23 additions & 0 deletions packages/sdk/phone-utils/src/getCountryEmoji.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import CountryData from 'country-data'
import { getCountryCode, getRegionCode } from './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 : ''
}
Loading