From 2bf45677e85b2ed05850966c2d25f7c7108dab09 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:05:24 +0000 Subject: [PATCH] fix: add phone mask overrides for Argentina and Finland to prevent digit truncation (#28203) * fix: add phone mask overrides for Argentina and Finland to prevent digit truncation Co-Authored-By: susan * refactor: extract CUSTOM_PHONE_MASKS to shared module to avoid duplication Address review feedback from cubic-dev-ai: extract CUSTOM_PHONE_MASKS into a shared phone-masks.ts module so both PhoneInput.tsx and the test file import from the same source of truth instead of duplicating the masks. Also fix non-null assertion lint warning in the test file. Original PR by devin-ai-integration[bot]. Co-Authored-By: bot_apk * fix: move custom message to expect() for toBeGreaterThanOrEqual type compatibility toBeGreaterThanOrEqual only accepts 1 argument. Move the custom error message to the expect() call instead. Co-Authored-By: bot_apk --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: susan Co-authored-by: bot_apk Co-authored-by: Eunjae Lee Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> --- .../components/phone-input/PhoneInput.test.ts | 53 +++++++++++++++++++ .../web/components/phone-input/PhoneInput.tsx | 11 ++-- .../web/components/phone-input/phone-masks.ts | 20 +++++++ 3 files changed, 76 insertions(+), 8 deletions(-) create mode 100644 apps/web/components/phone-input/PhoneInput.test.ts create mode 100644 apps/web/components/phone-input/phone-masks.ts 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: ".. ... .. ...", +};