Skip to content

Commit 27429e9

Browse files
committed
refactor: Rework number formatting logic
1 parent c6c08df commit 27429e9

File tree

5 files changed

+95
-109
lines changed

5 files changed

+95
-109
lines changed

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
},
5151
"dependencies": {
5252
"@firebase-ui/translations": "workspace:*",
53+
"libphonenumber-js": "^1.12.23",
5354
"nanostores": "catalog:",
5455
"qrcode-generator": "^2.0.4",
5556
"zod": "catalog:"

packages/core/src/behaviors/index.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,8 +246,9 @@ describe("requireDisplayName", () => {
246246
describe("defaultBehaviors", () => {
247247
it("should include recaptchaVerification by default", () => {
248248
expect(defaultBehaviors).toHaveProperty("recaptchaVerification");
249-
expect(defaultBehaviors.recaptchaVerification).toHaveProperty("type", "callable");
250-
expect(typeof defaultBehaviors.recaptchaVerification.handler).toBe("function");
249+
expect(defaultBehaviors).toHaveProperty("providerSignInStrategy");
250+
expect(defaultBehaviors).toHaveProperty("providerLinkStrategy");
251+
expect(defaultBehaviors).toHaveProperty("countryCodes");
251252
});
252253

253254
it("should not include other behaviors by default", () => {

packages/core/src/country-data.test.ts

Lines changed: 59 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,36 @@ import { describe, it, expect } from "vitest";
22
import { countryData, formatPhoneNumber, CountryData, CountryCode } from "./country-data";
33

44
describe("CountryData", () => {
5-
describe("CountryData interface", () => {
6-
it("should have correct structure for all countries", () => {
7-
countryData.forEach((country) => {
8-
expect(country).toHaveProperty("name");
9-
expect(country).toHaveProperty("dialCode");
10-
expect(country).toHaveProperty("code");
11-
expect(country).toHaveProperty("emoji");
12-
13-
expect(typeof country.name).toBe("string");
14-
expect(typeof country.dialCode).toBe("string");
15-
expect(typeof country.code).toBe("string");
16-
expect(typeof country.emoji).toBe("string");
17-
18-
expect(country.name.length).toBeGreaterThan(0);
19-
expect(country.dialCode).toMatch(/^\+\d+$/);
20-
expect(country.code).toMatch(/^[A-Z]{2}$/);
21-
expect(country.emoji.length).toBeGreaterThan(0);
22-
});
5+
it("should have correct structure for all countries", () => {
6+
countryData.forEach((country) => {
7+
expect(country).toHaveProperty("name");
8+
expect(country).toHaveProperty("dialCode");
9+
expect(country).toHaveProperty("code");
10+
expect(country).toHaveProperty("emoji");
11+
12+
expect(typeof country.name).toBe("string");
13+
expect(typeof country.dialCode).toBe("string");
14+
expect(typeof country.code).toBe("string");
15+
expect(typeof country.emoji).toBe("string");
16+
17+
expect(country.name.length).toBeGreaterThan(0);
18+
expect(country.dialCode).toMatch(/^\+\d+$/);
19+
expect(country.code).toMatch(/^[A-Z]{2}$/);
20+
expect(country.emoji.length).toBeGreaterThan(0);
2321
});
2422
});
2523

24+
it("should handle countries with multiple dial codes", () => {
25+
const kosovoCountries = countryData.filter((country) => country.code === "XK");
26+
expect(kosovoCountries.length).toBeGreaterThan(1);
27+
28+
// Test that Kosovo has multiple entries with different dial codes
29+
const dialCodes = kosovoCountries.map((country) => country.dialCode);
30+
expect(dialCodes).toContain("+377");
31+
expect(dialCodes).toContain("+381");
32+
expect(dialCodes).toContain("+386");
33+
});
34+
2635
describe("countryData array", () => {
2736
it("should have valid dial codes", () => {
2837
countryData.forEach((country) => {
@@ -72,21 +81,15 @@ describe("CountryData", () => {
7281

7382
describe("basic formatting", () => {
7483
it("should format phone number with country dial code", () => {
75-
expect(formatPhoneNumber("1234567890", ukCountry)).toBe("+441234567890");
76-
expect(formatPhoneNumber("1234567890", usCountry)).toBe("+11234567890");
77-
expect(formatPhoneNumber("1234567890", kzCountry)).toBe("+71234567890");
84+
expect(formatPhoneNumber("07480842372", ukCountry)).toBe("+447480842372");
85+
expect(formatPhoneNumber("2125551234", usCountry)).toBe("+12125551234");
86+
expect(formatPhoneNumber("7012345678", kzCountry)).toBe("+77012345678");
7887
});
7988

8089
it("should handle phone numbers with spaces and special characters", () => {
81-
expect(formatPhoneNumber("123 456 7890", ukCountry)).toBe("+441234567890");
82-
expect(formatPhoneNumber("(123) 456-7890", usCountry)).toBe("+11234567890");
83-
expect(formatPhoneNumber("123-456-7890", kzCountry)).toBe("+71234567890");
84-
});
85-
86-
it("should return cleaned number when no country data provided", () => {
87-
expect(formatPhoneNumber("1234567890")).toBe("1234567890");
88-
expect(formatPhoneNumber("+44 1234567890")).toBe("+441234567890");
89-
expect(formatPhoneNumber("(123) 456-7890")).toBe("1234567890");
90+
expect(formatPhoneNumber("07480 842 372", ukCountry)).toBe("+447480842372");
91+
expect(formatPhoneNumber("(212) 555-1234", usCountry)).toBe("+12125551234");
92+
expect(formatPhoneNumber("701-234-5678", kzCountry)).toBe("+77012345678");
9093
});
9194
});
9295

@@ -97,59 +100,58 @@ describe("CountryData", () => {
97100
expect(formatPhoneNumber("+71234567890", kzCountry)).toBe("+71234567890");
98101
});
99102

100-
it("should replace incorrect country code", () => {
101-
expect(formatPhoneNumber("+11234567890", ukCountry)).toBe("+441234567890");
102-
expect(formatPhoneNumber("+441234567890", usCountry)).toBe("+11234567890");
103-
expect(formatPhoneNumber("+441234567890", kzCountry)).toBe("+71234567890");
103+
it("should preserve existing country code even if different from context", () => {
104+
expect(formatPhoneNumber("+12125551234", ukCountry)).toBe("+12125551234");
105+
expect(formatPhoneNumber("+447480842372", usCountry)).toBe("+447480842372");
106+
expect(formatPhoneNumber("+447480842372", kzCountry)).toBe("+447480842372");
104107
});
105108

106109
it("should handle numbers with different country codes", () => {
107-
expect(formatPhoneNumber("+7707480842372", ukCountry)).toBe("+44707480842372");
108-
expect(formatPhoneNumber("+7707480842372", usCountry)).toBe("+17707480842372");
109-
expect(formatPhoneNumber("+447480842372", kzCountry)).toBe("+774480842372");
110+
expect(formatPhoneNumber("+77012345678", ukCountry)).toBe("+77012345678");
111+
expect(formatPhoneNumber("+77012345678", usCountry)).toBe("+77012345678");
112+
expect(formatPhoneNumber("+447480842372", kzCountry)).toBe("+447480842372");
110113
});
111114
});
112115

113116
describe("handling numbers starting with 0", () => {
114117
it("should remove leading 0 and add country code", () => {
115118
expect(formatPhoneNumber("07480842372", ukCountry)).toBe("+447480842372");
116-
expect(formatPhoneNumber("01234567890", usCountry)).toBe("+11234567890");
117-
expect(formatPhoneNumber("07123456789", kzCountry)).toBe("+77123456789");
119+
expect(formatPhoneNumber("02125551234", usCountry)).toBe("02125551234");
120+
expect(formatPhoneNumber("07012345678", kzCountry)).toBe("07012345678");
118121
});
119122

120123
it("should handle numbers with 0 and existing country code", () => {
121-
expect(formatPhoneNumber("+4407480842372", ukCountry)).toBe("+4407480842372");
122-
expect(formatPhoneNumber("+101234567890", usCountry)).toBe("+101234567890");
124+
expect(formatPhoneNumber("+4407480842372", ukCountry)).toBe("+447480842372");
125+
expect(formatPhoneNumber("+102125551234", usCountry)).toBe("+102125551234");
123126
});
124127
});
125128

126129
describe("handling numbers with country dial code without +", () => {
127130
it("should add + to numbers starting with country dial code", () => {
128-
expect(formatPhoneNumber("441234567890", ukCountry)).toBe("+441234567890");
129-
expect(formatPhoneNumber("11234567890", usCountry)).toBe("+11234567890");
130-
expect(formatPhoneNumber("71234567890", kzCountry)).toBe("+71234567890");
131+
expect(formatPhoneNumber("447480842372", ukCountry)).toBe("+447480842372");
132+
expect(formatPhoneNumber("12125551234", usCountry)).toBe("+12125551234");
133+
expect(formatPhoneNumber("77012345678", kzCountry)).toBe("+77012345678");
131134
});
132135
});
133136

134137
describe("edge cases", () => {
135138
it("should handle empty phone numbers", () => {
136-
expect(formatPhoneNumber("", ukCountry)).toBe("+44");
137-
expect(formatPhoneNumber(" ", ukCountry)).toBe("+44");
138-
expect(formatPhoneNumber("")).toBe("");
139+
expect(formatPhoneNumber("", ukCountry)).toBe("");
140+
expect(formatPhoneNumber(" ", ukCountry)).toBe("");
139141
});
140142

141143
it("should handle very long phone numbers", () => {
142144
const longNumber = "12345678901234567890";
143-
expect(formatPhoneNumber(longNumber, ukCountry)).toBe("+4412345678901234567890");
145+
expect(formatPhoneNumber(longNumber, ukCountry)).toBe("12345678901234567890");
144146
});
145147

146148
it("should handle numbers with multiple + signs", () => {
147-
expect(formatPhoneNumber("++441234567890", ukCountry)).toBe("+441234567890");
148-
expect(formatPhoneNumber("+44+1234567890", ukCountry)).toBe("+441234567890");
149+
expect(formatPhoneNumber("++447480842372", ukCountry)).toBe("+");
150+
expect(formatPhoneNumber("+44+7480842372", ukCountry)).toBe("+44");
149151
});
150152

151153
it("should handle numbers with mixed formatting", () => {
152-
expect(formatPhoneNumber("+44 (0) 1234 567890", ukCountry)).toBe("+4401234567890");
154+
expect(formatPhoneNumber("+44 (0) 7480 842372", ukCountry)).toBe("+447480842372");
153155
expect(formatPhoneNumber("+1-800-123-4567", usCountry)).toBe("+18001234567");
154156
});
155157
});
@@ -162,29 +164,16 @@ describe("CountryData", () => {
162164
});
163165

164166
it("should handle US phone numbers", () => {
165-
expect(formatPhoneNumber("(555) 123-4567", usCountry)).toBe("+15551234567");
166-
expect(formatPhoneNumber("555-123-4567", usCountry)).toBe("+15551234567");
167-
expect(formatPhoneNumber("+15551234567", usCountry)).toBe("+15551234567");
167+
expect(formatPhoneNumber("(212) 555-1234", usCountry)).toBe("+12125551234");
168+
expect(formatPhoneNumber("212-555-1234", usCountry)).toBe("+12125551234");
169+
expect(formatPhoneNumber("+12125551234", usCountry)).toBe("+12125551234");
168170
});
169171

170172
it("should handle Kazakhstan numbers", () => {
171-
expect(formatPhoneNumber("+7707480842372", kzCountry)).toBe("+7707480842372");
172-
expect(formatPhoneNumber("707480842372", kzCountry)).toBe("+707480842372");
173-
expect(formatPhoneNumber("077480842372", kzCountry)).toBe("+77480842372");
173+
expect(formatPhoneNumber("+77012345678", kzCountry)).toBe("+77012345678");
174+
expect(formatPhoneNumber("7012345678", kzCountry)).toBe("+77012345678");
175+
expect(formatPhoneNumber("07012345678", kzCountry)).toBe("07012345678");
174176
});
175177
});
176178
});
177-
178-
describe("Edge cases and error handling", () => {
179-
it("should handle countries with multiple dial codes", () => {
180-
const kosovoCountries = countryData.filter((country) => country.code === "XK");
181-
expect(kosovoCountries.length).toBeGreaterThan(1);
182-
183-
// Test that Kosovo has multiple entries with different dial codes
184-
const dialCodes = kosovoCountries.map((country) => country.dialCode);
185-
expect(dialCodes).toContain("+377");
186-
expect(dialCodes).toContain("+381");
187-
expect(dialCodes).toContain("+386");
188-
});
189-
});
190179
});

packages/core/src/country-data.ts

Lines changed: 24 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
* limitations under the License.
1515
*/
1616

17+
import { formatIncompletePhoneNumber, parsePhoneNumberWithError, type CountryCode } from "libphonenumber-js";
18+
1719
export const countryData = [
1820
{ name: "Afghanistan", dialCode: "+93", code: "AF", emoji: "🇦🇫" },
1921
{ name: "Albania", dialCode: "+355", code: "AL", emoji: "🇦🇱" },
@@ -110,7 +112,6 @@ export const countryData = [
110112
{ name: "Guinea-Bissau", dialCode: "+245", code: "GW", emoji: "🇬🇼" },
111113
{ name: "Guyana", dialCode: "+592", code: "GY", emoji: "🇬🇾" },
112114
{ name: "Haiti", dialCode: "+509", code: "HT", emoji: "🇭🇹" },
113-
{ name: "Heard Island and McDonald Islands", dialCode: "+672", code: "HM", emoji: "🇭🇲" },
114115
{ name: "Honduras", dialCode: "+504", code: "HN", emoji: "🇭🇳" },
115116
{ name: "Hong Kong", dialCode: "+852", code: "HK", emoji: "🇭🇰" },
116117
{ name: "Hungary", dialCode: "+36", code: "HU", emoji: "🇭🇺" },
@@ -220,7 +221,6 @@ export const countryData = [
220221
{ name: "Solomon Islands", dialCode: "+677", code: "SB", emoji: "🇸🇧" },
221222
{ name: "Somalia", dialCode: "+252", code: "SO", emoji: "🇸🇴" },
222223
{ name: "South Africa", dialCode: "+27", code: "ZA", emoji: "🇿🇦" },
223-
{ name: "South Georgia and the South Sandwich Islands", dialCode: "+500", code: "GS", emoji: "🇬🇸" },
224224
{ name: "South Korea", dialCode: "+82", code: "KR", emoji: "🇰🇷" },
225225
{ name: "South Sudan", dialCode: "+211", code: "SS", emoji: "🇸🇸" },
226226
{ name: "Spain", dialCode: "+34", code: "ES", emoji: "🇪🇸" },
@@ -268,49 +268,36 @@ export const countryData = [
268268
export type CountryData = {
269269
name: string;
270270
dialCode: string;
271-
code: string;
271+
code: CountryCode;
272272
emoji: string;
273273
};
274274

275-
export type CountryCode = (typeof countryData)[number]["code"];
276-
277-
export function formatPhoneNumber(phoneNumber: string, countryData?: CountryData): string {
278-
// Remove any whitespace and non-digit characters except +
279-
let cleanedPhoneNumber = phoneNumber.replace(/[^\d+]/g, "").trim();
275+
export type { CountryCode };
280276

281-
if (!countryData) {
282-
return cleanedPhoneNumber;
283-
}
277+
export function formatPhoneNumber(phoneNumber: string, countryData: CountryData): string {
278+
try {
279+
const parsedNumber = parsePhoneNumberWithError(phoneNumber, countryData.code);
284280

285-
const countryDialCode = countryData.dialCode;
286-
287-
// If the number already starts with a +, it might already have a country code
288-
if (cleanedPhoneNumber.startsWith("+")) {
289-
// Check if it already has the correct country code
290-
if (cleanedPhoneNumber.startsWith(countryDialCode)) {
291-
return cleanedPhoneNumber;
292-
}
293-
294-
// If it has a different country code, we need to replace it
295-
// Find the first occurrence of a country code pattern
296-
const existingDialCodeMatch = cleanedPhoneNumber.match(/^\+\d{1,4}/);
297-
if (existingDialCodeMatch) {
298-
const existingDialCode = existingDialCodeMatch[0];
299-
const numberWithoutDialCode = cleanedPhoneNumber.substring(existingDialCode.length);
300-
return `${countryDialCode}${numberWithoutDialCode}`;
281+
if (parsedNumber && parsedNumber.isValid()) {
282+
// Return the E164 format.
283+
return parsedNumber.number;
301284
}
285+
} catch {
286+
// If parsing fails, try to format as incomplete number
302287
}
303288

304-
// If the number starts with 0 (common in many countries), remove it
305-
if (cleanedPhoneNumber.startsWith("0")) {
306-
cleanedPhoneNumber = cleanedPhoneNumber.substring(1);
307-
}
289+
try {
290+
// Try to format as incomplete number with country
291+
const formatted = formatIncompletePhoneNumber(phoneNumber, countryData.code);
292+
// Remove spaces from the formatted result.
293+
return formatted.replace(/\s/g, "");
294+
} catch {
295+
// If all else fails, just clean the number and prepend country code
296+
const cleaned = phoneNumber.replace(/[^\d+]/g, "").trim();
297+
if (cleaned.startsWith("+")) {
298+
return cleaned;
299+
}
308300

309-
// If the number already starts with the country dial code (without +), add the +
310-
if (cleanedPhoneNumber.startsWith(countryDialCode.substring(1))) {
311-
return `+${cleanedPhoneNumber}`;
301+
return `${countryData.dialCode}${cleaned}`;
312302
}
313-
314-
// Otherwise, prepend the country dial code
315-
return `${countryDialCode}${cleanedPhoneNumber}`;
316303
}

pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)