fix: preserve UI config across iframe resets for element-click embeds (#27363)
* fix: preserve UI config across iframe resets for element-click embeds For element-click embeds, ui() is called before the iframe exists, and iframeReset() clears the queue when the modal is opened. This causes UI config like hideEventTypeDetails, cssVarsPerTheme, and styles to be lost. This fix stores UI config in a persistent property that survives iframe resets and applies it when the iframe becomes ready. Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * test: add assertion to verify hideEventTypeDetails config is applied in element-click embed Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * Add mergeUiConfig function and related tests for UI configuration merging - Introduced mergeUiConfig function to handle merging of UI configuration, specifically for cssVarsPerTheme at theme level. - Added comprehensive tests for mergeUiConfig covering various scenarios including merging themes, handling missing properties, and preserving existing configuration values. - Updated utils.test.ts to include tests for the new function and ensure correct behavior in different merging situations. * Fixes * Remove process import as it breaks embed.js * Simplify * fix name * fix name * fix: use forEach instead of for...of for URLSearchParams iteration Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
This commit is contained in:
co-authored by
hariom@cal.com
Devin AI
parent
3b2e3351c4
commit
ca5d931165
@@ -321,6 +321,13 @@ test.describe("Popup Tests", () => {
|
||||
await expect(embedIframe).toBeEmbedCalLink(calNamespace, embeds.getActionFiredDetails, {
|
||||
pathname: "/free/30min",
|
||||
});
|
||||
|
||||
if (!embedIframe) {
|
||||
throw new Error("Embed iframe not found");
|
||||
}
|
||||
|
||||
// Verify that event type details are hidden (hideEventTypeDetails: true was set via ui() call)
|
||||
await expect(embedIframe.locator('[data-testid="event-meta"]')).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
/// <reference types="../env" />
|
||||
import { FloatingButton } from "./FloatingButton/FloatingButton";
|
||||
import { Inline } from "./Inline/inline";
|
||||
import { ModalBox } from "./ModalBox/ModalBox";
|
||||
|
||||
import {
|
||||
EMBED_MODAL_IFRAME_FORCE_RELOAD_THRESHOLD_MS,
|
||||
EMBED_MODAL_IFRAME_SLOT_STALE_TIME,
|
||||
EMBED_MODAL_PRERENDER_PREVENT_THRESHOLD_MS,
|
||||
} from "./constants";
|
||||
import type { InterfaceWithParent, interfaceWithParent } from "./embed-iframe";
|
||||
import css from "./embed.css?inline";
|
||||
import type { InterfaceWithParent, interfaceWithParent } from "./embed-iframe";
|
||||
import { FloatingButton } from "./FloatingButton/FloatingButton";
|
||||
import { Inline } from "./Inline/inline";
|
||||
import { getScrollableAncestor } from "./lib/domUtils";
|
||||
import { getScrollByDistanceHandler } from "./lib/eventHandlers/scrollByDistanceEventHandler";
|
||||
import {
|
||||
buildConfigWithPrerenderRelatedFields,
|
||||
fromEntriesWithDuplicateKeys,
|
||||
isRouterPath,
|
||||
generateDataAttributes,
|
||||
getConfigProp,
|
||||
isRouterPath,
|
||||
isSameBookingLink,
|
||||
buildConfigWithPrerenderRelatedFields,
|
||||
} from "./lib/utils";
|
||||
import { SdkActionManager } from "./sdk-action-manager";
|
||||
import { ModalBox } from "./ModalBox/ModalBox";
|
||||
import type { EventData, EventDataMap } from "./sdk-action-manager";
|
||||
import { SdkActionManager } from "./sdk-action-manager";
|
||||
import tailwindCss from "./tailwindCss";
|
||||
import type { UiConfig, EmbedPageType, PrefillAndIframeAttrsConfig, ModalPrerenderOptions } from "./types";
|
||||
import type { EmbedPageType, ModalPrerenderOptions, PrefillAndIframeAttrsConfig, UiConfig } from "./types";
|
||||
import { getMaxHeightForModal } from "./ui-utils";
|
||||
|
||||
// Exporting for consumption by @calcom/embed-core user
|
||||
export type { EmbedEvent } from "./sdk-action-manager";
|
||||
export type { PrefillAndIframeAttrsConfig } from "./types";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type Rest<T extends any[] | undefined> = T extends [any, ...infer U] ? U : never;
|
||||
export type Message = {
|
||||
@@ -177,8 +179,8 @@ type SingleInstructionMap = {
|
||||
off: ["off", allPossibleCallbacksAndActions];
|
||||
} & {
|
||||
[K in Exclude<keyof CalApi, "on" | "off">]: CalApi[K] extends (...args: never[]) => void
|
||||
? [K, ...Parameters<CalApi[K]>]
|
||||
: never;
|
||||
? [K, ...Parameters<CalApi[K]>]
|
||||
: never;
|
||||
};
|
||||
|
||||
type SingleInstruction = SingleInstructionMap[keyof SingleInstructionMap];
|
||||
@@ -231,6 +233,11 @@ export class Cal {
|
||||
|
||||
api: CalApi;
|
||||
|
||||
/**
|
||||
* Commands for which the queue persists across iframe resets
|
||||
*/
|
||||
commandsPersistAcrossIframeResets: DoInIframeArg['method'][] = ["ui"];
|
||||
|
||||
isPrerendering?: boolean;
|
||||
|
||||
static actionsManagers: Record<Namespace, SdkActionManager>;
|
||||
@@ -361,14 +368,13 @@ export class Cal {
|
||||
}
|
||||
|
||||
// Merge searchParams from config onto the URL which might have query params already
|
||||
// @ts-ignore TS2802: URLSearchParams iteration requires downlevelIteration
|
||||
for (const [key, value] of searchParams) {
|
||||
searchParams.forEach((value, key) => {
|
||||
urlInstance.searchParams.append(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
// Very Important:Reset iframe ready flag and clear queue, as iframe might load a fresh URL and we need to check when it is ready.
|
||||
// Very Important: Reset iframe ready flag and clear queue, as iframe might load a fresh URL and we need to check correctly when it is ready.
|
||||
this.iframeReset();
|
||||
|
||||
|
||||
if (iframe.src === urlInstance.toString()) {
|
||||
// Ensure reload occurs even if the url is same - Though browser normally does it, but would be better to ensure it
|
||||
// This param has no other purpose except to ensure forced reload.
|
||||
@@ -408,7 +414,10 @@ export class Cal {
|
||||
|
||||
iframeReset() {
|
||||
this.iframeReady = false;
|
||||
this.iframeDoQueue = [];
|
||||
// Only keep UI related instructions in the queue, as we want to ensure that newly loaded iframe has the same UI configuration applied automatically
|
||||
this.iframeDoQueue = this.iframeDoQueue.filter((doInIframeArg) =>
|
||||
this.commandsPersistAcrossIframeResets.includes(doInIframeArg.method)
|
||||
);
|
||||
}
|
||||
|
||||
constructor(namespace: string, q: Queue) {
|
||||
@@ -604,7 +613,7 @@ export class Cal {
|
||||
: 0;
|
||||
const crossedReloadThreshold = previousEmbedRenderStartTime
|
||||
? timeSinceLastRender >
|
||||
(prerenderOptions?.iframeForceReloadThresholdMs ?? EMBED_MODAL_IFRAME_FORCE_RELOAD_THRESHOLD_MS)
|
||||
(prerenderOptions?.iframeForceReloadThresholdMs ?? EMBED_MODAL_IFRAME_FORCE_RELOAD_THRESHOLD_MS)
|
||||
: false;
|
||||
|
||||
const areSlotsStale = previousEmbedRenderStartTime
|
||||
@@ -862,7 +871,7 @@ class CalApi {
|
||||
|
||||
CalApi.initializedNamespaces.push(this.cal.namespace);
|
||||
|
||||
const { calOrigin: calOrigin, origin: origin, ...restConfig } = config;
|
||||
const { calOrigin, origin, ...restConfig } = config;
|
||||
|
||||
this.cal.__config.calOrigin = calOrigin || origin || this.cal.__config.calOrigin;
|
||||
|
||||
@@ -950,10 +959,10 @@ class CalApi {
|
||||
|
||||
template.innerHTML = `<cal-inline
|
||||
${generateDataAttributes({
|
||||
pageType,
|
||||
theme,
|
||||
layout,
|
||||
})}
|
||||
pageType,
|
||||
theme,
|
||||
layout,
|
||||
})}
|
||||
style="max-height:inherit;height:inherit;min-height:inherit;display:flex;position:relative;flex-wrap:wrap;width:100%">
|
||||
</cal-inline>
|
||||
<style>.cal-inline-container::-webkit-scrollbar{display:none}.cal-inline-container{scrollbar-width:none}</style>`;
|
||||
@@ -1187,7 +1196,7 @@ class CalApi {
|
||||
config: enrichedConfig,
|
||||
});
|
||||
// Send reloadInitiated message to iframe so it can track it
|
||||
// Send it after loadInIframe so that new iframe can process it.
|
||||
// Send it after loadInIframe so that new iframe can process it.
|
||||
this.cal.doInIframe({ method: "__reloadInitiated", arg: {} });
|
||||
} else if (actionToTake === "connect" || actionToTake === "connect-no-slots-fetch") {
|
||||
const paramsToAdd = fromEntriesWithDuplicateKeys(calLinkUrlObject.searchParams.entries());
|
||||
@@ -1196,8 +1205,8 @@ class CalApi {
|
||||
...enrichedConfig,
|
||||
...(actionToTake === "connect-no-slots-fetch"
|
||||
? {
|
||||
"cal.embed.noSlotsFetchOnConnect": "true",
|
||||
}
|
||||
"cal.embed.noSlotsFetchOnConnect": "true",
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
params: paramsToAdd,
|
||||
@@ -1239,10 +1248,10 @@ class CalApi {
|
||||
|
||||
template.innerHTML = `<cal-modal-box
|
||||
${generateDataAttributes({
|
||||
pageType,
|
||||
theme,
|
||||
layout,
|
||||
})}
|
||||
pageType,
|
||||
theme,
|
||||
layout,
|
||||
})}
|
||||
uid="${uid}">
|
||||
</cal-modal-box>`;
|
||||
this.cal.modalBox = template.content.children[0];
|
||||
@@ -1303,9 +1312,9 @@ class CalApi {
|
||||
|
||||
/**
|
||||
* Closes modal-based embeds programmatically.
|
||||
*
|
||||
*
|
||||
* @throws {Error} If called on an inline embed (only works for modal-based embeds)
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* ```javascript
|
||||
* // Close the modal after a successful booking
|
||||
@@ -1487,6 +1496,7 @@ class CalApi {
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
this.cal.doInIframe({ method: "ui", arg: uiConfig });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user