Skip to content

feat: Prerender booking link and reuse with headless router #20720

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 36 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
939e3e6
mvp done
hariombalhara Apr 10, 2025
53a3014
wip
hariombalhara Apr 10, 2025
4ad57e5
fix ts errors and other code improvements
hariombalhara Apr 10, 2025
f3fca1f
fix ts errors
hariombalhara Apr 10, 2025
c18532a
ensure mobile layout support
hariombalhara Apr 11, 2025
967ec30
Make skeleton responsive on screen resize
hariombalhara Apr 11, 2025
97b1986
refactor
hariombalhara Apr 11, 2025
42e8b94
Add test for EmbedElement
hariombalhara Apr 12, 2025
6a34235
Merge branch 'main' into embed-loader
hbjORbj Apr 14, 2025
48c7a6e
make skeleton closer to pixel perfect
retrogtx Apr 14, 2025
2b6239e
Address PR feedback
hariombalhara Apr 15, 2025
bb37045
Router-preloading
hariombalhara Apr 16, 2025
dbc437e
Merge remote-tracking branch 'origin/main' into router-preloading
hariombalhara Apr 17, 2025
5014dec
wip\
hariombalhara Apr 17, 2025
901a492
Merge remote-tracking branch 'origin/main' into router-preloading
hariombalhara Apr 22, 2025
f30b947
wip
hariombalhara Apr 22, 2025
e07c249
Merge remote-tracking branch 'origin/main' into router-preloading
hariombalhara Apr 24, 2025
240c81f
fix mrge.io feedback
hariombalhara Apr 24, 2025
7b579f4
wip
hariombalhara Apr 25, 2025
d0d3c38
Merge remote-tracking branch 'origin/main' into router-preloading
hariombalhara Apr 25, 2025
c00536c
Add README and lifecycle
hariombalhara Apr 25, 2025
bf96525
Add README and lifecycle
hariombalhara Apr 25, 2025
af9f64d
Merge remote-tracking branch 'origin/main' into router-preloading
hariombalhara Apr 28, 2025
2ab7f90
Update routing form-seed and some other fixes
hariombalhara Apr 28, 2025
98a5569
Merge branch 'router-preloading' of github.com:calcom/cal.com into ro…
hariombalhara Apr 28, 2025
6a1a601
remove linkFailed fix from the branch
hariombalhara Apr 28, 2025
c3dfa68
self-review
hariombalhara Apr 28, 2025
1338c63
self-review-2
hariombalhara Apr 28, 2025
4be639a
self-review-3
hariombalhara Apr 28, 2025
08e7197
Merge remote-tracking branch 'origin/main' into router-preloading
hariombalhara Apr 29, 2025
4b272ff
Handle soft connect\
hariombalhara Apr 29, 2025
d07b8b4
Merge remote-tracking branch 'origin/main' into router-preloading
hariombalhara Apr 29, 2025
e621bdf
Merge remote-tracking branch 'origin/main' into router-preloading
hariombalhara Apr 30, 2025
eb4f6d4
Update README and fix a bug with query parmas
hariombalhara May 1, 2025
69ad37f
Merge branch 'main' into router-preloading
hariombalhara May 1, 2025
fbcc795
Add one more case in routing-html playground
hariombalhara May 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions apps/web/pages/api/router/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import { getRoutedUrl } from "@calcom/lib/server/getRoutedUrl";

export default defaultHandler({
OPTIONS: Promise.resolve({
default: defaultResponder(async (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, cache-control, pragma");
res.status(204).end();
}),
}),
POST: Promise.resolve({
default: defaultResponder(async (req, res) => {
// getRoutedUrl has more detailed schema validation, we do a basic one here.
const params = req.body;
res.setHeader("Access-Control-Allow-Origin", "*");
const routedUrlData = await getRoutedUrl({ req, query: { ...params } });
if (routedUrlData?.notFound) {
return res.status(404).json({ status: "error", data: { message: "Form not found" } });
}

if (routedUrlData?.redirect?.destination) {
return res
.status(200)
.json({ status: "success", data: { redirect: routedUrlData.redirect.destination } });
}

if (routedUrlData?.props?.errorMessage) {
return res.status(400).json({ status: "error", data: { message: routedUrlData.props.errorMessage } });
}

if (routedUrlData?.props?.message) {
return res.status(200).json({ status: "success", data: { message: routedUrlData.props.message } });
}

return res
.status(500)
.json({ status: "error", data: { message: "Neither Route nor custom message found." } });
}),
}),
});
4 changes: 2 additions & 2 deletions apps/web/pages/router/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import PageWrapper from "@components/PageWrapper";

import { getServerSideProps } from "../../server/lib/router/getServerSideProps";

