forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {t} from '@lingui/core/macro'
2import {
3 isSupportedCountry,
4 ParseError,
5 parsePhoneNumber,
6 parsePhoneNumberWithError,
7 type PhoneNumber,
8} from 'libphonenumber-js/max'
9
10import {type CountryCode} from '#/lib/international-telephone-codes'
11
12/**
13 * Intended for after the user has finished inputting their phone number.
14 */
15export function processPhoneNumber(
16 number: string,
17 country: CountryCode,
18):
19 | {
20 valid: true
21 formatted: string
22 countryCode: CountryCode
23 }
24 | {
25 valid: false
26 reason?: string
27 } {
28 try {
29 const phoneNumber = parsePhoneNumberWithError(number, {
30 defaultCountry: country,
31 })
32 if (!phoneNumber.isValid()) {
33 return {valid: false, reason: t`Invalid phone number`}
34 }
35 const type = phoneNumber.getType()
36 if (
37 type !== 'MOBILE' &&
38 type !== 'FIXED_LINE_OR_MOBILE' &&
39 type !== 'PERSONAL_NUMBER'
40 ) {
41 return {
42 valid: false,
43 reason: t`Number should be a mobile number`,
44 }
45 }
46 let countryCode = country
47 if (phoneNumber.country && phoneNumber.country !== country) {
48 if (phoneNumber.country === 'AC' || phoneNumber.country === 'TA') {
49 countryCode = 'SH'
50 } else {
51 countryCode = phoneNumber.country
52 }
53 }
54 return {
55 valid: true,
56 formatted: formatE164lWithoutCountryCode(phoneNumber),
57 countryCode,
58 }
59 } catch (error) {
60 if (error instanceof ParseError) {
61 return {valid: false, reason: error.message}
62 } else {
63 return {valid: false}
64 }
65 }
66}
67
68/**
69 * Format a phone number as the international format with the prefix
70 * removed.
71 */
72function formatE164lWithoutCountryCode(phoneNumber: PhoneNumber) {
73 const intl = phoneNumber.format('E.164')
74 const prefix = '+' + phoneNumber.countryCallingCode
75 return intl.replace(prefix, '').trim()
76}
77
78/**
79 * Takes a country code and a prefix-less phone number and constructs a full phone number.
80 *
81 * Does not have nice error handling - if you're unsure if the number is valid, use
82 * `processPhoneNumber` instead
83 */
84export function constructFullPhoneNumber(
85 countryCode: CountryCode,
86 phoneNumber: string,
87) {
88 const result = parsePhoneNumber(phoneNumber, {defaultCountry: countryCode})
89 if (!result.isValid())
90 throw new Error('Invalid phone number passed to constructFullPhoneNumber')
91 return result.format('E.164')
92}
93
94/**
95 * Takes a phone number and applies human-readable formatting. Do not sent to the API - they
96 * expect E.164 format.
97 */
98export function prettyPhoneNumber(phoneNumber: string) {
99 const result = parsePhoneNumber(phoneNumber)
100 return result.formatNational()
101}
102
103/**
104 * Attempts to parse a phone number from a string, and returns the country code
105 * and the rest of the number if possible. If the number is invalid, returns undefined.
106 */
107export function getCountryCodeFromPastedNumber(
108 text: string,
109): {countryCode: CountryCode; rest: string} | undefined {
110 try {
111 const phoneNumber = parsePhoneNumber(text)
112 if (!phoneNumber.isValid()) {
113 return undefined
114 }
115 const countryCode = phoneNumber.country
116 // we don't have AC and TA in our dropdown - see `#/lib/international-telephone-codes`
117 if (countryCode && countryCode !== 'AC' && countryCode !== 'TA') {
118 return {
119 countryCode,
120 rest: formatE164lWithoutCountryCode(phoneNumber),
121 }
122 } else {
123 return undefined
124 }
125 } catch (error) {
126 return undefined
127 }
128}
129
130/**
131 * Normalizes a phone number into E.164 format
132 */
133export function normalizePhoneNumber(
134 rawNumber: string,
135 countryCode: string | undefined,
136 fallbackCountryCode: CountryCode,
137): string | null {
138 try {
139 const result = parsePhoneNumber(rawNumber, {
140 defaultCountry:
141 countryCode && isSupportedCountry(countryCode)
142 ? countryCode
143 : fallbackCountryCode,
144 })
145
146 if (!result.isValid()) return null
147
148 const type = result.getType()
149 if (
150 type !== 'MOBILE' &&
151 type !== 'FIXED_LINE_OR_MOBILE' &&
152 type !== 'PERSONAL_NUMBER'
153 ) {
154 return null
155 }
156
157 return result.format('E.164')
158 } catch (error) {
159 console.log('Failed to normalize phone number:', error)
160 return null
161 }
162}