From 5138c676b138f1b3dfe27adb8bc2130823749820 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Mon, 4 Apr 2022 21:14:04 +0530 Subject: [PATCH] Fix: Embed Fixes, UI configuration PRO Only, Tests (#2341) --- .gitignore | 10 +- apps/docs/pages/integrations/embed.mdx | 8 + .../booking/pages/AvailabilityPage.tsx | 5 +- apps/web/lib/hooks/useExposePlanGlobally.ts | 11 + apps/web/pages/[user].tsx | 5 +- apps/web/pages/[user]/[type].tsx | 1 + apps/web/pages/team/[slug].tsx | 3 +- apps/web/pages/team/[slug]/[type].tsx | 4 + packages/embeds/embed-core/README.md | 116 +++++-- packages/embeds/embed-core/index.html | 294 +++++++++++------- packages/embeds/embed-core/package.json | 4 +- .../playwright/config/globalSetup.ts | 3 + .../playwright/config/playwright.config.ts | 119 +++++++ .../playwright/fixtures/fixtures.ts | 3 + .../embed-core/playwright/lib/testUtils.ts | 21 ++ .../playwright/tests/action-based.test.ts | 17 + .../playwright/tests/inline.test.ts | 21 ++ .../embeds/embed-core/src/embed-iframe.ts | 146 +++++++-- packages/embeds/embed-core/src/embed.ts | 29 +- packages/embeds/embed-core/tsconfig.json | 2 +- packages/embeds/embed-snippet/package.json | 2 - packages/embeds/embed-snippet/src/index.ts | 11 +- packages/embeds/embed-snippet/vite.config.js | 2 +- 23 files changed, 628 insertions(+), 209 deletions(-) create mode 100644 apps/docs/pages/integrations/embed.mdx create mode 100644 apps/web/lib/hooks/useExposePlanGlobally.ts create mode 100644 packages/embeds/embed-core/playwright/config/globalSetup.ts create mode 100644 packages/embeds/embed-core/playwright/config/playwright.config.ts create mode 100644 packages/embeds/embed-core/playwright/fixtures/fixtures.ts create mode 100644 packages/embeds/embed-core/playwright/lib/testUtils.ts create mode 100644 packages/embeds/embed-core/playwright/tests/action-based.test.ts create mode 100644 packages/embeds/embed-core/playwright/tests/inline.test.ts diff --git a/.gitignore b/.gitignore index a5da3c9c..53ec8256 100644 --- a/.gitignore +++ b/.gitignore @@ -11,11 +11,11 @@ node_modules # testing coverage /test-results/ -playwright/videos -playwright/screenshots -playwright/artifacts -playwright/results -playwright/reports/* +**/playwright/videos +**/playwright/screenshots +**/playwright/artifacts +**/playwright/results +**/playwright/reports/* # next.js .next/ diff --git a/apps/docs/pages/integrations/embed.mdx b/apps/docs/pages/integrations/embed.mdx new file mode 100644 index 00000000..4503cb3b --- /dev/null +++ b/apps/docs/pages/integrations/embed.mdx @@ -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] diff --git a/apps/web/components/booking/pages/AvailabilityPage.tsx b/apps/web/components/booking/pages/AvailabilityPage.tsx index 67fd9c25..b927eb3c 100644 --- a/apps/web/components/booking/pages/AvailabilityPage.tsx +++ b/apps/web/components/booking/pages/AvailabilityPage.tsx @@ -19,6 +19,7 @@ import { FormattedNumber, IntlProvider } from "react-intl"; import { asStringOrNull } from "@lib/asStringOrNull"; import { timeZone } from "@lib/clock"; import { BASE_URL } from "@lib/config/constants"; +import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally"; import { useLocale } from "@lib/hooks/useLocale"; import useTheme from "@lib/hooks/useTheme"; import { isBrandingHidden } from "@lib/isBrandingHidden"; @@ -41,13 +42,13 @@ dayjs.extend(customParseFormat); type Props = AvailabilityTeamPageProps | AvailabilityPageProps; -const AvailabilityPage = ({ profile, eventType, workingHours, previousPage }: Props) => { +const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage }: Props) => { const router = useRouter(); const { rescheduleUid } = router.query; const { isReady, Theme } = useTheme(profile.theme); const { t } = useLocale(); const { contracts } = useContracts(); - + useExposePlanGlobally(plan); useEffect(() => { if (eventType.metadata.smartContractAddress) { const eventOwner = eventType.users[0]; diff --git a/apps/web/lib/hooks/useExposePlanGlobally.ts b/apps/web/lib/hooks/useExposePlanGlobally.ts new file mode 100644 index 00000000..ea59dce7 --- /dev/null +++ b/apps/web/lib/hooks/useExposePlanGlobally.ts @@ -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; + } +} diff --git a/apps/web/pages/[user].tsx b/apps/web/pages/[user].tsx index 05ba5418..6e0358be 100644 --- a/apps/web/pages/[user].tsx +++ b/apps/web/pages/[user].tsx @@ -4,13 +4,14 @@ import { GetServerSidePropsContext } from "next"; import dynamic from "next/dynamic"; import Link from "next/link"; import { useRouter } from "next/router"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { Toaster } from "react-hot-toast"; import { JSONObject } from "superjson/dist/types"; import { sdkActionManager, useEmbedStyles } from "@calcom/embed-core"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally"; import useTheme from "@lib/hooks/useTheme"; import prisma from "@lib/prisma"; import { inferSSRProps } from "@lib/types/inferSSRProps"; @@ -35,7 +36,7 @@ export default function User(props: inferSSRProps) { const eventTypeListItemEmbedStyles = useEmbedStyles("eventTypeListItem"); 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({}); return ( diff --git a/apps/web/pages/[user]/[type].tsx b/apps/web/pages/[user]/[type].tsx index 569b1d1f..9748dfb6 100644 --- a/apps/web/pages/[user]/[type].tsx +++ b/apps/web/pages/[user]/[type].tsx @@ -225,6 +225,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => brandColor: user.brandColor, darkBrandColor: user.darkBrandColor, }, + plan: user.plan, date: dateParam, eventType: eventTypeObject, workingHours, diff --git a/apps/web/pages/team/[slug].tsx b/apps/web/pages/team/[slug].tsx index 56aa697e..653f7620 100644 --- a/apps/web/pages/team/[slug].tsx +++ b/apps/web/pages/team/[slug].tsx @@ -7,6 +7,7 @@ import React from "react"; import Button from "@calcom/ui/Button"; import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar"; +import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally"; import { useLocale } from "@lib/hooks/useLocale"; import useTheme from "@lib/hooks/useTheme"; import { useToggleQuery } from "@lib/hooks/useToggleQuery"; @@ -27,7 +28,7 @@ function TeamPage({ team }: TeamPageProps) { const { isReady, Theme } = useTheme(); const showMembers = useToggleQuery("members"); const { t } = useLocale(); - + useExposePlanGlobally("PRO"); const eventTypes = (
    {team.eventTypes.map((type) => ( diff --git a/apps/web/pages/team/[slug]/[type].tsx b/apps/web/pages/team/[slug]/[type].tsx index 9c048399..8fe83b17 100644 --- a/apps/web/pages/team/[slug]/[type].tsx +++ b/apps/web/pages/team/[slug]/[type].tsx @@ -1,6 +1,8 @@ import { GetServerSidePropsContext } from "next"; import { JSONObject } from "superjson/dist/types"; +import { UserPlan } from "@calcom/prisma/client"; + import { asStringOrNull } from "@lib/asStringOrNull"; import { getWorkingHours } from "@lib/availability"; import prisma from "@lib/prisma"; @@ -109,6 +111,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => return { props: { + // Team is always pro + plan: "PRO" as UserPlan, profile: { name: team.name || team.slug, slug: team.slug, diff --git a/packages/embeds/embed-core/README.md b/packages/embeds/embed-core/README.md index 0a8edc5f..3dddfd90 100644 --- a/packages/embeds/embed-core/README.md +++ b/packages/embeds/embed-core/README.md @@ -14,49 +14,50 @@ See [index.html](index.html) to understand how it can be used. - `notes` - `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 - ```javascript - (function (C, A, L) { + ```javascript + (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 () { - api.q.push(arguments); - }; - const namespace = arguments[1]; - api.q = api.q || []; - namespace ? (cal.ns[namespace] = api) : null; - return; - } - cal.q.push(ar); - }; + 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, "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. - ```javascript - Cal("init) // Creates default instance. Give instruction to it as Cal("instruction") - ``` + ```javascript + 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 - Cal("init", "NAME_YOUR_OTHER_INSTANCE"); // Creates a named instance. Give instructions to it as Cal.ns.NAME_YOUR_OTHER_INSTANCE("instruction") - ``` + ```javascript + 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. @@ -92,4 +93,53 @@ yarn dev yarn build ``` -Make `dist/embed.umd.js` servable on URL http://cal.com/embed.js +Make `dist/embed.umd.js` servable on URL + +## 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 diff --git a/packages/embeds/embed-core/index.html b/packages/embeds/embed-core/index.html index facece68..6ad532f3 100644 --- a/packages/embeds/embed-core/index.html +++ b/packages/embeds/embed-core/index.html @@ -8,6 +8,9 @@ -