Fix: Embed Fixes, UI configuration PRO Only, Tests (#2341)
This commit is contained in:
parent
7c08e946c6
commit
5138c676b1
23 changed files with 628 additions and 209 deletions
10
.gitignore
vendored
10
.gitignore
vendored
|
@ -11,11 +11,11 @@ node_modules
|
||||||
# testing
|
# testing
|
||||||
coverage
|
coverage
|
||||||
/test-results/
|
/test-results/
|
||||||
playwright/videos
|
**/playwright/videos
|
||||||
playwright/screenshots
|
**/playwright/screenshots
|
||||||
playwright/artifacts
|
**/playwright/artifacts
|
||||||
playwright/results
|
**/playwright/results
|
||||||
playwright/reports/*
|
**/playwright/reports/*
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
.next/
|
.next/
|
||||||
|
|
8
apps/docs/pages/integrations/embed.mdx
Normal file
8
apps/docs/pages/integrations/embed.mdx
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
title: Embed Snippet
|
||||||
|
---
|
||||||
|
|
||||||
|
# Embed Snippet
|
||||||
|
|
||||||
|
The Embed Snippet allows your website visitors to book a meeting with you directly from your website. It works by you installing a small Javascript Snippet to your website.
|
||||||
|
[Mention possiblity of installation through tag managers as well]
|
|
@ -19,6 +19,7 @@ import { FormattedNumber, IntlProvider } from "react-intl";
|
||||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||||
import { timeZone } from "@lib/clock";
|
import { timeZone } from "@lib/clock";
|
||||||
import { BASE_URL } from "@lib/config/constants";
|
import { BASE_URL } from "@lib/config/constants";
|
||||||
|
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import useTheme from "@lib/hooks/useTheme";
|
import useTheme from "@lib/hooks/useTheme";
|
||||||
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
||||||
|
@ -41,13 +42,13 @@ dayjs.extend(customParseFormat);
|
||||||
|
|
||||||
type Props = AvailabilityTeamPageProps | AvailabilityPageProps;
|
type Props = AvailabilityTeamPageProps | AvailabilityPageProps;
|
||||||
|
|
||||||
const AvailabilityPage = ({ profile, eventType, workingHours, previousPage }: Props) => {
|
const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage }: Props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { rescheduleUid } = router.query;
|
const { rescheduleUid } = router.query;
|
||||||
const { isReady, Theme } = useTheme(profile.theme);
|
const { isReady, Theme } = useTheme(profile.theme);
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { contracts } = useContracts();
|
const { contracts } = useContracts();
|
||||||
|
useExposePlanGlobally(plan);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (eventType.metadata.smartContractAddress) {
|
if (eventType.metadata.smartContractAddress) {
|
||||||
const eventOwner = eventType.users[0];
|
const eventOwner = eventType.users[0];
|
||||||
|
|
11
apps/web/lib/hooks/useExposePlanGlobally.ts
Normal file
11
apps/web/lib/hooks/useExposePlanGlobally.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { UserPlan } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
export function useExposePlanGlobally(plan: UserPlan) {
|
||||||
|
// Don't wait for component to mount. Do it ASAP. Delaying it would delay UI Configuration.
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
// This variable is used by embed-iframe to determine if we should allow UI configuration
|
||||||
|
window.CalComPlan = plan;
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,13 +4,14 @@ import { GetServerSidePropsContext } from "next";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
import { JSONObject } from "superjson/dist/types";
|
import { JSONObject } from "superjson/dist/types";
|
||||||
|
|
||||||
import { sdkActionManager, useEmbedStyles } from "@calcom/embed-core";
|
import { sdkActionManager, useEmbedStyles } from "@calcom/embed-core";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|
||||||
|
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
|
||||||
import useTheme from "@lib/hooks/useTheme";
|
import useTheme from "@lib/hooks/useTheme";
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
@ -35,7 +36,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
|
||||||
const eventTypeListItemEmbedStyles = useEmbedStyles("eventTypeListItem");
|
const eventTypeListItemEmbedStyles = useEmbedStyles("eventTypeListItem");
|
||||||
const query = { ...router.query };
|
const query = { ...router.query };
|
||||||
delete query.user; // So it doesn't display in the Link (and make tests fail)
|
delete query.user; // So it doesn't display in the Link (and make tests fail)
|
||||||
|
useExposePlanGlobally("PRO");
|
||||||
const nameOrUsername = user.name || user.username || "";
|
const nameOrUsername = user.name || user.username || "";
|
||||||
const [evtsToVerify, setEvtsToVerify] = useState<EvtsToVerify>({});
|
const [evtsToVerify, setEvtsToVerify] = useState<EvtsToVerify>({});
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -225,6 +225,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
brandColor: user.brandColor,
|
brandColor: user.brandColor,
|
||||||
darkBrandColor: user.darkBrandColor,
|
darkBrandColor: user.darkBrandColor,
|
||||||
},
|
},
|
||||||
|
plan: user.plan,
|
||||||
date: dateParam,
|
date: dateParam,
|
||||||
eventType: eventTypeObject,
|
eventType: eventTypeObject,
|
||||||
workingHours,
|
workingHours,
|
||||||
|
|
|
@ -7,6 +7,7 @@ import React from "react";
|
||||||
import Button from "@calcom/ui/Button";
|
import Button from "@calcom/ui/Button";
|
||||||
|
|
||||||
import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar";
|
import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar";
|
||||||
|
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import useTheme from "@lib/hooks/useTheme";
|
import useTheme from "@lib/hooks/useTheme";
|
||||||
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
|
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
|
||||||
|
@ -27,7 +28,7 @@ function TeamPage({ team }: TeamPageProps) {
|
||||||
const { isReady, Theme } = useTheme();
|
const { isReady, Theme } = useTheme();
|
||||||
const showMembers = useToggleQuery("members");
|
const showMembers = useToggleQuery("members");
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
useExposePlanGlobally("PRO");
|
||||||
const eventTypes = (
|
const eventTypes = (
|
||||||
<ul className="space-y-3">
|
<ul className="space-y-3">
|
||||||
{team.eventTypes.map((type) => (
|
{team.eventTypes.map((type) => (
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { GetServerSidePropsContext } from "next";
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { JSONObject } from "superjson/dist/types";
|
import { JSONObject } from "superjson/dist/types";
|
||||||
|
|
||||||
|
import { UserPlan } from "@calcom/prisma/client";
|
||||||
|
|
||||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||||
import { getWorkingHours } from "@lib/availability";
|
import { getWorkingHours } from "@lib/availability";
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
|
@ -109,6 +111,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
// Team is always pro
|
||||||
|
plan: "PRO" as UserPlan,
|
||||||
profile: {
|
profile: {
|
||||||
name: team.name || team.slug,
|
name: team.name || team.slug,
|
||||||
slug: team.slug,
|
slug: team.slug,
|
||||||
|
|
|
@ -14,49 +14,50 @@ See [index.html](index.html) to understand how it can be used.
|
||||||
- `notes`
|
- `notes`
|
||||||
- `guests`
|
- `guests`
|
||||||
|
|
||||||
## How to use embed on any webpage no matter what framework.
|
## How to use embed on any webpage no matter what framework
|
||||||
|
|
||||||
- _Step-1._ Install the snippet
|
- _Step-1._ Install the snippet
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
(function (C, A, L) {
|
(function(C, A, L) {
|
||||||
|
let p = function(a, ar) {
|
||||||
|
a.q.push(ar);
|
||||||
|
};
|
||||||
let d = C.document;
|
let d = C.document;
|
||||||
C.Cal =
|
C.Cal = C.Cal || function() {
|
||||||
C.Cal ||
|
let cal = C.Cal;
|
||||||
function () {
|
let ar = arguments;
|
||||||
let cal = C.Cal;
|
if (!cal.loaded) {
|
||||||
let ar = arguments;
|
cal.ns = {};
|
||||||
if (!cal.loaded) {
|
cal.q = cal.q || [];
|
||||||
cal.ns = {};
|
d.head.appendChild(d.createElement("script")).src = A;
|
||||||
cal.q = cal.q || [];
|
cal.loaded = true;
|
||||||
d.head.appendChild(d.createElement("script")).src = A;
|
}
|
||||||
cal.loaded = true;
|
if (ar[0] === L) {
|
||||||
}
|
const api = function() {
|
||||||
if (ar[0] === L) {
|
p(api, arguments);
|
||||||
const api = function () {
|
};
|
||||||
api.q.push(arguments);
|
const namespace = ar[1];
|
||||||
};
|
api.q = api.q || [];
|
||||||
const namespace = arguments[1];
|
typeof namespace === "string" ? (cal.ns[namespace] = api) && p(api, ar) : p(cal, ar);
|
||||||
api.q = api.q || [];
|
return;
|
||||||
namespace ? (cal.ns[namespace] = api) : null;
|
}
|
||||||
return;
|
p(cal, ar);
|
||||||
}
|
};
|
||||||
cal.q.push(ar);
|
|
||||||
};
|
|
||||||
})(window, "https://cal.com/embed.js", "init");
|
})(window, "https://cal.com/embed.js", "init");
|
||||||
```
|
```
|
||||||
|
|
||||||
- _Step-2_. Give `init` instruction to it. It creates a queue so that even without embed.js being fetched, you can give instructions to embed.
|
- _Step-2_. Give `init` instruction to it. It creates a queue so that even without embed.js being fetched, you can give instructions to embed.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
Cal("init) // Creates default instance. Give instruction to it as Cal("instruction")
|
Cal("init) // Creates default instance. Give instruction to it as Cal("instruction")
|
||||||
```
|
```
|
||||||
|
|
||||||
**Optionally** if you want to install another instance of embed you can do
|
**Optionally** if you want to install another instance of embed you can do
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
Cal("init", "NAME_YOUR_OTHER_INSTANCE"); // Creates a named instance. Give instructions to it as Cal.ns.NAME_YOUR_OTHER_INSTANCE("instruction")
|
Cal("init", "NAME_YOUR_OTHER_INSTANCE"); // Creates a named instance. Give instructions to it as Cal.ns.NAME_YOUR_OTHER_INSTANCE("instruction")
|
||||||
```
|
```
|
||||||
|
|
||||||
- Step-1 and Step-2 must be followed in same order. After that you can give various instructions to embed as you like.
|
- Step-1 and Step-2 must be followed in same order. After that you can give various instructions to embed as you like.
|
||||||
|
|
||||||
|
@ -92,4 +93,53 @@ yarn dev
|
||||||
yarn build
|
yarn build
|
||||||
```
|
```
|
||||||
|
|
||||||
Make `dist/embed.umd.js` servable on URL http://cal.com/embed.js
|
Make `dist/embed.umd.js` servable on URL <http://cal.com/embed.js>
|
||||||
|
|
||||||
|
## Upcoming Improvements
|
||||||
|
|
||||||
|
- Unsupported Browsers and versions. Documenting them and gracefully handling that.
|
||||||
|
|
||||||
|
- Accessibility and UI/UX Issues
|
||||||
|
- Loader on ModalBox/popup
|
||||||
|
- 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 ?
|
||||||
|
|
||||||
|
- Bundling Related
|
||||||
|
- Minify CSS in embed.js
|
||||||
|
|
||||||
|
- Debuggability
|
||||||
|
- Send log messages from iframe to parent so that all logs can exist in a single queue forming a timeline.
|
||||||
|
- user should be able to use "on" instruction to understand what's going on in the system
|
||||||
|
- Error Tracking for embed.js
|
||||||
|
- Know where exactly it’s failing if it does.
|
||||||
|
|
||||||
|
- Improved Demo
|
||||||
|
- Seeding might be done for team event so that such an example is also available readily in index.html
|
||||||
|
|
||||||
|
- Dev Experience/Ease of Installation
|
||||||
|
- 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.
|
||||||
|
- Embed Code Generator
|
||||||
|
|
||||||
|
- UI Config Features
|
||||||
|
- Theme switch dynamically - If user switches the theme on website, he should be able to do it on embed.
|
||||||
|
- Text Color
|
||||||
|
- Brand color
|
||||||
|
- At some places Text is colored by using the color specific tailwind class. e.g. `text-gray-400` is the color of disabled date. He has 2 options, If user wants to customize that
|
||||||
|
- He can go and override the color on the class which doesn’t make sense
|
||||||
|
- He can identify the element and change the color by directly adding style, which might cause consistency issues if certain elements are missed.
|
||||||
|
- Challenges
|
||||||
|
- How would the user add on hover styles just using style attribute ?
|
||||||
|
- React Component
|
||||||
|
- `onClick` support with preloading
|
||||||
|
|
||||||
|
## Pending Documentation
|
||||||
|
|
||||||
|
- READMEs
|
||||||
|
- How to make a new element configurable using UI instruction ?
|
||||||
|
- Why do we NOT want to provide completely flexible CSS customization by adding whatever CSS user wants. ?
|
||||||
|
|
||||||
|
- docs.cal.com
|
||||||
|
- A complete document on how to use embed
|
||||||
|
|
||||||
|
- app.cal.com
|
||||||
|
- Get Embed code for each event-type
|
||||||
|
|
|
@ -8,6 +8,9 @@
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
(function (C, A, L) {
|
(function (C, A, L) {
|
||||||
|
let p = function (a, ar) {
|
||||||
|
a.q.push(ar);
|
||||||
|
};
|
||||||
let d = C.document;
|
let d = C.document;
|
||||||
C.Cal =
|
C.Cal =
|
||||||
C.Cal ||
|
C.Cal ||
|
||||||
|
@ -22,25 +25,18 @@
|
||||||
}
|
}
|
||||||
if (ar[0] === L) {
|
if (ar[0] === L) {
|
||||||
const api = function () {
|
const api = function () {
|
||||||
api.q.push(arguments);
|
p(api, arguments);
|
||||||
};
|
};
|
||||||
const namespace = arguments[1];
|
const namespace = ar[1];
|
||||||
api.q = api.q || [];
|
api.q = api.q || [];
|
||||||
namespace ? (cal.ns[namespace] = api) : null;
|
typeof namespace === "string" ? (cal.ns[namespace] = api) && p(api, ar) : p(cal, ar);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
cal.q.push(ar);
|
p(cal, ar);
|
||||||
};
|
};
|
||||||
})(window, "//localhost:3002/dist/embed.umd.js", "init");
|
})(window, "//localhost:3002/dist/embed.umd.js", "init");
|
||||||
</script>
|
</script>
|
||||||
<script>
|
|
||||||
Cal("init");
|
|
||||||
|
|
||||||
// Create a namespace "second". It can be accessed as Cal.ns.second with the exact same API as Cal
|
|
||||||
Cal("init", "second");
|
|
||||||
|
|
||||||
// Create a namespace "third". It can be accessed as Cal.ns.second with the exact same API as Cal
|
|
||||||
Cal("init", "third");
|
|
||||||
</script>
|
|
||||||
<style>
|
<style>
|
||||||
.debug {
|
.debug {
|
||||||
/* border: 1px solid black; */
|
/* border: 1px solid black; */
|
||||||
|
@ -54,7 +50,7 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h3>This page has a non responsive version accessible <a href="?nonResponsive">here</a></h3>
|
<h3>This page has a non responsive version accessible <a href="?nonResponsive">here</a></h3>
|
||||||
<h3>Pre-render test page available at <a href="?prerender-test">here</a></h3>
|
<h3>Pre-render test page available at <a href="?only=prerender-test">here</a></h3>
|
||||||
<div>
|
<div>
|
||||||
<button data-cal-link="free">Book with Free User</button>
|
<button data-cal-link="free">Book with Free User</button>
|
||||||
<div>
|
<div>
|
||||||
|
@ -67,13 +63,15 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="namespaces-test">
|
<div id="namespaces-test">
|
||||||
<div class="debug">
|
<div class="debug" id="cal-booking-place-default">
|
||||||
<h2>Default Namespace(Cal)<i>[Black Theme][Guests(janedoe@gmail.com and test@gmail.com)]</i></h2>
|
<h2>
|
||||||
|
Default Namespace(Cal)<i>[Dark Theme][inline][Guests(janedoe@gmail.com and test@gmail.com)]</i>
|
||||||
|
</h2>
|
||||||
<div>
|
<div>
|
||||||
<i><a href="?only=ns:default">Test in Zen Mode</a></i>
|
<i><a href="?only=ns:default">Test in Zen Mode</a></i>
|
||||||
</div>
|
</div>
|
||||||
<i id="booking-status-"> You would see last Booking page action in my place </i>
|
<i class="last-action"> You would see last Booking page action in my place </i>
|
||||||
<div id="cal-booking-place-" style="max-height: 30vh; overflow: scroll">
|
<div style="max-height: 30vh; overflow: scroll" class="place">
|
||||||
<div>
|
<div>
|
||||||
if you render booking embed in me, I would not let it be more than 30vh in height. So you would
|
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
|
have to scroll to see the entire content
|
||||||
|
@ -81,15 +79,15 @@
|
||||||
<div class="loader" id="cal-booking-loader-">Loading .....</div>
|
<div class="loader" id="cal-booking-loader-">Loading .....</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="debug">
|
<div class="debug" id="cal-booking-place-second">
|
||||||
<h2>Namespace "second"(Cal.ns.second)[Custom Styling]</h2>
|
<h2>Namespace "second"(Cal.ns.second)[Custom Styling][inline]</h2>
|
||||||
<div>
|
<div>
|
||||||
<i><a href="?only=ns:second">Test in Zen Mode</a></i>
|
<i><a href="?only=ns:second">Test in Zen Mode</a></i>
|
||||||
</div>
|
</div>
|
||||||
<i id="booking-status-second">
|
<i class="last-action">
|
||||||
<i>You would see last Booking page action in my place</i>
|
<i>You would see last Booking page action in my place</i>
|
||||||
</i>
|
</i>
|
||||||
<div id="cal-booking-place-second">
|
<div class="place">
|
||||||
<div>If you render booking embed in me, I won't restrict you. The entire page is yours.</div>
|
<div>If you render booking embed in me, I won't restrict you. The entire page is yours.</div>
|
||||||
<button
|
<button
|
||||||
onclick="(function () {Cal.ns.second('ui', {styles:{eventTypeListItem:{backgroundColor:'blue'}}})})()">
|
onclick="(function () {Cal.ns.second('ui', {styles:{eventTypeListItem:{backgroundColor:'blue'}}})})()">
|
||||||
|
@ -102,124 +100,182 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="debug">
|
<div class="debug" id="cal-booking-place-third">
|
||||||
<h2>Namespace "third"(Cal.ns.third)</h2>
|
<h2>Namespace "third"(Cal.ns.third)[inline]</h2>
|
||||||
<div>
|
<div>
|
||||||
<i><a href="?only=ns:third">Test in Zen Mode</a></i>
|
<i><a href="?only=ns:third">Test in Zen Mode</a></i>
|
||||||
</div>
|
</div>
|
||||||
<i id="booking-status-third">
|
<i class="last-action">
|
||||||
<i>You would see last Booking page action in my place</i>
|
<i>You would see last Booking page action in my place</i>
|
||||||
</i>
|
</i>
|
||||||
<div id="cal-booking-place-third" style="width: 30%">
|
<div style="width: 30%" class="place">
|
||||||
|
<div>If you render booking embed in me, I would not let you be more than 30% wide</div>
|
||||||
|
<div class="loader" id="cal-booking-loader-third">Loading .....</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="debug" id="cal-booking-place-fourth">
|
||||||
|
<h2>Namespace "fourth"(Cal.ns.fourth)[Team Event Test][inline]</h2>
|
||||||
|
<div>
|
||||||
|
<i><a href="?only=ns:fourth">Test in Zen Mode</a></i>
|
||||||
|
</div>
|
||||||
|
<i class="last-action">
|
||||||
|
<i>You would see last Booking page action in my place</i>
|
||||||
|
</i>
|
||||||
|
<div style="width: 30%" class="place">
|
||||||
<div>If you render booking embed in me, I would not let you be more than 30% wide</div>
|
<div>If you render booking embed in me, I would not let you be more than 30% wide</div>
|
||||||
<div class="loader" id="cal-booking-loader-third">Loading .....</div>
|
<div class="loader" id="cal-booking-loader-third">Loading .....</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
const searchParams = new URL(document.URL).searchParams;
|
|
||||||
const only = searchParams.get("only");
|
|
||||||
// In prerender-test, we would want to test just the prerender case and nothing else.
|
|
||||||
if (searchParams.get("prerender-test") === null) {
|
|
||||||
if (!only || only === "ns:default") {
|
|
||||||
Cal("inline", {
|
|
||||||
elementOrSelector: "#cal-booking-place-",
|
|
||||||
calLink: "pro?case=1",
|
|
||||||
config: {
|
|
||||||
name: "John Doe",
|
|
||||||
email: "johndoe@gmail.com",
|
|
||||||
notes: "Test Meeting",
|
|
||||||
guests: ["janedoe@gmail.com", "test@gmail.com"],
|
|
||||||
theme: "dark",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!only || only === "ns:second") {
|
|
||||||
// Bulk API is supported - Keep all configuration at one place.
|
|
||||||
Cal.ns.second(
|
|
||||||
[
|
|
||||||
"inline",
|
|
||||||
{
|
|
||||||
elementOrSelector: "#cal-booking-place-second",
|
|
||||||
calLink: "pro?case=2",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"ui",
|
|
||||||
{
|
|
||||||
styles: {
|
|
||||||
body: {
|
|
||||||
background: "white",
|
|
||||||
},
|
|
||||||
eventTypeListItem: {
|
|
||||||
backgroundColor: "#D3D3D3",
|
|
||||||
},
|
|
||||||
enabledDateButton: {
|
|
||||||
backgroundColor: "#D3D3D3",
|
|
||||||
},
|
|
||||||
disabledDateButton: {
|
|
||||||
backgroundColor: "lightslategray",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!only || only === "ns:third") {
|
|
||||||
Cal.ns.third(
|
|
||||||
[
|
|
||||||
"inline",
|
|
||||||
{
|
|
||||||
elementOrSelector: "#cal-booking-place-third",
|
|
||||||
calLink: "pro?case=3",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"ui",
|
|
||||||
{
|
|
||||||
styles: {
|
|
||||||
body: {
|
|
||||||
background: "white",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
document.getElementById("namespaces-test").style.display = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
Cal("preload", {
|
|
||||||
calLink: "free",
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<script>
|
<script>
|
||||||
const callback = function (e) {
|
const callback = function (e) {
|
||||||
const detail = e.detail;
|
const detail = e.detail;
|
||||||
const namespace = detail.namespace;
|
const namespace = detail.namespace || "default";
|
||||||
|
|
||||||
if (detail.type === "linkReady") {
|
if (detail.type === "linkReady") {
|
||||||
document.getElementById("cal-booking-loader-" + namespace).remove();
|
document.querySelector(`#cal-booking-place-${namespace} .loader`).remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById(`booking-status-${namespace}`).innerHTML = JSON.stringify(e.detail);
|
document.querySelector(`#cal-booking-place-${namespace} .last-action`).innerHTML = JSON.stringify(
|
||||||
|
e.detail
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Cal("on", {
|
const searchParams = new URL(document.URL).searchParams;
|
||||||
action: "*",
|
const only = searchParams.get("only");
|
||||||
callback,
|
if (!only || only === "ns:default") {
|
||||||
});
|
Cal("init", {
|
||||||
Cal.ns.second("on", {
|
debug: 1,
|
||||||
action: "*",
|
origin: "http://localhost:3000",
|
||||||
callback,
|
});
|
||||||
});
|
|
||||||
Cal.ns.third("on", {
|
Cal("inline", {
|
||||||
action: "*",
|
elementOrSelector: "#cal-booking-place-default .place",
|
||||||
callback,
|
calLink: "pro?case=1",
|
||||||
});
|
config: {
|
||||||
|
name: "John Doe",
|
||||||
|
email: "johndoe@gmail.com",
|
||||||
|
notes: "Test Meeting",
|
||||||
|
guests: ["janedoe@gmail.com", "test@gmail.com"],
|
||||||
|
theme: "dark",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
Cal("on", {
|
||||||
|
action: "*",
|
||||||
|
callback,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!only || only === "ns:second") {
|
||||||
|
// Create a namespace "second". It can be accessed as Cal.ns.second with the exact same API as Cal
|
||||||
|
Cal("init", "second", {
|
||||||
|
debug: 1,
|
||||||
|
origin: "http://localhost:3000",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bulk API is supported - Keep all configuration at one place.
|
||||||
|
Cal.ns.second(
|
||||||
|
[
|
||||||
|
"inline",
|
||||||
|
{
|
||||||
|
elementOrSelector: "#cal-booking-place-second .place",
|
||||||
|
calLink: "pro?case=2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"ui",
|
||||||
|
{
|
||||||
|
styles: {
|
||||||
|
body: {
|
||||||
|
background: "white",
|
||||||
|
},
|
||||||
|
eventTypeListItem: {
|
||||||
|
backgroundColor: "#D3D3D3",
|
||||||
|
},
|
||||||
|
enabledDateButton: {
|
||||||
|
backgroundColor: "#D3D3D3",
|
||||||
|
},
|
||||||
|
disabledDateButton: {
|
||||||
|
backgroundColor: "lightslategray",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
Cal.ns.second("on", {
|
||||||
|
action: "*",
|
||||||
|
callback,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!only || only === "ns:third") {
|
||||||
|
// Create a namespace "third". It can be accessed as Cal.ns.second with the exact same API as Cal
|
||||||
|
Cal("init", "third", {
|
||||||
|
debug: 1,
|
||||||
|
origin: "http://localhost:3000",
|
||||||
|
});
|
||||||
|
Cal.ns.third(
|
||||||
|
[
|
||||||
|
"inline",
|
||||||
|
{
|
||||||
|
elementOrSelector: "#cal-booking-place-third .place",
|
||||||
|
calLink: "pro/30min",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"ui",
|
||||||
|
{
|
||||||
|
styles: {
|
||||||
|
body: {
|
||||||
|
background: "white",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
Cal.ns.third("on", {
|
||||||
|
action: "*",
|
||||||
|
callback,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!only || only === "ns:fourth") {
|
||||||
|
Cal("init", "fourth", {
|
||||||
|
debug: 1,
|
||||||
|
origin: "http://localhost:3000",
|
||||||
|
});
|
||||||
|
Cal.ns.fourth(
|
||||||
|
[
|
||||||
|
"inline",
|
||||||
|
{
|
||||||
|
elementOrSelector: "#cal-booking-place-fourth .place",
|
||||||
|
calLink: "team/test-team",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"ui",
|
||||||
|
{
|
||||||
|
styles: {
|
||||||
|
body: {
|
||||||
|
background: "white",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
Cal.ns.fourth("on", {
|
||||||
|
action: "*",
|
||||||
|
callback,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!only || only === "prerender-test") {
|
||||||
|
Cal("preload", {
|
||||||
|
calLink: "free",
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<script></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -5,10 +5,12 @@
|
||||||
"main": "./index.ts",
|
"main": "./index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
"build:cal": "NEXT_PUBLIC_WEBSITE_URL='https://cal.com' yarn build",
|
||||||
"vite": "vite",
|
"vite": "vite",
|
||||||
"dev": "run-p 'build --watch' 'vite --port 3002 --strict-port --open'",
|
"dev": "run-p 'build --watch' 'vite --port 3002 --strict-port --open'",
|
||||||
"type-check": "tsc --pretty --noEmit",
|
"type-check": "tsc --pretty --noEmit",
|
||||||
"lint": "eslint --ext .ts,.js src"
|
"lint": "eslint --ext .ts,.js src",
|
||||||
|
"test-playwright": "yarn playwright test --config=playwright/config/playwright.config.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^2.8.6",
|
"vite": "^2.8.6",
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
async function globalSetup(/* config: FullConfig */) {}
|
||||||
|
|
||||||
|
export default globalSetup;
|
|
@ -0,0 +1,119 @@
|
||||||
|
import { PlaywrightTestConfig, Frame, devices, expect } from "@playwright/test";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
|
const outputDir = path.join("../results");
|
||||||
|
const testDir = path.join("../tests");
|
||||||
|
|
||||||
|
const config: PlaywrightTestConfig = {
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: 1,
|
||||||
|
workers: 1,
|
||||||
|
timeout: 60_000,
|
||||||
|
reporter: [
|
||||||
|
[process.env.CI ? "github" : "list"],
|
||||||
|
[
|
||||||
|
"html",
|
||||||
|
{ outputFolder: path.join(__dirname, "..", "reports", "playwright-html-report"), open: "never" },
|
||||||
|
],
|
||||||
|
["junit", { outputFile: path.join(__dirname, "..", "reports", "results.xml") }],
|
||||||
|
],
|
||||||
|
globalSetup: require.resolve("./globalSetup"),
|
||||||
|
outputDir,
|
||||||
|
webServer: {
|
||||||
|
// Start App Server manually - Can't be handled here. See https://github.com/microsoft/playwright/issues/8206
|
||||||
|
command: "yarn workspace @calcom/embed-core dev",
|
||||||
|
port: 3002,
|
||||||
|
timeout: 60_000,
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
},
|
||||||
|
use: {
|
||||||
|
baseURL: "http://localhost:3002",
|
||||||
|
locale: "en-US",
|
||||||
|
trace: "retain-on-failure",
|
||||||
|
headless: !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS,
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "chromium",
|
||||||
|
testDir,
|
||||||
|
use: { ...devices["Desktop Chrome"] },
|
||||||
|
},
|
||||||
|
/* {
|
||||||
|
name: "firefox",
|
||||||
|
use: { ...devices["Desktop Firefox"] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "webkit",
|
||||||
|
use: { ...devices["Desktop Safari"] },
|
||||||
|
}, */
|
||||||
|
],
|
||||||
|
};
|
||||||
|
export type ExpectedUrlDetails = {
|
||||||
|
searchParams?: Record<string, string | string[]>;
|
||||||
|
pathname?: string;
|
||||||
|
origin?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace PlaywrightTest {
|
||||||
|
//FIXME: how to restrict it to Frame only
|
||||||
|
interface Matchers<R> {
|
||||||
|
toBeEmbedCalLink(expectedUrlDetails?: ExpectedUrlDetails): R;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect.extend({
|
||||||
|
async toBeEmbedCalLink(iframe: Frame, expectedUrlDetails: ExpectedUrlDetails = {}) {
|
||||||
|
if (!iframe || !iframe.url) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () => `Expected to provide an iframe, got ${iframe}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const u = new URL(iframe.url());
|
||||||
|
const frameElement = await iframe.frameElement();
|
||||||
|
|
||||||
|
if (!(await frameElement.isVisible())) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () => `Expected iframe to be visible`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const pathname = u.pathname;
|
||||||
|
const expectedPathname = expectedUrlDetails.pathname;
|
||||||
|
if (expectedPathname && expectedPathname !== pathname) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () => `Expected pathname to be ${expectedPathname} but got ${pathname}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const origin = u.origin;
|
||||||
|
const expectedOrigin = expectedUrlDetails.origin;
|
||||||
|
if (expectedOrigin && expectedOrigin !== origin) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () => `Expected origin to be ${expectedOrigin} but got ${origin}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchParams = u.searchParams;
|
||||||
|
const expectedSearchParams = expectedUrlDetails.searchParams || {};
|
||||||
|
for (let [expectedKey, expectedValue] of Object.entries(expectedSearchParams)) {
|
||||||
|
const value = searchParams.get(expectedKey);
|
||||||
|
if (value !== expectedValue) {
|
||||||
|
return {
|
||||||
|
message: () => `${expectedKey} should have value ${expectedValue} but got value ${value}`,
|
||||||
|
pass: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
pass: true,
|
||||||
|
message: () => `passed`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export default config;
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { test as base } from "@playwright/test";
|
||||||
|
|
||||||
|
export const test = base.extend({});
|
21
packages/embeds/embed-core/playwright/lib/testUtils.ts
Normal file
21
packages/embeds/embed-core/playwright/lib/testUtils.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { Page } from "@playwright/test";
|
||||||
|
|
||||||
|
export function todo(title: string) {
|
||||||
|
test.skip(title, () => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getEmbedIframe = async ({ page, pathname }: { page: Page; pathname: string }) => {
|
||||||
|
// FIXME: Need to wait for the iframe to be properly added to shadow dom. There should be a no time boundation way to do it.
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, 1000);
|
||||||
|
});
|
||||||
|
let embedIframe = page.frame("cal-embed");
|
||||||
|
if (!embedIframe) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const u = new URL(embedIframe.url());
|
||||||
|
if (u.pathname === pathname) {
|
||||||
|
return embedIframe;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { expect } from "@playwright/test";
|
||||||
|
|
||||||
|
import { test } from "../fixtures/fixtures";
|
||||||
|
import { todo, getEmbedIframe } from "../lib/testUtils";
|
||||||
|
|
||||||
|
test("should open embed iframe on click", async ({ page }) => {
|
||||||
|
await page.goto("/?only=prerender-test");
|
||||||
|
let embedIframe = await getEmbedIframe({ page, pathname: "/free" });
|
||||||
|
expect(embedIframe).toBeFalsy();
|
||||||
|
|
||||||
|
await page.click('[data-cal-link="free"]');
|
||||||
|
|
||||||
|
embedIframe = await getEmbedIframe({ page, pathname: "/free" });
|
||||||
|
expect(embedIframe).toBeEmbedCalLink({
|
||||||
|
pathname: "/free",
|
||||||
|
});
|
||||||
|
});
|
21
packages/embeds/embed-core/playwright/tests/inline.test.ts
Normal file
21
packages/embeds/embed-core/playwright/tests/inline.test.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { expect, Frame } from "@playwright/test";
|
||||||
|
|
||||||
|
import { test } from "../fixtures/fixtures";
|
||||||
|
import { todo } from "../lib/testUtils";
|
||||||
|
|
||||||
|
test("Inline Iframe - Configured with Dark Theme", async ({ page }) => {
|
||||||
|
await page.goto("/?only=ns:default");
|
||||||
|
const embedIframe = page.frame({ url: /.*pro.*/ });
|
||||||
|
expect(embedIframe).toBeEmbedCalLink({
|
||||||
|
pathname: "/pro",
|
||||||
|
searchParams: {
|
||||||
|
theme: "dark",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
todo(
|
||||||
|
"Ensure that on all pages - [user], [user]/[type], team/[slug], team/[slug]/book, UI styling works if these pages are directly linked in embed"
|
||||||
|
);
|
||||||
|
|
||||||
|
todo("Check that UI Configuration doesn't work for Free Plan");
|
|
@ -2,6 +2,51 @@ import { useState, useEffect, CSSProperties } from "react";
|
||||||
|
|
||||||
import { sdkActionManager } from "./sdk-event";
|
import { sdkActionManager } from "./sdk-event";
|
||||||
|
|
||||||
|
let isSafariBrowser = false;
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const ua = navigator.userAgent.toLowerCase();
|
||||||
|
isSafariBrowser = ua.includes("safari") && !ua.includes("chrome");
|
||||||
|
if (isSafariBrowser) {
|
||||||
|
log("Safari Detected: Using setTimeout instead of rAF");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function keepRunningAsap(fn: (...arg: any) => void) {
|
||||||
|
if (isSafariBrowser) {
|
||||||
|
// https://adpiler.com/blog/the-full-solution-why-do-animations-run-slower-in-safari/
|
||||||
|
return setTimeout(fn, 50);
|
||||||
|
}
|
||||||
|
return requestAnimationFrame(fn);
|
||||||
|
}
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
CalEmbed: {
|
||||||
|
__logQueue?: any[];
|
||||||
|
};
|
||||||
|
CalComPlan: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function log(...args: any[]) {
|
||||||
|
let namespace;
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const searchParams = new URL(document.URL).searchParams;
|
||||||
|
namespace = typeof searchParams.get("embed") !== "undefined" ? "" : "_unknown_";
|
||||||
|
//TODO: Send postMessage to parent to get all log messages in the same queue.
|
||||||
|
window.CalEmbed = window.CalEmbed || {};
|
||||||
|
const logQueue = (window.CalEmbed.__logQueue = window.CalEmbed.__logQueue || []);
|
||||||
|
args.push({
|
||||||
|
ns: namespace,
|
||||||
|
});
|
||||||
|
args.unshift("CAL:");
|
||||||
|
logQueue.push(args);
|
||||||
|
if (searchParams.get("debug")) {
|
||||||
|
console.log(...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Only allow certain styles to be modified so that when we make any changes to HTML, we know what all embed styles might be impacted.
|
// Only allow certain styles to be modified so that when we make any changes to HTML, we know what all embed styles might be impacted.
|
||||||
// Keep this list to minimum, only adding those styles which are really needed.
|
// Keep this list to minimum, only adding those styles which are really needed.
|
||||||
interface EmbedStyles {
|
interface EmbedStyles {
|
||||||
|
@ -66,15 +111,23 @@ export const useEmbedStyles = (elementName: ElementName) => {
|
||||||
return styles[elementName] || {};
|
return styles[elementName] || {};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function unhideBody() {
|
||||||
|
document.body.style.display = "block";
|
||||||
|
}
|
||||||
// If you add a method here, give type safety to parent manually by adding it to embed.ts. Look for "parentKnowsIframeReady" in it
|
// If you add a method here, give type safety to parent manually by adding it to embed.ts. Look for "parentKnowsIframeReady" in it
|
||||||
export const methods = {
|
export const methods = {
|
||||||
ui: function style(uiConfig: UiConfig) {
|
ui: function style(uiConfig: UiConfig) {
|
||||||
// TODO: Create automatic logger for all methods. Useful for debugging.
|
// TODO: Create automatic logger for all methods. Useful for debugging.
|
||||||
console.log("Method: ui called", uiConfig);
|
log("Method: ui called", uiConfig);
|
||||||
|
if (window.CalComPlan && window.CalComPlan !== "PRO") {
|
||||||
|
log(`Upgrade to PRO for "ui" instruction to work`, window.CalComPlan);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const stylesConfig = uiConfig.styles;
|
const stylesConfig = uiConfig.styles;
|
||||||
|
|
||||||
// In case where parent gives instructions before setEmbedStyles is set.
|
// In case where parent gives instructions before CalComPlan is set.
|
||||||
if (!setEmbedStyles) {
|
// This is easily possible as React takes time to initialize and render components where this variable is set.
|
||||||
|
if (!window.CalComPlan) {
|
||||||
return requestAnimationFrame(() => {
|
return requestAnimationFrame(() => {
|
||||||
style(uiConfig);
|
style(uiConfig);
|
||||||
});
|
});
|
||||||
|
@ -88,7 +141,8 @@ export const methods = {
|
||||||
setEmbedStyles(stylesConfig);
|
setEmbedStyles(stylesConfig);
|
||||||
},
|
},
|
||||||
parentKnowsIframeReady: () => {
|
parentKnowsIframeReady: () => {
|
||||||
document.body.style.display = "block";
|
log("Method: `parentKnowsIframeReady` called");
|
||||||
|
unhideBody();
|
||||||
sdkActionManager?.fire("linkReady", {});
|
sdkActionManager?.fire("linkReady", {});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -104,18 +158,41 @@ const messageParent = (data: any) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function keepParentInformedAboutDimensionChanges() {
|
function keepParentInformedAboutDimensionChanges() {
|
||||||
let knownHiddenHeight: Number | null = null;
|
console.log("keepParentInformedAboutDimensionChanges executed");
|
||||||
|
|
||||||
|
let knownIframeHeight: Number | null = null;
|
||||||
let numDimensionChanges = 0;
|
let numDimensionChanges = 0;
|
||||||
requestAnimationFrame(function informAboutScroll() {
|
let isFirstTime = true;
|
||||||
// Because of scroll="no", this much is hidden from the user.
|
let isWindowLoadComplete = false;
|
||||||
const hiddenHeight = document.documentElement.scrollHeight - window.innerHeight;
|
keepRunningAsap(function informAboutScroll() {
|
||||||
|
if (document.readyState !== "complete") {
|
||||||
|
// Wait for window to load to correctly calculate the initial scroll height.
|
||||||
|
keepRunningAsap(informAboutScroll);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isWindowLoadComplete) {
|
||||||
|
// On Safari, even though document.readyState is complete, still the page is not rendered and we can't compute documentElement.scrollHeight correctly
|
||||||
|
// Postponing to just next cycle allow us to fix this.
|
||||||
|
setTimeout(() => {
|
||||||
|
isWindowLoadComplete = true;
|
||||||
|
informAboutScroll();
|
||||||
|
}, 10);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const documentScrollHeight = document.documentElement.scrollHeight;
|
||||||
|
const contentHeight = document.documentElement.offsetHeight;
|
||||||
|
// During first render let iframe tell parent that how much is the expected height to avoid scroll.
|
||||||
|
// Parent would set the same value as the height of iframe which would prevent scroll.
|
||||||
|
// On subsequent renders, consider html height as the height of the iframe. If we don't do this, then if iframe get's bigger in height, it would never shrink
|
||||||
|
let iframeHeight = isFirstTime ? documentScrollHeight : contentHeight;
|
||||||
|
isFirstTime = false;
|
||||||
// TODO: Handle width as well.
|
// TODO: Handle width as well.
|
||||||
if (knownHiddenHeight !== hiddenHeight) {
|
if (knownIframeHeight !== iframeHeight) {
|
||||||
knownHiddenHeight = hiddenHeight;
|
knownIframeHeight = iframeHeight;
|
||||||
numDimensionChanges++;
|
numDimensionChanges++;
|
||||||
// FIXME: This event shouldn't be subscribable by the user. Only by the SDK.
|
// FIXME: This event shouldn't be subscribable by the user. Only by the SDK.
|
||||||
sdkActionManager?.fire("dimension-changed", {
|
sdkActionManager?.fire("dimension-changed", {
|
||||||
hiddenHeight,
|
iframeHeight,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Parent Counterpart would change the dimension of iframe and thus page's dimension would be impacted which is recursive.
|
// Parent Counterpart would change the dimension of iframe and thus page's dimension would be impacted which is recursive.
|
||||||
|
@ -125,28 +202,39 @@ function keepParentInformedAboutDimensionChanges() {
|
||||||
console.warn("Too many dimension changes detected.");
|
console.warn("Too many dimension changes detected.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
requestAnimationFrame(informAboutScroll);
|
keepRunningAsap(informAboutScroll);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof window !== "undefined" && !location.search.includes("prerender=true")) {
|
if (typeof window !== "undefined") {
|
||||||
sdkActionManager?.on("*", (e) => {
|
const url = new URL(document.URL);
|
||||||
const detail = e.detail;
|
if (url.searchParams.get("prerender") !== "true" && typeof url.searchParams.get("embed") !== "undefined") {
|
||||||
//console.log(detail.fullType, detail.type, detail.data);
|
log("Initializing embed-iframe");
|
||||||
messageParent(detail);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("message", (e) => {
|
// If embed link is opened in top, and not in iframe. Let the page be visible.
|
||||||
const data: Record<string, any> = e.data;
|
if (top === window) {
|
||||||
if (!data) {
|
unhideBody();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const method: keyof typeof methods = data.method;
|
|
||||||
if (data.originator === "CAL" && typeof method === "string") {
|
|
||||||
methods[method]?.(data.arg);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
keepParentInformedAboutDimensionChanges();
|
sdkActionManager?.on("*", (e) => {
|
||||||
sdkActionManager?.fire("iframeReady", {});
|
const detail = e.detail;
|
||||||
|
//console.log(detail.fullType, detail.type, detail.data);
|
||||||
|
log(detail);
|
||||||
|
messageParent(detail);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("message", (e) => {
|
||||||
|
const data: Record<string, any> = e.data;
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const method: keyof typeof methods = data.method;
|
||||||
|
if (data.originator === "CAL" && typeof method === "string") {
|
||||||
|
methods[method]?.(data.arg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
keepParentInformedAboutDimensionChanges();
|
||||||
|
sdkActionManager?.fire("iframeReady", {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,10 @@ import { SdkActionManager } from "./sdk-action-manager";
|
||||||
declare module "*.css";
|
declare module "*.css";
|
||||||
|
|
||||||
type Namespace = string;
|
type Namespace = string;
|
||||||
type Config = Record<"origin", "string">;
|
type Config = {
|
||||||
|
origin: string;
|
||||||
|
debug: 1;
|
||||||
|
};
|
||||||
|
|
||||||
const globalCal = (window as CalWindow).Cal;
|
const globalCal = (window as CalWindow).Cal;
|
||||||
|
|
||||||
|
@ -135,9 +138,8 @@ export class Cal {
|
||||||
queryObject?: Record<string, string | string[]>;
|
queryObject?: Record<string, string | string[]>;
|
||||||
}) {
|
}) {
|
||||||
const iframe = (this.iframe = document.createElement("iframe"));
|
const iframe = (this.iframe = document.createElement("iframe"));
|
||||||
// FIXME: scrolling seems deprecated, though it works on Chrome. What's the recommended way to do it?
|
|
||||||
iframe.scrolling = "no";
|
|
||||||
iframe.className = "cal-embed";
|
iframe.className = "cal-embed";
|
||||||
|
iframe.name = "cal-embed";
|
||||||
const config = this.getConfig();
|
const config = this.getConfig();
|
||||||
|
|
||||||
// Prepare searchParams from config
|
// Prepare searchParams from config
|
||||||
|
@ -152,6 +154,9 @@ export class Cal {
|
||||||
|
|
||||||
const urlInstance = new URL(`${config.origin}/${calLink}`);
|
const urlInstance = new URL(`${config.origin}/${calLink}`);
|
||||||
urlInstance.searchParams.set("embed", this.namespace);
|
urlInstance.searchParams.set("embed", this.namespace);
|
||||||
|
if (config.debug) {
|
||||||
|
urlInstance.searchParams.set("debug", config.debug);
|
||||||
|
}
|
||||||
|
|
||||||
// Merge searchParams from config onto the URL which might have query params already
|
// Merge searchParams from config onto the URL which might have query params already
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
|
@ -162,13 +167,14 @@ export class Cal {
|
||||||
return iframe;
|
return iframe;
|
||||||
}
|
}
|
||||||
|
|
||||||
init(namespaceOrConfig: string | Config, config: Config = {} as Config) {
|
init(namespaceOrConfig?: string | Config, config: Config = {} as Config) {
|
||||||
if (namespaceOrConfig.hasOwnProperty("origin")) {
|
if (typeof namespaceOrConfig !== "string") {
|
||||||
config = namespaceOrConfig as Config;
|
config = (namespaceOrConfig || {}) as Config;
|
||||||
}
|
}
|
||||||
if (config?.origin) {
|
if (config?.origin) {
|
||||||
this.__config.origin = config.origin;
|
this.__config.origin = config.origin;
|
||||||
}
|
}
|
||||||
|
this.__config.debug = config.debug;
|
||||||
}
|
}
|
||||||
|
|
||||||
getConfig() {
|
getConfig() {
|
||||||
|
@ -307,7 +313,8 @@ export class Cal {
|
||||||
|
|
||||||
constructor(namespace: string, q: InstructionQueue) {
|
constructor(namespace: string, q: InstructionQueue) {
|
||||||
this.__config = {
|
this.__config = {
|
||||||
origin: import.meta.env.NEXT_PUBLIC_WEBSITE_URL || "https://cal.com",
|
// Keep cal.com hardcoded till the time embed.js deployment to cal.com/embed.js is automated. This is to prevent accidentally pushing of localhost domain to production
|
||||||
|
origin: /*import.meta.env.NEXT_PUBLIC_WEBSITE_URL || */ "https://cal.com",
|
||||||
};
|
};
|
||||||
this.namespace = namespace;
|
this.namespace = namespace;
|
||||||
this.actionManager = new SdkActionManager(namespace);
|
this.actionManager = new SdkActionManager(namespace);
|
||||||
|
@ -323,12 +330,16 @@ export class Cal {
|
||||||
this.actionManager.on("dimension-changed", (e) => {
|
this.actionManager.on("dimension-changed", (e) => {
|
||||||
const { data } = e.detail;
|
const { data } = e.detail;
|
||||||
const iframe = this.iframe!;
|
const iframe = this.iframe!;
|
||||||
|
|
||||||
if (!iframe) {
|
if (!iframe) {
|
||||||
// Iframe might be pre-rendering
|
// Iframe might be pre-rendering
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let proposedHeightByIframeWebsite = parseFloat(getComputedStyle(iframe).height) + data.hiddenHeight;
|
let proposedHeightByIframeWebsite = data.iframeHeight;
|
||||||
iframe.style.height = proposedHeightByIframeWebsite;
|
iframe.style.height = proposedHeightByIframeWebsite + "px";
|
||||||
|
// It ensures that if the iframe is so tall that it can't fit in the parent window without scroll. Then force the scroll by restricting the max-height to innerHeight
|
||||||
|
// This case is reproducible when viewing in ModalBox on Mobile.
|
||||||
|
iframe.style.maxHeight = window.innerHeight + "px";
|
||||||
});
|
});
|
||||||
|
|
||||||
this.actionManager.on("iframeReady", (e) => {
|
this.actionManager.on("iframeReady", (e) => {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"extends": "@calcom/tsconfig/base.json",
|
"extends": "@calcom/tsconfig/base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "ESNext",
|
"module": "esnext",
|
||||||
"moduleResolution": "Node",
|
"moduleResolution": "Node",
|
||||||
"baseUrl": "."
|
"baseUrl": "."
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,9 +3,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --port=3002",
|
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
|
||||||
"type-check": "tsc --pretty --noEmit",
|
"type-check": "tsc --pretty --noEmit",
|
||||||
"lint": "eslint --ext .ts,.js src"
|
"lint": "eslint --ext .ts,.js src"
|
||||||
},
|
},
|
||||||
|
|
|
@ -22,6 +22,9 @@ export interface CalWindow extends Window {
|
||||||
export default function EmbedSnippet(url = "https://cal.com/embed.js") {
|
export default function EmbedSnippet(url = "https://cal.com/embed.js") {
|
||||||
/*! Copy the code below and paste it in script tag of your website */
|
/*! Copy the code below and paste it in script tag of your website */
|
||||||
(function (C: CalWindow, A, L) {
|
(function (C: CalWindow, A, L) {
|
||||||
|
let p = function (a: any, ar: any) {
|
||||||
|
a.q.push(ar);
|
||||||
|
};
|
||||||
let d = C.document;
|
let d = C.document;
|
||||||
C.Cal =
|
C.Cal =
|
||||||
C.Cal ||
|
C.Cal ||
|
||||||
|
@ -37,14 +40,14 @@ export default function EmbedSnippet(url = "https://cal.com/embed.js") {
|
||||||
|
|
||||||
if (ar[0] === L) {
|
if (ar[0] === L) {
|
||||||
const api: { (): void; q: any[] } = function () {
|
const api: { (): void; q: any[] } = function () {
|
||||||
api.q.push(arguments);
|
p(api, arguments);
|
||||||
};
|
};
|
||||||
const namespace = arguments[1];
|
const namespace = ar[1];
|
||||||
api.q = api.q || [];
|
api.q = api.q || [];
|
||||||
namespace ? (cal.ns![namespace] = api) : null;
|
typeof namespace === "string" ? (cal.ns![namespace] = api) && p(api, ar) : p(cal, ar);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cal.q!.push(ar as unknown as Instruction);
|
p(cal, ar);
|
||||||
};
|
};
|
||||||
})(
|
})(
|
||||||
window,
|
window,
|
||||||
|
|
|
@ -4,7 +4,7 @@ const { defineConfig } = require("vite");
|
||||||
module.exports = defineConfig({
|
module.exports = defineConfig({
|
||||||
build: {
|
build: {
|
||||||
lib: {
|
lib: {
|
||||||
entry: path.resolve(__dirname, "index.ts"),
|
entry: path.resolve(__dirname, "src", "index.ts"),
|
||||||
name: "snippet",
|
name: "snippet",
|
||||||
fileName: (format) => `snippet.${format}.js`,
|
fileName: (format) => `snippet.${format}.js`,
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue