feat: add booker lifecycle SDK events for embed tracking (#25569)
* feat: add embed prerendering support and enhance event handling - Introduced `useIsEmbedPrerendering` hook to determine if the embed is in prerender mode. - Updated `useAvailabilityEvents` to prevent firing events during prerendering. - Added `bookerLoadedEvent` and `availabilityRefreshed` types to `EventDataMap`. - Implemented `useFirebookerLoadedEvent` to manage firing the booker loaded event conditionally. - Refactored event firing logic in `useSchedule` and `BookerWebWrapper` components to utilize new hooks. * feat: add embed prerendering support and enhance event handling - Introduced `useIsEmbedPrerendering` hook to determine if the embed is in prerender mode. - Updated `useAvailabilityEvents` to prevent firing events during prerendering. - Added `bookerLoadedEvent` and `availabilityRefreshed` types to `EventDataMap`. - Implemented `useFirebookerLoadedEvent` to manage firing the booker loaded event conditionally. - Refactored event firing logic in `useSchedule` and `BookerWebWrapper` components to utilize new hooks. * feat: enhance embed event handling and introduce link reopening detection - Added `useEmbedReopened` hook to track when the embed is reopened. - Updated `BookerWebWrapper` to reset event firing state upon embed reopening. - Refactored event firing logic to use `bookerViewed` instead of `bookerLoadedEvent`. - Introduced scheduling for event firing in `useSchedule` to ensure correct order of events during prerendering. * feat: add lifecycle diagrams for inline and modal embeds - Introduced `inline-embed-lifecycle.mermaid` and `modal-embed-lifecycle.mermaid` files to visualize the lifecycle events and states of inline and modal embeds. - Updated `LIFECYCLE.md` to reference the new diagrams and provide a clearer explanation of the embed lifecycle processes. - Added `modal-prerendering-flow.mermaid` to illustrate the prerendering flow for modal embeds. - Enhanced the routing playground with new features and improved event handling for availability and booking events. * refactor: update event handling for booker lifecycle events - Replaced `availabilityLoaded` event with `bookerReady` to better reflect the state when the booker view is fully loaded and ready for interaction. - Updated related documentation and diagrams to reflect changes in event triggers and descriptions. - Adjusted internal state management to track `viewId` instead of `reopenCount` for distinguishing between initial views and reopens. - Added tests for new event handling logic to ensure correct firing of `bookerViewed`, `bookerReopened`, and `bookerReady` events. * refactor: enhance embed event handling and state management - Updated event handling for booker lifecycle events, replacing `resetViewVariables` with `resetPageData` to manage page-specific state. - Introduced new utility functions for managing event firing states and reload initiation. - Refactored `fireBookerViewedEvent` and `fireBookerReadyEvent` to utilize the new state management functions. - Added comprehensive tests for the updated event handling logic and state resets to ensure correct functionality across various scenarios. * refactor: update embed iframe configuration and utility functions - Reduced `slotsStaleTimeMs` from 30 seconds to 10 seconds and `iframeForceReloadThresholdMs` from 100 seconds to 30 seconds for improved responsiveness. - Refactored utility functions to use `isBrowser` for client-side checks instead of `isClientSide`. - Removed unused `isPrerendering` function and updated related documentation for clarity. - Enhanced event handling by exporting `useBookerEmbedEvents` from the appropriate module for better accessibility. * refactor: update embed iframe configuration and utility functions - Reduced `slotsStaleTimeMs` from 30 seconds to 10 seconds and `iframeForceReloadThresholdMs` from 100 seconds to 30 seconds for improved responsiveness. - Refactored utility functions to use `isBrowser` for client-side checks instead of `isClientSide`. - Removed unused `isPrerendering` function and updated related documentation for clarity. - Enhanced event handling by exporting `useBookerEmbedEvents` from the appropriate module for better accessibility. * fix cubic feedback
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { renderHook } from "@testing-library/react";
|
||||
|
||||
import { sdkActionManager } from "../../sdk-event";
|
||||
import { embedStore, resetPageData } from "../lib/embedStore";
|
||||
import { useBookerEmbedEvents } from "../react-hooks";
|
||||
import { fakeCurrentDocumentUrl } from "./test-utils";
|
||||
|
||||
const createTestSchedule = (overrides?: {
|
||||
isSuccess?: boolean;
|
||||
dataUpdatedAt?: number;
|
||||
}) => ({
|
||||
isSuccess: overrides?.isSuccess ?? true,
|
||||
dataUpdatedAt: overrides?.dataUpdatedAt ?? Date.now(),
|
||||
});
|
||||
|
||||
const createTestEventConfig = (overrides?: {
|
||||
eventId?: number | undefined;
|
||||
eventSlug?: string | undefined;
|
||||
}) => ({
|
||||
eventId: overrides && "eventId" in overrides ? overrides.eventId : 123,
|
||||
eventSlug: overrides && "eventSlug" in overrides ? overrides.eventSlug : "test-event",
|
||||
});
|
||||
|
||||
const createTestEmbedState = (overrides?: {
|
||||
viewId?: number | null;
|
||||
bookerViewedHasFired?: boolean;
|
||||
bookerReopenedHasFired?: boolean;
|
||||
bookerReloadedHasFired?: boolean;
|
||||
bookerReadyHasFired?: boolean;
|
||||
reloadInitiated?: boolean;
|
||||
}) => {
|
||||
embedStore.viewId = overrides?.viewId ?? null;
|
||||
embedStore.pageData.eventsState.bookerViewed.hasFired =
|
||||
overrides?.bookerViewedHasFired ?? false;
|
||||
embedStore.pageData.eventsState.bookerReopened.hasFired =
|
||||
overrides?.bookerReopenedHasFired ?? false;
|
||||
embedStore.pageData.eventsState.bookerReloaded.hasFired =
|
||||
overrides?.bookerReloadedHasFired ?? false;
|
||||
embedStore.pageData.eventsState.bookerReady.hasFired = overrides?.bookerReadyHasFired ?? false;
|
||||
embedStore.pageData.reloadInitiated = overrides?.reloadInitiated ?? false;
|
||||
};
|
||||
|
||||
const expectEventFired = (
|
||||
firedEvents: Array<{ type: string; data: unknown }>,
|
||||
eventType: string,
|
||||
expectedData?: unknown
|
||||
) => {
|
||||
const event = firedEvents.find((e) => e.type === eventType);
|
||||
expect(event).toBeDefined();
|
||||
if (expectedData) {
|
||||
expect(event).toMatchObject({ type: eventType, data: expectedData });
|
||||
}
|
||||
return event;
|
||||
};
|
||||
|
||||
const expectEventNotFired = (
|
||||
firedEvents: Array<{ type: string; data: unknown }>,
|
||||
eventType: string
|
||||
) => {
|
||||
const event = firedEvents.find((e) => e.type === eventType);
|
||||
expect(event).toBeUndefined();
|
||||
};
|
||||
|
||||
const expectEventCount = (
|
||||
firedEvents: Array<{ type: string; data: unknown }>,
|
||||
eventType: string,
|
||||
expectedCount: number
|
||||
) => {
|
||||
const events = firedEvents.filter((e) => e.type === eventType);
|
||||
expect(events).toHaveLength(expectedCount);
|
||||
};
|
||||
|
||||
describe("useBookerEmbedEvents", () => {
|
||||
let firedEvents: Array<{ type: string; data: unknown }> = [];
|
||||
|
||||
beforeEach(() => {
|
||||
createTestEmbedState();
|
||||
firedEvents = [];
|
||||
|
||||
if (!sdkActionManager) {
|
||||
throw new Error("sdkActionManager is not defined");
|
||||
}
|
||||
vi.spyOn(sdkActionManager, "fire").mockImplementation((type, data) => {
|
||||
firedEvents.push({ type, data });
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("bookerViewed event", () => {
|
||||
it("should fire bookerViewed when first view opens with slots loaded", () => {
|
||||
fakeCurrentDocumentUrl();
|
||||
createTestEmbedState({ viewId: 1 });
|
||||
|
||||
const eventConfig = createTestEventConfig();
|
||||
const schedule = createTestSchedule({ isSuccess: true });
|
||||
|
||||
renderHook(() => useBookerEmbedEvents({ ...eventConfig, schedule }));
|
||||
|
||||
expect(firedEvents).toHaveLength(2);
|
||||
expectEventFired(firedEvents, "bookerViewed", {
|
||||
eventId: eventConfig.eventId,
|
||||
eventSlug: eventConfig.eventSlug,
|
||||
slotsLoaded: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should fire bookerViewed when first view opens without slots loaded", () => {
|
||||
fakeCurrentDocumentUrl();
|
||||
createTestEmbedState({ viewId: 1 });
|
||||
|
||||
const eventConfig = createTestEventConfig();
|
||||
const schedule = createTestSchedule({ isSuccess: false });
|
||||
|
||||
renderHook(() => useBookerEmbedEvents({ ...eventConfig, schedule }));
|
||||
|
||||
expect(firedEvents).toHaveLength(1);
|
||||
expectEventFired(firedEvents, "bookerViewed", {
|
||||
eventId: null,
|
||||
eventSlug: null,
|
||||
slotsLoaded: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should fire bookerReopened when embed is reopened after being closed", () => {
|
||||
fakeCurrentDocumentUrl();
|
||||
createTestEmbedState({ viewId: 2 });
|
||||
|
||||
const eventConfig = createTestEventConfig();
|
||||
const schedule = createTestSchedule({ isSuccess: true });
|
||||
|
||||
renderHook(() => useBookerEmbedEvents({ ...eventConfig, schedule }));
|
||||
|
||||
expect(firedEvents).toHaveLength(2);
|
||||
expectEventFired(firedEvents, "bookerReopened", {
|
||||
eventId: eventConfig.eventId,
|
||||
eventSlug: eventConfig.eventSlug,
|
||||
slotsLoaded: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should fire bookerReloaded when reload is initiated", () => {
|
||||
fakeCurrentDocumentUrl();
|
||||
createTestEmbedState({ viewId: 1, reloadInitiated: true });
|
||||
|
||||
const eventConfig = createTestEventConfig();
|
||||
const schedule = createTestSchedule({ isSuccess: true });
|
||||
|
||||
renderHook(() => useBookerEmbedEvents({ ...eventConfig, schedule }));
|
||||
|
||||
expect(firedEvents).toHaveLength(2);
|
||||
expectEventFired(firedEvents, "bookerReloaded", {
|
||||
eventId: eventConfig.eventId,
|
||||
eventSlug: eventConfig.eventSlug,
|
||||
slotsLoaded: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should not fire events multiple times when component rerenders", () => {
|
||||
fakeCurrentDocumentUrl();
|
||||
createTestEmbedState({ viewId: 1 });
|
||||
|
||||
const eventConfig = createTestEventConfig();
|
||||
const schedule = createTestSchedule({ isSuccess: true });
|
||||
|
||||
const { rerender } = renderHook(() =>
|
||||
useBookerEmbedEvents({ ...eventConfig, schedule })
|
||||
);
|
||||
|
||||
expect(firedEvents).toHaveLength(2);
|
||||
|
||||
rerender();
|
||||
expect(firedEvents).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bookerReady event", () => {
|
||||
it("should fire bookerReady once per view when slots are loaded", () => {
|
||||
fakeCurrentDocumentUrl();
|
||||
createTestEmbedState({ viewId: 1 });
|
||||
|
||||
const eventConfig = createTestEventConfig();
|
||||
const schedule = createTestSchedule({ isSuccess: true });
|
||||
|
||||
const { rerender } = renderHook(() =>
|
||||
useBookerEmbedEvents({ ...eventConfig, schedule })
|
||||
);
|
||||
|
||||
expectEventFired(firedEvents, "bookerReady", {
|
||||
eventId: eventConfig.eventId,
|
||||
eventSlug: eventConfig.eventSlug,
|
||||
});
|
||||
|
||||
rerender();
|
||||
expectEventCount(firedEvents, "bookerReady", 1);
|
||||
});
|
||||
|
||||
it("should not fire bookerReady when slots are not loaded", () => {
|
||||
fakeCurrentDocumentUrl();
|
||||
createTestEmbedState({ viewId: 1 });
|
||||
|
||||
const eventConfig = createTestEventConfig();
|
||||
const schedule = createTestSchedule({ isSuccess: false });
|
||||
|
||||
renderHook(() => useBookerEmbedEvents({ ...eventConfig, schedule }));
|
||||
|
||||
expectEventNotFired(firedEvents, "bookerReady");
|
||||
});
|
||||
|
||||
it("should not fire bookerReady when eventId is missing", () => {
|
||||
fakeCurrentDocumentUrl();
|
||||
createTestEmbedState({ viewId: 1 });
|
||||
|
||||
const eventConfig = createTestEventConfig({ eventId: undefined });
|
||||
const schedule = createTestSchedule({ isSuccess: true });
|
||||
|
||||
renderHook(() => useBookerEmbedEvents({ ...eventConfig, schedule }));
|
||||
|
||||
expectEventNotFired(firedEvents, "bookerReady");
|
||||
});
|
||||
|
||||
it("should not fire bookerReady when eventSlug is missing", () => {
|
||||
fakeCurrentDocumentUrl();
|
||||
createTestEmbedState({ viewId: 1 });
|
||||
|
||||
const eventConfig = createTestEventConfig({ eventSlug: undefined });
|
||||
const schedule = createTestSchedule({ isSuccess: true });
|
||||
|
||||
renderHook(() => useBookerEmbedEvents({ ...eventConfig, schedule }));
|
||||
|
||||
expectEventNotFired(firedEvents, "bookerReady");
|
||||
});
|
||||
});
|
||||
|
||||
describe("prerendering", () => {
|
||||
it("should not fire events when embed is in prerendering mode", () => {
|
||||
fakeCurrentDocumentUrl({ params: { prerender: "true" } });
|
||||
createTestEmbedState({ viewId: 1 });
|
||||
|
||||
const eventConfig = createTestEventConfig();
|
||||
const schedule = createTestSchedule({ isSuccess: true });
|
||||
|
||||
renderHook(() => useBookerEmbedEvents({ ...eventConfig, schedule }));
|
||||
|
||||
expect(firedEvents).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resetPageData", () => {
|
||||
it("should reset all page-specific data including hasFired flags", () => {
|
||||
createTestEmbedState({
|
||||
bookerViewedHasFired: true,
|
||||
bookerReopenedHasFired: true,
|
||||
bookerReloadedHasFired: true,
|
||||
bookerReadyHasFired: true,
|
||||
reloadInitiated: true,
|
||||
});
|
||||
|
||||
resetPageData();
|
||||
|
||||
expect(embedStore.pageData.eventsState.bookerViewed.hasFired).toBe(false);
|
||||
expect(embedStore.pageData.eventsState.bookerReopened.hasFired).toBe(false);
|
||||
expect(embedStore.pageData.eventsState.bookerReloaded.hasFired).toBe(false);
|
||||
expect(embedStore.pageData.eventsState.bookerReady.hasFired).toBe(false);
|
||||
expect(embedStore.pageData.reloadInitiated).toBe(false);
|
||||
});
|
||||
|
||||
it("should allow events to fire again after reset when new view starts", () => {
|
||||
fakeCurrentDocumentUrl();
|
||||
createTestEmbedState({ viewId: 1 });
|
||||
|
||||
const eventConfig = createTestEventConfig();
|
||||
const schedule = createTestSchedule({ isSuccess: true });
|
||||
|
||||
renderHook(() => useBookerEmbedEvents({ ...eventConfig, schedule }));
|
||||
|
||||
expect(firedEvents.length).toBeGreaterThan(0);
|
||||
firedEvents = [];
|
||||
|
||||
resetPageData();
|
||||
createTestEmbedState({ viewId: 2 });
|
||||
|
||||
renderHook(() => useBookerEmbedEvents({ ...eventConfig, schedule }));
|
||||
|
||||
expect(firedEvents.length).toBeGreaterThan(0);
|
||||
expectEventFired(firedEvents, "bookerReopened");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
SetStyles,
|
||||
setNonStylesConfig,
|
||||
} from "../../types";
|
||||
import { runAsap } from "./utils";
|
||||
import { runAsap, log } from "./utils";
|
||||
|
||||
export const enum EMBED_IFRAME_STATE {
|
||||
NOT_INITIALIZED,
|
||||
@@ -115,6 +115,153 @@ export const embedStore = {
|
||||
* We maintain a list of all setUiConfig setters that are in use at the moment so that we can update all those components.
|
||||
*/
|
||||
setUiConfig: [] as ((arg0: UiConfig) => void)[],
|
||||
/**
|
||||
* Unique identifier for the current link view. Incremented on each `linkReady` event (non-prerendering).
|
||||
* - null = not yet initialized (before first linkReady)
|
||||
* - 1 = first view (triggers bookerViewed)
|
||||
* - 2+ = subsequent views/reopens (triggers bookerReopened)
|
||||
*
|
||||
* `linkReady` fires when iframe content is fully ready for user interaction
|
||||
* (after content height is known and slots are loaded if skeleton loader is used).
|
||||
*/
|
||||
viewId: null as number | null,
|
||||
/**
|
||||
* Page-specific state that gets reset when a new page/view loads.
|
||||
* All state that should be cleared between page views should be stored here.
|
||||
*/
|
||||
pageData: {
|
||||
/**
|
||||
* State for tracking embed events (bookerViewed, bookerReady, etc.)
|
||||
*/
|
||||
eventsState: {
|
||||
/**
|
||||
* Tracks whether bookerViewed event has been fired for the current view.
|
||||
* Reset to false when linkReady fires (via resetPageData).
|
||||
*/
|
||||
bookerViewed: {
|
||||
hasFired: false,
|
||||
},
|
||||
/**
|
||||
* Tracks whether bookerReopened event has been fired for the current view.
|
||||
* Reset to false when linkReady fires (via resetPageData).
|
||||
*/
|
||||
bookerReopened: {
|
||||
hasFired: false,
|
||||
},
|
||||
/**
|
||||
* Tracks whether bookerReloaded event has been fired for the current view.
|
||||
* Reset to false when linkReady fires (via resetPageData).
|
||||
*/
|
||||
bookerReloaded: {
|
||||
hasFired: false,
|
||||
},
|
||||
/**
|
||||
* Tracks whether bookerReady event has been fired for the current view.
|
||||
* Reset to false when linkReady fires (via resetPageData).
|
||||
*/
|
||||
bookerReady: {
|
||||
hasFired: false,
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Flag to indicate that a reload was initiated and bookerReloaded should fire on next linkReady.
|
||||
* Set to true when __reloadInitiated is received.
|
||||
* Reset after firing bookerReloaded on linkReady.
|
||||
*/
|
||||
reloadInitiated: false,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Resets all page-specific data
|
||||
*/
|
||||
export function resetPageData() {
|
||||
log('Resetting page data');
|
||||
embedStore.pageData = {
|
||||
eventsState: {
|
||||
bookerViewed: {
|
||||
hasFired: false,
|
||||
},
|
||||
bookerReopened: {
|
||||
hasFired: false,
|
||||
},
|
||||
bookerReloaded: {
|
||||
hasFired: false,
|
||||
},
|
||||
bookerReady: {
|
||||
hasFired: false,
|
||||
},
|
||||
},
|
||||
reloadInitiated: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Type for direct properties of pageData (excluding nested objects like eventsState)
|
||||
*/
|
||||
type PageDataDirectProps = Omit<typeof embedStore.pageData, "eventsState">;
|
||||
|
||||
/**
|
||||
* Gets a direct property from pageData in a type-safe way
|
||||
*/
|
||||
export function getPageDataProp<K extends keyof PageDataDirectProps>(
|
||||
key: K
|
||||
): PageDataDirectProps[K] {
|
||||
return embedStore.pageData[key] as PageDataDirectProps[K];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a direct property on pageData in a type-safe way
|
||||
*/
|
||||
export function setPageDataProp<K extends keyof PageDataDirectProps>(
|
||||
key: K,
|
||||
value: PageDataDirectProps[K]
|
||||
): void {
|
||||
embedStore.pageData[key] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event names that have a hasFired state
|
||||
*/
|
||||
type EventNameWithHasFiredState = "bookerViewed" | "bookerReopened" | "bookerReloaded" | "bookerReady";
|
||||
|
||||
/**
|
||||
* Gets whether a particular event has fired
|
||||
*/
|
||||
export function getEventHasFired(eventName: EventNameWithHasFiredState): boolean {
|
||||
return embedStore.pageData.eventsState[eventName].hasFired;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether a particular event has fired
|
||||
*/
|
||||
export function setEventHasFired(eventName: EventNameWithHasFiredState, value: boolean): void {
|
||||
embedStore.pageData.eventsState[eventName].hasFired = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether reload was initiated
|
||||
*/
|
||||
export function getReloadInitiated(): boolean {
|
||||
return getPageDataProp("reloadInitiated");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether reload was initiated
|
||||
*/
|
||||
export function setReloadInitiated(value: boolean): void {
|
||||
setPageDataProp("reloadInitiated", value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the viewId counter (1 = first view, 2+ = reopens).
|
||||
*/
|
||||
export function incrementView(): void {
|
||||
if (!embedStore.viewId) {
|
||||
embedStore.viewId = 1;
|
||||
} else {
|
||||
embedStore.viewId++;
|
||||
}
|
||||
}
|
||||
|
||||
export type EmbedStore = typeof embedStore;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { sdkActionManager } from "../../sdk-event";
|
||||
import { type EmbedStore } from "../lib/embedStore";
|
||||
|
||||
export const isBrowser = typeof window !== "undefined";
|
||||
|
||||
export function runAsap(fn: (...arg: unknown[]) => void) {
|
||||
// We don't use rAF because it runs slower in Safari plus doesn't run if the iframe is hidden sometimes
|
||||
return setTimeout(fn, 50);
|
||||
@@ -14,6 +16,10 @@ export function isBookerReady() {
|
||||
return window._embedBookerState === "slotsDone";
|
||||
}
|
||||
|
||||
export const isPrerendering = () => {
|
||||
return new URL(document.URL).searchParams.get("prerender") === "true";
|
||||
};
|
||||
|
||||
function isSkeletonSupportedPageType() {
|
||||
const url = new URL(document.URL);
|
||||
const pageType = url.searchParams.get("cal.embed.pageType");
|
||||
@@ -98,13 +104,13 @@ export function keepParentInformedAboutDimensionChanges({ embedStore }: { embedS
|
||||
// Use, .height as that gives more accurate value in floating point. Also, do a ceil on the total sum so that whatever happens there is enough iframe size to avoid scroll.
|
||||
const contentHeight = Math.ceil(
|
||||
parseFloat(mainElementStyles.height) +
|
||||
parseFloat(mainElementStyles.marginTop) +
|
||||
parseFloat(mainElementStyles.marginBottom)
|
||||
parseFloat(mainElementStyles.marginTop) +
|
||||
parseFloat(mainElementStyles.marginBottom)
|
||||
);
|
||||
const contentWidth = Math.ceil(
|
||||
parseFloat(mainElementStyles.width) +
|
||||
parseFloat(mainElementStyles.marginLeft) +
|
||||
parseFloat(mainElementStyles.marginRight)
|
||||
parseFloat(mainElementStyles.marginLeft) +
|
||||
parseFloat(mainElementStyles.marginRight)
|
||||
);
|
||||
|
||||
// During first render let iframe tell parent that how much is the expected height to avoid scroll.
|
||||
@@ -176,3 +182,31 @@ export const recordResponseIfQueued = async (params: Record<string, string | str
|
||||
}
|
||||
return routingFormResponseId;
|
||||
};
|
||||
|
||||
/**
|
||||
* embedStore dependency free version of getNamespace
|
||||
* For internal use only
|
||||
*/
|
||||
function getNamespace() {
|
||||
if (isBrowser) {
|
||||
return window?.getEmbedNamespace?.() ?? null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function log(...args: unknown[]) {
|
||||
if (isBrowser) {
|
||||
const namespace = getNamespace();
|
||||
const searchParams = new URL(document.URL).searchParams;
|
||||
const logQueue = (window.CalEmbed.__logQueue = window.CalEmbed.__logQueue || []);
|
||||
args.push({
|
||||
ns: namespace,
|
||||
url: document.URL,
|
||||
});
|
||||
args.unshift("CAL:");
|
||||
logQueue.push(args);
|
||||
if (searchParams.get("debug") || process.env.INTEGRATION_TEST_MODE === "true") {
|
||||
console.log("Child:", ...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
"use client";
|
||||
/**
|
||||
* All hooks defined in this file must be client side hooks and must not be executed in server side.
|
||||
* So, they should start with isBrowser check.
|
||||
*/
|
||||
import { sdkActionManager } from "../sdk-event";
|
||||
import { embedStore, getEventHasFired, setEventHasFired, getReloadInitiated, setReloadInitiated } from "./lib/embedStore";
|
||||
import { isBrowser, isPrerendering } from "./lib/utils";
|
||||
|
||||
/**
|
||||
* Fires bookerViewed, bookerReopened, or bookerReloaded events
|
||||
*/
|
||||
const fireBookerViewedEvent = ({
|
||||
eventId,
|
||||
eventSlug,
|
||||
slotsLoaded,
|
||||
}: {
|
||||
eventId: number | undefined;
|
||||
eventSlug: string | undefined;
|
||||
slotsLoaded: boolean;
|
||||
}) => {
|
||||
const isFirstTime = embedStore.viewId === 1;
|
||||
const isReload = getReloadInitiated();
|
||||
const eventName: "bookerViewed" | "bookerReopened" | "bookerReloaded" = isReload ? "bookerReloaded" : isFirstTime ? "bookerViewed" : "bookerReopened";
|
||||
|
||||
if (getEventHasFired(eventName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fireEvent(eventName);
|
||||
|
||||
function fireEvent(eventName: "bookerViewed" | "bookerReopened" | "bookerReloaded") {
|
||||
setEventHasFired(eventName, true);
|
||||
if (slotsLoaded) {
|
||||
if (eventId && eventSlug) {
|
||||
sdkActionManager?.fire(eventName, {
|
||||
eventId,
|
||||
eventSlug,
|
||||
slotsLoaded: true,
|
||||
});
|
||||
} else {
|
||||
// This situation shouldn't arise but not throwing error because for a tracking event we don't want to crash booker
|
||||
console.error("BookerViewed event not fired because slotsLoaded is true but eventId or eventSlug are falsy");
|
||||
}
|
||||
} else {
|
||||
sdkActionManager?.fire(eventName, {
|
||||
eventId: null,
|
||||
eventSlug: null,
|
||||
slotsLoaded: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fires bookerReady event when booker view is loaded and slots are ready.
|
||||
* Only fires once per link view when slots are successfully loaded.
|
||||
*/
|
||||
const fireBookerReadyEvent = ({
|
||||
eventId,
|
||||
eventSlug,
|
||||
slotsLoaded,
|
||||
}: {
|
||||
eventId: number;
|
||||
eventSlug: string;
|
||||
slotsLoaded: boolean;
|
||||
}) => {
|
||||
if (!slotsLoaded) {
|
||||
return;
|
||||
}
|
||||
if (!getEventHasFired("bookerReady")) {
|
||||
sdkActionManager?.fire("bookerReady", { eventId, eventSlug });
|
||||
setEventHasFired("bookerReady", true);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook that fires embed events (bookerViewed/bookerReopened/bookerReloaded and bookerReady).
|
||||
* Manages event state using embedStore to prevent duplicate events and track modal reopens/reloads.
|
||||
*/
|
||||
export const useBookerEmbedEvents = ({
|
||||
eventId,
|
||||
eventSlug,
|
||||
schedule,
|
||||
}: {
|
||||
eventId: number | undefined;
|
||||
eventSlug: string | undefined;
|
||||
schedule: {
|
||||
isSuccess: boolean;
|
||||
dataUpdatedAt: number;
|
||||
};
|
||||
}) => {
|
||||
if (!isBrowser) {
|
||||
return;
|
||||
}
|
||||
const viewId = embedStore.viewId;
|
||||
|
||||
if (isPrerendering() || !viewId) {
|
||||
return;
|
||||
}
|
||||
// first BookerViewed event happens, followed by bookerReady if slots are loaded
|
||||
fireBookerViewedEvent({
|
||||
eventId,
|
||||
eventSlug,
|
||||
slotsLoaded: schedule.isSuccess,
|
||||
});
|
||||
|
||||
if (eventId && eventSlug) {
|
||||
fireBookerReadyEvent({
|
||||
eventId,
|
||||
eventSlug,
|
||||
slotsLoaded: schedule.isSuccess,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user