diff --git a/apps/web/components/phone-input/PhoneInput.test.ts b/apps/web/components/phone-input/PhoneInput.test.ts new file mode 100644 index 0000000000..51c074da81 --- /dev/null +++ b/apps/web/components/phone-input/PhoneInput.test.ts @@ -0,0 +1,53 @@ +import { isValidPhoneNumber, parsePhoneNumberFromString } from "libphonenumber-js/max"; +import { describe, expect, it } from "vitest"; +import { CUSTOM_PHONE_MASKS } from "./phone-masks"; + +/** Count the digit placeholders (dots) in a react-phone-input-2 mask */ +const countMaskDigits = (mask: string): number => (mask.match(/\./g) || []).length; + +/** + * Test phone numbers that must be accepted for each country with a custom mask. + * These are realistic mobile and landline numbers validated by libphonenumber-js. + */ +const COUNTRY_TEST_NUMBERS: Record = { + ci: [{ number: "+2250797764877", description: "Ivory Coast 10-digit mobile" }], + bj: [{ number: "+2290165526657", description: "Benin 10-digit mobile" }], + at: [{ number: "+436641234567", description: "Austria mobile" }], + ar: [ + { number: "+5491112345678", description: "Argentina mobile (Buenos Aires, 11 national digits)" }, + { number: "+5493414123456", description: "Argentina mobile (Rosario, 11 national digits)" }, + { number: "+541112345678", description: "Argentina landline (10 national digits)" }, + ], + fi: [ + { number: "+3584012345678", description: "Finland 10-digit mobile" }, + { number: "+358501234567", description: "Finland 9-digit mobile" }, + ], +}; + +describe("CUSTOM_PHONE_MASKS", () => { + describe.each(Object.entries(CUSTOM_PHONE_MASKS))("mask for %s", (countryCode, mask) => { + const maskDigits = countMaskDigits(mask); + const testNumbers = COUNTRY_TEST_NUMBERS[countryCode] || []; + + it("should have enough digit placeholders for all valid phone numbers", () => { + for (const { number, description } of testNumbers) { + const parsed = parsePhoneNumberFromString(number); + expect(parsed, `Failed to parse ${description}: ${number}`).toBeTruthy(); + + const nationalDigits = parsed?.nationalNumber.length ?? 0; + expect( + maskDigits, + `Mask for ${countryCode} has ${maskDigits} digit slots but ${description} (${number}) needs ${nationalDigits} national digits` + ).toBeGreaterThanOrEqual(nationalDigits); + } + }); + + it("should have test numbers that pass libphonenumber-js validation", () => { + for (const { number, description } of testNumbers) { + expect(isValidPhoneNumber(number), `${description} (${number}) should be a valid phone number`).toBe( + true + ); + } + }); + }); +}); diff --git a/apps/web/components/phone-input/PhoneInput.tsx b/apps/web/components/phone-input/PhoneInput.tsx index 8894a9cc3d..a82ab7b1d6 100644 --- a/apps/web/components/phone-input/PhoneInput.tsx +++ b/apps/web/components/phone-input/PhoneInput.tsx @@ -2,20 +2,15 @@ import { isSupportedCountry } from "libphonenumber-js"; import type { CSSProperties } from "react"; -import { useState, useEffect } from "react"; +import { useEffect, useState } from "react"; import PhoneInput from "react-phone-input-2"; import "react-phone-input-2/lib/style.css"; import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform"; -import { useBookerStore, type CountryCode } from "@calcom/features/bookings/Booker/store"; +import { type CountryCode, useBookerStore } from "@calcom/features/bookings/Booker/store"; import { trpc } from "@calcom/trpc/react"; import classNames from "@calcom/ui/classNames"; - -const CUSTOM_PHONE_MASKS = { - ci: ".. .. .. .. ..", - bj: ".. .. .. .. ..", - at: "... ..........", -}; +import { CUSTOM_PHONE_MASKS } from "./phone-masks"; export type PhoneInputProps = { value?: string; diff --git a/apps/web/components/phone-input/phone-masks.ts b/apps/web/components/phone-input/phone-masks.ts new file mode 100644 index 0000000000..1dcd02818e --- /dev/null +++ b/apps/web/components/phone-input/phone-masks.ts @@ -0,0 +1,20 @@ +/** + * Custom phone masks to override outdated masks in react-phone-input-2. + * The library's built-in masks can lag behind countries' numbering plan changes, + * causing valid phone numbers to be truncated (users can't type enough digits). + * + * Each dot (.) represents a digit placeholder. When a country updates its + * numbering plan to use longer numbers, add an override here. + */ +export const CUSTOM_PHONE_MASKS = { + /** Ivory Coast: migrated from 8 to 10 digits in 2021 */ + ci: ".. .. .. .. ..", + /** Benin: migrated from 8 to 10 digits in 2025 */ + bj: ".. .. .. .. ..", + /** Austria: variable-length numbers up to 13 digits */ + at: "... ..........", + /** Argentina: mobile numbers require 11 national digits (9 + area code + subscriber) */ + ar: "(..) .........", + /** Finland: some mobile numbers use 10 national digits */ + fi: ".. ... .. ...", +};