fix(companion): event type links for org user#5
Conversation
Addresses Cubic AI review feedback (confidence 9/10): The username was hardcoded to 'username' but still used by BasicsTab as a fallback for URL display when bookingUrl is unavailable. This restores the useEffect that fetches the actual username from CalComAPIService.getUsername(). Co-Authored-By: unknown <>
| const bookingUrl = | ||
| `https://cal.com/${ | ||
| eventType.users?.[0]?.username || "user" | ||
| }/${eventType.slug}`; |
There was a problem hiding this comment.
🔴 The first copy button handler (line 1125) was only reformatted but not updated to use eventType.bookingUrl || fallback, unlike the preview handler directly above it (line 1081) and the second copy handler (line 1871), which were both correctly updated. For org users, clicking "Copy Link" on event types in the first section will always produce https://cal.com/... instead of the org-specific booking URL, while Preview works correctly right next to it.
Extended reasoning...
What the bug is
This PR adds bookingUrl support for org users across the companion extension. The content.ts file has two sets of event type UI (for different contexts), each with a preview button handler and a copy button handler. All four handlers previously hardcoded https://cal.com/username/slug. The PR correctly updated three of these four handlers to use eventType.bookingUrl || fallback, but missed the first copy handler.
The specific code path
In content.ts, the first set of event type buttons has:
- Preview handler (line 1081-1085): Correctly updated to
eventType.bookingUrl || https://cal.com/... - Copy handler (line 1125-1128): Only reformatted (whitespace changes) - still uses
https://cal.com/...without theeventType.bookingUrl ||prefix
Meanwhile, the second set of buttons has both handlers correctly updated:
- Preview handler (line 1826-1828): Uses
eventType.bookingUrl || fallback - Copy handler (line 1871-1873): Uses
eventType.bookingUrl || fallback
Why existing code does not prevent it
There is no shared utility function for building booking URLs in content.ts. Each handler constructs the URL inline, so each occurrence must be updated independently. This one was simply missed during the PR.
Step-by-step proof
- An org user (e.g., with booking URL
https://i.cal.com/keith/30min) loads a page where the extension injects event type buttons (first context). - The user clicks Preview - the preview handler reads
eventType.bookingUrlwhich ishttps://i.cal.com/keith/30minand opens the correct URL. - The user clicks Copy Link - the copy handler ignores
eventType.bookingUrlentirely and constructshttps://cal.com/keith/30min, copying the WRONG URL. - The user pastes the copied link and sends it to someone. The recipient gets a broken or incorrect link.
Impact
Org users who copy event type links from the first section of the browser extension will share incorrect URLs. This defeats the core purpose of the PR for this specific action, creating an inconsistency where Preview works but Copy Link does not.
How to fix
Change line 1125-1128 from:
const bookingUrl =
`https://cal.com/${eventType.users?.[0]?.username || "user"}/${eventType.slug}`;to:
const bookingUrl =
eventType.bookingUrl ||
`https://cal.com/${eventType.users?.[0]?.username || "user"}/${eventType.slug}`;This matches the pattern used in the preview handler above and the second copy handler.
| if (!bookingUrl) { | ||
| showErrorAlert("Error", "Booking URL not available. Please save the event type first."); | ||
| return; |
There was a problem hiding this comment.
🔴 setBookingUrl is never called anywhere in this component, so the bookingUrl state remains "" forever. This means handlePreview and handleCopyLink always show "Booking URL not available" errors, and BasicsTab always falls back to cal.com/{username}/ instead of showing the correct org URL. Add if (eventType.bookingUrl) setBookingUrl(eventType.bookingUrl); inside applyEventTypeData.
Extended reasoning...
What the bug is
The PR introduces a new bookingUrl state variable at line 180 (const [bookingUrl, setBookingUrl] = useState<string>("")) and replaces the old buildEventTypeLink() calls in handlePreview and handleCopyLink with this state variable. However, setBookingUrl is never called anywhere in the entire component — the setter function is completely unused after declaration.
The specific code path
When event type data is fetched from the API, the applyEventTypeData callback (starting around line 451) is invoked. This callback meticulously loads dozens of fields from the fetched event type: title, slug, description, lengthInMinutes, hidden, scheduleId, locations, beforeEventBuffer, afterEventBuffer, minimumBookingNotice, slotInterval, bookingLimitsCount, disableCancelling, disableRescheduling, recurrence, seats, and many more. But it conspicuously omits any call to setBookingUrl(eventType.bookingUrl). Since bookingUrl is initialized to "" (empty string, which is falsy), it never changes.
Step-by-step proof
- User navigates to event-type-detail for an existing event type
fetchEventTypeData()is called, which fetches the event type from the API (the API now returns abookingUrlfield per the type changes in this PR)applyEventTypeData(eventType)is called with the fetched dataapplyEventTypeDatasets title, slug, description, duration, hidden, scheduleId, etc. — but never callssetBookingUrl(eventType.bookingUrl)bookingUrlstate remains""- User taps "Preview" →
handlePreviewat line 949 checksif (!bookingUrl)→ true → shows error "Booking URL not available. Please save the event type first." - User taps "Copy Link" →
handleCopyLinkat line 957 → same guard → same error BasicsTabreceivesbookingUrl=""as a prop → the URL prefix IIFE at BasicsTab line 254 checksif (props.bookingUrl)→ false → falls back tocal.com/{props.username}/
Impact
This completely breaks three features in the event type detail view:
- Preview always fails with an error message
- Copy Link always fails with an error message
- URL prefix in BasicsTab always shows the wrong domain for org users (the entire purpose of this PR)
Notably, the list views (index.tsx, index.ios.tsx) work correctly because they use eventType.bookingUrl directly from the fetched data object, not from component state. The bug is isolated to the detail view where local state was introduced but never populated.
Fix
Add if (eventType.bookingUrl) setBookingUrl(eventType.bookingUrl); inside the applyEventTypeData callback, alongside the other field assignments. This is a one-line fix that follows the exact same pattern as every other field loaded in that function.
Note on duplicate concern
One verifier flagged this as a potential duplicate of bug_001. If bug_001 covers the same root cause (missing setBookingUrl call in applyEventTypeData), these are indeed the same underlying issue. However, this report provides the complete analysis of all three affected code paths (preview, copy link, and BasicsTab URL prefix) for full visibility.
| const handleCopyLink = async (eventType: EventType) => { | ||
| if (!eventType.bookingUrl) { | ||
| showErrorAlert("Error", "Booking URL not available for this event type."); | ||
| return; | ||
| } | ||
| try { | ||
| const link = await CalComAPIService.buildEventTypeLink(eventType.slug); | ||
| await Clipboard.setStringAsync(link); | ||
| await Clipboard.setStringAsync(eventType.bookingUrl); | ||
| showSuccessAlert("Link Copied", "Event type link copied!"); |
There was a problem hiding this comment.
🔴 The mobile app handleCopyLink, handlePreview, and _handleShare functions in both index.ios.tsx and index.tsx show an error and return early when eventType.bookingUrl is absent, with no fallback URL construction. Since bookingUrl is typed as optional (bookingUrl?: string), this will break copy/preview/share for any event type where the API does not return it. The extension code (content.ts, linkedin.ts) correctly handles this with a fallback to constructing the URL from username + slug -- the mobile app should use the same pattern.
Extended reasoning...
What the bug is
The PR removes CalComAPIService.buildEventTypeLink() from the mobile app and replaces it with direct usage of eventType.bookingUrl. However, the replacement code has no fallback when bookingUrl is undefined -- it simply shows an error alert ("Booking URL not available for this event type.") and returns early. This affects six functions across two files:
- index.ios.tsx: handleCopyLink (line 113), _handleShare (line 127), handlePreview (line 234)
- index.tsx: handleCopyLink (line 135), _handleShare (line 146), handlePreview (line 287)
The inconsistency within this PR
The extension code in the same PR handles this correctly. In content.ts and linkedin.ts, the pattern used is:
const bookingUrl = eventType.bookingUrl || `https://cal.com/${eventType.users?.[0]?.username || "user"}/${eventType.slug}`;This defensive fallback ensures the feature always works even when bookingUrl is absent. The mobile app code does not use any fallback.
Why existing code doesn't prevent it
The EventType interface in event-types.types.ts explicitly types bookingUrl as optional: bookingUrl?: string. This means the TypeScript type system allows bookingUrl to be undefined. The old code used CalComAPIService.buildEventTypeLink(eventType.slug) which always worked by constructing a URL from the user's username and slug -- this fallback has been removed entirely for the mobile app.
Step-by-step proof
- User opens the mobile app and views their event types list
- The API returns an event type where bookingUrl is not populated (e.g., a newly created event type, an older API response, or a non-org user)
- User taps the context menu and selects "Copy link", "Preview", or "Share"
- The handler checks
if (!eventType.bookingUrl)-- this is true since bookingUrl is undefined showErrorAlert("Error", "Booking URL not available for this event type.")is called- The function returns early -- the user cannot copy, preview, or share the link
- Meanwhile, the same event type works perfectly in the browser extension because it falls back to constructing the URL from username + slug
Impact
Copy link, preview, and share are core features for event types. If the API ever returns an event type without bookingUrl, all three actions become completely non-functional on mobile, showing an unhelpful error message. The old code always worked because it constructed the URL locally.
How to fix
Apply the same fallback pattern used in the extension code. For example, in the event type list handlers:
const url = eventType.bookingUrl || `https://cal.com/${eventType.users?.[0]?.username || "user"}/${eventType.slug}`;This should be applied to all six affected functions across both index.ios.tsx and index.tsx.
| duration?: number; | ||
| description?: string; | ||
| users?: Array<{ username?: string }>; | ||
| bookingUrl?: string; |
There was a problem hiding this comment.
🟣 Pre-existing: The Gmail integration section of content.ts still hardcodes https://cal.com/ URLs in three places (lines 2233, 2297, 3685), producing incorrect booking links for org users (e.g., cal.com/keith/30min instead of i.cal.com/keith/30min). The PR fixes this exact pattern elsewhere in the file but missed these three Gmail locations. The Gmail section type definitions (lines 2899-2905, 3293-3300) and the generateEmailEmbedHTML function signature (line 2188) also lack bookingUrl.
Extended reasoning...
What the bug is
The PR systematically replaces hardcoded https://cal.com/ URL construction with eventType.bookingUrl || fallback throughout content.ts (and other files). However, three locations in the Gmail integration section of the same file were missed and still manually construct URLs using https://cal.com/username/slug.
Affected locations
- Line 2233 (generateEmailEmbedHTML): Constructs slot-specific booking URLs as https://cal.com/username/slug?duration=...
- Line 2297 (generateEmailEmbedHTML): The "See all available times" link hardcodes https://cal.com/username/slug?cal.tz=...
- Line 3685 (Gmail slot insertion handler): Constructs https://cal.com/selectedUsername/selectedSlug as the base URL for inserted calendar links
Additionally, the Gmail section type definitions at lines 2899-2905 and 3293-3300 do not include bookingUrl in their inline event type interfaces (unlike the updated sections at lines 836, 879, 894). The generateEmailEmbedHTML function signature at line 2188 defines its eventType parameter as { slug: string; title?: string } — also missing bookingUrl.
Step-by-step proof
- An org user (e.g., username "keith" on org domain i.cal.com) has event types fetched from the API. The API returns bookingUrl: "https://i.cal.com/keith/30min" for each event type.
- The PR updated the LinkedIn and messaging popup sections to use eventType.bookingUrl || fallback, so those generate correct URLs like https://i.cal.com/keith/30min.
- However, when the user uses the Gmail email embed feature, generateEmailEmbedHTML at line 2233 constructs https://cal.com/keith/30min?duration=30&date=... — wrong domain.
- The "See all available times" link at line 2297 similarly produces https://cal.com/keith/30min?cal.tz=... — wrong domain.
- When inserting a slot link via the Gmail slot insertion button at line 3685, it constructs https://cal.com/keith/30min — again wrong.
- All three URLs would 404 or show the wrong user's page since org users are hosted on their org subdomain, not cal.com.
Impact
Org users using the Gmail integration features (email embed with time slots, "See all available times" link, and direct slot insertion) will generate broken or incorrect booking URLs. Recipients clicking these links would not reach the correct booking page.
How to fix
- Add bookingUrl?: string to the Gmail section type definitions (lines 2899-2905 and 3293-3300).
- Add bookingUrl?: string to the generateEmailEmbedHTML eventType parameter type and pass it through.
- At lines 2233 and 2297, use eventType.bookingUrl to derive the base URL instead of hardcoding https://cal.com/username/...
- At line 3685, use selectedEventType.bookingUrl || fallback like the pattern used elsewhere in the PR.
Benchmark PR from agentic-review-benchmarks#5