-
Notifications
You must be signed in to change notification settings - Fork 9.3k
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
hariombalhara
wants to merge
36
commits into
main
Choose a base branch
from
router-preloading
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+3,046
−210
Open
Changes from all commits
Commits
Show all changes
36 commits
Select commit
Hold shift + click to select a range
939e3e6
mvp done
hariombalhara 53a3014
wip
hariombalhara 4ad57e5
fix ts errors and other code improvements
hariombalhara f3fca1f
fix ts errors
hariombalhara c18532a
ensure mobile layout support
hariombalhara 967ec30
Make skeleton responsive on screen resize
hariombalhara 97b1986
refactor
hariombalhara 42e8b94
Add test for EmbedElement
hariombalhara 6a34235
Merge branch 'main' into embed-loader
hbjORbj 48c7a6e
make skeleton closer to pixel perfect
retrogtx 2b6239e
Address PR feedback
hariombalhara bb37045
Router-preloading
hariombalhara dbc437e
Merge remote-tracking branch 'origin/main' into router-preloading
hariombalhara 5014dec
wip\
hariombalhara 901a492
Merge remote-tracking branch 'origin/main' into router-preloading
hariombalhara f30b947
wip
hariombalhara e07c249
Merge remote-tracking branch 'origin/main' into router-preloading
hariombalhara 240c81f
fix mrge.io feedback
hariombalhara 7b579f4
wip
hariombalhara d0d3c38
Merge remote-tracking branch 'origin/main' into router-preloading
hariombalhara c00536c
Add README and lifecycle
hariombalhara bf96525
Add README and lifecycle
hariombalhara af9f64d
Merge remote-tracking branch 'origin/main' into router-preloading
hariombalhara 2ab7f90
Update routing form-seed and some other fixes
hariombalhara 98a5569
Merge branch 'router-preloading' of github.com:calcom/cal.com into ro…
hariombalhara 6a1a601
remove linkFailed fix from the branch
hariombalhara c3dfa68
self-review
hariombalhara 1338c63
self-review-2
hariombalhara 4be639a
self-review-3
hariombalhara 08e7197
Merge remote-tracking branch 'origin/main' into router-preloading
hariombalhara 4b272ff
Handle soft connect\
hariombalhara d07b8b4
Merge remote-tracking branch 'origin/main' into router-preloading
hariombalhara e621bdf
Merge remote-tracking branch 'origin/main' into router-preloading
hariombalhara eb4f6d4
Update README and fix a bug with query parmas
hariombalhara 69ad37f
Merge branch 'main' into router-preloading
hariombalhara fbcc795
Add one more case in routing-html playground
hariombalhara File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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." } }); | ||
}), | ||
}), | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now
message
anderrorMessage
are differentiated, earlier message itself haderrorMessage