diff --git a/apps/web/playwright/fixtures/embeds.ts b/apps/web/playwright/fixtures/embeds.ts index 5c867dcfbe..f38f824a72 100644 --- a/apps/web/playwright/fixtures/embeds.ts +++ b/apps/web/playwright/fixtures/embeds.ts @@ -3,12 +3,17 @@ import type { Page } from "@playwright/test"; export const createEmbedsFixture = (page: Page) => { return { /** - * @deprecated Use gotoPlayground instead + * @deprecated + * Use 'gotoPlayground' instead, to navigate. It calls `addEmbedListeners` automatically. */ async addEmbedListeners(calNamespace: string) { await page.addInitScript( ({ calNamespace }: { calNamespace: string }) => { - console.log("PlaywrightTest:", "Adding listener for __iframeReady on namespace:", calNamespace); + console.log( + "PlaywrightTest - InitScript:", + "Adding listener for __iframeReady on namespace:", + calNamespace + ); // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore window.eventsFiredStoreForPlaywright = window.eventsFiredStoreForPlaywright || {}; @@ -43,14 +48,18 @@ export const createEmbedsFixture = (page: Page) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore api = window.Cal.ns[calNamespace]; + console.log("Using api from namespace-", { calNamespace, api }); + } + if (!api) { + console.log(`namespace "${calNamespace}" not found yet - Trying again`); + setTimeout(tryAddingListener, 500); + return; } console.log("PlaywrightTest:", `Adding listener for __iframeReady on namespace:${calNamespace}`); - if (!api) { - throw new Error(`namespace "${calNamespace}" not found`); - } api("on", { action: "*", callback: (e) => { + console.log("Playwright Embed Fixture: Received event", e); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore window.iframeReady = true; // Technically if there are multiple cal embeds, it can be set due to some other iframe. But it works for now. Improve it when it doesn't work diff --git a/packages/app-store/wordpress/plugin.php b/packages/app-store/wordpress/plugin.php index 2263e96352..85750bbb00 100644 --- a/packages/app-store/wordpress/plugin.php +++ b/packages/app-store/wordpress/plugin.php @@ -16,7 +16,7 @@ global $post;extract(shortcode_atts(array( 'for' => $post->post_title, ), $atts)); if(empty($content)) $content='Embed Cal.com'; - +// TODO: How to reuse embed-snippet export here? return ' '; } add_shortcode('cal', 'cal_shortcode'); \ No newline at end of file diff --git a/packages/embeds/embed-core/index.html b/packages/embeds/embed-core/index.html index 21298b5908..b757cd5080 100644 --- a/packages/embeds/embed-core/index.html +++ b/packages/embeds/embed-core/index.html @@ -20,43 +20,126 @@ (() => { const url = new URL(document.URL); // Only run the example specified by only=, avoids distraction and faster to test. - const only = (window.only = url.searchParams.get("only")); + const only = (window.only = url.searchParams.get("only") || ''); const elementIdentifier = only !== "all" ? only.replace("ns:", "") : null; if (elementIdentifier) { location.hash = "#cal-booking-place-" + elementIdentifier + "-iframe"; } })(); + + function addInlineEmbedInNewNamespaceWithoutReload(selector) { + Cal("init", "withoutReloadNamespace", { + debug: true, + calOrigin: "http://localhost:3000", + }); + + Cal.ns.withoutReloadNamespace("inline", { + elementOrSelector: selector, + calLink: "pro?case=addInlineEmbedInANewNamespaceWithoutReload", + }); + }; + C.Cal = + C.Cal || + function () { + let cal = C.Cal; + let ar = arguments; + if (!cal.loaded) { + cal.ns = {}; + cal.q = cal.q || []; + d.head.appendChild(d.createElement("script")).src = A; + cal.loaded = true; + } + if (ar[0] === L) { + const api = function () { + p(api, arguments); + }; + const namespace = ar[1]; + api.q = api.q || []; + if (typeof namespace === "string"){ + // Make sure that even after re-execution of the snippet, the namespace is not overridden + cal.ns[namespace] = cal.ns[namespace] || api; + p(cal.ns[namespace], ar); + p(cal, ['initNamespace', namespace]) + } else p(cal, ar); + return; + } + p(cal, ar); + }; + })(window, "//localhost:3000/embed/embed.js", "init"); + } + embedSnippet(); + + +
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
With Dark Color Scheme for the Page Non responsive version of this page here Show Upcoming Bookings --> +

Inline Embed Examples

diff --git a/packages/embeds/embed-core/playwright/tests/namespacing.e2e.ts b/packages/embeds/embed-core/playwright/tests/namespacing.e2e.ts new file mode 100644 index 0000000000..c3a8840abc --- /dev/null +++ b/packages/embeds/embed-core/playwright/tests/namespacing.e2e.ts @@ -0,0 +1,67 @@ +import { expect } from "@playwright/test"; + +// eslint-disable-next-line no-restricted-imports +import { test } from "@calcom/web/playwright/lib/fixtures"; + +import { getEmbedIframe } from "../lib/testUtils"; + +test.describe("Namespacing", () => { + test.describe("Inline Embed", () => { + test("Add inline embed using a namespace without reload", async ({ page, embeds }) => { + const calNamespace = "withoutReloadNamespace"; + await embeds.gotoPlayground({ calNamespace, url: "/" }); + await page.click("#add-inline-embed-in-a-new-namespace-without-reload-button"); + const embedIframe = await getEmbedIframe({ calNamespace, page, pathname: "/pro" }); + expect(embedIframe).toBeEmbedCalLink(calNamespace, embeds.getActionFiredDetails, { + pathname: "/pro", + searchParams: { + case: "addInlineEmbedInANewNamespaceWithoutReload", + }, + }); + }); + + test("Double install Embed Snippet with inline embed using a namespace", async ({ page, embeds }) => { + const calNamespace = "doubleInstall"; + await embeds.gotoPlayground({ calNamespace, url: "/" }); + await page.click("#double-install-snippet-with-inline-embed-non-default-namespace-button"); + const embedIframe = await getEmbedIframe({ calNamespace, page, pathname: "/pro" }); + expect(embedIframe).toBeEmbedCalLink(calNamespace, embeds.getActionFiredDetails, { + pathname: "/pro", + searchParams: { + case: "doubleInstallSnippetWithInlineEmbedWithNonDefaultNamespace", + }, + }); + expect(await page.locator("iframe").count()).toBe(1); + }); + + test("Double install Embed Snippet with inline embed without a namespace(i.e. default namespace)", async ({ + page, + embeds, + }) => { + const calNamespace = ""; + await embeds.gotoPlayground({ calNamespace, url: "/" }); + await page.click("#double-install-snippet-with-inline-embed-default-namespace-button"); + const embedIframe = await getEmbedIframe({ calNamespace, page, pathname: "/pro" }); + expect(embedIframe).toBeEmbedCalLink(calNamespace, embeds.getActionFiredDetails, { + pathname: "/pro", + searchParams: { + case: "doubleInstallSnippetWithInlineEmbed", + }, + }); + expect(await page.locator("iframe").count()).toBe(1); + }); + }); + test("Different namespaces can have different init configs", async ({ page, embeds }) => { + await Promise.all([ + embeds.addEmbedListeners("namespace-init-test-1"), + embeds.addEmbedListeners("namespace-init-test-2"), + ]); + + await page.goto("/"); + await page.click("#two-different-namespace-with-different-init-config"); + const namespace1IframeSrc = await page.locator("iframe").nth(0).getAttribute("src"); + const namespace2IframeSrc = await page.locator("iframe").nth(1).getAttribute("src"); + expect(namespace1IframeSrc).toContain("http://localhost:3000/pro"); + expect(namespace2IframeSrc).toContain("http://127.0.0.1:3000/pro"); + }); +}); diff --git a/packages/embeds/embed-core/src/embed.ts b/packages/embeds/embed-core/src/embed.ts index a7a0a96049..bb9bd88fac 100644 --- a/packages/embeds/embed-core/src/embed.ts +++ b/packages/embeds/embed-core/src/embed.ts @@ -211,7 +211,7 @@ export class Cal { return instruction; } - processQueue(queue: IArguments[]) { + processQueue(queue: Queue) { queue.forEach((instruction) => { this.processInstruction(instruction); }); @@ -315,7 +315,7 @@ export class Cal { } } - constructor(namespace: string, q: IArguments[]) { + constructor(namespace: string, q: Queue) { this.__config = { // Use WEBAPP_URL till full page reload problem with website URL is solved calOrigin: WEBAPP_URL, @@ -413,9 +413,22 @@ class CalApi { this.cal = cal; } + /** + * If namespaceOrConfig is a string, config is available in config argument + * If namespaceOrConfig is an object, namespace is assumed to be default and config isn't provided + */ init(namespaceOrConfig?: string | InitArgConfig, config = {} as InitArgConfig) { + let initForNamespace = ""; if (typeof namespaceOrConfig !== "string") { config = (namespaceOrConfig || {}) as Config; + } else { + initForNamespace = namespaceOrConfig; + } + + // Just in case 'init' instruction belongs to another namespace, ignore it + // Though it shouldn't happen normally as the snippet takes care of delegating the init instruction to appropriate namespace queue + if (initForNamespace !== this.cal.namespace) { + return; } CalApi.initializedNamespaces.push(this.cal.namespace); @@ -426,6 +439,16 @@ class CalApi { this.cal.__config = { ...this.cal.__config, ...restConfig }; } + + /** + * Used when a non-default namespace is to be initialized + * It allows default queue to take care of instantiation of the non-default namespace queue + */ + initNamespace(namespace: string) { + // Creating this instance automatically starts processing the queue for the namespace + globalCal.ns[namespace].instance = + globalCal.ns[namespace].instance || new Cal(namespace, globalCal.ns[namespace].q); + } /** * It is an instruction that adds embed iframe inline as last child of the element */ @@ -458,6 +481,12 @@ class CalApi { }, }); + // If someone re-executes inline embed instruction, we want to ensure that duplicate inlineEl isn't added to the page per namespace + if (this.cal.inlineEl && document.body.contains(this.cal.inlineEl)) { + console.warn("Inline embed already exists. Ignoring this call"); + return; + } + config = config || {}; if (typeof config.iframeAttrs === "string" || config.iframeAttrs instanceof Array) { throw new Error("iframeAttrs should be an object"); @@ -766,13 +795,16 @@ class CalApi { } } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type Queue = any[]; + // This is a full fledged Cal instance but doesn't have ns property because it would be nested inside an ns instance already export interface GlobalCalWithoutNs { (methodName: T, ...arg: Rest): void; /** Marks that the embed.js is loaded. Avoids re-downloading it. */ loaded?: boolean; /** Maintains a queue till the time embed.js isn't loaded */ - q: IArguments[]; + q: Queue; /** If user registers multiple namespaces, those are available here */ instance?: Cal; __css?: string; @@ -800,8 +832,11 @@ export interface CalWindow extends Window { const DEFAULT_NAMESPACE = ""; globalCal.instance = new Cal(DEFAULT_NAMESPACE, globalCal.q); + +// Namespaces created before embed.js executes are instantiated here for old Embed Snippets which don't use 'initNamespace' instruction +// Snippets that support 'initNamespace' instruction don't really need this but it is okay if it's done because it's idempotent for (const [ns, api] of Object.entries(globalCal.ns)) { - api.instance = new Cal(ns, api.q); + api.instance = api.instance ?? new Cal(ns, api.q); } /** diff --git a/packages/embeds/embed-core/src/preview.ts b/packages/embeds/embed-core/src/preview.ts index e8a9096e78..b4dcddc253 100644 --- a/packages/embeds/embed-core/src/preview.ts +++ b/packages/embeds/embed-core/src/preview.ts @@ -6,10 +6,12 @@ const embedLibUrl = searchParams.get("embedLibUrl"); if (!bookerUrl || !embedLibUrl) { throw new Error('Can\'t Preview: Missing "bookerUrl" or "embedLibUrl" query parameter'); } +// TODO: Reuse the embed code snippet from the embed-snippet package - Not able to use it because of circular dependency // Install Cal Embed Code Snippet (function (C, A, L) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const p = function (a: any, ar: any) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + const p = function (a, ar) { a.q.push(ar); }; const d = C.document; @@ -17,6 +19,7 @@ if (!bookerUrl || !embedLibUrl) { C.Cal || function () { const cal = C.Cal; + // eslint-disable-next-line prefer-rest-params const ar = arguments; if (!cal.loaded) { @@ -26,22 +29,25 @@ if (!bookerUrl || !embedLibUrl) { cal.loaded = true; } if (ar[0] === L) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const api: { (): void; q?: any[] } = function () { + const api = function () { // eslint-disable-next-line prefer-rest-params p(api, arguments); }; const namespace = ar[1]; - api.q = api.q || []; // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore - typeof namespace === "string" ? (cal.ns[namespace] = api) && p(api, ar) : p(cal, ar); + api.q = api.q || []; + if (typeof namespace === "string") { + // Make sure that even after re-execution of the snippet, the namespace is not overridden + cal.ns[namespace] = cal.ns[namespace] || api; + p(cal.ns[namespace], ar); + p(cal, ["initNamespace", namespace]); + } else p(cal, ar); return; } p(cal, ar); }; -})(window, embedLibUrl, "init"); - +})(window, "//localhost:3000/embed/embed.js", "init"); const previewWindow = window; previewWindow.Cal.fingerprint = process.env.EMBED_PUBLIC_EMBED_FINGER_PRINT as string; diff --git a/packages/embeds/embed-react/element-click.html b/packages/embeds/embed-react/element-click.html new file mode 100644 index 0000000000..54a5cc6fc1 --- /dev/null +++ b/packages/embeds/embed-react/element-click.html @@ -0,0 +1,10 @@ + + + + + + + +
+ + diff --git a/packages/embeds/embed-react/element-click.tsx b/packages/embeds/embed-react/element-click.tsx new file mode 100644 index 0000000000..b58710ba72 --- /dev/null +++ b/packages/embeds/embed-react/element-click.tsx @@ -0,0 +1,31 @@ +/** + * @fileoverview This file is an example file and tells how to use the element-click popup embed in a React application. This is also used by playwright e2e + */ +import { useEffect } from "react"; +import * as React from "react"; +import ReactDom from "react-dom"; + +import { getCalApi } from "./src/index"; + +const calNamespace = "element-click"; +function App() { + useEffect(() => { + (async function () { + const cal = await getCalApi({ + embedJsUrl: "http://localhost:3000/embed/embed.js", + namespace: calNamespace, + }); + cal("ui", { styles: { branding: { brandColor: "#000000" } }, hideEventTypeDetails: false }); + })(); + }, []); + return ( + + ); +} + +ReactDom.render(, document.getElementById("root")); diff --git a/packages/embeds/embed-react/floating.tsx b/packages/embeds/embed-react/floating.tsx index 56c9285f87..d936be10e3 100644 --- a/packages/embeds/embed-react/floating.tsx +++ b/packages/embeds/embed-react/floating.tsx @@ -1,10 +1,6 @@ -/* First make sure that you have installed the package */ - -/* If you are using yarn */ -// yarn add @calcom/embed-react - -/* If you are using npm */ -// npm install @calcom/embed-react +/** + * @fileoverview This file is an example file and tells how to use floating popup button in a React application. This is also used by playwright e2e + */ import { useEffect } from "react"; import * as React from "react"; import ReactDom from "react-dom"; @@ -14,8 +10,17 @@ import { getCalApi } from "./src/index"; function App() { useEffect(() => { (async function () { - const cal = await getCalApi("http://localhost:3000/embed/embed.js"); - cal("floatingButton", { calLink: "teampro/abc", calOrigin: "http://localhost:3000" }); + const cal = await getCalApi({ + namespace: "floating", + embedJsUrl: "http://localhost:3000/embed/embed.js", + }); + cal("floatingButton", { + calLink: "pro", + calOrigin: "http://localhost:3000", + config: { + theme: "dark", + }, + }); cal("ui", { styles: { branding: { brandColor: "#000000" } }, hideEventTypeDetails: false }); })(); }, []); diff --git a/packages/embeds/embed-react/index.html b/packages/embeds/embed-react/index.html index 833271fe8c..6a3ae833c5 100644 --- a/packages/embeds/embed-react/index.html +++ b/packages/embeds/embed-react/index.html @@ -1,10 +1,16 @@ - - - - - -
+

Playground has following demos

+ diff --git a/packages/embeds/embed-react/inline.html b/packages/embeds/embed-react/inline.html new file mode 100644 index 0000000000..833271fe8c --- /dev/null +++ b/packages/embeds/embed-react/inline.html @@ -0,0 +1,10 @@ + + + + + + + +
+ + diff --git a/packages/embeds/embed-react/inline.tsx b/packages/embeds/embed-react/inline.tsx index 252f4a57ac..a5e704f1cb 100644 --- a/packages/embeds/embed-react/inline.tsx +++ b/packages/embeds/embed-react/inline.tsx @@ -1,3 +1,6 @@ +/** + * @fileoverview This file is an example file and tells how to use the Cal component in a React application. This is also used by playwright e2e + */ import * as React from "react"; import { useEffect } from "react"; import { useState } from "react"; @@ -7,7 +10,9 @@ import ReactDom from "react-dom"; // There are tests in test/built which verifiy that the types from built package are correctly generated and exported correctly. import Cal, { getCalApi } from "./src/index"; -const api = getCalApi(); +const api = getCalApi({ + namespace: "inline", +}); function App() { const [, setLoaded] = useState(false); @@ -37,6 +42,7 @@ function App() { }, }); }); + return () => { api.then((api) => { api("off", { @@ -54,6 +60,7 @@ function App() { { - test("should verify that the iframe got created with correct URL", async ({ - page, - embeds: { getActionFiredDetails, addEmbedListeners }, - }) => { - //TODO: Do it with page.goto automatically - await addEmbedListeners(""); - await page.goto("/"); - const calNamespace = ""; - const embedIframe = await getEmbedIframe({ calNamespace, page, pathname: "/pro" }); - expect(embedIframe).toBeEmbedCalLink("", getActionFiredDetails, { - pathname: "/pro", - searchParams: { - theme: "dark", - }, +test.describe("React Embed", () => { + test.describe("Inline", () => { + test("should verify that the iframe got created with correct URL - namespaced", async ({ + page, + embeds, + }) => { + const calNamespace = "inline"; + await embeds.gotoPlayground({ url: "/inline.html", calNamespace }); + const embedIframe = await getEmbedIframe({ calNamespace, page, pathname: "/pro" }); + expect(embedIframe).toBeEmbedCalLink("", embeds.getActionFiredDetails, { + pathname: "/pro", + searchParams: { + theme: "dark", + }, + }); + // expect(await page.screenshot()).toMatchSnapshot("react-component-inline.png"); + }); + }); + + test.describe("Floating button Popup", () => { + test("should verify that the iframe got created with correct URL - namespaced", async ({ + page, + embeds, + }) => { + const calNamespace = "floating"; + await page.waitForLoadState(); + await embeds.gotoPlayground({ url: "/floating.html", calNamespace }); + + await page.click("text=Book my Cal"); + + const embedIframe = await getEmbedIframe({ calNamespace, page, pathname: "/pro" }); + expect(embedIframe).toBeEmbedCalLink(calNamespace, embeds.getActionFiredDetails, { + pathname: "/pro", + searchParams: { + theme: "dark", + }, + }); + }); + }); + + test.describe("Element Click Popup", () => { + test("should verify that the iframe got created with correct URL - namespaced", async ({ + page, + embeds, + }) => { + const calNamespace = "element-click"; + await embeds.gotoPlayground({ url: "/element-click.html", calNamespace }); + await page.waitForLoadState(); + await page.click("text=Click me"); + + const embedIframe = await getEmbedIframe({ calNamespace, page, pathname: "/pro" }); + expect(embedIframe).toBeEmbedCalLink(calNamespace, embeds.getActionFiredDetails, { + pathname: "/pro", + searchParams: { + theme: "dark", + }, + }); }); - // expect(await page.screenshot()).toMatchSnapshot("react-component-inline.png"); }); }); diff --git a/packages/embeds/embed-react/src/index.ts b/packages/embeds/embed-react/src/index.ts index c5690473db..ae5fbc3a6e 100644 --- a/packages/embeds/embed-react/src/index.ts +++ b/packages/embeds/embed-react/src/index.ts @@ -1,14 +1,32 @@ "use client"; -import type { GlobalCal } from "@calcom/embed-core"; +import type { GlobalCal, GlobalCalWithoutNs } from "@calcom/embed-core"; import EmbedSnippet from "@calcom/embed-snippet"; import Cal from "./Cal"; -export const getCalApi = (embedJsUrl?: string): Promise => - new Promise(function tryReadingFromWindow(resolve) { - EmbedSnippet(embedJsUrl); - const api = window.Cal; +export function getCalApi(options?: { + embedJsUrl?: string; + namespace?: string; +}): Promise; +export function getCalApi(embedJsUrl: string): Promise; + +export function getCalApi( + optionsOrEmbedJsUrl?: + | { + embedJsUrl?: string; + namespace?: string; + } + | string +): Promise { + const options = + typeof optionsOrEmbedJsUrl === "string" ? { embedJsUrl: optionsOrEmbedJsUrl } : optionsOrEmbedJsUrl ?? {}; + + const { namespace = "", embedJsUrl } = options; + return new Promise(function tryReadingFromWindow(resolve) { + const globalCal = EmbedSnippet(embedJsUrl); + globalCal("init", namespace); + const api = namespace ? globalCal.ns[namespace as keyof typeof globalCal.ns] : globalCal; if (!api) { setTimeout(() => { tryReadingFromWindow(resolve); @@ -17,5 +35,6 @@ export const getCalApi = (embedJsUrl?: string): Promise => } resolve(api); }); +} export default Cal; diff --git a/packages/embeds/embed-snippet/README.md b/packages/embeds/embed-snippet/README.md index a09ab18f6d..9b3ed5bebb 100644 --- a/packages/embeds/embed-snippet/README.md +++ b/packages/embeds/embed-snippet/README.md @@ -4,7 +4,7 @@ Vanilla JS embed snippet that is responsible to fetch @calcom/embed-core and thu ## Development -`yarn build` will generate dist/snippet.es.js +`yarn build` will generate dist/snippet.es.js. If you are going to test react embeds, make sure that you have built it so that they get the upto-date snippet - which can be used as `` diff --git a/packages/embeds/embed-snippet/src/index.ts b/packages/embeds/embed-snippet/src/index.ts index 6ebe0b38f5..59ed7703a4 100644 --- a/packages/embeds/embed-snippet/src/index.ts +++ b/packages/embeds/embed-snippet/src/index.ts @@ -1,5 +1,7 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ + /* eslint-disable @typescript-eslint/ban-ts-comment,prefer-rest-params,prefer-const */ -import type { GlobalCal, GlobalCalWithoutNs } from "@calcom/embed-core"; +import type { GlobalCal, GlobalCalWithoutNs, Queue } from "@calcom/embed-core"; // FIXME: embed-snippet is a published package and shouldn't import from @calcom/types which is unpublished // This isn't a problem at the moment because embed-snippet isn't directly imported and embed-react which uses it doesn't depend on this // eslint-disable-next-line no-restricted-imports @@ -15,9 +17,19 @@ const WEBAPP_URL = const EMBED_LIB_URL = import.meta.env.EMBED_PUBLIC_EMBED_LIB_URL || `${WEBAPP_URL}/embed/embed.js`; +type QueuePushArg = { + [k: number]: Queue[number]; +}; + +/** + * When modifying this snippet, make sure to keep the snippets in following places in sync + * 1. EmbedTabs.tsx + * 2. embed-core/index.html + * 3. app-store/wordpress/plugin.php + */ export default function EmbedSnippet(url = EMBED_LIB_URL) { (function (C, A, L) { - let p = function (a: GlobalCalWithoutNs, ar: IArguments) { + let p = function (a: GlobalCalWithoutNs, ar: QueuePushArg) { a.q.push(ar); }; let d = C.document; @@ -42,10 +54,13 @@ export default function EmbedSnippet(url = EMBED_LIB_URL) { }; const namespace = ar[1]; api.q = api.q || []; - typeof namespace === "string" - ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (cal.ns![namespace] = api) && p(api, ar) - : p(cal as GlobalCal, ar); + if (typeof namespace === "string") { + // Make sure that even after re-execution of the snippet, the namespace is not overridden + cal.ns![namespace] = cal.ns![namespace] || api; + p(cal.ns![namespace], ar); + // Inform the default namespace queue to initialize this namespace + p(cal as GlobalCal, ["initNamespace", namespace]); + } else p(cal as GlobalCal, ar); return; } p(cal as GlobalCal, ar); diff --git a/packages/features/embed/lib/EmbedCodes.tsx b/packages/features/embed/lib/EmbedCodes.tsx index f9845961f6..7ef08f026a 100644 --- a/packages/features/embed/lib/EmbedCodes.tsx +++ b/packages/features/embed/lib/EmbedCodes.tsx @@ -30,13 +30,14 @@ export const Codes = { const width = getDimension(previewState.inline.width); const height = getDimension(previewState.inline.height); const namespaceProp = `${namespace ? `namespace="${namespace}"` : ""}`; + const argumentForGetCalApi = getArgumentForGetCalApi(namespace); return code` import Cal, { getCalApi } from "@calcom/embed-react"; import { useEffect } from "react"; export default function MyApp() { useEffect(()=>{ (async function () { - const cal = await getCalApi(); + const cal = await getCalApi(${argumentForGetCalApi ? JSON.stringify(argumentForGetCalApi) : ""}); ${uiInstructionCode} })(); }, []) @@ -58,13 +59,14 @@ export const Codes = { uiInstructionCode: string; namespace: string; }) => { + const argumentForGetCalApi = getArgumentForGetCalApi(namespace); return code` import { getCalApi } from "@calcom/embed-react"; import { useEffect } from "react"; export default function MyApp() { useEffect(()=>{ (async function () { - const cal = await getCalApi(${IS_SELF_HOSTED ? `"${embedLibUrl}"` : ""}); + const cal = await getCalApi(${argumentForGetCalApi ? JSON.stringify(argumentForGetCalApi) : ""}); ${getApiName({ namespace, mainApiName: "cal" })}("floatingButton", ${floatingButtonArg}); ${uiInstructionCode} })(); @@ -84,13 +86,14 @@ export const Codes = { embedCalOrigin: string; namespace: string; }) => { + const argumentForGetCalApi = getArgumentForGetCalApi(namespace); return code` import { getCalApi } from "@calcom/embed-react"; import { useEffect } from "react"; export default function MyApp() { useEffect(()=>{ (async function () { - const cal = await getCalApi(${IS_SELF_HOSTED ? `"${embedLibUrl}"` : ""}); + const cal = await getCalApi(${argumentForGetCalApi ? JSON.stringify(argumentForGetCalApi) : ""}); ${uiInstructionCode} })(); }, []) @@ -196,3 +199,9 @@ const code = (partsWithoutBlock: TemplateStringsArray, ...blocksOrVariables: str } return constructedCode.join(""); }; + +function getArgumentForGetCalApi(namespace: string) { + const libUrl = IS_SELF_HOSTED ? embedLibUrl : undefined; + const argumentForGetCalApi = namespace ? { namespace, embedLibUrl: libUrl } : { embedLibUrl: libUrl }; + return argumentForGetCalApi; +} diff --git a/packages/features/embed/lib/EmbedTabs.tsx b/packages/features/embed/lib/EmbedTabs.tsx index 654ee273bb..4c2dacc013 100644 --- a/packages/features/embed/lib/EmbedTabs.tsx +++ b/packages/features/embed/lib/EmbedTabs.tsx @@ -275,7 +275,9 @@ const getInstructionString = ({ function useGetEmbedSnippetString(namespace: string | null) { const bookerUrl = useBookerUrl(); // TODO: Import this string from @calcom/embed-snippet - return `(function (C, A, L) { let p = function (a, ar) { a.q.push(ar); }; let d = C.document; C.Cal = C.Cal || function () { let cal = C.Cal; let ar = arguments; if (!cal.loaded) { cal.ns = {}; cal.q = cal.q || []; d.head.appendChild(d.createElement("script")).src = A; cal.loaded = true; } if (ar[0] === L) { const api = function () { p(api, arguments); }; const namespace = ar[1]; api.q = api.q || []; typeof namespace === "string" ? (cal.ns[namespace] = api) && p(api, ar) : p(cal, ar); return; } p(cal, ar); }; })(window, "${embedLibUrl}", "init"); + // Right now the problem is that embed-snippet export is not minified and has comments which makes it unsuitable for giving it to users. + // If we can minify that during build time and then import the built code here, that could work + return `(function (C, A, L) { let p = function (a, ar) { a.q.push(ar); }; let d = C.document; C.Cal = C.Cal || function () { let cal = C.Cal; let ar = arguments; if (!cal.loaded) { cal.ns = {}; cal.q = cal.q || []; d.head.appendChild(d.createElement("script")).src = A; cal.loaded = true; } if (ar[0] === L) { const api = function () { p(api, arguments); }; const namespace = ar[1]; api.q = api.q || []; if(typeof namespace === "string"){cal.ns[namespace] = cal.ns[namespace] || api;p(cal.ns[namespace], ar);p(cal, ["initNamespace", namespace]);} else p(cal, ar); return;} p(cal, ar); }; })(window, "${embedLibUrl}", "init"); Cal("init", ${namespace ? `"${namespace}",` : ""} {origin:"${bookerUrl}"}); `; }