fix: Timezone in Email Embed not working #15525 (#15537)

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
This commit is contained in:
authored and GitHub committed 2025-01-03 08:43:09 +00:00
1 parent 727f7df2c2
commit 821a16648f
14 files changed
+132 -44

No files matched your search

@@ -6,10 +6,10 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Badge, Dialog, DialogContent } from "@calcom/ui";
import { getDurationFormatted } from "../../../components/event-meta/Duration";
import { useTimePreferences } from "../../../lib";
import { useBookerStore } from "../../store";
import { FromTime } from "../../utils/dates";
import { useEvent } from "../../utils/event";
import { useBookerTime } from "../hooks/useBookerTime";
const BookEventFormWrapper = ({ children, onCancel }: { onCancel: () => void; children: ReactNode }) => {
const { data } = useEvent();
@@ -43,7 +43,7 @@ export const BookEventFormWrapperComponent = ({
const { i18n, t } = useLocale();
const selectedTimeslot = useBookerStore((state) => state.selectedTimeslot);
const selectedDuration = useBookerStore((state) => state.selectedDuration);
const { timeFormat, timezone } = useTimePreferences();
const { timeFormat, timezone } = useBookerTime();
if (!selectedTimeslot) {
return null;
}
@@ -19,6 +19,7 @@ import i18nConfigration from "../../../../../i18n.json";
import { fadeInUp } from "../config";
import { useBookerStore } from "../store";
import { FromToTime } from "../utils/dates";
import { useBookerTime } from "./hooks/useBookerTime";
const WebTimezoneSelect = dynamic(
() => import("@calcom/ui/components/form/timezone-select/TimezoneSelect").then((mod) => mod.TimezoneSelect),
@@ -80,7 +81,9 @@ export const EventMeta = ({
};
locale?: string | null;
}) => {
const { setTimezone, timeFormat, timezone } = useTimePreferences();
const { timeFormat, timezone } = useBookerTime();
const [setTimezone] = useTimePreferences((state) => [state.setTimezone]);
const [setBookerStoreTimezone] = useBookerStore((state) => [state.setTimezone], shallow);
const selectedDuration = useBookerStore((state) => state.selectedDuration);
const selectedTimeslot = useBookerStore((state) => state.selectedTimeslot);
const bookerState = useBookerStore((state) => state.state);
@@ -214,7 +217,10 @@ export const EventMeta = ({
container: () => "max-w-full",
}}
value={timezone}
onChange={(tz) => setTimezone(tz.value)}
onChange={({ value }) => {
setTimezone(value);
setBookerStoreTimezone(value);
}}
isDisabled={event.lockTimeZoneToggleOnBookingPage}
/>
</span>
@@ -0,0 +1,22 @@
import { shallow } from "zustand/shallow";
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
import { useTimePreferences } from "../../../lib/timePreferences";
import { getBookerTimezone } from "../../utils/getBookerTimezone";
export const useBookerTime = () => {
const [timezoneFromBookerStore] = useBookerStore((state) => [state.timezone], shallow);
const { timezone: timezoneFromTimePreferences, timeFormat } = useTimePreferences();
const timezone = getBookerTimezone({
storeTimezone: timezoneFromBookerStore,
bookerUserPreferredTimezone: timezoneFromTimePreferences,
});
return {
timezone,
timeFormat,
timezoneFromBookerStore,
timezoneFromTimePreferences,
};
};
@@ -32,6 +32,7 @@ type StoreInitializeType = {
durationConfig?: number[] | null;
org?: string | null;
isInstantMeeting?: boolean;
timezone?: string | null;
teamMemberEmail?: string | null;
crmOwnerRecordType?: string | null;
crmAppSlug?: string | null;
@@ -152,6 +153,9 @@ export type BookerStore = {
org?: string | null;
setOrg: (org: string | null | undefined) => void;
timezone: string | null;
setTimezone: (timezone: string | null) => void;
teamMemberEmail?: string | null;
crmOwnerRecordType?: string | null;
crmAppSlug?: string | null;
@@ -246,6 +250,12 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
set({ seatedEventData });
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.
timezone: getQueryParam("cal.tz") ?? null,
setTimezone: (timezone: string | null) => {
set({ timezone });
},
initialize: ({
username,
eventSlug,
@@ -260,6 +270,7 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
durationConfig,
org,
isInstantMeeting,
timezone = null,
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
@@ -275,6 +286,7 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
get().bookingUid === bookingUid &&
get().bookingData?.responses.email === bookingData?.responses.email &&
get().layout === layout &&
get().timezone === timezone &&
get().rescheduledBy === rescheduledBy &&
get().teamMemberEmail === teamMemberEmail &&
get().crmOwnerRecordType === crmOwnerRecordType &&
@@ -293,6 +305,7 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
layout: layout || BookerLayouts.MONTH_VIEW,
isTeamEvent: isTeamEvent || false,
durationConfig,
timezone,
// Preselect today's date in week / column view, since they use this to show the week title.
selectedDate:
selectedDateInStore ||
@@ -388,6 +401,7 @@ export const useInitializeBookerStore = ({
durationConfig,
org,
isInstantMeeting,
timezone = null,
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
@@ -408,6 +422,7 @@ export const useInitializeBookerStore = ({
verifiedEmail,
durationConfig,
isInstantMeeting,
timezone,
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
@@ -427,6 +442,7 @@ export const useInitializeBookerStore = ({
verifiedEmail,
durationConfig,
isInstantMeeting,
timezone,
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
@@ -1,11 +1,10 @@
import { usePathname } from "next/navigation";
import { shallow } from "zustand/shallow";
import { useSchedule } from "@calcom/features/schedules";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { trpc } from "@calcom/trpc/react";
import { useTimePreferences } from "../../lib/timePreferences";
import { useBookerTime } from "../components/hooks/useBookerTime";
import { useBookerStore } from "../store";
export type useEventReturnType = ReturnType<typeof useEvent>;
@@ -88,7 +87,7 @@ export const useScheduleForEvent = ({
fromRedirectOfNonOrgLink?: boolean;
isTeamEvent?: boolean;
} = {}) => {
const { timezone } = useTimePreferences();
const { timezone } = useBookerTime();
const [usernameFromStore, eventSlugFromStore, monthFromStore, durationFromStore] = useBookerStore(
(state) => [state.username, state.eventSlug, state.month, state.selectedDuration],
shallow
@@ -97,8 +96,6 @@ export const useScheduleForEvent = ({
const searchParams = useCompatSearchParams();
const rescheduleUid = searchParams?.get("rescheduleUid");
const pathname = usePathname();
const schedule = useSchedule({
username: usernameFromStore ?? username,
eventSlug: eventSlugFromStore ?? eventSlug,
@@ -0,0 +1,14 @@
/**
* TODO: We could convert it into a hook and then we can access bookerStore and timePreferences stores directly here, avoiding the need to do it by the caller
*/
export const getBookerTimezone = ({
storeTimezone,
bookerUserPreferredTimezone,
}: {
storeTimezone: string | null;
bookerUserPreferredTimezone: string;
}) => {
// BookerStore timezone is the one that is updated no matter what could be the reason of the update
// e.g. timezone configured through cal.tz query param is available there but not in the preferences as those are booker user's preferences
return storeTimezone ?? bookerUserPreferredTimezone;
};
@@ -15,9 +15,9 @@ import { localStorage } from "@calcom/lib/webstorage";
import type { IGetAvailableSlots } from "@calcom/trpc/server/routers/viewer/slots/util";
import { Button, Icon, SkeletonText } from "@calcom/ui";
import { useBookerTime } from "../Booker/components/hooks/useBookerTime";
import { useBookerStore } from "../Booker/store";
import { getQueryParam } from "../Booker/utils/query-param";
import { useTimePreferences } from "../lib";
import { useCheckOverlapWithOverlay } from "../lib/useCheckOverlapWithOverlay";
import { SeatsAvailabilityText } from "./SeatsAvailabilityText";
@@ -64,8 +64,8 @@ const SlotItem = ({
const { t } = useLocale();
const overlayCalendarToggled =
getQueryParam("overlayCalendar") === "true" || localStorage?.getItem("overlayCalendarSwitchDefault");
const [timeFormat, timezone] = useTimePreferences((state) => [state.timeFormat, state.timezone]);
getQueryParam("overlayCalendar") === "true" || localStorage.getItem("overlayCalendarSwitchDefault");
const { timeFormat, timezone } = useBookerTime();
const bookingData = useBookerStore((state) => state.bookingData);
const layout = useBookerStore((state) => state.layout);
const { data: eventData } = event;
@@ -8,7 +8,7 @@ import { getRecurringFreq } from "@calcom/lib/recurringStrings";
import { Tooltip, Alert } from "@calcom/ui";
import { Input } from "@calcom/ui";
import { useTimePreferences } from "../../lib";
import { useBookerTime } from "../../Booker/components/hooks/useBookerTime";
export const EventOccurences = ({ event }: { event: Pick<BookerEvent, "recurringEvent"> }) => {
const maxOccurences = event.recurringEvent?.count || null;
@@ -23,7 +23,7 @@ export const EventOccurences = ({ event }: { event: Pick<BookerEvent, "recurring
);
const selectedTimeslot = useBookerStore((state) => state.selectedTimeslot);
const bookerState = useBookerStore((state) => state.state);
const { timezone, timeFormat } = useTimePreferences();
const { timezone, timeFormat } = useBookerTime();
const [warning, setWarning] = useState(false);
// Set initial value in booker store.
useEffect(() => {
@@ -1,8 +1,8 @@
import React, { useEffect, useMemo, useRef } from "react";
import { useTimePreferences } from "@calcom/features/bookings/lib/timePreferences";
import { classNames } from "@calcom/lib";
import { useBookerTime } from "../../../bookings/Booker/components/hooks/useBookerTime";
import { useCalendarStore } from "../state/store";
import "../styles/styles.css";
import type { CalendarComponentProps } from "../types/state";
@@ -23,7 +23,7 @@ export function Calendar(props: CalendarComponentProps) {
const containerOffset = useRef<HTMLDivElement | null>(null);
const schedulerGrid = useRef<HTMLOListElement | null>(null);
const initialState = useCalendarStore((state) => state.initState);
const { timezone } = useTimePreferences();
const { timezone } = useBookerTime();
const startDate = useCalendarStore((state) => state.startDate);
const endDate = useCalendarStore((state) => state.endDate);
@@ -1,8 +1,8 @@
import { useEffect, useState, useRef } from "react";
import dayjs from "@calcom/dayjs";
import { useTimePreferences } from "@calcom/features/bookings/lib";
import { useBookerTime } from "../../../../bookings/Booker/components/hooks/useBookerTime";
import { useCalendarStore } from "../../state/store";
function calculateMinutesFromStart(startHour: number, currentHour: number, currentMinute: number) {
@@ -19,7 +19,7 @@ export function CurrentTime() {
startHour: state.startHour || 0,
endHour: state.endHour || 23,
}));
const { timeFormat, timezone } = useTimePreferences();
const { timeFormat, timezone } = useBookerTime();
useEffect(() => {
// Set the container scroll position based on the current time.
@@ -3,10 +3,10 @@ import { shallow } from "zustand/shallow";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import { useTimePreferences } from "@calcom/features/bookings/lib";
import { classNames } from "@calcom/lib";
import { OutOfOfficeInSlots } from "../../../../bookings/Booker/components/OutOfOfficeInSlots";
import { useBookerTime } from "../../../../bookings/Booker/components/hooks/useBookerTime";
import { useCalendarStore } from "../../state/store";
import type { CalendarAvailableTimeslots } from "../../types/state";
import type { GridCellToDateProps } from "../../utils";
@@ -39,7 +39,7 @@ type AvailableCellProps = {
};
export function AvailableCellsForDay({ availableSlots, day, startHour }: AvailableCellProps) {
const { timezone } = useTimePreferences();
const { timezone } = useBookerTime();
const date = dayjs(day);
const dateFormatted = date.format("YYYY-MM-DD");
const slotsForToday = availableSlots && availableSlots[dateFormatted];
@@ -136,7 +136,7 @@ type CellProps = {
};
function Cell({ isDisabled, topOffsetMinutes, timeSlot }: CellProps) {
const { timeFormat } = useTimePreferences();
const { timeFormat } = useBookerTime();
const { onEmptyCellClick, hoverEventDuration } = useCalendarStore(
(state) => ({
+51 -17
View File
@@ -13,7 +13,6 @@ import dayjs from "@calcom/dayjs";
import { AvailableTimes, AvailableTimesHeader } from "@calcom/features/bookings";
import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
import { useEvent, useScheduleForEvent } from "@calcom/features/bookings/Booker/utils/event";
import { useTimePreferences } from "@calcom/features/bookings/lib/timePreferences";
import DatePicker from "@calcom/features/calendars/DatePicker";
import { useNonEmptyScheduleDays } from "@calcom/features/schedules";
import { useSlotsForDate } from "@calcom/features/schedules/lib/use-schedule/useSlotsForDate";
@@ -41,6 +40,7 @@ import {
TimezoneSelect,
} from "@calcom/ui";
import { useBookerTime } from "../bookings/Booker/components/hooks/useBookerTime";
import { buildCssVarsPerTheme } from "./lib/buildCssVarsPerTheme";
import { getDimension } from "./lib/getDimension";
import type { EmbedTabs, EmbedType, EmbedTypes, PreviewState } from "./types";
@@ -69,6 +69,19 @@ const queryParamsForDialog = [
"month",
];
function chooseTimezone({
timezoneFromBookerStore,
timezoneFromTimePreferences,
userSettingsTimezone,
}: {
timezoneFromBookerStore: string | null;
timezoneFromTimePreferences: string;
userSettingsTimezone: string | undefined;
}) {
// We prefer user's timezone configured in settings at the moment - Might be a better idea to prefer timezoneFromTimePreferences over user settings as the user might be in different timezone
return timezoneFromBookerStore ?? userSettingsTimezone ?? timezoneFromTimePreferences;
}
function useRouterHelpers() {
const router = useRouter();
const searchParams = useCompatSearchParams();
@@ -146,15 +159,21 @@ const EmailEmbed = ({
username,
orgSlug,
isTeamEvent,
userSettingsTimezone,
}: {
eventType?: EventType;
username: string;
orgSlug?: string;
isTeamEvent: boolean;
userSettingsTimezone?: string;
}) => {
const { t, i18n } = useLocale();
const [timezone] = useTimePreferences((state) => [state.timezone]);
const { timezoneFromBookerStore, timezoneFromTimePreferences } = useBookerTime();
const timezone = chooseTimezone({
timezoneFromBookerStore,
timezoneFromTimePreferences,
userSettingsTimezone,
});
useInitializeBookerStore({
username,
@@ -169,15 +188,17 @@ const EmailEmbed = ({
(state) => [state.month, state.selectedDate, state.selectedDatesAndTimes],
shallow
);
const [setSelectedDate, setMonth, setSelectedDatesAndTimes, setSelectedTimeslot] = useBookerStore(
(state) => [
state.setSelectedDate,
state.setMonth,
state.setSelectedDatesAndTimes,
state.setSelectedTimeslot,
],
shallow
);
const [setSelectedDate, setMonth, setSelectedDatesAndTimes, setSelectedTimeslot, setTimezone] =
useBookerStore(
(state) => [
state.setSelectedDate,
state.setMonth,
state.setSelectedDatesAndTimes,
state.setSelectedTimeslot,
state.setTimezone,
],
shallow
);
const event = useEvent();
const schedule = useScheduleForEvent({ orgSlug, eventId: eventType?.id, isTeamEvent });
const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots);
@@ -306,7 +327,7 @@ const EmailEmbed = ({
<Collapsible open>
<CollapsibleContent>
<div className="text-default mb-[9px] text-sm">{t("timezone")}</div>
<TimezoneSelect id="timezone" value={timezone} isDisabled />
<TimezoneSelect id="timezone" value={timezone} onChange={({ value }) => setTimezone(value)} />
</CollapsibleContent>
</Collapsible>
</div>
@@ -321,6 +342,7 @@ const EmailEmbedPreview = ({
month,
selectedDateAndTime,
calLink,
userSettingsTimezone,
}: {
eventType: EventType;
timezone?: string;
@@ -329,9 +351,15 @@ const EmailEmbedPreview = ({
month?: string;
selectedDateAndTime: { [key: string]: string[] };
calLink: string;
userSettingsTimezone?: string;
}) => {
const { t } = useLocale();
const [timeFormat, timezone] = useTimePreferences((state) => [state.timeFormat, state.timezone]);
const { timeFormat, timezoneFromBookerStore, timezoneFromTimePreferences } = useBookerTime();
const timezone = chooseTimezone({
timezoneFromBookerStore,
timezoneFromTimePreferences,
userSettingsTimezone,
});
if (!eventType) {
return null;
@@ -392,7 +420,10 @@ const EmailEmbedPreview = ({
Object.keys(selectedDateAndTime)
.sort()
.map((key) => {
const selectedDate = dayjs(key).tz(timezone).format("dddd, MMMM D, YYYY");
const firstSlotOfSelectedDay = selectedDateAndTime[key][0];
const selectedDate = dayjs(firstSlotOfSelectedDay)
.tz(timezone)
.format("dddd, MMMM D, YYYY");
return (
<table
key={key}
@@ -431,7 +462,7 @@ const EmailEmbedPreview = ({
eventType.teamId !== null ? "team/" : ""
}${username}/${eventType.slug}?duration=${
eventType.length
}&date=${key}&month=${month}&slot=${time}`;
}&date=${key}&month=${month}&slot=${time}&cal.tz=${timezone}`;
return (
<td
key={time}
@@ -495,7 +526,7 @@ const EmailEmbedPreview = ({
<a
className="more"
data-testid="see_all_available_times"
href={`${eventType.bookerUrl}/${calLink}`}
href={`${eventType.bookerUrl}/${calLink}?cal.tz=${timezone}`}
style={{
textDecoration: "none",
cursor: "pointer",
@@ -559,6 +590,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
{ id: parsedEventId },
{ enabled: !Number.isNaN(parsedEventId) && embedType === "email", refetchOnWindowFocus: false }
);
const { data: userSettings } = trpc.viewer.me.useQuery();
const teamSlug = !!eventTypeData?.team ? eventTypeData.team.slug : null;
@@ -769,6 +801,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
<EmailEmbed
eventType={eventTypeData?.eventType}
username={teamSlug ?? (data?.user.username as string)}
userSettingsTimezone={userSettings?.timeZone}
orgSlug={data?.user?.org?.slug}
isTeamEvent={!!teamSlug}
/>
@@ -1120,6 +1153,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
eventType={eventTypeData?.eventType}
emailContentRef={emailContentRef}
username={teamSlug ?? (data?.user.username as string)}
userSettingsTimezone={userSettings?.timeZone}
month={month as string}
selectedDateAndTime={
selectedDatesAndTimes
@@ -44,6 +44,7 @@ export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => {
const bookingUid =
typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("bookingUid") : null;
const date = dayjs(selectedDate).format("YYYY-MM-DD");
const timezone = searchParams?.get("cal.tz") || null;
useEffect(() => {
// This event isn't processed by BookingPageTagManager because BookingPageTagManager hasn't loaded when it is fired. I think we should have a queue in fire method to handle this.
@@ -58,6 +59,7 @@ export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => {
bookingUid: bookingUid,
layout: bookerLayout.defaultLayout,
org: props.entity.orgSlug,
timezone,
});
const [bookerState, _] = useBookerStore((state) => [state.state, state.setState], shallow);
@@ -1,11 +1,8 @@
import { useBookerTime } from "@calcom/features/bookings/Booker/components/hooks/useBookerTime";
import type { UseBookingFormReturnType } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm";
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
import { setLastBookingResponse } from "@calcom/features/bookings/Booker/utils/lastBookingResponse";
import {
useTimePreferences,
mapBookingToMutationInput,
mapRecurringBookingToMutationInput,
} from "@calcom/features/bookings/lib";
import { mapBookingToMutationInput, mapRecurringBookingToMutationInput } from "@calcom/features/bookings/lib";
import type { BookerEvent } from "@calcom/features/bookings/types";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { BookingCreateBody } from "@calcom/prisma/zod-utils";
@@ -41,7 +38,7 @@ export const useHandleBookEvent = ({
const setFormValues = useBookerStore((state) => state.setFormValues);
const timeslot = useBookerStore((state) => state.selectedTimeslot);
const duration = useBookerStore((state) => state.selectedDuration);
const { timezone } = useTimePreferences();
const { timezone } = useBookerTime();
const rescheduleUid = useBookerStore((state) => state.rescheduleUid);
const rescheduledBy = useBookerStore((state) => state.rescheduledBy);
const { t, i18n } = useLocale();