import Script from "next/script"; import { getEventTypeAppData } from "@calcom/app-store/_utils/getEventTypeAppData"; import { appStoreMetadata } from "@calcom/app-store/bookerAppsMetaData"; import type { Tag } from "@calcom/app-store/types"; import { sdkActionManager } from "@calcom/lib/sdk-event"; import type { AppMeta } from "@calcom/types/App"; import type { appDataSchemas } from "./apps.schemas.generated"; const PushEventPrefix = "cal_analytics_app_"; // AnalyticApp has appData.tag always set type AnalyticApp = Omit & { appData: Omit, "tag"> & { tag: NonNullable["tag"]>; }; }; const getPushEventScript = ({ tag, appId }: { tag: Tag; appId: string }) => { if (!tag.pushEventScript) { return tag.pushEventScript; } return { ...tag.pushEventScript, // In case of complex pushEvent implementations, we could think about exporting a pushEvent function from the analytics app maybe but for now this should suffice content: tag.pushEventScript?.content?.replace("$pushEvent", `${PushEventPrefix}_${appId}`), }; }; function getAnalyticsApps(eventType: Parameters[0]) { return Object.entries(appStoreMetadata).reduce( (acc, entry) => { const [appId, app] = entry; const eventTypeAppData = getEventTypeAppData(eventType, appId as keyof typeof appDataSchemas); if (!eventTypeAppData || !app.appData?.tag) { return acc; } acc[appId] = { meta: app as AnalyticApp, eventTypeAppData: eventTypeAppData, }; return acc; }, {} as Record< string, { meta: AnalyticApp; eventTypeAppData: ReturnType; } > ); } export function handleEvent(event: { detail: Record & { type: string } }) { const { type: name, ...data } = event.detail; // Don't push internal events to analytics apps // They are meant for internal use like helping embed make some decisions if (name.startsWith("__")) { return false; } Object.entries(window).forEach(([prop, value]) => { if (!prop.startsWith(PushEventPrefix) || typeof value !== "function") { return; } // Find the pushEvent if defined by the analytics app const pushEvent = window[prop as keyof typeof window]; pushEvent({ name, data, }); }); // Support sending all events to opener which is currently used by ReroutingDialog to identify if the booking is successfully rescheduled. if (window.opener) { window.opener.postMessage( { type: `CAL:${name}`, ...data, }, "*" ); } return true; } export default function BookingPageTagManager({ eventType, }: { eventType: Parameters[0]; }) { const analyticsApps = getAnalyticsApps(eventType); return ( <> {Object.entries(analyticsApps).map(([appId, { meta: app, eventTypeAppData }]) => { const tag = app.appData.tag; const parseValue = (val: T): T => { if (!val) { return val; } // Only support UpperCase,_and numbers in template variables. This prevents accidental replacement of other strings. const regex = /\{([A-Z_\d]+)\}/g; let matches; while ((matches = regex.exec(val))) { const variableName = matches[1]; if (eventTypeAppData[variableName]) { // Replace if value is available. It can possible not be a template variable that just matches the regex. val = val.replace( new RegExp(`{${variableName}}`, "g"), eventTypeAppData[variableName] ) as NonNullable; } } return val; }; const pushEventScript = getPushEventScript({ tag, appId }); return tag.scripts.concat(pushEventScript ? [pushEventScript] : []).map((script, index) => { const parsedAttributes: NonNullable<(typeof tag.scripts)[number]["attrs"]> = {}; const attrs = script.attrs || {}; Object.entries(attrs).forEach(([name, value]) => { if (typeof value === "string") { value = parseValue(value); } parsedAttributes[name] = value; }); return (