Skip to content

Commit 6e9ec11

Browse files
hariombalharahbjORbjretrogtx
authored
feat: Prerender booking link and reuse with headless router (#20720)
* mvp done * wip * fix ts errors and other code improvements * fix ts errors * ensure mobile layout support * Make skeleton responsive on screen resize * refactor * Add test for EmbedElement * make skeleton closer to pixel perfect * Address PR feedback * Router-preloading ## What does this PR do? <!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. --> - Fixes #XXXX (GitHub issue number) - Fixes CAL-XXXX (Linear issue number - should be visible at the bottom of the GitHub issue description) ## Visual Demo (For contributors especially) A visual demonstration is strongly recommended, for both the original and new change **(video / image - any one)**. #### Video Demo (if applicable): - Show screen recordings of the issue or feature. - Demonstrate how to reproduce the issue, the behavior before and after the change. #### Image Demo (if applicable): - Add side-by-side screenshots of the original and updated change. - Highlight any significant change(s). ## Mandatory Tasks (DO NOT REMOVE) - [ ] I have self-reviewed the code (A decent size PR without self-review might be rejected). - [ ] I have updated the developer docs in /docs if this PR makes changes that would require a [documentation change](https://cal.com/docs). If N/A, write N/A here and check the checkbox. - [ ] I confirm automated tests are in place that prove my fix is effective or that my feature works. ## How should this be tested? <!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. Write details that help to start the tests --> - Are there environment variables that should be set? - What are the minimal test data to have? - What is expected (happy path) to have (input and output)? - Any other important info that could help to test that PR ## Checklist <!-- Remove bullet points below that don't apply to you --> - I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md) - My code doesn't follow the style guidelines of this project - I haven't commented my code, particularly in hard-to-understand areas - I haven't checked if my changes generate no new warnings * wip\ * wip * fix mrge.io feedback * wip * Add README and lifecycle * Add README and lifecycle * Update routing form-seed and some other fixes * remove linkFailed fix from the branch * self-review * self-review-2 * self-review-3 * Handle soft connect\ * Update README and fix a bug with query parmas * Add one more case in routing-html playground --------- Co-authored-by: Benny Joo <sldisek783@gmail.com> Co-authored-by: amrit <iamamrit27@gmail.com>
1 parent 3ee6a7e commit 6e9ec11

26 files changed

Lines changed: 3047 additions & 210 deletions

File tree

apps/web/pages/api/router/index.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
2+
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
3+
import { getRoutedUrl } from "@calcom/lib/server/getRoutedUrl";
4+
5+
export default defaultHandler({
6+
OPTIONS: Promise.resolve({
7+
default: defaultResponder(async (req, res) => {
8+
res.setHeader("Access-Control-Allow-Origin", "*");
9+
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
10+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, cache-control, pragma");
11+
res.status(204).end();
12+
}),
13+
}),
14+
POST: Promise.resolve({
15+
default: defaultResponder(async (req, res) => {
16+
// getRoutedUrl has more detailed schema validation, we do a basic one here.
17+
const params = req.body;
18+
res.setHeader("Access-Control-Allow-Origin", "*");
19+
const routedUrlData = await getRoutedUrl({ req, query: { ...params } });
20+
if (routedUrlData?.notFound) {
21+
return res.status(404).json({ status: "error", data: { message: "Form not found" } });
22+
}
23+
24+
if (routedUrlData?.redirect?.destination) {
25+
return res
26+
.status(200)
27+
.json({ status: "success", data: { redirect: routedUrlData.redirect.destination } });
28+
}
29+
30+
if (routedUrlData?.props?.errorMessage) {
31+
return res.status(400).json({ status: "error", data: { message: routedUrlData.props.errorMessage } });
32+
}
33+
34+
if (routedUrlData?.props?.message) {
35+
return res.status(200).json({ status: "success", data: { message: routedUrlData.props.message } });
36+
}
37+
38+
return res
39+
.status(500)
40+
.json({ status: "error", data: { message: "Neither Route nor custom message found." } });
41+
}),
42+
}),
43+
});

apps/web/pages/router/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import PageWrapper from "@components/PageWrapper";
88

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

11-
export default function Router({ form, message }: inferSSRProps<typeof getServerSideProps>) {
11+
export default function Router({ form, message, errorMessage }: inferSSRProps<typeof getServerSideProps>) {
1212
return (
1313
<>
1414
<Head>
@@ -17,7 +17,7 @@ export default function Router({ form, message }: inferSSRProps<typeof getServer
1717
<div className="mx-auto my-0 max-w-3xl md:my-24">
1818
<div className="w-full max-w-4xl ltr:mr-2 rtl:ml-2">
1919
<div className="text-default bg-default -mx-4 rounded-sm border border-neutral-200 p-4 py-6 sm:mx-0 sm:px-8">
20-
<div>{message}</div>
20+
<div>{message || errorMessage}</div>
2121
</div>
2222
</div>
2323
</div>

packages/embeds/LIFECYCLE.md

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
# Cal.com Embed Lifecycle Events
2+
3+
This document details the lifecycle events and states of Cal.com embeds, showing the interaction flow between the parent page and the iframe.
4+
5+
## Core Lifecycle Sequence
6+
7+
```mermaid
8+
sequenceDiagram
9+
participant Parent as Parent (User Page)
10+
participant Embed as Embed (iframe)
11+
participant Store as EmbedStore(iframe)
12+
participant UI as UI Components(iframe)
13+
14+
Note over Parent: embed.js loads
15+
16+
alt Inline Embed
17+
Parent->>Parent: Create cal-element
18+
Parent->>Parent: Create iframe (visibility: hidden)
19+
Parent->>Parent: Show loader
20+
else Modal Embed
21+
Note over Parent: No action unless prerender
22+
end
23+
24+
alt Prerender Flow
25+
Note over Parent: Set prerender=true in URL
26+
Parent->>Store: Set prerenderState="inProgress"
27+
Note over Store: Limited events allowed
28+
Parent->>Parent: Create hidden iframe
29+
Parent->>Parent: load booker(no slots)
30+
Note over Parent: Wait for connect() call
31+
end
32+
33+
alt Modal CTA clicked
34+
Parent->>Parent: Create cal-modal-box
35+
Parent->>Parent: Create iframe (visibility: hidden)
36+
Parent->>Parent: Show loader
37+
end
38+
Note over Embed: background: transparent(stays transparent)
39+
Note over Embed: body tag set to visibility: hidden waiting to be shown
40+
Note over Embed: iframe webpage starts rendering
41+
42+
Note over Store: Initialize EmbedStore state
43+
Store->>Store: Set NOT_INITIALIZED state
44+
Store->>Store: Initialize UI config & theme
45+
46+
Note over Parent: Process URL params for prefill
47+
Parent->>Store: Set prefill data from URL
48+
Note over Store: Auto-populate form fields
49+
50+
Embed->>Parent: __iframeReady event
51+
Note over Parent,Embed: Embed ready to receive messages
52+
Note over Store: Set state to INITIALIZED
53+
54+
Store->>UI: Apply theme configuration
55+
Note over UI: DEPRECATED: styles prop
56+
Note over UI: Use cssVarsPerTheme instead
57+
Store->>UI: Apply cssVarsPerTheme
58+
59+
Embed->>Parent: __dimensionChanged event
60+
Note over Parent: Calculate and adjust iframe dimensions
61+
Note over Store: Update parentInformedAboutContentHeight
62+
63+
alt isBookerPage
64+
Note over Embed: Wait for booker ready state
65+
end
66+
67+
Embed->>Parent: linkReady event
68+
Note over Parent: Changes loading state to done
69+
Note over Parent: Removes loader
70+
Note over Parent: Sets iframe visibility to visible
71+
72+
Parent->>Embed: parentKnowsIframeReady event
73+
Note over Embed: Makes body visible
74+
Note over Store: Update UI configuration
75+
76+
alt Prerendering Active
77+
Note over Store: Set prerenderState to completed
78+
Parent->>Store: connect() with new config
79+
Store->>Store: Reset parentInformedAboutContentHeight
80+
Store->>Parent: Update iframe with new params
81+
Note over Store: Remove prerender params
82+
end
83+
84+
loop Dimension Monitoring
85+
Embed->>Embed: Monitor content size changes
86+
alt Dimensions Changed
87+
Embed->>Parent: __dimensionChanged event
88+
Parent->>Parent: Adjust iframe size to avoid scrollbar
89+
end
90+
end
91+
92+
alt Route Changes
93+
UI->>Store: Update UI state
94+
Store->>Parent: __routeChanged event
95+
Parent->>Parent: Handle navigation
96+
Note over Store: Preserve prefill data
97+
end
98+
```
99+
100+
## Detailed State Management
101+
102+
### EmbedStore States
103+
- `NOT_INITIALIZED`: Initial state when iframe is created
104+
- `INITIALIZED`: After __iframeReady event is processed
105+
- `prerenderState`: Can be null | "inProgress" | "completed"
106+
107+
### Visibility States
108+
1. Initial Creation:
109+
- iframe.style.visibility = "hidden"
110+
- body.style.visibility = "hidden"
111+
112+
2. After __iframeReady:
113+
- iframe becomes visible (unless prerendering)
114+
115+
3. After parentKnowsIframeReady:
116+
- body becomes visible
117+
- Background remains transparent
118+
119+
## Event Details
120+
121+
1. **Initial Load**
122+
- embed.js loads in parent page
123+
- For inline embeds: Creates elements immediately
124+
- For modal embeds: Waits for CTA click (unless prerendering)
125+
126+
2. **iframe Creation**
127+
- iframe is created with `visibility: hidden`
128+
- Loader is shown (default or skeleton)
129+
- EmbedStore initialized
130+
131+
3. **__iframeReady Event**
132+
- Fired by: Iframe
133+
- Indicates: Embed is ready to receive messages
134+
- Actions:
135+
- Sets iframeReady flag to true
136+
- Makes iframe visible (unless prerendering)
137+
- Processes queued iframe commands
138+
139+
4. **__dimensionChanged Event**
140+
- Fired by: Iframe
141+
- Purpose: Maintain proper iframe sizing
142+
- Triggers:
143+
- On initial load
144+
- When content size changes
145+
- After window load completes
146+
147+
5. **linkReady Event**
148+
- Fired by: Iframe
149+
- Indicates: iframe is fully ready for use
150+
- Requirements:
151+
- parentInformedAboutContentHeight must be true
152+
- For booker pages: booker must be in ready state
153+
- Actions:
154+
- Parent removes loader
155+
- Parent makes iframe visible
156+
157+
6. **parentKnowsIframeReady Event**
158+
- Fired by: Parent
159+
- Indicates: Parent acknowledges iframe readiness
160+
- Actions:
161+
- Makes body visible
162+
- For prerendering: marks prerenderState as "completed"
163+
164+
## Prerendering Flow
165+
166+
The prerendering flow follows a special path:
167+
168+
1. Initial State:
169+
- prerenderState: null
170+
171+
2. During Prerender:
172+
- prerenderState: "inProgress"
173+
- Limited events allowed (only __iframeReady, __dimensionChanged)
174+
- iframe and body remain hidden
175+
176+
3. After Connect:
177+
- prerenderState: "completed"
178+
- Full event flow enabled
179+
- Visibility states updated
180+
181+
## Command Queue System
182+
183+
The embed system implements a command queue to handle instructions before the iframe is ready:
184+
185+
1. Commands are queued if iframe isn't ready:
186+
```typescript
187+
if (!this.iframeReady) {
188+
this.iframeDoQueue.push(doInIframeArg);
189+
return;
190+
}
191+
```
192+
193+
2. Queue is processed after __iframeReady event:
194+
- All queued commands are executed in order
195+
- New commands are executed immediately
196+
197+
## Error Handling
198+
199+
Page Load Errors:
200+
- System monitors CalComPageStatus
201+
- On non-200 status: fires linkFailed event
202+
- Includes error code and URL information

0 commit comments

Comments
 (0)