diff --git a/apps/docs/pages/integrations/embed.mdx b/apps/docs/pages/integrations/embed.mdx
index 652ead23..40cde9d9 100644
--- a/apps/docs/pages/integrations/embed.mdx
+++ b/apps/docs/pages/integrations/embed.mdx
@@ -9,8 +9,8 @@ The Embed allows your website visitors to book a meeting with you directly from
## Install on any website
- _Step-1._ Install the Vanilla JS Snippet
-
- ```javascript
+```html
+
+```
## Install with a Framework
@@ -72,18 +68,20 @@ Show the embed inline inside a container element. It would take the width and he
_Vanilla JS_
-```javascript
-Cal("inline", {
- elementOrSelector: "Your Embed Container Selector Path", // You can also provide an element directly
- calLink: "jane", // The link that you want to embed. It would open https://cal.com/jane in embed
- config: {
- name: "John Doe", // Prefill Name
- email: "johndoe@gmail.com", // Prefill Email
- notes: "Test Meeting", // Prefill Notes
- guests: ["janedoe@gmail.com", "test@gmail.com"], // Prefill Guests
- theme: "dark", // "dark" or "light" theme
- },
-});
+```html
+
```
@@ -146,8 +144,10 @@ Consider an instruction as a function with that name and that would be called wi
Appends embed inline as the child of the element.
-```javascript
+```html
+
````
- `elementOrSelector` - Give it either a valid CSS selector or an HTMLElement instance directly
@@ -158,8 +158,10 @@ Cal("inline", { elementOrSelector, calLink });
Configure UI for embed. Make it look part of your webpage.
-```javascript
+```html
+
```
- `styles` - It supports styling for `body` and `eventTypeListItem`. Right now we support just background on these two.
@@ -170,15 +172,18 @@ Usage:
If you want to open cal link on some action. Make it pop open instantly by preloading it.
-```javascript
+```html
+
```
- `calLink` - Cal Link that you want to embed e.g. john. Just give the username. No need to give the full URL [https://cal.com/john]()
## Actions
You can listen to an action that occurs in embedded cal link as follows. You can think of them as DOM events. We are avoiding the term "events" to not confuse it with Cal Events.
-```javascript
+```html
+
```
Following are the list of supported actions.
diff --git a/apps/web/components/booking/pages/AvailabilityPage.tsx b/apps/web/components/booking/pages/AvailabilityPage.tsx
index 6adcfe08..fa549c75 100644
--- a/apps/web/components/booking/pages/AvailabilityPage.tsx
+++ b/apps/web/components/booking/pages/AvailabilityPage.tsx
@@ -18,7 +18,14 @@ import { useRouter } from "next/router";
import { useEffect, useMemo, useState } from "react";
import { FormattedNumber, IntlProvider } from "react-intl";
-import { useEmbedStyles, useIsEmbed, useIsBackgroundTransparent, sdkActionManager } from "@calcom/embed-core";
+import {
+ useEmbedStyles,
+ useIsEmbed,
+ useIsBackgroundTransparent,
+ sdkActionManager,
+ useEmbedType,
+ useEmbedNonStylesConfig,
+} from "@calcom/embed-core";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -56,6 +63,8 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
const { t, i18n } = useLocale();
const { contracts } = useContracts();
const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker");
+ const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
+ const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
let isBackgroundTransparent = useIsBackgroundTransparent();
useExposePlanGlobally(plan);
useEffect(() => {
@@ -146,18 +155,19 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
+ (selectedDate ? "max-w-5xl" : "max-w-3xl")
+ )}>
{isReady && (
{/* mobile: details */}
diff --git a/apps/web/components/booking/pages/BookingPage.tsx b/apps/web/components/booking/pages/BookingPage.tsx
index dd22a8fb..78805642 100644
--- a/apps/web/components/booking/pages/BookingPage.tsx
+++ b/apps/web/components/booking/pages/BookingPage.tsx
@@ -18,7 +18,13 @@ import { FormattedNumber, IntlProvider } from "react-intl";
import { ReactMultiEmail } from "react-multi-email";
import { useMutation } from "react-query";
-import { useIsEmbed, useIsBackgroundTransparent } from "@calcom/embed-core";
+import {
+ useIsEmbed,
+ useEmbedStyles,
+ useIsBackgroundTransparent,
+ useEmbedType,
+ useEmbedNonStylesConfig,
+} from "@calcom/embed-core";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
@@ -71,6 +77,8 @@ const BookingPage = ({
}: BookingPageProps) => {
const { t, i18n } = useLocale();
const isEmbed = useIsEmbed();
+ const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
+ const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
const router = useRouter();
const { contracts } = useContracts();
const { data: session } = useSession();
@@ -298,16 +306,17 @@ const BookingPage = ({
{isReady && (
diff --git a/apps/web/ee/components/stripe/PaymentPage.tsx b/apps/web/ee/components/stripe/PaymentPage.tsx
index bed4afd9..08606bdd 100644
--- a/apps/web/ee/components/stripe/PaymentPage.tsx
+++ b/apps/web/ee/components/stripe/PaymentPage.tsx
@@ -1,5 +1,6 @@
import { CreditCardIcon } from "@heroicons/react/solid";
import { Elements } from "@stripe/react-stripe-js";
+import classNames from "classnames";
import dayjs from "dayjs";
import timezone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray";
@@ -8,6 +9,7 @@ import Head from "next/head";
import React, { FC, useEffect, useState } from "react";
import { FormattedNumber, IntlProvider } from "react-intl";
+import { sdkActionManager, useIsEmbed } from "@calcom/embed-core";
import getStripe from "@calcom/stripe/client";
import PaymentComponent from "@ee/components/stripe/Payment";
import { PaymentPageProps } from "@ee/pages/payment/[uid]";
@@ -26,16 +28,33 @@ const PaymentPage: FC
= (props) => {
const [is24h, setIs24h] = useState(isBrowserLocale24h());
const [date, setDate] = useState(dayjs.utc(props.booking.startTime));
const { isReady, Theme } = useTheme(props.profile.theme);
-
+ const isEmbed = useIsEmbed();
useEffect(() => {
+ let embedIframeWidth = 0;
setDate(date.tz(localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess()));
setIs24h(!!localStorage.getItem("timeOption.is24hClock"));
- }, []);
+ if (isEmbed) {
+ requestAnimationFrame(function fixStripeIframe() {
+ // HACK: Look for stripe iframe and center position it just above the embed content
+ const stripeIframeWrapper = document.querySelector(
+ 'iframe[src*="https://js.stripe.com/v3/authorize-with-url-inner"]'
+ )?.parentElement;
+ if (stripeIframeWrapper) {
+ stripeIframeWrapper.style.margin = "0 auto";
+ stripeIframeWrapper.style.width = embedIframeWidth + "px";
+ }
+ requestAnimationFrame(fixStripeIframe);
+ });
+ sdkActionManager?.on("__dimensionChanged", (e) => {
+ embedIframeWidth = e.detail.data.iframeWidth as number;
+ });
+ }
+ }, [isEmbed]);
const eventName = props.booking.title;
return isReady ? (
-
+
@@ -51,7 +70,10 @@ const PaymentPage: FC = (props) => {
diff --git a/apps/web/lib/hooks/useTheme.tsx b/apps/web/lib/hooks/useTheme.tsx
index fbcec078..9af36630 100644
--- a/apps/web/lib/hooks/useTheme.tsx
+++ b/apps/web/lib/hooks/useTheme.tsx
@@ -18,6 +18,8 @@ function applyThemeAndAddListener(theme: string) {
document.documentElement.classList.remove("dark");
}
} else {
+ document.documentElement.classList.remove("dark");
+ document.documentElement.classList.remove("light");
document.documentElement.classList.add(theme);
}
};
@@ -33,15 +35,16 @@ export default function useTheme(theme?: Maybe
) {
const embedTheme = useEmbedTheme();
// Embed UI configuration takes more precedence over App Configuration
theme = embedTheme || theme;
-
+ const [_theme, setTheme] = useState>(null);
useEffect(() => {
// TODO: isReady doesn't seem required now. This is also impacting PSI Score for pages which are using isReady.
setIsReady(true);
+ setTheme(theme);
}, []);
function Theme() {
const code = applyThemeAndAddListener.toString();
- const themeStr = theme ? `"${theme}"` : null;
+ const themeStr = _theme ? `"${_theme}"` : null;
return (
diff --git a/apps/web/pages/[user].tsx b/apps/web/pages/[user].tsx
index 1218612f..ddf754b0 100644
--- a/apps/web/pages/[user].tsx
+++ b/apps/web/pages/[user].tsx
@@ -1,6 +1,7 @@
import { ArrowRightIcon } from "@heroicons/react/outline";
import { BadgeCheckIcon } from "@heroicons/react/solid";
import { UserPlan } from "@prisma/client";
+import classNames from "classnames";
import { GetServerSidePropsContext } from "next";
import dynamic from "next/dynamic";
import Link from "next/link";
@@ -9,9 +10,10 @@ import React, { useEffect, useState } from "react";
import { Toaster } from "react-hot-toast";
import { JSONObject } from "superjson/dist/types";
-import { sdkActionManager, useEmbedStyles, useIsEmbed } from "@calcom/embed-core";
+import { sdkActionManager, useEmbedNonStylesConfig, useEmbedStyles, useIsEmbed } from "@calcom/embed-core";
import defaultEvents, {
getDynamicEventDescription,
+ getGroupName,
getUsernameList,
getUsernameSlugLink,
} from "@calcom/lib/defaultEvents";
@@ -23,6 +25,7 @@ import prisma from "@lib/prisma";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { inferSSRProps } from "@lib/types/inferSSRProps";
+import CustomBranding from "@components/CustomBranding";
import AvatarGroup from "@components/ui/AvatarGroup";
import { AvatarSSR } from "@components/ui/AvatarSSR";
@@ -37,7 +40,7 @@ interface EvtsToVerify {
}
export default function User(props: inferSSRProps) {
- const { users } = props;
+ const { users, profile } = props;
const [user] = users; //To be used when we only have a single user, not dynamic group
const { Theme } = useTheme(user.theme);
const { t } = useLocale();
@@ -102,13 +105,15 @@ export default function User(props: inferSSRProps) {
))}
);
+ const isEmbed = useIsEmbed();
const eventTypeListItemEmbedStyles = useEmbedStyles("eventTypeListItem");
+ const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
+ const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
const query = { ...router.query };
delete query.user; // So it doesn't display in the Link (and make tests fail)
useExposePlanGlobally("PRO");
const nameOrUsername = user.name || user.username || "";
const [evtsToVerify, setEvtsToVerify] = useState({});
- const isEmbed = useIsEmbed();
const telemetry = useTelemetry();
useEffect(() => {
@@ -128,8 +133,17 @@ export default function User(props: inferSSRProps) {
username={isDynamicGroup ? dynamicUsernames.join(", ") : (user.username as string) || ""}
// avatar={user.avatar || undefined}
/>
-
-
+
+
+
+
{isSingleUser && ( // When we deal with a single user, not dynamic group
@@ -284,6 +298,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
email: true,
name: true,
bio: true,
+ brandColor: true,
+ darkBrandColor: true,
avatar: true,
theme: true,
plan: true,
@@ -298,10 +314,36 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
notFound: true,
};
}
-
const isDynamicGroup = users.length > 1;
+ const dynamicNames = isDynamicGroup
+ ? users.map((user) => {
+ return user.name || "";
+ })
+ : [];
const [user] = users; //to be used when dealing with single user, not dynamic group
+
+ const profile = isDynamicGroup
+ ? {
+ name: getGroupName(dynamicNames),
+ image: null,
+ theme: null,
+ weekStart: "Sunday",
+ brandColor: "",
+ darkBrandColor: "",
+ allowDynamicBooking: users.some((user) => {
+ return !user.allowDynamicBooking;
+ })
+ ? false
+ : true,
+ }
+ : {
+ name: user.name || user.username,
+ image: user.avatar,
+ theme: user.theme,
+ brandColor: user.brandColor,
+ darkBrandColor: user.darkBrandColor,
+ };
const usersIds = users.map((user) => user.id);
const credentials = await prisma.credential.findMany({
where: {
@@ -337,6 +379,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
return {
props: {
users,
+ profile,
user: {
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
},
diff --git a/apps/web/pages/_document.tsx b/apps/web/pages/_document.tsx
index 9360d3aa..a927bd5f 100644
--- a/apps/web/pages/_document.tsx
+++ b/apps/web/pages/_document.tsx
@@ -5,7 +5,7 @@ type Props = Record
& DocumentProps;
class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const initialProps = await Document.getInitialProps(ctx);
- const isEmbed = ctx.req?.url?.includes("embed");
+ const isEmbed = ctx.req?.url?.includes("embed=");
return { ...initialProps, isEmbed };
}
@@ -27,7 +27,9 @@ class MyDocument extends Document {
{/* Keep the embed hidden till parent initializes and gives it the appropriate styles */}
-
+
diff --git a/apps/web/pages/success.tsx b/apps/web/pages/success.tsx
index f39a496f..65cda6e2 100644
--- a/apps/web/pages/success.tsx
+++ b/apps/web/pages/success.tsx
@@ -12,7 +12,12 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState, useRef } from "react";
-import { useIsEmbed, useEmbedStyles, useIsBackgroundTransparent } from "@calcom/embed-core";
+import {
+ useIsEmbed,
+ useEmbedStyles,
+ useIsBackgroundTransparent,
+ useEmbedNonStylesConfig,
+} from "@calcom/embed-core";
import { sdkActionManager } from "@calcom/embed-core";
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -88,7 +93,7 @@ function RedirectionToast({ url }: { url: string }) {
return (
<>
-
+
@@ -142,6 +147,9 @@ export default function Success(props: inferSSRProps
)
const isBackgroundTransparent = useIsBackgroundTransparent();
const isEmbed = useIsEmbed();
+ const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
+ const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
+
const attendeeName = typeof name === "string" ? name : "Nameless";
const eventNameObject = {
@@ -214,19 +222,22 @@ export default function Success(props: inferSSRProps)
description={needsConfirmation ? t("booking_submitted") : t("booking_confirmed")}
/>
-
-
+
+
{isSuccessRedirectAvailable(eventType) && eventType.successRedirectUrl ? (
) : null}{" "}
-
+
)
)}
- {userIsOwner && (
+ {userIsOwner && !isEmbed && (
diff --git a/apps/web/pages/team/[slug].tsx b/apps/web/pages/team/[slug].tsx
index 12013b3c..1e2160d4 100644
--- a/apps/web/pages/team/[slug].tsx
+++ b/apps/web/pages/team/[slug].tsx
@@ -86,7 +86,7 @@ function TeamPage({ team }: TeamPageProps) {
-
+
## Known Bugs and Upcoming Improvements
- Unsupported Browsers and versions. Documenting them and gracefully handling that.
+- Need to create a booking Shell so that common changes for embed can be applied there.
- Accessibility and UI/UX Issues
- let user choose the loader for ModalBox
- If website owner links the booking page directly for an event, should the user be able to go to events-listing page using back button ?
- Let user specify both dark and light theme colors. Right now the colors specified are for light theme.
- - Embed doesn't adapt to screen size without page refresh.
- - Try opening in portrait mode and then go to landscape mode.
- - In inline mode, due to changing height of iframe, the content goes beyond the fold. Automatic scroll needs to be implemented.
- - On Availability page, when selecting date, width doesn't increase. max-width is there but because of strict width restriction with iframe, it doesn't allow it to expand.
+ - Transparent support is not properly done for team links
+ - Maybe don't set border radius in inline mode or give option to configure border radius.
- Branding
- Powered by Cal.com and 'Try it for free'. Should they be shown only for FREE account.
- Branding at the bottom has been removed for UI improvements, need to see where to add it.
- API
- - Allow loader color customization using UI command itself too.
+ - Allow loader color customization using UI command itself too. Right now it's possible using CSS only.
- Automation Tests
- Run automation tests in CI
@@ -71,8 +70,6 @@ Make `dist/embed.umd.js` servable on URL
- Need to reduce the number of colors on booking page, so that UI configuration is simpler
- Dev Experience/Ease of Installation
- - Improved Demo
- - Seeding might be done for team event so that such an example is also available readily in index.html
- Do we need a one liner(like `window.dataLayer.push`) to inform SDK of something even if snippet is not yet on the page but would be there e.g. through GTM it would come late on the page ?
- Might be better to pass all configuration using a single base64encoded query param to booking page.
@@ -81,7 +78,7 @@ Make `dist/embed.umd.js` servable on URL
- Custom written Tailwind CSS is sent multiple times for different custom elements.
- Embed Code Generator
-
+- Option to disable redirect banner and let parent handle redirect.
- Release Issues
- Compatibility Issue - When embed-iframe.js is updated in such a way that it is not compatible with embed.js, doing a release might break the embed for some time. e.g. iframeReady event let's say get's changed to something else
- Best Case scenario - App and Website goes live at the same time. A website using embed loads the same updated and thus compatible versions of embed.js and embed-iframe.js
diff --git a/packages/embeds/embed-core/index.html b/packages/embeds/embed-core/index.html
index 6f6a53a8..cf7842da 100644
--- a/packages/embeds/embed-core/index.html
+++ b/packages/embeds/embed-core/index.html
@@ -5,6 +5,15 @@
if (!location.search.includes("nonResponsive")) {
document.write(' ');
}
+ (()=> {
+ const url = new URL(document.URL);
+ // Only run the example specified by only=, avoids distraction and faster to test.
+ const only = url.searchParams.get("only");
+ const namespace = only ? only.replace("ns:",""): null
+ if (namespace) {
+ location.hash="#cal-booking-place-" + namespace + "-iframe"
+ }
+ })()
@@ -64,7 +72,7 @@
This page has a non responsive version accessible here
Pre-render test page available at here
-
Book with Free User[Light Theme]
+
Book with Free User[Light Theme]
Corresponding Cal Link is being preloaded. Assuming that it would take you some time to click this
@@ -74,9 +82,12 @@
>
Other Popup Examples
-
Book with Free User[Dark Theme]
-
Book with Test Team[Light Theme]
-
Book with Test Team[Dark Theme]
+
Book with Free User[Auto Theme]
+
Book with Free User[Dark Theme]
+
Book with Test Team[Light Theme]
+
Book with Test Team[Dark Theme]
+
See Team Links [Auto Theme]
+
Book Paid Event [Auto Theme]
Embed for Pages behind authentication
Show Upcoming Bookings
@@ -85,18 +96,14 @@
- Default Namespace(Cal)[Dark Theme][inline][Guests(janedoe@gmail.com and test@gmail.com)]
+ Default Namespace(Cal)[Dark Theme][inline][Guests(janedoe@example.com and test@example.com)]
You would see last Booking page action in my place
-
- if you render booking embed in me, I would not let it be more than 30vh in height. So you would
- have to scroll to see the entire content
-
-
+
Loading .....
@@ -109,7 +116,7 @@
You would see last Booking page action in my place
-
If you render booking embed in me, I won't restrict you. The entire page is yours.
+
If you render booking embed in me, I won't restrict you. The entire page is yours. Content is by default aligned center
Change eventTypeListItem
bg color
@@ -117,6 +124,21 @@
Change body
bg color
+
+ Align left
+
+
+ Align Center
+
+
+ Change Date Button Color
+
+
Loading .....
@@ -136,7 +158,7 @@
-
Namespace "fourth"(Cal.ns.fourth)[Team Event Test][inline]
+
Namespace "fourth"(Cal.ns.fourth)[Team Event Test][inline taking entire width]
@@ -150,6 +172,23 @@
+
+
Namespace "fifth"(Cal.ns.fifth)[Team Event Test][inline along with some content]
+
+
+ You would see last Booking page action in my place
+
+
+
+ On the right side you can book a team meeting =>
+
+
+
+
+
+