export default function Router({ form, message }: inferSSRProps<typeof getServerSideProps>) {
export default function Router({ form, message, errorMessage }: inferSSRProps<typeof getServerSideProps>) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now message and errorMessage are differentiated, earlier message itself had errorMessage

return (
<>
<Head>
Expand All @@ -17,7 +17,7 @@ export default function Router({ form, message }: inferSSRProps<typeof getServer
<div className="mx-auto my-0 max-w-3xl md:my-24">
<div className="w-full max-w-4xl ltr:mr-2 rtl:ml-2">
<div className="text-default bg-default -mx-4 rounded-sm border border-neutral-200 p-4 py-6 sm:mx-0 sm:px-8">
<div>{message}</div>
<div>{message || errorMessage}</div>
</div>
</div>
</div>
Expand Down
202 changes: 202 additions & 0 deletions packages/embeds/LIFECYCLE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
# Cal.com Embed Lifecycle Events

This document details the lifecycle events and states of Cal.com embeds, showing the interaction flow between the parent page and the iframe.

## Core Lifecycle Sequence

```mermaid
sequenceDiagram
participant Parent as Parent (User Page)
participant Embed as Embed (iframe)
participant Store as EmbedStore(iframe)
participant UI as UI Components(iframe)

Note over Parent: embed.js loads

alt Inline Embed
Parent->>Parent: Create cal-element
Parent->>Parent: Create iframe (visibility: hidden)
Parent->>Parent: Show loader
else Modal Embed
Note over Parent: No action unless prerender
end

alt Prerender Flow
Note over Parent: Set prerender=true in URL
Parent->>Store: Set prerenderState="inProgress"
Note over Store: Limited events allowed
Parent->>Parent: Create hidden iframe
Parent->>Parent: load booker(no slots)
Note over Parent: Wait for connect() call
end

alt Modal CTA clicked
Parent->>Parent: Create cal-modal-box
Parent->>Parent: Create iframe (visibility: hidden)
Parent->>Parent: Show loader
end
Note over Embed: background: transparent(stays transparent)
Note over Embed: body tag set to visibility: hidden waiting to be shown
Note over Embed: iframe webpage starts rendering

Note over Store: Initialize EmbedStore state
Store->>Store: Set NOT_INITIALIZED state
Store->>Store: Initialize UI config & theme

Note over Parent: Process URL params for prefill
Parent->>Store: Set prefill data from URL
Note over Store: Auto-populate form fields

Embed->>Parent: __iframeReady event
Note over Parent,Embed: Embed ready to receive messages
Note over Store: Set state to INITIALIZED

Store->>UI: Apply theme configuration
Note over UI: DEPRECATED: styles prop
Note over UI: Use cssVarsPerTheme instead
Store->>UI: Apply cssVarsPerTheme

Embed->>Parent: __dimensionChanged event
Note over Parent: Calculate and adjust iframe dimensions
Note over Store: Update parentInformedAboutContentHeight

alt isBookerPage
Note over Embed: Wait for booker ready state
end

Embed->>Parent: linkReady event
Note over Parent: Changes loading state to done
Note over Parent: Removes loader
Note over Parent: Sets iframe visibility to visible

Parent->>Embed: parentKnowsIframeReady event
Note over Embed: Makes body visible
Note over Store: Update UI configuration

alt Prerendering Active
Note over Store: Set prerenderState to completed
Parent->>Store: connect() with new config
Store->>Store: Reset parentInformedAboutContentHeight
Store->>Parent: Update iframe with new params
Note over Store: Remove prerender params
end

loop Dimension Monitoring
Embed->>Embed: Monitor content size changes
alt Dimensions Changed
Embed->>Parent: __dimensionChanged event
Parent->>Parent: Adjust iframe size to avoid scrollbar
end
end

alt Route Changes
UI->>Store: Update UI state
Store->>Parent: __routeChanged event
Parent->>Parent: Handle navigation
Note over Store: Preserve prefill data
end
```

## Detailed State Management

### EmbedStore States
- `NOT_INITIALIZED`: Initial state when iframe is created
- `INITIALIZED`: After __iframeReady event is processed
- `prerenderState`: Can be null | "inProgress" | "completed"

### Visibility States
1. Initial Creation:
- iframe.style.visibility = "hidden"
- body.style.visibility = "hidden"

2. After __iframeReady:
- iframe becomes visible (unless prerendering)

3. After parentKnowsIframeReady:
- body becomes visible
- Background remains transparent

## Event Details

1. **Initial Load**
- embed.js loads in parent page
- For inline embeds: Creates elements immediately
- For modal embeds: Waits for CTA click (unless prerendering)

2. **iframe Creation**
- iframe is created with `visibility: hidden`
- Loader is shown (default or skeleton)
- EmbedStore initialized

3. **__iframeReady Event**
- Fired by: Iframe
- Indicates: Embed is ready to receive messages
- Actions:
- Sets iframeReady flag to true
- Makes iframe visible (unless prerendering)
- Processes queued iframe commands

4. **__dimensionChanged Event**
- Fired by: Iframe
- Purpose: Maintain proper iframe sizing
- Triggers:
- On initial load
- When content size changes
- After window load completes

5. **linkReady Event**
- Fired by: Iframe
- Indicates: iframe is fully ready for use
- Requirements:
- parentInformedAboutContentHeight must be true
- For booker pages: booker must be in ready state
- Actions:
- Parent removes loader
- Parent makes iframe visible

6. **parentKnowsIframeReady Event**
- Fired by: Parent
- Indicates: Parent acknowledges iframe readiness
- Actions:
- Makes body visible
- For prerendering: marks prerenderState as "completed"

## Prerendering Flow

The prerendering flow follows a special path:

1. Initial State:
- prerenderState: null

2. During Prerender:
- prerenderState: "inProgress"
- Limited events allowed (only __iframeReady, __dimensionChanged)
- iframe and body remain hidden

3. After Connect:
- prerenderState: "completed"
- Full event flow enabled
- Visibility states updated

## Command Queue System

The embed system implements a command queue to handle instructions before the iframe is ready:

1. Commands are queued if iframe isn't ready:
```typescript
if (!this.iframeReady) {
this.iframeDoQueue.push(doInIframeArg);
return;
}
```

2. Queue is processed after __iframeReady event:
- All queued commands are executed in order
- New commands are executed immediately

## Error Handling

Page Load Errors:
- System monitors CalComPageStatus
- On non-200 status: fires linkFailed event
- Includes error code and URL information
Loading
Loading