From 037cb8ee2a83c1d725d5d913d47911a5997bee01 Mon Sep 17 00:00:00 2001 From: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Date: Mon, 17 Nov 2025 17:56:10 +0200 Subject: [PATCH] feat: add defaultPhoneCountry prop with ISO 3166-1 alpha-2 type safety (#25204) * feat: add defaultPhoneCountry prop to BookerPlatformWrapper - Add defaultPhoneCountry to BookerStore type and implementation - Add defaultPhoneCountry prop to BookerPlatformWrapper types - Pass defaultPhoneCountry through store initialization - Update PhoneInput to use defaultPhoneCountry from store - Support default phone country extension for phone inputs in booker form * feat: add strict typing for defaultPhoneCountry with ISO 3166-1 alpha-2 codes - Define CountryCode type using ISO 3166-1 alpha-2 country codes - Update defaultPhoneCountry prop type in BookerPlatformWrapper to use CountryCode - Update defaultPhoneCountry type in BookerStore to use CountryCode - Ensures type safety by only allowing valid country codes like 'us', 'gb', 'ee', etc. - Fix lint warnings by prefixing type-only constants with underscore Co-Authored-By: morgan@cal.com * refactor: export CountryCode from store to avoid duplication - Export CountryCode type from packages/features/bookings/Booker/store.ts - Import CountryCode in packages/platform/atoms/booker/types.ts from store - Remove duplicate CountryCode definition from types.ts - Maintains single source of truth for country code type definition Co-Authored-By: morgan@cal.com * fix: add type-safe casts for CountryCode in PhoneInput - Import CountryCode type from store - Add explicit type annotation to useState - Add safe type casts with isSupportedCountry validation - Validate navigator.language country code before using it - Fixes CI type error: string not assignable to CountryCode Co-Authored-By: morgan@cal.com * docs: add defaultPhoneCountry prop documentation and changeset - Add defaultPhoneCountry prop to booker.mdx documentation - Add changeset for minor version bump - Document ISO 3166-1 alpha-2 country code support Co-Authored-By: morgan@cal.com --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Rajiv Sahal --- .changeset/little-rings-repeat.md | 5 +++ docs/platform/atoms/booker.mdx | 1 + packages/features/bookings/Booker/store.ts | 40 ++++++++++++++++++- .../components/phone-input/PhoneInput.tsx | 28 ++++++++++--- .../atoms/booker/BookerPlatformWrapper.tsx | 3 ++ packages/platform/atoms/booker/types.ts | 5 ++- 6 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 .changeset/little-rings-repeat.md diff --git a/.changeset/little-rings-repeat.md b/.changeset/little-rings-repeat.md new file mode 100644 index 0000000000..dd0aa1a46c --- /dev/null +++ b/.changeset/little-rings-repeat.md @@ -0,0 +1,5 @@ +--- +"@calcom/atoms": minor +--- + +Add defaultPhoneCountry prop to BookerPlatformWrapper with ISO 3166-1 alpha-2 type safety diff --git a/docs/platform/atoms/booker.mdx b/docs/platform/atoms/booker.mdx index 8ed941bb31..caaeaf8b6f 100644 --- a/docs/platform/atoms/booker.mdx +++ b/docs/platform/atoms/booker.mdx @@ -131,6 +131,7 @@ Below is a list of props that can be passed to the booker atom. | showNoAvailabilityDialog | No | Boolean indicating if the no availability dialog should be shown, defaults to true. | | silentlyHandleCalendarFailures | No | Boolean when true the booker still displays slots when the third party calendars credentials are invalid or expired, Booker may show stale availability when enabled | | hideEventMetadata | No | Boolean that controls the visibility of the event metadata sidebar. When `true`, hides the left sidebar containing event details like title, description, duration, and host information. Defaults to `false`. | +| defaultPhoneCountry | No | Sets the default country code for phone number inputs in the booking form. Accepts ISO 3166-1 alpha-2 country codes (e.g., `"us"`, `"gb"`, `"in"`, `"ee"`). When set, phone inputs will default to the specified country's dialing code. | ## Styling diff --git a/packages/features/bookings/Booker/store.ts b/packages/features/bookings/Booker/store.ts index 80b1b0cd8f..c6f2627a58 100644 --- a/packages/features/bookings/Booker/store.ts +++ b/packages/features/bookings/Booker/store.ts @@ -15,6 +15,36 @@ import type { GetBookingType } from "../lib/get-booking"; import type { BookerState, BookerLayout } from "./types"; import { updateQueryParam, getQueryParam, removeQueryParam } from "./utils/query-param"; +const _iso_3166_1_alpha_2_codes = [ + "ad", "ae", "af", "ag", "ai", "al", "am", "ao", "aq", "ar", "as", "at", "au", "aw", "ax", "az", + "ba", "bb", "bd", "be", "bf", "bg", "bh", "bi", "bj", "bl", "bm", "bn", "bo", "bq", "br", "bs", "bt", "bv", "bw", "by", "bz", + "ca", "cc", "cd", "cf", "cg", "ch", "ci", "ck", "cl", "cm", "cn", "co", "cr", "cu", "cv", "cw", "cx", "cy", "cz", + "de", "dj", "dk", "dm", "do", "dz", + "ec", "ee", "eg", "eh", "er", "es", "et", + "fi", "fj", "fk", "fm", "fo", "fr", + "ga", "gb", "gd", "ge", "gf", "gg", "gh", "gi", "gl", "gm", "gn", "gp", "gq", "gr", "gs", "gt", "gu", "gw", "gy", + "hk", "hm", "hn", "hr", "ht", "hu", + "id", "ie", "il", "im", "in", "io", "iq", "ir", "is", "it", + "je", "jm", "jo", "jp", + "ke", "kg", "kh", "ki", "km", "kn", "kp", "kr", "kw", "ky", "kz", + "la", "lb", "lc", "li", "lk", "lr", "ls", "lt", "lu", "lv", "ly", + "ma", "mc", "md", "me", "mf", "mg", "mh", "mk", "ml", "mm", "mn", "mo", "mp", "mq", "mr", "ms", "mt", "mu", "mv", "mw", "mx", "my", "mz", + "na", "nc", "ne", "nf", "ng", "ni", "nl", "no", "np", "nr", "nu", "nz", + "om", + "pa", "pe", "pf", "pg", "ph", "pk", "pl", "pm", "pn", "pr", "ps", "pt", "pw", "py", + "qa", + "re", "ro", "rs", "ru", "rw", + "sa", "sb", "sc", "sd", "se", "sg", "sh", "si", "sj", "sk", "sl", "sm", "sn", "so", "sr", "ss", "st", "sv", "sx", "sy", + "tc", "td", "tf", "tg", "th", "tj", "tk", "tl", "tm", "tn", "to", "tr", "tt", "tv", "tw", "tz", + "ua", "ug", "um", "us", "uy", "uz", + "va", "vc", "ve", "vg", "vi", "vn", "vu", + "wf", "ws", + "ye", "yt", + "za", "zm", "zw" +] as const; + +export type CountryCode = typeof _iso_3166_1_alpha_2_codes[number]; + /** * Arguments passed into store initializer, containing @@ -44,6 +74,7 @@ export type StoreInitializeType = { crmRecordId?: string | null; isPlatform?: boolean; allowUpdatingUrlParams?: boolean; + defaultPhoneCountry?: CountryCode; }; type SeatedEventData = { @@ -183,6 +214,7 @@ export type BookerStore = { crmRecordId?: string | null; isPlatform?: boolean; allowUpdatingUrlParams?: boolean; + defaultPhoneCountry?: CountryCode | null; }; /** @@ -329,6 +361,7 @@ export const createBookerStore = () => crmRecordId, isPlatform = false, allowUpdatingUrlParams = true, + defaultPhoneCountry, }: StoreInitializeType) => { const selectedDateInStore = get().selectedDate; @@ -372,6 +405,7 @@ export const createBookerStore = () => crmRecordId, isPlatform, allowUpdatingUrlParams, + defaultPhoneCountry, }); if (durationConfig?.includes(Number(getQueryParam("duration")))) { @@ -461,6 +495,7 @@ export const createBookerStore = () => }, isPlatform: false, allowUpdatingUrlParams: true, + defaultPhoneCountry: null, })); /** @@ -489,6 +524,7 @@ export const useInitializeBookerStore = ({ crmRecordId, isPlatform = false, allowUpdatingUrlParams = true, + defaultPhoneCountry, }: StoreInitializeType) => { const initializeStore = useBookerStore((state) => state.initialize); useEffect(() => { @@ -513,6 +549,7 @@ export const useInitializeBookerStore = ({ crmRecordId, isPlatform, allowUpdatingUrlParams, + defaultPhoneCountry, }); }, [ initializeStore, @@ -536,5 +573,6 @@ export const useInitializeBookerStore = ({ crmRecordId, isPlatform, allowUpdatingUrlParams, + defaultPhoneCountry, ]); -}; \ No newline at end of file +}; diff --git a/packages/features/components/phone-input/PhoneInput.tsx b/packages/features/components/phone-input/PhoneInput.tsx index e1a35af2c4..cfd3d80d21 100644 --- a/packages/features/components/phone-input/PhoneInput.tsx +++ b/packages/features/components/phone-input/PhoneInput.tsx @@ -7,6 +7,7 @@ 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 { trpc } from "@calcom/trpc/react"; import classNames from "@calcom/ui/classNames"; @@ -33,6 +34,8 @@ function BasePhoneInput({ ...rest }: PhoneInputProps) { const isPlatform = useIsPlatform(); + const defaultPhoneCountryFromStore = useBookerStore((state) => state.defaultPhoneCountry); + const effectiveDefaultCountry = defaultPhoneCountryFromStore || defaultCountry; // This is to trigger validation on prefill value changes useEffect(() => { @@ -63,7 +66,7 @@ function BasePhoneInput({ value={value ? value.trim().replace(/^\+?/, "+") : undefined} enableSearch disableSearchIcon - country={defaultCountry} + country={effectiveDefaultCountry} inputProps={{ name, required: rest.required, @@ -150,7 +153,8 @@ function BasePhoneInputWeb({ } const useDefaultCountry = () => { - const [defaultCountry, setDefaultCountry] = useState("us"); + const defaultPhoneCountryFromStore = useBookerStore((state) => state.defaultPhoneCountry); + const [defaultCountry, setDefaultCountry] = useState(defaultPhoneCountryFromStore || "us"); const query = trpc.viewer.public.countryCode.useQuery(undefined, { refetchOnWindowFocus: false, refetchOnReconnect: false, @@ -159,16 +163,28 @@ const useDefaultCountry = () => { useEffect( function refactorMeWithoutEffect() { + if (defaultPhoneCountryFromStore) { + setDefaultCountry(defaultPhoneCountryFromStore); + return; + } + const data = query.data; if (!data?.countryCode) { return; } - isSupportedCountry(data?.countryCode) - ? setDefaultCountry(data.countryCode.toLowerCase()) - : setDefaultCountry(navigator.language.split("-")[1]?.toLowerCase() || "us"); + if (isSupportedCountry(data?.countryCode)) { + setDefaultCountry(data.countryCode.toLowerCase() as CountryCode); + } else { + const navCountry = navigator.language.split("-")[1]?.toUpperCase(); + if (navCountry && isSupportedCountry(navCountry)) { + setDefaultCountry(navCountry.toLowerCase() as CountryCode); + } else { + setDefaultCountry("us"); + } + } }, - [query.data] + [query.data, defaultPhoneCountryFromStore] ); return defaultCountry; diff --git a/packages/platform/atoms/booker/BookerPlatformWrapper.tsx b/packages/platform/atoms/booker/BookerPlatformWrapper.tsx index 6c5cab384f..baea488ec6 100644 --- a/packages/platform/atoms/booker/BookerPlatformWrapper.tsx +++ b/packages/platform/atoms/booker/BookerPlatformWrapper.tsx @@ -70,6 +70,7 @@ const BookerPlatformWrapperComponent = ( showNoAvailabilityDialog, silentlyHandleCalendarFailures = false, hideEventMetadata = false, + defaultPhoneCountry, } = props; const layout = BookerLayouts[view]; @@ -185,6 +186,7 @@ const BookerPlatformWrapperComponent = ( bookingData, isPlatform: true, allowUpdatingUrlParams, + defaultPhoneCountry, }); useInitializeBookerStoreContext({ ...props, @@ -201,6 +203,7 @@ const BookerPlatformWrapperComponent = ( bookingData, isPlatform: true, allowUpdatingUrlParams, + defaultPhoneCountry, }); const [dayCount] = useBookerStoreContext((state) => [state.dayCount, state.setDayCount], shallow); const selectedDate = useBookerStoreContext((state) => state.selectedDate); diff --git a/packages/platform/atoms/booker/types.ts b/packages/platform/atoms/booker/types.ts index bb4d16f90a..378c3fd594 100644 --- a/packages/platform/atoms/booker/types.ts +++ b/packages/platform/atoms/booker/types.ts @@ -3,7 +3,7 @@ import type React from "react"; import type { BookerProps } from "@calcom/features/bookings/Booker"; -import type { BookerStore } from "@calcom/features/bookings/Booker/store"; +import type { BookerStore, CountryCode } from "@calcom/features/bookings/Booker/store"; import type { Timezone, VIEW_TYPE } from "@calcom/features/bookings/Booker/types"; import type { BookingCreateBody } from "@calcom/features/bookings/lib/bookingCreateBodySchema"; import type { BookingResponse } from "@calcom/platform-libraries"; @@ -88,6 +88,7 @@ export type BookerPlatformWrapperAtomProps = Omit< roundRobinHideOrgAndTeam?: boolean; silentlyHandleCalendarFailures?: boolean; hideEventMetadata?: boolean; + defaultPhoneCountry?: CountryCode; }; export type BookerPlatformWrapperAtomPropsForIndividual = BookerPlatformWrapperAtomProps & { @@ -101,4 +102,4 @@ export type BookerPlatformWrapperAtomPropsForTeam = BookerPlatformWrapperAtomPro isTeamEvent: true; teamId: number; routingFormSearchParams?: RoutingFormSearchParams; -}; \ No newline at end of file +};