fix: refresh slots on timezone change for booker timezone restrictions (#27491)

* fix: refresh slots on timezone change for booker timezone restrictions

* refactor: use useMemo for timezone change detection

* revert: remove unnecessary formatting changes

* feat: add timezone refresh for platform components

Add timezone change detection and slot refresh to BookerPlatformWrapper
and EventTypeCalendarViewComponent to handle restriction schedules with
useBookerTimezone enabled.

* refactor: extract timezone slot refresh logic into reusable hook

* Update packages/platform/atoms/calendar-view/EventTypeCalendarViewComponent.tsx

Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>

* Update packages/platform/atoms/calendar-view/EventTypeCalendarViewComponent.tsx

Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>

* fix

* fix: prevent unnecessary getSchedule calls when useBookerTimezone is disabled

* fix: add timezone fields to BookerEvent type

* fix: add missing properties to BookerEvent and BookerEventProfile types

* trying to fix type errors

* Add restrictionScheduleId and useBookerTimezone fields

* fix: correct import path for useBookerTime hook

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* fix: explicitly include restrictionScheduleId and useBookerTimezone in getPublicEvent return

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* chore: trigger fresh CI build

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* fix: cast event.data to BookerEvent for timezone fields access

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* refactor: address review feedback for timezone slot refresh

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* refactor: add explicit return type to event handler to ensure type propagation

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* refactor: extract useStableTimezone hook and remove dead timezone detection code

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

---------

Co-authored-by: hackice20 <yashkam431@gmail.com>
Co-authored-by: Yash <116657771+hackice20@users.noreply.github.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Syed Ali Shahbaz
2026-02-17 07:46:40 -03:00
committed by GitHub
co-authored by ali@cal.com hackice20 Yash Morgan Devin AI
parent ab4eff1fe1
commit 4c73695d3a
9 changed files with 140 additions and 22 deletions
@@ -19,7 +19,7 @@ import { useInitializeBookerStore } from "@calcom/features/bookings/Booker/store
import { useEvent, useScheduleForEvent } from "@calcom/web/modules/schedules/hooks/useEvent";
import { useBrandColors } from "@calcom/features/bookings/Booker/utils/use-brand-colors";
import type { getPublicEvent } from "@calcom/features/eventtypes/lib/getPublicEvent";
import { DEFAULT_DARK_BRAND_COLOR, DEFAULT_LIGHT_BRAND_COLOR, WEBAPP_URL } from "@calcom/lib/constants";
import { DEFAULT_LIGHT_BRAND_COLOR, DEFAULT_DARK_BRAND_COLOR, WEBAPP_URL } from "@calcom/lib/constants";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { localStorage } from "@calcom/lib/webstorage";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
@@ -42,11 +42,11 @@ const BookerWebWrapperComponent = (props: BookerWebWrapperAtomProps): JSX.Elemen
});
const event = props.eventData
? {
data: props.eventData,
isSuccess: true,
isError: false,
isPending: false,
}
data: props.eventData,
isSuccess: true,
isError: false,
isPending: false,
}
: clientFetchedEvent;
const bookerLayout = useBookerLayout(event.data?.profile?.bookerLayouts);
@@ -149,6 +149,9 @@ const BookerWebWrapperComponent = (props: BookerWebWrapperAtomProps): JSX.Elemen
useApiV2: props.useApiV2,
bookerLayout,
...(props.entity.orgSlug ? { orgSlug: props.entity.orgSlug } : {}),
restrictionSchedule: event.data?.restrictionScheduleId
? { id: event.data.restrictionScheduleId, useBookerTimezone: event.data.useBookerTimezone }
: undefined,
});
const bookings = useBookings({
event,
+7 -2
View File
@@ -6,6 +6,7 @@ import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { trpc } from "@calcom/trpc/react";
import { useBookerTime } from "@calcom/features/bookings/Booker/hooks/useBookerTime";
import { useStableTimezone } from "@calcom/features/bookings/Booker/hooks/useStableTimezone";
export type useEventReturnType = ReturnType<typeof useEvent>;
export type useScheduleForEventReturnType = ReturnType<typeof useScheduleForEvent>;
@@ -71,6 +72,7 @@ export const useScheduleForEvent = ({
isTeamEvent,
useApiV2 = true,
bookerLayout,
restrictionSchedule,
}: {
username?: string | null;
eventSlug?: string | null;
@@ -92,13 +94,16 @@ export const useScheduleForEvent = ({
extraDays: number;
columnViewExtraDays: { current: number };
};
restrictionSchedule?: { id: number | null; useBookerTimezone: boolean };
}) => {
const { timezone } = useBookerTime();
const { timezone: rawTimezone } = useBookerTime();
const [usernameFromStore, eventSlugFromStore, monthFromStore, durationFromStore] = useBookerStoreContext(
(state) => [state.username, state.eventSlug, state.month, state.selectedDuration],
shallow
);
const effectiveTimezone = useStableTimezone(rawTimezone, restrictionSchedule);
const searchParams = useCompatSearchParams();
const rescheduleUid = searchParams?.get("rescheduleUid");
@@ -106,7 +111,7 @@ export const useScheduleForEvent = ({
username: usernameFromStore ?? username,
eventSlug: eventSlugFromStore ?? eventSlug,
eventId,
timezone,
timezone: effectiveTimezone,
selectedDate,
dayCount,
rescheduleUid,
@@ -0,0 +1,74 @@
/**
* @vitest-environment jsdom
*/
import { renderHook } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { useStableTimezone } from "./useStableTimezone";
describe("useStableTimezone", () => {
it("returns the raw timezone when no restriction schedule is provided", () => {
const { result } = renderHook(() => useStableTimezone("America/New_York"));
expect(result.current).toBe("America/New_York");
});
it("returns the raw timezone when restriction schedule is undefined", () => {
const { result } = renderHook(() => useStableTimezone("America/New_York", undefined));
expect(result.current).toBe("America/New_York");
});
it("returns the raw timezone when restriction schedule id is null", () => {
const { result } = renderHook(() =>
useStableTimezone("America/New_York", { id: null, useBookerTimezone: false })
);
expect(result.current).toBe("America/New_York");
});
it("returns the raw timezone when restriction schedule id is 0", () => {
const { result } = renderHook(() =>
useStableTimezone("America/New_York", { id: 0, useBookerTimezone: false })
);
expect(result.current).toBe("America/New_York");
});
it("returns the raw timezone when useBookerTimezone is true (timezone should follow the booker)", () => {
const { result } = renderHook(() =>
useStableTimezone("America/New_York", { id: 1, useBookerTimezone: true })
);
expect(result.current).toBe("America/New_York");
});
it("pins to initial timezone when restriction schedule exists and useBookerTimezone is false", () => {
let timezone = "America/New_York";
const { result, rerender } = renderHook(() =>
useStableTimezone(timezone, { id: 1, useBookerTimezone: false })
);
expect(result.current).toBe("America/New_York");
timezone = "Europe/London";
rerender();
expect(result.current).toBe("America/New_York");
});
it("follows timezone changes when useBookerTimezone is true", () => {
let timezone = "America/New_York";
const { result, rerender } = renderHook(() =>
useStableTimezone(timezone, { id: 1, useBookerTimezone: true })
);
expect(result.current).toBe("America/New_York");
timezone = "Europe/London";
rerender();
expect(result.current).toBe("Europe/London");
});
it("follows timezone changes when there is no restriction schedule", () => {
let timezone = "America/New_York";
const { result, rerender } = renderHook(() => useStableTimezone(timezone));
expect(result.current).toBe("America/New_York");
timezone = "Europe/London";
rerender();
expect(result.current).toBe("Europe/London");
});
});
@@ -0,0 +1,13 @@
import { useRef } from "react";
export function useStableTimezone(
timezone: string,
restrictionSchedule?: { id: number | null; useBookerTimezone: boolean }
): string {
const initialRef = useRef(timezone);
const shouldPin =
restrictionSchedule?.id != null &&
restrictionSchedule.id > 0 &&
restrictionSchedule.useBookerTimezone === false;
return shouldPin ? initialRef.current : timezone;
}
+8 -6
View File
@@ -1,5 +1,3 @@
import type { ErrorOption, FieldPath } from "react-hook-form";
import type { RegularBookingCreateResult } from "@calcom/features/bookings/lib/dto/types";
import type { Slots } from "@calcom/features/calendars/lib/types";
import type { PublicEventType } from "@calcom/features/eventtypes/lib/getPublicEvent";
@@ -26,7 +24,10 @@ type BookerEventUser = Pick<
bookerUrl: string;
};
type BookerEventProfile = Pick<PublicEvent["profile"], "name" | "image" | "bookerLayouts">;
type BookerEventProfile = Pick<
PublicEvent["profile"],
"name" | "image" | "bookerLayouts" | "brandColor" | "darkBrandColor" | "theme" | "weekStart" | "username"
>;
// Re-export Slots from the server-safe location
export type { Slots };
@@ -40,10 +41,10 @@ export type BookerEvent = Pick<
| "recurringEvent"
| "entity"
| "locations"
| "enablePerHostLocations"
| "metadata"
| "isDynamic"
| "requiresConfirmation"
| "requiresBookerEmailVerification"
| "price"
| "currency"
| "lockTimeZoneToggleOnBookingPage"
@@ -66,13 +67,14 @@ export type BookerEvent = Pick<
| "interfaceLanguage"
| "team"
| "owner"
| "restrictionScheduleId"
| "useBookerTimezone"
> & {
subsetOfUsers: BookerEventUser[];
showInstantEventConnectNowModal: boolean;
enablePerHostLocations?: boolean;
} & { profile: BookerEventProfile };
export type ValidationErrors<T extends object> = { key: FieldPath<T>; error: ErrorOption }[];
export type EventPrice = { currency: string; price: number; displayAlternateSymbol?: boolean };
export enum EventDetailBlocks {
@@ -172,6 +172,8 @@ export const getPublicEventSelect = (fetchAllUsers: boolean) => {
hidden: true,
assignAllTeamMembers: true,
rescheduleWithSameRoundRobinHost: true,
restrictionScheduleId: true,
useBookerTimezone: true,
parent: {
select: {
team: {
@@ -341,6 +343,8 @@ export const getPublicEvent = async (
return {
...defaultEvent,
bookingFields: getBookingFieldsWithSystemFields({ ...defaultEvent, disableBookingTitle }),
restrictionScheduleId: null,
useBookerTimezone: false,
// Clears meta data since we don't want to send this in the public api.
subsetOfUsers: users.map((user) => ({
...user,
@@ -592,6 +596,8 @@ export const getPublicEvent = async (
disableRescheduling: event.disableRescheduling,
allowReschedulingCancelledBookings: event.allowReschedulingCancelledBookings,
interfaceLanguage: event.interfaceLanguage,
restrictionScheduleId: event.restrictionScheduleId,
useBookerTimezone: event.useBookerTimezone,
};
};
@@ -7,9 +7,10 @@ import {
useBookerStoreContext,
useInitializeBookerStoreContext,
} from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { useBookerLayout } from "@calcom/features/bookings/Booker/hooks/useBookerLayout";
import { useBookingForm } from "@calcom/features/bookings/Booker/hooks/useBookingForm";
import { useLocalSet } from "@calcom/features/bookings/Booker/hooks/useLocalSet";
import { useBookerLayout } from "@calcom/features/bookings/Booker/components/hooks/useBookerLayout";
import { useBookingForm } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm";
import { useLocalSet } from "@calcom/features/bookings/Booker/components/hooks/useLocalSet";
import { useStableTimezone } from "@calcom/features/bookings/Booker/hooks/useStableTimezone";
import { useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
import { useTimePreferences } from "@calcom/features/bookings/lib";
import type { ConnectedDestinationCalendars } from "@calcom/features/calendars/lib/getConnectedDestinationCalendars";
@@ -243,7 +244,13 @@ const BookerPlatformWrapperComponent = (
return restFormValues;
}, [restFormValues]);
const { timezone } = useTimePreferences();
const { timezone: rawTimezone } = useTimePreferences();
const timezone = useStableTimezone(
rawTimezone,
event?.data?.restrictionScheduleId != null
? { id: event.data.restrictionScheduleId, useBookerTimezone: event.data.useBookerTimezone }
: undefined
);
const [calculatedStartTime, calculatedEndTime] = useTimesForSchedule({
month,
@@ -315,7 +322,7 @@ const BookerPlatformWrapperComponent = (
}
}, [schedule.data, schedule.isPending, schedule.error, onTimeslotsLoaded]);
const bookerForm = useBookingForm({
const bookerForm= useBookingForm({
event: event?.data,
sessionEmail:
session?.data?.email && clientId
@@ -9,6 +9,7 @@ import { Header } from "@calcom/features/bookings/components/Header";
import { BookerSection } from "@calcom/features/bookings/components/Section";
import { useAvailableTimeSlots } from "@calcom/features/bookings/Booker/hooks/useAvailableTimeSlots";
import { useBookerLayout } from "@calcom/features/bookings/Booker/hooks/useBookerLayout";
import { useStableTimezone } from "@calcom/features/bookings/Booker/hooks/useStableTimezone";
import { useTimePreferences } from "@calcom/features/bookings/lib";
import { LargeCalendar } from "./components/LargeCalendar";
import { getUsernameList } from "@calcom/features/eventtypes/lib/defaultEvents";
@@ -68,7 +69,13 @@ export const EventTypeCalendarViewComponent = (
bookerLayout,
});
const { timezone } = useTimePreferences();
const { timezone: rawTimezone } = useTimePreferences();
const timezone = useStableTimezone(
rawTimezone,
event?.data?.restrictionScheduleId != null
? { id: event.data.restrictionScheduleId, useBookerTimezone: event.data.useBookerTimezone }
: undefined
);
const isDynamic = useMemo(() => {
return getUsernameList(username ?? "").length > 1;
}, [username]);
@@ -112,7 +119,7 @@ export const EventTypeCalendarViewComponent = (
const selectedEventDuration = useBookerStoreContext((state) => state.selectedDuration);
const eventDuration = selectedEventDuration || event?.data?.length || 30;
const availableTimeSlots = useAvailableTimeSlots({ schedule: schedule.data, eventDuration });
const availableTimeSlots= useAvailableTimeSlots({ schedule: schedule.data, eventDuration });
return (
<AtomsWrapper>
@@ -1,3 +1,4 @@
import type { PublicEventType } from "@calcom/features/eventtypes/lib/getPublicEvent";
import { EventRepository } from "@calcom/features/eventtypes/repositories/EventRepository";
import type { TEventInputSchema } from "./event.schema";
@@ -7,7 +8,7 @@ interface EventHandlerOptions {
userId?: number;
}
export const eventHandler = async ({ input, userId }: EventHandlerOptions) => {
export const eventHandler = async ({ input, userId }: EventHandlerOptions): Promise<PublicEventType> => {
return await EventRepository.getPublicEvent(input, userId);
};