Skip to content

Commit

Permalink
馃摓 phone number formatter package (#2750)
Browse files Browse the repository at this point in the history
  • Loading branch information
eylamf committed Apr 4, 2024
1 parent 22ee719 commit 9b8285e
Show file tree
Hide file tree
Showing 15 changed files with 748 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/sour-elephants-try.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/phone': major
---

Phone number formatter initial release
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ These libraries compose together to help you create performant modern JS apps th

## Usage

The Quilt repo is managed as a monorepo that is composed of 70 npm packages and one Ruby gem.
The Quilt repo is managed as a monorepo that is composed of 71 npm packages and one Ruby gem.
Each package/gem has its own `README.md` and documentation describing usage.

### Package Index
Expand Down Expand Up @@ -52,6 +52,7 @@ Each package/gem has its own `README.md` and documentation describing usage.
| [@shopify/name](packages/name) | <a href="https://badge.fury.io/js/%40shopify%2Fname"><img src="https://badge.fury.io/js/%40shopify%2Fname.svg" width="200px" /></a> | Name-related utilities |
| [@shopify/network](packages/network) | <a href="https://badge.fury.io/js/%40shopify%2Fnetwork"><img src="https://badge.fury.io/js/%40shopify%2Fnetwork.svg" width="200px" /></a> | Common values related to dealing with the network |
| [@shopify/performance](packages/performance) | <a href="https://badge.fury.io/js/%40shopify%2Fperformance"><img src="https://badge.fury.io/js/%40shopify%2Fperformance.svg" width="200px" /></a> | Primitives for collecting browser performance metrics |
| [@shopify/phone](packages/phone) | <a href="https://badge.fury.io/js/%40shopify%2Fphone"><img src="https://badge.fury.io/js/%40shopify%2Fphone.svg" width="200px" /></a> | Phone number utilities for formatting phone numbers |
| [@shopify/polyfills](packages/polyfills) | <a href="https://badge.fury.io/js/%40shopify%2Fpolyfills"><img src="https://badge.fury.io/js/%40shopify%2Fpolyfills.svg" width="200px" /></a> | Blessed polyfills for web platform features |
| [@shopify/predicates](packages/predicates) | <a href="https://badge.fury.io/js/%40shopify%2Fpredicates"><img src="https://badge.fury.io/js/%40shopify%2Fpredicates.svg" width="200px" /></a> | A set of common JavaScript predicates |
| [@shopify/react-app-bridge-universal-provider](packages/react-app-bridge-universal-provider) | <a href="https://badge.fury.io/js/%40shopify%2Freact-app-bridge-universal-provider"><img src="https://badge.fury.io/js/%40shopify%2Freact-app-bridge-universal-provider.svg" width="200px" /></a> | A self-serializing/deserializing `app-bridge-react` provider that works for isomorphic applications |
Expand Down
5 changes: 5 additions & 0 deletions packages/phone/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Changelog

## 1.0.0

- `@shopify/phone` package
57 changes: 57 additions & 0 deletions packages/phone/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# `@shopify/phone`

[![Build Status](https://github.com/Shopify/quilt/workflows/Node-CI/badge.svg?branch=main)](https://github.com/Shopify/quilt/actions?query=workflow%3ANode-CI)
[![Build Status](https://github.com/Shopify/quilt/workflows/Ruby-CI/badge.svg?branch=main)](https://github.com/Shopify/quilt/actions?query=workflow%3ARuby-CI)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE.md) [![npm version](https://badge.fury.io/js/%40shopify%2Fphone.svg)](https://badge.fury.io/js/%40shopify%2Fphone.svg) [![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/@shopify/phone.svg)](https://img.shields.io/bundlephobia/minzip/@shopify/phone.svg)

Phone number utilities for formatting phone numbers.

## Installation

```bash
yarn add @shopify/phone
```

## PhoneNumberFormatter methods

```ts
import PhoneNumberFormatter from '@shopify/phone';

// Pass a region code to the constructor
const phoneFormatter = new PhoneNumberFormatter('US');
const formatted = phoneFormatter.format(myPhoneNumber);
```

#### `format(phoneNumber: string): string`

Formats the given phone number

#### `update(regionCode: string): void`

Update formatter regionCode which will format number based on that (eg: 'CA' | 'JP' etc.)

#### `getNormalizedNumber(phoneNumber: string): string`

Returns phone number in E164 format

#### `getNationalNumber(phoneNumber: string): string`

Returns the non-formatted version without the country code

#### `requiresItalianLeadingZero(phoneNumber: string)`

Indicates if the leading zero of a national number should be retained when dialling internationally

#### `updateCountryCode(phoneNumber: string): void`

Updates the country code of the formatter based on the phoneNumber passed

## Exported functions

### `getRegionCodeFromNumber(phoneNumber: string): string`

Returns the region code from the provided phone number

### `getCountryCodeFromNumber(phoneNumber: string): number | undefined`

Returns the country code from the provided phone number
51 changes: 51 additions & 0 deletions packages/phone/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"name": "@shopify/phone",
"version": "0.0.0",
"license": "MIT",
"description": "Phone number utilities for formatting phone numbers",
"main": "index.js",
"types": "./build/ts/index.d.ts",
"scripts": {},
"sideEffects": false,
"publishConfig": {
"access": "public",
"@shopify:registry": "https://registry.npmjs.org"
},
"author": "Shopify Inc.",
"repository": {
"type": "git",
"url": "git+https://github.com/Shopify/quilt.git",
"directory": "packages/phone"
},
"bugs": {
"url": "https://github.com/Shopify/quilt/issues"
},
"homepage": "https://github.com/Shopify/quilt/blob/main/packages/phone/README.md",
"engines": {
"node": "^14.17.0 || >=16.0.0"
},
"dependencies": {
"google-libphonenumber": "^3.2.17"
},
"devDependencies": {
"@types/google-libphonenumber": "^7.4.16"
},
"files": [
"build/",
"!build/*.tsbuildinfo",
"!build/ts/**/tests/",
"index.js",
"index.mjs",
"index.esnext"
],
"module": "index.mjs",
"esnext": "index.esnext",
"exports": {
".": {
"types": "./build/ts/index.d.ts",
"esnext": "./index.esnext",
"import": "./index.mjs",
"require": "./index.js"
}
}
}
6 changes: 6 additions & 0 deletions packages/phone/rollup.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {buildConfig} from '../../config/rollup.mjs';

export default buildConfig(import.meta.url, {
entries: ['./src/index.ts'],
entrypoints: {index: './src/index.ts'},
});
173 changes: 173 additions & 0 deletions packages/phone/src/PhoneNumberFormatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import {AsYouTypeFormatter, PhoneNumberFormat} from 'google-libphonenumber';

import {
digitOnlyNumber,
FORMATTING_CHARACTERS_REGEX,
getRegionCodeForNumber,
phoneUtil,
} from './utilities';

const US_COUNTRY_CODE = '1';
const RUSSIA_COUNTRY_CODE = '7';

export default class PhoneNumberFormatter {
formatter: AsYouTypeFormatter;
regionCode: string;
countryCode: number;

constructor(regionCode: string) {
this.regionCode = regionCode;
this.countryCode = getCountryCodeFromRegionCode(regionCode);
this.formatter = new AsYouTypeFormatter(this.regionCode);
}

format(phoneNumber: string): string {
this.updateCountryCode(phoneNumber);
this.formatter.clear();
return formatNumber(phoneNumber, this.formatter) || '';
}

/*
* Update formatter regionCode which will format number based on that
* region code
* @regionCode: eg: 'CA' | 'JP' etc.
*/
update(regionCode: string): void {
this.countryCode = getCountryCodeFromRegionCode(regionCode);
this.regionCode = regionCode;
this.formatter = new AsYouTypeFormatter(this.regionCode);
}

// This returns phone number in E164 format.
getNormalizedNumber(phoneNumber: string): string {
try {
const parsedPhoneNumber = phoneUtil().parseAndKeepRawInput(
phoneNumber,
this.regionCode,
);
return phoneUtil().format(parsedPhoneNumber, PhoneNumberFormat.E164);
} catch (_err) {
return phoneNumber;
}
}

getNationalNumber(phoneNumber: string): string {
const onlyDigitsNumber = digitOnlyNumber(phoneNumber);
if (!this.regionCode || onlyDigitsNumber.length < 4) {
// phoneUtil() cannot find the national number when the phone number is too short
// so we try to guess based on single digit country codes (1 - US, 7 - RU)
if (
onlyDigitsNumber.startsWith(US_COUNTRY_CODE) ||
onlyDigitsNumber.startsWith(RUSSIA_COUNTRY_CODE)
) {
return phoneNumber.slice(3);
}
return '';
}
try {
const nationalNumber = phoneUtil()
.parseAndKeepRawInput(phoneNumber, this.regionCode)
.getNationalNumber();

return nationalNumber ? nationalNumber.toString() : '';
} catch (_err) {
return '';
}
}

// Indicates if the leading zero of a national number should be retained when dialling internationally
requiresItalianLeadingZero(phoneNumber: string) {
try {
return Boolean(
phoneUtil()
.parseAndKeepRawInput(phoneNumber, this.regionCode)
.getItalianLeadingZero(),
);
} catch {
return undefined;
}
}

/*
* Updates the country code of the formatter based on the phoneNumber passed
*/
private updateCountryCode(phoneNumber: string): void {
if (!phoneNumber.includes('+') || digitOnlyNumber(phoneNumber).length < 1) {
return;
}

let newRegionCode;

/* If country code is 1, the region code can be CA or US, so we need to
* guess base on the phone number.
* This can only be done when phone number is long enough
*/
if (digitOnlyNumber(phoneNumber).length > 4) {
newRegionCode = getRegionCodeForNumber(phoneNumber);
} else {
const newCountryCode = getCountryCodeFromNumber(phoneNumber);
if (newCountryCode && this.countryCode !== newCountryCode) {
newRegionCode = getRegionCodeForCountryCode(newCountryCode);
}
}

if (newRegionCode) {
this.update(newRegionCode);
}
}
}

function getCountryCodeFromRegionCode(regionCode: string): number {
return phoneUtil().getCountryCodeForRegion(regionCode);
}

export function getRegionCodeFromNumber(phoneNumber: string): string {
const countryCodeFromNumber = getCountryCodeFromNumber(phoneNumber);

return (
getRegionCodeForNumber(phoneNumber) ||
(countryCodeFromNumber &&
getRegionCodeForCountryCode(countryCodeFromNumber)) ||
getRegionCodeForCountryCode(1)
);
}

export function getCountryCodeFromNumber(
phoneNumber: string,
): number | undefined {
/*
* we map (1 - US, 7 - RU) when the phone number is too short (this handles the key by key input)
*/
const onlyDigitsNumber = digitOnlyNumber(phoneNumber);
if (onlyDigitsNumber.length < 4) {
if (onlyDigitsNumber.startsWith(US_COUNTRY_CODE)) {
return Number(US_COUNTRY_CODE);
}
if (onlyDigitsNumber.startsWith(RUSSIA_COUNTRY_CODE)) {
return Number(RUSSIA_COUNTRY_CODE);
}
}

try {
return phoneUtil().parse(phoneNumber).getCountryCode();
} catch (_err) {
return parseInt(phoneNumber.split(' ')[0].replace('+', ''), 10);
}
}

function getRegionCodeForCountryCode(countryCode: number): string {
return phoneUtil().getRegionCodeForCountryCode(countryCode);
}

function formatNumber(
phoneNumber: string,
formatter: AsYouTypeFormatter,
): string | undefined {
let newValue;

for (const char of phoneNumber.replace(FORMATTING_CHARACTERS_REGEX, '')) {
newValue = formatter.inputDigit(char);
}

return newValue;
}
2 changes: 2 additions & 0 deletions packages/phone/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export {default, getRegionCodeFromNumber} from './PhoneNumberFormatter';
export * from './utilities';

0 comments on commit 9b8285e

Please sign in to comment.