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 <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 <morgan@cal.com> * fix: add type-safe casts for CountryCode in PhoneInput - Import CountryCode type from store - Add explicit type annotation to useState<CountryCode> - 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 <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 <morgan@cal.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in>
This commit is contained in:
6 files changed
+73
-9
No files matched your search
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@calcom/atoms": minor
|
||||
---
|
||||
|
||||
Add defaultPhoneCountry prop to BookerPlatformWrapper with ISO 3166-1 alpha-2 type safety
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
};
|
||||
};
|
||||
@@ -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<CountryCode>(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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
Reference in new issue
Block a user