diff --git a/apps/web/components/Embed.tsx b/apps/web/components/Embed.tsx index 44d5fa5d1f..7d5d0d56a1 100644 --- a/apps/web/components/Embed.tsx +++ b/apps/web/components/Embed.tsx @@ -1133,12 +1133,7 @@ export const EmbedDialog = () => { const router = useRouter(); const embedUrl: string = router.query.embedUrl as string; return ( - { - if (!open) window.resetEmbedStatus(); - }}> + {!router.query.embedType ? ( ) : ( diff --git a/packages/embeds/embed-core/embed-iframe-init.ts b/packages/embeds/embed-core/embed-iframe-init.ts index e46f49f0a3..3b73d4fdfe 100644 --- a/packages/embeds/embed-core/embed-iframe-init.ts +++ b/packages/embeds/embed-core/embed-iframe-init.ts @@ -1,34 +1,38 @@ +import type { EmbedThemeConfig } from "./embed-iframe"; + export default function EmbedInitIframe() { if (typeof window === "undefined" || window.isEmbed) { return; } - const calEmbedMode = typeof new URL(document.URL).searchParams.get("embed") === "string"; + const embedNameSpaceFromQueryParam = new URL(document.URL).searchParams.get("embed"); - /* Iframe Name */ - window.name.includes("cal-embed"); + // Namespace is initially set in query param `embed` but the query param might get lost during soft navigation + // So, we also check for the namespace in `window.name` which is set when iframe is created by embed.ts and persists for the duration of iframe's life + // Note that, window.name isn't lost during hard navigation as well. Though, hard navigation isn't something that would happen in the app, but it's critical to be able to detect embed mode even after that(just in case) + // We might just use window.name but if just in case something resets the `window.name`, we will still have the namespace in query param + const embedNamespace = + typeof embedNameSpaceFromQueryParam === "string" + ? embedNameSpaceFromQueryParam + : window.name.replace(/cal-embed=(.*)/, "$1").trim(); window.isEmbed = () => { - // Once an embed mode always an embed mode - return calEmbedMode; - }; - - window.resetEmbedStatus = () => { - try { - // eslint-disable-next-line @calcom/eslint/avoid-web-storage - window.sessionStorage.removeItem("calEmbedMode"); - } catch (e) {} + // By default namespace is "". That would also work if we just check the type of variable + return typeof embedNamespace == "string"; }; window.getEmbedTheme = () => { + // Note that embedStore.theme is lost if hard navigation occurs.(Though, it isn't something that we expect to happen normally) + if (window.CalEmbed.embedStore.theme) { + // It is important to ensure that the theme is consistent during browsing so that ThemeProvider doesn't get different themes to show and it avoids theme switching. + return window.CalEmbed.embedStore.theme; + } const url = new URL(document.URL); - return url.searchParams.get("theme") as "light" | "dark" | null; + return url.searchParams.get("theme") as EmbedThemeConfig | null; }; window.getEmbedNamespace = () => { - const url = new URL(document.URL); - const namespace = url.searchParams.get("embed"); - return namespace; + return embedNamespace; }; window.CalEmbed = window.CalEmbed || {}; diff --git a/packages/embeds/embed-core/index.html b/packages/embeds/embed-core/index.html index f674a8f8c2..ce56a57b71 100644 --- a/packages/embeds/embed-core/index.html +++ b/packages/embeds/embed-core/index.html @@ -159,7 +159,7 @@

- [Dark Theme][Guests(janedoe@example.com and test@example.com)] + [Dark Theme][Guests(janedoe@example.com and test@example.com)](Default Namespace)

diff --git a/packages/embeds/embed-core/playground.ts b/packages/embeds/embed-core/playground.ts index 9de4179f70..1b8ec53945 100644 --- a/packages/embeds/embed-core/playground.ts +++ b/packages/embeds/embed-core/playground.ts @@ -1,6 +1,5 @@ -import type { GlobalCal, GlobalCalWithoutNs } from "./src/embed"; +import type { GlobalCal } from "./src/embed"; -type A = GlobalCalWithoutNs; const Cal = window.Cal as GlobalCal; const callback = function (e) { const detail = e.detail; @@ -8,7 +7,7 @@ const callback = function (e) { }; const searchParams = new URL(document.URL).searchParams; -const only = window.only; +const only = searchParams.get("only"); if (only === "all" || only === "ns:default") { Cal("init", { diff --git a/packages/embeds/embed-core/playwright/lib/testUtils.ts b/packages/embeds/embed-core/playwright/lib/testUtils.ts index ab270f8903..abe6fd6efe 100644 --- a/packages/embeds/embed-core/playwright/lib/testUtils.ts +++ b/packages/embeds/embed-core/playwright/lib/testUtils.ts @@ -34,7 +34,15 @@ export const getBooking = async (bookingId: string) => { return booking; }; -export const getEmbedIframe = async ({ page, pathname }: { page: Page; pathname: string }) => { +export const getEmbedIframe = async ({ + calNamespace, + page, + pathname, +}: { + calNamespace: string; + page: Page; + pathname: string; +}) => { // We can't seem to access page.frame till contentWindow is available. So wait for that. const iframeReady = await page.evaluate(() => { return new Promise((resolve) => { @@ -66,7 +74,7 @@ export const getEmbedIframe = async ({ page, pathname }: { page: Page; pathname: // We just verified that iframeReady is true here, so obviously embedIframe is not null // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const embedIframe = page.frame("cal-embed")!; + const embedIframe = page.frame(`cal-embed=${calNamespace}`)!; const u = new URL(embedIframe.url()); if (u.pathname === pathname + "/embed") { return embedIframe; diff --git a/packages/embeds/embed-core/playwright/tests/action-based.e2e.ts b/packages/embeds/embed-core/playwright/tests/action-based.e2e.ts index fc1bd21c24..4a6bf87652 100644 --- a/packages/embeds/embed-core/playwright/tests/action-based.e2e.ts +++ b/packages/embeds/embed-core/playwright/tests/action-based.e2e.ts @@ -32,7 +32,7 @@ async function bookFirstFreeUserEventThroughEmbed({ await embedButtonLocator.click(); - const embedIframe = await getEmbedIframe({ page, pathname: "/free" }); + const embedIframe = await getEmbedIframe({ calNamespace, page, pathname: "/free" }); await expect(embedIframe).toBeEmbedCalLink(calNamespace, getActionFiredDetails, { pathname: "/free", @@ -58,12 +58,12 @@ test.describe("Popup Tests", () => { const calNamespace = "prerendertestLightTheme"; await addEmbedListeners(calNamespace); await page.goto("/?only=prerender-test"); - let embedIframe = await getEmbedIframe({ page, pathname: "/free" }); + let embedIframe = await getEmbedIframe({ calNamespace, page, pathname: "/free" }); expect(embedIframe).toBeFalsy(); await page.click('[data-cal-link="free?light&popup"]'); - embedIframe = await getEmbedIframe({ page, pathname: "/free" }); + embedIframe = await getEmbedIframe({ calNamespace, page, pathname: "/free" }); await expect(embedIframe).toBeEmbedCalLink(calNamespace, getActionFiredDetails, { pathname: "/free", @@ -93,7 +93,7 @@ test.describe("Popup Tests", () => { await page.goto(`/?popupRescheduleId=${booking.uid}`); await page.click('[data-cal-namespace="popupReschedule"]'); - const embedIframe = await getEmbedIframe({ page, pathname: booking.eventSlug }); + const embedIframe = await getEmbedIframe({ calNamespace, page, pathname: booking.eventSlug }); if (!embedIframe) { throw new Error("Embed iframe not found"); } @@ -117,12 +117,20 @@ test.describe("Popup Tests", () => { const calNamespace = "routingFormAuto"; await addEmbedListeners(calNamespace); await page.goto("/?only=prerender-test"); - let embedIframe = await getEmbedIframe({ page, pathname: "/forms/948ae412-d995-4865-875a-48302588de03" }); + let embedIframe = await getEmbedIframe({ + calNamespace, + page, + pathname: "/forms/948ae412-d995-4865-875a-48302588de03", + }); expect(embedIframe).toBeFalsy(); await page.click( `[data-cal-namespace=${calNamespace}][data-cal-link="forms/948ae412-d995-4865-875a-48302588de03"]` ); - embedIframe = await getEmbedIframe({ page, pathname: "/forms/948ae412-d995-4865-875a-48302588de03" }); + embedIframe = await getEmbedIframe({ + calNamespace, + page, + pathname: "/forms/948ae412-d995-4865-875a-48302588de03", + }); if (!embedIframe) { throw new Error("Routing Form embed iframe not found"); } diff --git a/packages/embeds/embed-core/playwright/tests/inline.e2e.ts b/packages/embeds/embed-core/playwright/tests/inline.e2e.ts index 2c8de09aab..da1169f670 100644 --- a/packages/embeds/embed-core/playwright/tests/inline.e2e.ts +++ b/packages/embeds/embed-core/playwright/tests/inline.e2e.ts @@ -12,8 +12,9 @@ test("Inline Iframe - Configured with Dark Theme", async ({ await deleteAllBookingsByEmail("embed-user@example.com"); await addEmbedListeners(""); await page.goto("/?only=ns:default"); - const embedIframe = await getEmbedIframe({ page, pathname: "/pro" }); - expect(embedIframe).toBeEmbedCalLink("", getActionFiredDetails, { + const calNamespace = ""; + const embedIframe = await getEmbedIframe({ calNamespace, page, pathname: "/pro" }); + expect(embedIframe).toBeEmbedCalLink(calNamespace, getActionFiredDetails, { pathname: "/pro", searchParams: { theme: "dark", diff --git a/packages/embeds/embed-core/src/embed-iframe.ts b/packages/embeds/embed-core/src/embed-iframe.ts index 65e173e8e3..66b3c71655 100644 --- a/packages/embeds/embed-core/src/embed-iframe.ts +++ b/packages/embeds/embed-core/src/embed-iframe.ts @@ -11,7 +11,8 @@ type Theme = "dark" | "light"; export type EmbedThemeConfig = Theme | "auto"; export type UiConfig = { hideEventTypeDetails?: boolean; - theme?: EmbedThemeConfig; + // If theme not provided we would get null + theme?: EmbedThemeConfig | null; styles?: EmbedStyles & EmbedNonStylesConfig; //TODO: Extract from tailwind the list of all custom variables and support them in auto-completion as well as runtime validation. Followup with listing all variables in Embed Snippet Generator UI. cssVarsPerTheme?: Record>; @@ -46,9 +47,8 @@ declare global { }; CalComPageStatus: string; isEmbed?: () => boolean; - resetEmbedStatus: () => void; getEmbedNamespace: () => string | null; - getEmbedTheme: () => "dark" | "light" | null; + getEmbedTheme: () => EmbedThemeConfig | null; } } @@ -434,7 +434,7 @@ if (isBrowser) { // Exposes certain global variables/fns that are used by the app to get interface with the embed. embedInit(); const url = new URL(document.URL); - embedStore.theme = window?.getEmbedTheme?.() as UiConfig["theme"]; + embedStore.theme = window?.getEmbedTheme?.(); if (url.searchParams.get("prerender") !== "true" && window?.isEmbed?.()) { log("Initializing embed-iframe"); // HACK diff --git a/packages/embeds/embed-core/src/embed.ts b/packages/embeds/embed-core/src/embed.ts index 94de053d20..d8b0c9fefe 100644 --- a/packages/embeds/embed-core/src/embed.ts +++ b/packages/embeds/embed-core/src/embed.ts @@ -215,7 +215,7 @@ export class Cal { }) { const iframe = (this.iframe = document.createElement("iframe")); iframe.className = "cal-embed"; - iframe.name = "cal-embed"; + iframe.name = `cal-embed=${this.namespace}`; const config = this.getConfig(); const { iframeAttrs, ...restQueryObject } = queryObject; diff --git a/packages/embeds/embed-react/playwright/tests/basic.e2e.ts b/packages/embeds/embed-react/playwright/tests/basic.e2e.ts index f4466b6725..8eabd97e2c 100644 --- a/packages/embeds/embed-react/playwright/tests/basic.e2e.ts +++ b/packages/embeds/embed-react/playwright/tests/basic.e2e.ts @@ -12,7 +12,8 @@ test.describe("Inline Embed", () => { //TODO: Do it with page.goto automatically await addEmbedListeners(""); await page.goto("/"); - const embedIframe = await getEmbedIframe({ page, pathname: "/pro" }); + const calNamespace = ""; + const embedIframe = await getEmbedIframe({ calNamespace, page, pathname: "/pro" }); expect(embedIframe).toBeEmbedCalLink("", getActionFiredDetails, { pathname: "/pro", searchParams: {