diff --git a/docs/platform/atoms/booker.mdx b/docs/platform/atoms/booker.mdx
index c62c43a57f..efc95db129 100644
--- a/docs/platform/atoms/booker.mdx
+++ b/docs/platform/atoms/booker.mdx
@@ -120,6 +120,8 @@ Below is a list of props that can be passed to the booker atom.
| view | No | Specifies the layout of the booker atom into column, week, or month view |
| metadata | No | Used to pass custom metadata values into the booker. Metadata should be an object eg: `{ bookingSource: "website", userRole: "admin" }` |
| bannerUrl | No | Adds custom banner to the booker atom |
+| onBookerStateChange | No | Callback function that is triggered when the state of the booker atom changes. |
+| allowUpdatingUrlParams | No | Boolean indicating if the URL parameters should be updated, defaults to false. |
Along with the props, booker atom accepts custom styles via the **customClassNames** prop. Below is a list of props that fall under this **customClassNames** prop.
diff --git a/packages/features/bookings/Booker/store.ts b/packages/features/bookings/Booker/store.ts
index 591544cf79..584a9f9640 100644
--- a/packages/features/bookings/Booker/store.ts
+++ b/packages/features/bookings/Booker/store.ts
@@ -36,6 +36,8 @@ type StoreInitializeType = {
teamMemberEmail?: string | null;
crmOwnerRecordType?: string | null;
crmAppSlug?: string | null;
+ isPlatform?: boolean;
+ allowUpdatingUrlParams?: boolean;
};
type SeatedEventData = {
@@ -161,6 +163,8 @@ export type BookerStore = {
teamMemberEmail?: string | null;
crmOwnerRecordType?: string | null;
crmAppSlug?: string | null;
+ isPlatform?: boolean;
+ allowUpdatingUrlParams?: boolean;
};
/**
@@ -180,7 +184,9 @@ export const useBookerStore = create((set, get) => ({
if (["week_view", "column_view"].includes(layout) && !get().selectedDate) {
set({ selectedDate: dayjs().format("YYYY-MM-DD") });
}
- updateQueryParam("layout", layout);
+ if (!get().isPlatform || get().allowUpdatingUrlParams) {
+ updateQueryParam("layout", layout);
+ }
return set({ layout });
},
selectedDate: getQueryParam("date") || null,
@@ -194,14 +200,14 @@ export const useBookerStore = create((set, get) => ({
const currentSelection = dayjs(get().selectedDate);
const newSelection = dayjs(selectedDate);
set({ selectedDate });
- if (!omitUpdatingParams) {
+ if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) {
updateQueryParam("date", selectedDate ?? "");
}
// Setting month make sure small calendar in fullscreen layouts also updates.
if (newSelection.month() !== currentSelection.month()) {
set({ month: newSelection.format("YYYY-MM") });
- if (!omitUpdatingParams) {
+ if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) {
updateQueryParam("month", newSelection.format("YYYY-MM"));
}
}
@@ -223,11 +229,15 @@ export const useBookerStore = create((set, get) => ({
if (newSelection.month() !== currentSelection.month()) {
set({ month: newSelection.format("YYYY-MM") });
- updateQueryParam("month", newSelection.format("YYYY-MM"));
+ if (!get().isPlatform || get().allowUpdatingUrlParams) {
+ updateQueryParam("month", newSelection.format("YYYY-MM"));
+ }
}
set({ selectedDate: newSelectionFormatted });
- updateQueryParam("date", newSelectionFormatted);
+ if (!get().isPlatform || get().allowUpdatingUrlParams) {
+ updateQueryParam("date", newSelectionFormatted);
+ }
},
username: null,
eventSlug: null,
@@ -249,7 +259,9 @@ export const useBookerStore = create((set, get) => ({
return;
}
set({ month, selectedTimeslot: null });
- updateQueryParam("month", month ?? "");
+ if (!get().isPlatform || get().allowUpdatingUrlParams) {
+ updateQueryParam("month", month ?? "");
+ }
get().setSelectedDate(null);
},
dayCount: BOOKER_NUMBER_OF_DAYS_TO_LOAD > 0 ? BOOKER_NUMBER_OF_DAYS_TO_LOAD : null,
@@ -265,10 +277,13 @@ export const useBookerStore = create((set, get) => ({
},
setSeatedEventData: (seatedEventData: SeatedEventData) => {
set({ seatedEventData });
- updateQueryParam("bookingUid", seatedEventData.bookingUid ?? "null");
+ if (!get().isPlatform || get().allowUpdatingUrlParams) {
+ updateQueryParam("bookingUid", seatedEventData.bookingUid ?? "null");
+ }
},
// This is different from timeZone in timePreferencesStore, because timeZone in timePreferencesStore is the preferred timezone of the booker,
// it is the timezone configured through query param. So, this is in a way the preference of the person who shared the link.
+ // it is the timezone configured through query param. So, this is in a way the preference of the person who shared the link.
timezone: getQueryParam("cal.tz") ?? null,
setTimezone: (timezone: string | null) => {
set({ timezone });
@@ -291,6 +306,8 @@ export const useBookerStore = create((set, get) => ({
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
+ isPlatform = false,
+ allowUpdatingUrlParams = true,
}: StoreInitializeType) => {
const selectedDateInStore = get().selectedDate;
@@ -330,6 +347,8 @@ export const useBookerStore = create((set, get) => ({
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
+ isPlatform,
+ allowUpdatingUrlParams,
});
if (durationConfig?.includes(Number(getQueryParam("duration")))) {
@@ -353,7 +372,9 @@ export const useBookerStore = create((set, get) => ({
"minutes"
);
set({ selectedDuration: originalBookingLength });
- updateQueryParam("duration", originalBookingLength ?? "");
+ if (!isPlatform || allowUpdatingUrlParams) {
+ updateQueryParam("duration", originalBookingLength ?? "");
+ }
}
if (month) set({ month });
@@ -367,9 +388,12 @@ export const useBookerStore = create((set, get) => ({
selectedTimeslot,
isInstantMeeting,
});
- updateQueryParam("month", month);
- updateQueryParam("date", selectedDate ?? "");
- updateQueryParam("slot", selectedTimeslot ?? "", false);
+
+ if (!isPlatform || allowUpdatingUrlParams) {
+ updateQueryParam("month", month);
+ updateQueryParam("date", selectedDate ?? "");
+ updateQueryParam("slot", selectedTimeslot ?? "", false);
+ }
}
//removeQueryParam("layout");
},
@@ -377,7 +401,9 @@ export const useBookerStore = create((set, get) => ({
selectedDuration: null,
setSelectedDuration: (selectedDuration: number | null) => {
set({ selectedDuration });
- updateQueryParam("duration", selectedDuration ?? "");
+ if (!get().isPlatform || get().allowUpdatingUrlParams) {
+ updateQueryParam("duration", selectedDuration ?? "");
+ }
},
setBookingData: (bookingData: GetBookingType | null | undefined) => {
set({ bookingData: bookingData ?? null });
@@ -396,7 +422,9 @@ export const useBookerStore = create((set, get) => ({
},
setSelectedTimeslot: (selectedTimeslot: string | null) => {
set({ selectedTimeslot });
- updateQueryParam("slot", selectedTimeslot ?? "", false);
+ if (!get().isPlatform || get().allowUpdatingUrlParams) {
+ updateQueryParam("slot", selectedTimeslot ?? "", false);
+ }
},
formValues: {},
setFormValues: (formValues: Record) => {
@@ -406,6 +434,8 @@ export const useBookerStore = create((set, get) => ({
setOrg: (org: string | null | undefined) => {
set({ org });
},
+ isPlatform: false,
+ allowUpdatingUrlParams: true,
}));
export const useInitializeBookerStore = ({
@@ -426,6 +456,8 @@ export const useInitializeBookerStore = ({
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
+ isPlatform = false,
+ allowUpdatingUrlParams = true,
}: StoreInitializeType) => {
const initializeStore = useBookerStore((state) => state.initialize);
useEffect(() => {
@@ -447,6 +479,8 @@ export const useInitializeBookerStore = ({
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
+ isPlatform,
+ allowUpdatingUrlParams,
});
}, [
initializeStore,
@@ -467,5 +501,7 @@ export const useInitializeBookerStore = ({
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
+ isPlatform,
+ allowUpdatingUrlParams,
]);
};
diff --git a/packages/platform/atoms/booker/BookerPlatformWrapper.tsx b/packages/platform/atoms/booker/BookerPlatformWrapper.tsx
index ea67c57a27..c928667f3a 100644
--- a/packages/platform/atoms/booker/BookerPlatformWrapper.tsx
+++ b/packages/platform/atoms/booker/BookerPlatformWrapper.tsx
@@ -1,5 +1,7 @@
import { useQueryClient } from "@tanstack/react-query";
-import { useMemo, useEffect, useCallback, useState } from "react";
+// eslint-disable-next-line no-restricted-imports
+import debounce from "lodash/debounce";
+import { useMemo, useEffect, useCallback, useState, useRef } from "react";
import { shallow } from "zustand/shallow";
import dayjs from "@calcom/dayjs";
@@ -40,6 +42,7 @@ import { AtomsWrapper } from "../src/components/atoms-wrapper";
import type {
BookerPlatformWrapperAtomPropsForIndividual,
BookerPlatformWrapperAtomPropsForTeam,
+ BookerStoreValues,
} from "./types";
export const BookerPlatformWrapper = (
@@ -53,6 +56,8 @@ export const BookerPlatformWrapper = (
crmAppSlug,
crmOwnerRecordType,
preventEventTypeRedirect,
+ onBookerStateChange,
+ allowUpdatingUrlParams = false,
} = props;
const layout = BookerLayouts[view];
@@ -70,6 +75,48 @@ export const BookerPlatformWrapper = (
const [isOverlayCalendarEnabled, setIsOverlayCalendarEnabled] = useState(
Boolean(localStorage?.getItem?.("overlayCalendarSwitchDefault"))
);
+ const prevStateRef = useRef(null);
+ const getStateValues = useCallback(
+ (state: ReturnType): BookerStoreValues => {
+ return Object.fromEntries(
+ Object.entries(state).filter(([_, value]) => typeof value !== "function")
+ ) as BookerStoreValues;
+ },
+ []
+ );
+ const debouncedStateChange = useMemo(() => {
+ return debounce(
+ (currentStateValues: BookerStoreValues, callback: (values: BookerStoreValues) => void) => {
+ const prevState = prevStateRef.current;
+ const stateChanged = !prevState || JSON.stringify(prevState) !== JSON.stringify(currentStateValues);
+
+ if (stateChanged) {
+ callback(currentStateValues);
+ prevStateRef.current = currentStateValues;
+ }
+ },
+ 50
+ );
+ }, []);
+
+ useEffect(() => {
+ if (!onBookerStateChange) return;
+
+ const unsubscribe = useBookerStore.subscribe((state) => {
+ const currentStateValues = getStateValues(state);
+ debouncedStateChange(currentStateValues, onBookerStateChange);
+ });
+
+ // Initial call with current state
+ const initialState = getStateValues(useBookerStore.getState());
+ onBookerStateChange(initialState);
+ prevStateRef.current = initialState;
+
+ return () => {
+ unsubscribe();
+ debouncedStateChange.cancel();
+ };
+ }, [onBookerStateChange, getStateValues, debouncedStateChange]);
useGetBookingForReschedule({
uid: props.rescheduleUid ?? props.bookingUid ?? "",
@@ -166,6 +213,8 @@ export const BookerPlatformWrapper = (
org: props.entity?.orgSlug,
username,
bookingData,
+ isPlatform: true,
+ allowUpdatingUrlParams,
});
const [dayCount] = useBookerStore((state) => [state.dayCount, state.setDayCount], shallow);
const selectedDate = useBookerStore((state) => state.selectedDate);
@@ -397,6 +446,13 @@ export const BookerPlatformWrapper = (
},
[setIsOverlayCalendarEnabled]
);
+ const selectedDateProp = useMemo(
+ () => dayjs(props.selectedDate).format("YYYY-MM-DD"),
+ [props.selectedDate]
+ );
+ useEffect(() => {
+ setSelectedDate(selectedDateProp, true);
+ }, [selectedDateProp]);
useEffect(() => {
// reset booker whenever it's unmounted
@@ -408,7 +464,6 @@ export const BookerPlatformWrapper = (
setSelectedDuration(null);
setOrg(null);
setSelectedMonth(null);
- setSelectedDuration(null);
if (props.rescheduleUid) {
// clean booking data from cache
queryClient.removeQueries({
diff --git a/packages/platform/atoms/booker/types.ts b/packages/platform/atoms/booker/types.ts
index a7daa643ca..16a2e0df88 100644
--- a/packages/platform/atoms/booker/types.ts
+++ b/packages/platform/atoms/booker/types.ts
@@ -1,4 +1,5 @@
import type { BookerProps } from "@calcom/features/bookings/Booker";
+import type { BookerStore } from "@calcom/features/bookings/Booker/store";
import type { BookingResponse } from "@calcom/platform-libraries";
import type {
ApiSuccessResponse,
@@ -10,6 +11,30 @@ import type { BookerLayouts } from "@calcom/prisma/zod-utils";
import type { UseCreateBookingInput } from "../hooks/bookings/useCreateBooking";
+// Type that includes only the data values from BookerStore (excluding functions)
+export type BookerStoreValues = Omit<
+ BookerStore,
+ | "setState"
+ | "setLayout"
+ | "setSelectedDate"
+ | "setSelectedDatesAndTimes"
+ | "addToSelectedDate"
+ | "setVerifiedEmail"
+ | "setMonth"
+ | "setDayCount"
+ | "setSeatedEventData"
+ | "setTimezone"
+ | "initialize"
+ | "setSelectedDuration"
+ | "setBookingData"
+ | "setRecurringEventCount"
+ | "setOccurenceCount"
+ | "setTentativeSelectedTimeslots"
+ | "setSelectedTimeslot"
+ | "setFormValues"
+ | "setOrg"
+>;
+
export type BookerPlatformWrapperAtomProps = Omit<
BookerProps,
"username" | "entity" | "isTeamEvent" | "teamId"
@@ -39,6 +64,7 @@ export type BookerPlatformWrapperAtomProps = Omit<
onReserveSlotError?: (data: ApiErrorResponse) => void;
onDeleteSlotSuccess?: (data: ApiSuccessResponseWithoutData) => void;
onDeleteSlotError?: (data: ApiErrorResponse) => void;
+ onBookerStateChange?: (state: BookerStoreValues) => void;
locationUrl?: string;
view?: VIEW_TYPE;
metadata?: Record;
@@ -46,6 +72,7 @@ export type BookerPlatformWrapperAtomProps = Omit<
onDryRunSuccess?: () => void;
hostsLimit?: number;
preventEventTypeRedirect?: boolean;
+ allowUpdatingUrlParams?: boolean;
};
type VIEW_TYPE = keyof typeof BookerLayouts;
diff --git a/packages/platform/examples/base/src/pages/booking.tsx b/packages/platform/examples/base/src/pages/booking.tsx
index 462957d94e..8d63c82849 100644
--- a/packages/platform/examples/base/src/pages/booking.tsx
+++ b/packages/platform/examples/base/src/pages/booking.tsx
@@ -101,6 +101,10 @@ export default function Bookings(props: { calUsername: string; calEmail: string
router.push(`/${data.data.uid}`);
}
}}
+ onBookerStateChange={(bookerState) => {
+ console.log("Booker state updated:", bookerState);
+ // You can perform any actions based on the updated state here
+ }}
metadata={{ CustomKey: "CustomValue" }}
duration={eventTypeDuration}
customClassNames={{
@@ -120,6 +124,8 @@ export default function Bookings(props: { calUsername: string; calEmail: string
? { isTeamEvent: true, teamId: teams?.[0]?.id || 0 }
: { username: props.calUsername })}
hostsLimit={3}
+ selectedDate={new Date("2025-03-25")}
+ allowUpdatingUrlParams={true}
/>
>
)}