diff --git a/companion/app/(tabs)/(availability)/availability-detail.tsx b/companion/app/(tabs)/(availability)/availability-detail.tsx index f31ab0aecb2a4c..ca62bc228c6b52 100644 --- a/companion/app/(tabs)/(availability)/availability-detail.tsx +++ b/companion/app/(tabs)/(availability)/availability-detail.tsx @@ -88,7 +88,7 @@ export default function AvailabilityDetail() { marginRight: -8, }} > - + diff --git a/companion/app/(tabs)/(bookings)/index.ios.tsx b/companion/app/(tabs)/(bookings)/index.ios.tsx index 7f6e4d83d0e7c0..36e57d70f09ff5 100644 --- a/companion/app/(tabs)/(bookings)/index.ios.tsx +++ b/companion/app/(tabs)/(bookings)/index.ios.tsx @@ -62,7 +62,7 @@ export default function Bookings() { label: currentFilterOption?.label || "Filter", labelStyle: { fontWeight: "600", - color: "#007AFF", + color: "#000000", }, menu: { title: "Filter by Status", @@ -123,7 +123,7 @@ export default function Bookings() { }, labelStyle: { fontWeight: "600", - color: "#007AFF", + color: "#000000", }, menu: { title: menuItems.length > 0 ? "Filter by Event Type" : "No Event Types", diff --git a/companion/app/(tabs)/(bookings)/index.tsx b/companion/app/(tabs)/(bookings)/index.tsx index aec5e06d3d2e4d..d6e8c7ec748526 100644 --- a/companion/app/(tabs)/(bookings)/index.tsx +++ b/companion/app/(tabs)/(bookings)/index.tsx @@ -62,11 +62,16 @@ export default function Bookings() { diff --git a/companion/app/(tabs)/(event-types)/event-type-detail.tsx b/companion/app/(tabs)/(event-types)/event-type-detail.tsx index fd1b7baea0b205..46aa4cfd6b6981 100644 --- a/companion/app/(tabs)/(event-types)/event-type-detail.tsx +++ b/companion/app/(tabs)/(event-types)/event-type-detail.tsx @@ -177,6 +177,7 @@ export default function EventTypeDetail() { const [conferencingOptions, setConferencingOptions] = useState([]); const [conferencingLoading, setConferencingLoading] = useState(false); const [eventTypeData, setEventTypeData] = useState(null); + const [bookingUrl, setBookingUrl] = useState(""); const [saving, setSaving] = useState(false); const [beforeEventBuffer, setBeforeEventBuffer] = useState("No buffer time"); const [afterEventBuffer, setAfterEventBuffer] = useState("No buffer time"); @@ -945,29 +946,19 @@ export default function EventTypeDetail() { }; const handlePreview = async () => { - const eventTypeSlug = eventSlug || "preview"; - let link: string; - try { - link = await CalComAPIService.buildEventTypeLink(eventTypeSlug); - } catch (error) { - safeLogError("Failed to generate preview link:", error); - showErrorAlert("Error", "Failed to generate preview link. Please try again."); + if (!bookingUrl) { + showErrorAlert("Error", "Booking URL not available. Please save the event type first."); return; } - await openInAppBrowser(link, "event type preview"); + await openInAppBrowser(bookingUrl, "event type preview"); }; const handleCopyLink = async () => { - const eventTypeSlug = eventSlug || "event-link"; - let link: string; - try { - link = await CalComAPIService.buildEventTypeLink(eventTypeSlug); - } catch (error) { - safeLogError("Failed to copy link:", error); - showErrorAlert("Error", "Failed to copy link. Please try again."); + if (!bookingUrl) { + showErrorAlert("Error", "Booking URL not available. Please save the event type first."); return; } - await Clipboard.setStringAsync(link); + await Clipboard.setStringAsync(bookingUrl); showSuccessAlert("Success", "Link copied!"); }; @@ -1279,7 +1270,10 @@ export default function EventTypeDetail() { {/* Tab Navigation Dropdown Menu */} - + {tabs.find((tab) => tab.id === activeTab)?.label ?? "Basics"} @@ -1433,6 +1427,7 @@ export default function EventTypeDetail() { onUpdateLocation={handleUpdateLocation} locationOptions={getLocationOptionsForDropdown()} conferencingLoading={conferencingLoading} + bookingUrl={bookingUrl} /> ) : null} @@ -2342,15 +2337,17 @@ export default function EventTypeDetail() { Hidden - + + + diff --git a/companion/app/(tabs)/(event-types)/index.ios.tsx b/companion/app/(tabs)/(event-types)/index.ios.tsx index 8c9898b7ac3e21..c4e8fb57dc3d64 100644 --- a/companion/app/(tabs)/(event-types)/index.ios.tsx +++ b/companion/app/(tabs)/(event-types)/index.ios.tsx @@ -1,6 +1,6 @@ -import { Button, ContextMenu, Host, HStack, Image as SwiftUIImage } from "@expo/ui/swift-ui"; +import { Button, Host, Image as SwiftUIImage } from "@expo/ui/swift-ui"; import * as Haptics from "expo-haptics"; -import { buttonStyle, controlSize, fixedSize, frame, padding } from "@expo/ui/swift-ui/modifiers"; +import { buttonStyle, controlSize, frame, padding } from "@expo/ui/swift-ui/modifiers"; import { Ionicons } from "@expo/vector-icons"; import * as Clipboard from "expo-clipboard"; import { isLiquidGlassAvailable } from "expo-glass-effect"; @@ -28,7 +28,7 @@ import { useUserProfile, } from "@/hooks"; import { useEventTypeFilter } from "@/hooks/useEventTypeFilter"; -import { CalComAPIService, type EventType } from "@/services/calcom"; +import type { EventType } from "@/services/calcom"; import { showErrorAlert, showSuccessAlert } from "@/utils/alerts"; import { openInAppBrowser } from "@/utils/browser"; import { getAvatarUrl } from "@/utils/getAvatarUrl"; @@ -111,9 +111,12 @@ export default function EventTypesIOS() { }; 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!"); } catch { showErrorAlert("Error", "Failed to copy link. Please try again."); @@ -121,11 +124,14 @@ export default function EventTypesIOS() { }; const _handleShare = 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 Share.share({ message: `Book a meeting: ${eventType.title}`, - url: link, + url: eventType.bookingUrl, }); } catch { showErrorAlert("Error", "Failed to share link. Please try again."); @@ -226,10 +232,12 @@ export default function EventTypesIOS() { }; const handlePreview = 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); - // For mobile, use in-app browser - await openInAppBrowser(link, "event type preview"); + await openInAppBrowser(eventType.bookingUrl, "event type preview"); } catch { console.error("Failed to open preview"); showErrorAlert("Error", "Failed to open preview. Please try again."); diff --git a/companion/app/(tabs)/(event-types)/index.tsx b/companion/app/(tabs)/(event-types)/index.tsx index 246b8ed04ddc58..ec7c486a73b5b5 100644 --- a/companion/app/(tabs)/(event-types)/index.tsx +++ b/companion/app/(tabs)/(event-types)/index.tsx @@ -37,7 +37,7 @@ import { useEventTypes, } from "@/hooks"; import { useEventTypeFilter } from "@/hooks/useEventTypeFilter"; -import { CalComAPIService, type EventType } from "@/services/calcom"; +import type { EventType } from "@/services/calcom"; import { showErrorAlert, showSuccessAlert } from "@/utils/alerts"; import { openInAppBrowser } from "@/utils/browser"; import { getEventDuration } from "@/utils/getEventDuration"; @@ -133,10 +133,12 @@ export default function EventTypes() { }; 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!"); } catch { showErrorAlert("Error", "Failed to copy link. Please try again."); @@ -144,11 +146,14 @@ export default function EventTypes() { }; const _handleShare = 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 Share.share({ message: `Book a meeting: ${eventType.title}`, - url: link, + url: eventType.bookingUrl, }); } catch { showErrorAlert("Error", "Failed to share link. Please try again."); @@ -280,14 +285,15 @@ export default function EventTypes() { }; const handlePreview = 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); - // Open in browser if (Platform.OS === "web") { - window.open(link, "_blank"); + window.open(eventType.bookingUrl, "_blank"); } else { - // For mobile, use in-app browser - await openInAppBrowser(link, "event type preview"); + await openInAppBrowser(eventType.bookingUrl, "event type preview"); } } catch { console.error("Failed to open preview"); @@ -443,6 +449,7 @@ export default function EventTypes() { /> @@ -507,6 +514,7 @@ export default function EventTypes() { /> diff --git a/companion/app/profile-sheet.ios.tsx b/companion/app/profile-sheet.ios.tsx index b08381b1317105..2f85b575290177 100644 --- a/companion/app/profile-sheet.ios.tsx +++ b/companion/app/profile-sheet.ios.tsx @@ -147,7 +147,7 @@ export default function ProfileSheet() { }} > {/* Profile Header */} - + {isLoading ? ( diff --git a/companion/app/profile-sheet.tsx b/companion/app/profile-sheet.tsx index c2f84263268f8f..0ad0aa5e69f889 100644 --- a/companion/app/profile-sheet.tsx +++ b/companion/app/profile-sheet.tsx @@ -124,7 +124,7 @@ export default function ProfileSheet() { contentContainerStyle={{ paddingBottom: insets.bottom + 20 }} > {/* Profile Header */} - + {isLoading ? ( diff --git a/companion/components/Header.tsx b/companion/components/Header.tsx index 3d2d5db3bc6cc5..665b2ac8f3a250 100644 --- a/companion/components/Header.tsx +++ b/companion/components/Header.tsx @@ -103,11 +103,14 @@ export function Header({ {filterOptions && filterOptions.length > 0 && onFilterChange && ( - - + + {activeFilterLabel} - + @@ -143,11 +146,11 @@ export function Header({ {option.label} @@ -165,11 +168,11 @@ export function Header({ - + {/* Badge for active filters */} {eventTypeFilterConfig.activeFilterCount > 0 && ( @@ -206,12 +209,12 @@ export function Header({ : "text-outline" } size={16} - color={eventTypeFilterConfig.sortBy === "alphabetical" ? "#007AFF" : "#666"} + color={eventTypeFilterConfig.sortBy === "alphabetical" ? "#000000" : "#666"} /> @@ -228,12 +231,12 @@ export function Header({ : "calendar-outline" } size={16} - color={eventTypeFilterConfig.sortBy === "newest" ? "#007AFF" : "#666"} + color={eventTypeFilterConfig.sortBy === "newest" ? "#000000" : "#666"} /> @@ -250,12 +253,12 @@ export function Header({ : "time-outline" } size={16} - color={eventTypeFilterConfig.sortBy === "duration" ? "#007AFF" : "#666"} + color={eventTypeFilterConfig.sortBy === "duration" ? "#000000" : "#666"} /> @@ -286,12 +289,12 @@ export function Header({ : "eye-off-outline" } size={16} - color={eventTypeFilterConfig.filters.hiddenOnly ? "#007AFF" : "#666"} + color={eventTypeFilterConfig.filters.hiddenOnly ? "#000000" : "#666"} /> @@ -307,12 +310,12 @@ export function Header({ eventTypeFilterConfig.filters.paidOnly ? "checkmark-circle" : "cash-outline" } size={16} - color={eventTypeFilterConfig.filters.paidOnly ? "#007AFF" : "#666"} + color={eventTypeFilterConfig.filters.paidOnly ? "#000000" : "#666"} /> @@ -330,12 +333,12 @@ export function Header({ : "people-outline" } size={16} - color={eventTypeFilterConfig.filters.seatedOnly ? "#007AFF" : "#666"} + color={eventTypeFilterConfig.filters.seatedOnly ? "#000000" : "#666"} /> @@ -356,13 +359,13 @@ export function Header({ } size={16} color={ - eventTypeFilterConfig.filters.requiresConfirmationOnly ? "#007AFF" : "#666" + eventTypeFilterConfig.filters.requiresConfirmationOnly ? "#000000" : "#666" } /> @@ -382,12 +385,12 @@ export function Header({ : "repeat-outline" } size={16} - color={eventTypeFilterConfig.filters.recurringOnly ? "#007AFF" : "#666"} + color={eventTypeFilterConfig.filters.recurringOnly ? "#000000" : "#666"} /> diff --git a/companion/components/event-type-detail/tabs/AdvancedTab.tsx b/companion/components/event-type-detail/tabs/AdvancedTab.tsx index cd07f39424f22e..0a009da1ccba25 100644 --- a/companion/components/event-type-detail/tabs/AdvancedTab.tsx +++ b/companion/components/event-type-detail/tabs/AdvancedTab.tsx @@ -138,7 +138,7 @@ function SettingRow({ ) : null} - + ) => void; locationOptions: LocationOptionGroup[]; conferencingLoading: boolean; + bookingUrl?: string; } // Section header @@ -201,12 +202,12 @@ function SettingRow({ {title} - + = (props) => { URL - cal.com/{props.username}/ + {(() => { + // Parse bookingUrl to get domain prefix (e.g., "i.cal.com/" or "cal.com/username/") + if (props.bookingUrl) { + try { + const url = new URL(props.bookingUrl); + // Get path without the last segment (slug) + const pathParts = url.pathname.split("/").filter(Boolean); + pathParts.pop(); // Remove slug + // Compute prefix outside try/catch for React Compiler + let prefix = "/"; + if (pathParts.length > 0) { + prefix = `/${pathParts.join("/")}/`; + } + return `${url.protocol}//${url.hostname}${prefix}`; + } catch { + // fallback + } + } + return `cal.com/${props.username}/`; + })()} ) : null} - + ) : null} - + {title} @@ -36,10 +54,11 @@ export function EventTypeDescription({ normalizedDescription }: EventTypeDescrip interface EventTypeLinkProps { username?: string; slug: string; + bookingUrl?: string; } -export function EventTypeLink({ username, slug }: EventTypeLinkProps) { - const linkText = username ? `/${username}/${slug}` : `/${slug}`; +export function EventTypeLink({ username, slug, bookingUrl }: EventTypeLinkProps) { + const linkText = getDisplayUrl(bookingUrl, username, slug); return {linkText}; } diff --git a/companion/extension/entrypoints/background/index.ts b/companion/extension/entrypoints/background/index.ts index d48cf40b192624..a8db6627405878 100644 --- a/companion/extension/entrypoints/background/index.ts +++ b/companion/extension/entrypoints/background/index.ts @@ -278,6 +278,7 @@ function getTabsAPI(): typeof chrome.tabs | null { function getActionAPI(): typeof chrome.action | null { const api = getBrowserAPI(); // Safari uses browserAction (Manifest V2), Chrome uses action (Manifest V3) + // biome-ignore lint/suspicious/noExplicitAny: Safari's browserAction API is not in Chrome types return api?.action || (api as any)?.browserAction || null; } diff --git a/companion/extension/entrypoints/content.ts b/companion/extension/entrypoints/content.ts index 1422a85e8013e6..892cb907b4e306 100644 --- a/companion/extension/entrypoints/content.ts +++ b/companion/extension/entrypoints/content.ts @@ -833,6 +833,7 @@ export default defineContentScript({ duration?: number; description?: string; users?: Array<{ username?: string }>; + bookingUrl?: string; }> = []; if (isCacheValid && eventTypesCache) { @@ -874,6 +875,7 @@ export default defineContentScript({ duration?: number; description?: string; users?: Array<{ username?: string }>; + bookingUrl?: string; }>; } ).data @@ -889,6 +891,7 @@ export default defineContentScript({ duration?: number; description?: string; users?: Array<{ username?: string }>; + bookingUrl?: string; }>; } ).data; @@ -1075,9 +1078,11 @@ export default defineContentScript({ previewBtn.addEventListener("click", (e) => { e.stopPropagation(); - const bookingUrl = `https://cal.com/${ - eventType.users?.[0]?.username || "user" - }/${eventType.slug}`; + const bookingUrl = + eventType.bookingUrl || + `https://cal.com/${ + eventType.users?.[0]?.username || "user" + }/${eventType.slug}`; window.open(bookingUrl, "_blank"); }); previewBtn.addEventListener("mouseenter", () => { @@ -1117,9 +1122,10 @@ export default defineContentScript({ copyBtn.addEventListener("click", (e) => { e.stopPropagation(); // Copy to clipboard - const bookingUrl = `https://cal.com/${ - eventType.users?.[0]?.username || "user" - }/${eventType.slug}`; + const bookingUrl = + `https://cal.com/${ + eventType.users?.[0]?.username || "user" + }/${eventType.slug}`; navigator.clipboard .writeText(bookingUrl) .then(() => { @@ -1285,11 +1291,12 @@ export default defineContentScript({ function insertEventTypeLink(eventType: { slug: string; users?: Array<{ username?: string }>; + bookingUrl?: string; }): void { // Construct the Cal.com booking link - const bookingUrl = `https://cal.com/${eventType.users?.[0]?.username || "user"}/${ - eventType.slug - }`; + const bookingUrl = + eventType.bookingUrl || + `https://cal.com/${eventType.users?.[0]?.username || "user"}/${eventType.slug}`; // Try to insert at cursor position in the compose field const inserted = insertTextAtCursor(bookingUrl); @@ -1312,11 +1319,12 @@ export default defineContentScript({ function _copyEventTypeLink(eventType: { slug: string; users?: Array<{ username?: string }>; + bookingUrl?: string; }): void { // Construct the Cal.com booking link - const bookingUrl = `https://cal.com/${eventType.users?.[0]?.username || "user"}/${ - eventType.slug - }`; + const bookingUrl = + eventType.bookingUrl || + `https://cal.com/${eventType.users?.[0]?.username || "user"}/${eventType.slug}`; // Try to insert at cursor position in the compose field const inserted = insertTextAtCursor(bookingUrl); @@ -1574,6 +1582,7 @@ export default defineContentScript({ duration?: number; description?: string; users?: Array<{ username?: string }>; + bookingUrl?: string; }> = []; if (isCacheValid && eventTypesCache) { @@ -1615,6 +1624,7 @@ export default defineContentScript({ duration?: number; description?: string; users?: Array<{ username?: string }>; + bookingUrl?: string; }>; } ).data @@ -1630,6 +1640,7 @@ export default defineContentScript({ duration?: number; description?: string; users?: Array<{ username?: string }>; + bookingUrl?: string; }>; } ).data; @@ -1815,9 +1826,9 @@ export default defineContentScript({ previewBtn.addEventListener("click", (e) => { e.stopPropagation(); - const bookingUrl = `https://cal.com/${ - eventType.users?.[0]?.username || "user" - }/${eventType.slug}`; + const bookingUrl = + eventType.bookingUrl || + `https://cal.com/${eventType.users?.[0]?.username || "user"}/${eventType.slug}`; window.open(bookingUrl, "_blank"); }); previewBtn.addEventListener("mouseenter", () => { @@ -1857,9 +1868,9 @@ export default defineContentScript({ copyBtn.addEventListener("click", (e) => { e.stopPropagation(); // Copy to clipboard - const bookingUrl = `https://cal.com/${ - eventType.users?.[0]?.username || "user" - }/${eventType.slug}`; + const bookingUrl = + eventType.bookingUrl || + `https://cal.com/${eventType.users?.[0]?.username || "user"}/${eventType.slug}`; navigator.clipboard .writeText(bookingUrl) .then(() => { @@ -2025,11 +2036,12 @@ export default defineContentScript({ function insertEventTypeLink(eventType: { slug: string; users?: Array<{ username?: string }>; + bookingUrl?: string; }) { // Construct the Cal.com booking link - const bookingUrl = `https://cal.com/${eventType.users?.[0]?.username || "user"}/${ - eventType.slug - }`; + const bookingUrl = + eventType.bookingUrl || + `https://cal.com/${eventType.users?.[0]?.username || "user"}/${eventType.slug}`; // Try to insert at cursor position in the message field const inserted = insertTextAtCursor(bookingUrl); diff --git a/companion/extension/lib/linkedin.ts b/companion/extension/lib/linkedin.ts index f3daf7a906de39..4e10603bd41b12 100644 --- a/companion/extension/lib/linkedin.ts +++ b/companion/extension/lib/linkedin.ts @@ -115,6 +115,7 @@ interface EventType { duration?: number; description?: string; users?: Array<{ username?: string }>; + bookingUrl?: string; } interface ButtonSize { @@ -664,7 +665,10 @@ export function initLinkedInIntegration() { } function buildBookingUrl(eventType: EventType): string { - return `https://cal.com/${eventType.users?.[0]?.username || "user"}/${eventType.slug}`; + return ( + eventType.bookingUrl || + `https://cal.com/${eventType.users?.[0]?.username || "user"}/${eventType.slug}` + ); } function handleFetchError(error: unknown, menu: HTMLElement, tooltipsToCleanup: HTMLElement[]) { diff --git a/companion/services/calcom.ts b/companion/services/calcom.ts index 74b7a2df6ced66..2af939d9bb7727 100644 --- a/companion/services/calcom.ts +++ b/companion/services/calcom.ts @@ -318,18 +318,6 @@ async function getUserProfile(): Promise { return _userProfilePromise; } -// Get cached username or fetch if not available -async function getUsername(): Promise { - const profile = await getUserProfile(); - return profile.username; -} - -// Build shareable link for event type -async function buildEventTypeLink(eventTypeSlug: string): Promise { - const username = await getUsername(); - return `https://cal.com/${username}/${eventTypeSlug}`; -} - // Clear cached profile (useful for logout) function clearUserProfile(): void { _userProfile = null; @@ -1663,6 +1651,16 @@ async function deleteEventTypePrivateLink(eventTypeId: number, linkId: number): } } +// Helper to get username +async function getUsername(): Promise { + try { + const profile = await getUserProfile(); + return profile.username; + } catch (error) { + throw new Error("Failed to get username"); + } +} + // Export as object to satisfy noStaticOnlyClass rule export const CalComAPIService = { setAccessToken, @@ -1673,7 +1671,6 @@ export const CalComAPIService = { updateUserProfile, getUserProfile, getUsername, - buildEventTypeLink, clearUserProfile, testRawBookingsAPI, deleteEventType, diff --git a/companion/services/types/event-types.types.ts b/companion/services/types/event-types.types.ts index 0cb6f1a494d959..bd63ee3c19ce01 100644 --- a/companion/services/types/event-types.types.ts +++ b/companion/services/types/event-types.types.ts @@ -278,6 +278,9 @@ export interface EventType { // Optimized slots (API V2) showOptimizedSlots?: boolean; + + // Booking URL (API V2) - full booking URL for this event type + bookingUrl?: string; } export interface CreateEventTypeInput {