fix(companion): event type links for org user (#26775)

* checkpoint 1

* checkpoint 2

* update extension header

* fix some minor ios ui bugs

* fix(companion): restore username fetching for URL fallback display

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 <>

* complete devins code change

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Dhairyashil Shinde
2026-01-13 05:37:56 +05:30
committed by GitHub
co-authored by unknown Devin AI
parent 8dbe3848e7
commit 95688b38fa
21 changed files with 200 additions and 123 deletions
@@ -88,7 +88,7 @@ export default function AvailabilityDetail() {
marginRight: -8,
}}
>
<Ionicons name="ellipsis-horizontal-circle" size={24} color="#007AFF" />
<Ionicons name="ellipsis-horizontal-circle" size={24} color="#000000" />
</TouchableOpacity>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
@@ -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",
+7 -2
View File
@@ -62,11 +62,16 @@ export default function Bookings() {
<DropdownMenuTrigger asChild>
<AppPressable
className="flex-row items-center rounded-lg border border-gray-200 bg-white"
style={{ paddingHorizontal: 8, paddingVertical: 6 }}
style={{
paddingHorizontal: 8,
paddingVertical: 6,
flexDirection: "row",
alignItems: "center",
}}
>
<Ionicons name="options-outline" size={14} color="#333" />
<Text
className={`text-sm ${selectedEventTypeId !== null ? "text-[#007AFF] font-semibold" : "text-[#333]"}`}
className={`text-sm ${selectedEventTypeId !== null ? "text-[#000000] font-semibold" : "text-[#333]"}`}
style={{ marginLeft: 4 }}
numberOfLines={1}
>
@@ -177,6 +177,7 @@ export default function EventTypeDetail() {
const [conferencingOptions, setConferencingOptions] = useState<ConferencingOption[]>([]);
const [conferencingLoading, setConferencingLoading] = useState(false);
const [eventTypeData, setEventTypeData] = useState<EventType | null>(null);
const [bookingUrl, setBookingUrl] = useState<string>("");
const [saving, setSaving] = useState(false);
const [beforeEventBuffer, setBeforeEventBuffer] = useState("No buffer time");
const [afterEventBuffer, setAfterEventBuffer] = useState("No buffer time");
@@ -456,6 +457,7 @@ export default function EventTypeDetail() {
if (eventType.description) setEventDescription(eventType.description);
if (eventType.lengthInMinutes) setEventDuration(eventType.lengthInMinutes.toString());
if (eventType.hidden !== undefined) setIsHidden(eventType.hidden);
if (eventType.bookingUrl) setBookingUrl(eventType.bookingUrl);
// Load schedule ID - this will be used by fetchSchedules to select the correct schedule
if (eventType.scheduleId) {
@@ -945,29 +947,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 +1271,10 @@ export default function EventTypeDetail() {
{/* Tab Navigation Dropdown Menu */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<AppPressable className="flex-row items-center gap-1 px-2 py-2">
<AppPressable
className="flex-row items-center gap-1 px-2 py-2"
style={{ flexDirection: "row", alignItems: "center" }}
>
<Text className="text-[16px] font-semibold text-[#000000]" numberOfLines={1}>
{tabs.find((tab) => tab.id === activeTab)?.label ?? "Basics"}
</Text>
@@ -1433,6 +1428,7 @@ export default function EventTypeDetail() {
onUpdateLocation={handleUpdateLocation}
locationOptions={getLocationOptionsForDropdown()}
conferencingLoading={conferencingLoading}
bookingUrl={bookingUrl}
/>
) : null}
@@ -2342,15 +2338,17 @@ export default function EventTypeDetail() {
<View className="bg-white pl-4">
<View
className="flex-row items-center justify-between pr-4"
style={{ height: 44 }}
style={{ height: 44, flexDirection: "row", alignItems: "center" }}
>
<Text className="text-[17px] text-black">Hidden</Text>
<Switch
value={isHidden}
onValueChange={setIsHidden}
trackColor={{ false: "#E5E5EA", true: "#000000" }}
thumbColor="#FFFFFF"
/>
<View style={{ justifyContent: "center", height: "100%" }}>
<Switch
value={isHidden}
onValueChange={setIsHidden}
trackColor={{ false: "#E5E5EA", true: "#000000" }}
thumbColor="#FFFFFF"
/>
</View>
</View>
</View>
</View>
@@ -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.");
+19 -11
View File
@@ -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() {
/>
<TouchableOpacity
className="min-w-[60px] flex-row items-center justify-center gap-1 rounded-lg bg-black px-2.5 py-2"
style={{ flexDirection: "row", alignItems: "center", justifyContent: "center" }}
onPress={handleCreateNew}
>
<Ionicons name="add" size={18} color="#fff" />
@@ -507,6 +514,7 @@ export default function EventTypes() {
/>
<TouchableOpacity
className="min-w-[60px] flex-row items-center justify-center gap-1 rounded-lg bg-black px-2.5 py-2"
style={{ flexDirection: "row", alignItems: "center", justifyContent: "center" }}
onPress={handleCreateNew}
>
<Ionicons name="add" size={18} color="#fff" />
+1 -1
View File
@@ -147,7 +147,7 @@ export default function ProfileSheet() {
}}
>
{/* Profile Header */}
<View className="mt-20 border-b border-gray-200 px-6">
<View className="mt-20 px-6">
{isLoading ? (
<View className="items-center py-8">
<ActivityIndicator size="large" color="#000" />
+1 -1
View File
@@ -124,7 +124,7 @@ export default function ProfileSheet() {
contentContainerStyle={{ paddingBottom: insets.bottom + 20 }}
>
{/* Profile Header */}
<View className="border-b border-gray-200 px-6 py-6">
<View className="px-6 py-6">
{isLoading ? (
<View className="items-center py-8">
<ActivityIndicator size="large" color="#000" />
+26 -23
View File
@@ -103,11 +103,14 @@ export function Header({
{filterOptions && filterOptions.length > 0 && onFilterChange && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<AppPressable className="flex-row items-center gap-1 px-2 py-2">
<Text className="text-[16px] font-semibold text-[#007AFF]">
<AppPressable
className="flex-row items-center gap-1 px-2 py-2"
style={{ flexDirection: "row", alignItems: "center" }}
>
<Text className="text-[16px] font-semibold text-[#000000]">
{activeFilterLabel}
</Text>
<Ionicons name="chevron-down" size={16} color="#007AFF" />
<Ionicons name="chevron-down" size={16} color="#000000" />
</AppPressable>
</DropdownMenuTrigger>
@@ -143,11 +146,11 @@ export function Header({
<Ionicons
name={isSelected ? "checkmark-circle" : getFilterIcon(option.key)}
size={16}
color={isSelected ? "#007AFF" : "#666"}
color={isSelected ? "#000000" : "#666"}
/>
<Text
className={
isSelected ? "text-base font-semibold text-[#007AFF]" : "text-base"
isSelected ? "text-base font-semibold text-[#000000]" : "text-base"
}
>
{option.label}
@@ -165,11 +168,11 @@ export function Header({
<DropdownMenu>
<DropdownMenuTrigger asChild>
<AppPressable className="relative p-2">
<Ionicons name="options-outline" size={22} color="#007AFF" />
<Ionicons name="options-outline" size={22} color="#000000" />
{/* Badge for active filters */}
{eventTypeFilterConfig.activeFilterCount > 0 && (
<View
className="absolute -top-0.5 -right-0.5 items-center justify-center rounded-full bg-[#007AFF]"
className="absolute -top-0.5 -right-0.5 items-center justify-center rounded-full bg-[#000000]"
style={{ width: 16, height: 16 }}
>
<Text className="text-[10px] font-bold text-white">
@@ -206,12 +209,12 @@ export function Header({
: "text-outline"
}
size={16}
color={eventTypeFilterConfig.sortBy === "alphabetical" ? "#007AFF" : "#666"}
color={eventTypeFilterConfig.sortBy === "alphabetical" ? "#000000" : "#666"}
/>
<Text
className={
eventTypeFilterConfig.sortBy === "alphabetical"
? "text-base font-semibold text-[#007AFF]"
? "text-base font-semibold text-[#000000]"
: "text-base"
}
>
@@ -228,12 +231,12 @@ export function Header({
: "calendar-outline"
}
size={16}
color={eventTypeFilterConfig.sortBy === "newest" ? "#007AFF" : "#666"}
color={eventTypeFilterConfig.sortBy === "newest" ? "#000000" : "#666"}
/>
<Text
className={
eventTypeFilterConfig.sortBy === "newest"
? "text-base font-semibold text-[#007AFF]"
? "text-base font-semibold text-[#000000]"
: "text-base"
}
>
@@ -250,12 +253,12 @@ export function Header({
: "time-outline"
}
size={16}
color={eventTypeFilterConfig.sortBy === "duration" ? "#007AFF" : "#666"}
color={eventTypeFilterConfig.sortBy === "duration" ? "#000000" : "#666"}
/>
<Text
className={
eventTypeFilterConfig.sortBy === "duration"
? "text-base font-semibold text-[#007AFF]"
? "text-base font-semibold text-[#000000]"
: "text-base"
}
>
@@ -286,12 +289,12 @@ export function Header({
: "eye-off-outline"
}
size={16}
color={eventTypeFilterConfig.filters.hiddenOnly ? "#007AFF" : "#666"}
color={eventTypeFilterConfig.filters.hiddenOnly ? "#000000" : "#666"}
/>
<Text
className={
eventTypeFilterConfig.filters.hiddenOnly
? "text-base font-semibold text-[#007AFF]"
? "text-base font-semibold text-[#000000]"
: "text-base"
}
>
@@ -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"}
/>
<Text
className={
eventTypeFilterConfig.filters.paidOnly
? "text-base font-semibold text-[#007AFF]"
? "text-base font-semibold text-[#000000]"
: "text-base"
}
>
@@ -330,12 +333,12 @@ export function Header({
: "people-outline"
}
size={16}
color={eventTypeFilterConfig.filters.seatedOnly ? "#007AFF" : "#666"}
color={eventTypeFilterConfig.filters.seatedOnly ? "#000000" : "#666"}
/>
<Text
className={
eventTypeFilterConfig.filters.seatedOnly
? "text-base font-semibold text-[#007AFF]"
? "text-base font-semibold text-[#000000]"
: "text-base"
}
>
@@ -356,13 +359,13 @@ export function Header({
}
size={16}
color={
eventTypeFilterConfig.filters.requiresConfirmationOnly ? "#007AFF" : "#666"
eventTypeFilterConfig.filters.requiresConfirmationOnly ? "#000000" : "#666"
}
/>
<Text
className={
eventTypeFilterConfig.filters.requiresConfirmationOnly
? "text-base font-semibold text-[#007AFF]"
? "text-base font-semibold text-[#000000]"
: "text-base"
}
>
@@ -382,12 +385,12 @@ export function Header({
: "repeat-outline"
}
size={16}
color={eventTypeFilterConfig.filters.recurringOnly ? "#007AFF" : "#666"}
color={eventTypeFilterConfig.filters.recurringOnly ? "#000000" : "#666"}
/>
<Text
className={
eventTypeFilterConfig.filters.recurringOnly
? "text-base font-semibold text-[#007AFF]"
? "text-base font-semibold text-[#000000]"
: "text-base"
}
>
@@ -138,7 +138,7 @@ function SettingRow({
<View className="bg-white pl-4">
<View
className={`flex-row items-center pr-4 ${!isLast ? "border-b border-[#E5E5E5]" : ""}`}
style={{ height }}
style={{ height, flexDirection: "row", alignItems: "center" }}
>
<TouchableOpacity
className="flex-1 flex-row items-center"
@@ -154,7 +154,7 @@ function SettingRow({
<Ionicons name="chevron-down" size={12} color="#C7C7CC" style={{ marginLeft: 6 }} />
) : null}
</TouchableOpacity>
<View style={{ alignSelf: "center" }}>
<View style={{ alignSelf: "center", justifyContent: "center" }}>
<Switch
value={value}
onValueChange={onValueChange}
@@ -37,6 +37,7 @@ interface BasicsTabProps {
onUpdateLocation: (locationId: string, updates: Partial<LocationItem>) => void;
locationOptions: LocationOptionGroup[];
conferencingLoading: boolean;
bookingUrl?: string;
}
// Section header
@@ -201,12 +202,12 @@ function SettingRow({
<View className="bg-white pl-4">
<View
className={`flex-row items-center pr-4 ${!isLast ? "border-b border-[#E5E5E5]" : ""}`}
style={{ height }}
style={{ height, flexDirection: "row", alignItems: "center" }}
>
<Text className="flex-1 text-[17px] text-black" style={{ fontWeight: "400" }}>
{title}
</Text>
<View style={{ alignSelf: "center" }}>
<View style={{ alignSelf: "center", justifyContent: "center" }}>
<Switch
value={value}
onValueChange={onValueChange}
@@ -253,7 +254,26 @@ export const BasicsTab: React.FC<BasicsTabProps> = (props) => {
<Text className="mb-2 text-[13px] text-[#6D6D72]">URL</Text>
<View className="flex-row items-center overflow-hidden rounded-lg bg-[#F2F2F7]">
<Text className="bg-[#E5E5EA] px-3 py-2 text-[15px] text-[#666]">
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.hostname}${prefix}`;
} catch {
// fallback
}
}
return `cal.com/${props.username}/`;
})()}
</Text>
<TextInput
className="flex-1 px-3 py-2 text-[17px] text-black"
@@ -124,7 +124,7 @@ function SettingRow({
<View className="bg-white pl-4">
<View
className={`flex-row items-center pr-4 ${!isLast ? "border-b border-[#E5E5E5]" : ""}`}
style={{ height: 44 }}
style={{ height: 44, flexDirection: "row", alignItems: "center" }}
>
<TouchableOpacity
className="flex-1 flex-row items-center"
@@ -140,7 +140,7 @@ function SettingRow({
<Ionicons name="chevron-down" size={12} color="#C7C7CC" style={{ marginLeft: 6 }} />
) : null}
</TouchableOpacity>
<View style={{ alignSelf: "center" }}>
<View style={{ alignSelf: "center", justifyContent: "center" }}>
<Switch
value={value}
onValueChange={onValueChange}
@@ -89,7 +89,7 @@ function SettingRow({
<View className="bg-white pl-4">
<View
className={`flex-row items-center pr-4 ${!isLast ? "border-b border-[#E5E5E5]" : ""}`}
style={{ minHeight: 44 }}
style={{ minHeight: 44, flexDirection: "row", alignItems: "center" }}
>
<TouchableOpacity
className="flex-1 flex-row items-center py-3"
@@ -104,7 +104,7 @@ function SettingRow({
<Ionicons name="chevron-down" size={12} color="#C7C7CC" style={{ marginLeft: 6 }} />
) : null}
</TouchableOpacity>
<View style={{ alignSelf: "center" }}>
<View style={{ alignSelf: "center", justifyContent: "center" }}>
<Switch
value={value}
onValueChange={onValueChange}
@@ -100,6 +100,7 @@ export const EventTypeListItem = ({
title={item.title}
username={item.users?.[0]?.username}
slug={item.slug}
bookingUrl={item.bookingUrl}
/>
<EventTypeDescription normalizedDescription={normalizedDescription} />
<EventTypeBadges
@@ -51,6 +51,7 @@ export const EventTypeListItem = ({
title={item.title}
username={item.users?.[0]?.username}
slug={item.slug}
bookingUrl={item.bookingUrl}
/>
<EventTypeDescription normalizedDescription={normalizedDescription} />
<EventTypeBadges
@@ -4,14 +4,32 @@ import Svg, { Path } from "react-native-svg";
import { Tooltip } from "@/components/Tooltip";
import type { EventType } from "@/services/types/event-types.types";
/**
* Parse bookingUrl to get display text (domain + path).
* Falls back to /{username}/{slug} if bookingUrl is not available.
*/
function getDisplayUrl(bookingUrl?: string, username?: string, slug?: string): string {
if (bookingUrl) {
try {
const url = new URL(bookingUrl);
// Return domain + pathname (e.g., "i.cal.com/keith/30min")
return url.hostname + url.pathname;
} catch {
// fallback if URL parsing fails
}
}
return username ? `/${username}/${slug}` : `/${slug}`;
}
interface EventTypeTitleProps {
title: string;
username?: string;
slug: string;
bookingUrl?: string;
}
export function EventTypeTitle({ title, username, slug }: EventTypeTitleProps) {
const linkText = username ? `/${username}/${slug}` : `/${slug}`;
export function EventTypeTitle({ title, username, slug, bookingUrl }: EventTypeTitleProps) {
const linkText = getDisplayUrl(bookingUrl, username, slug);
return (
<View className="mb-1 flex-row flex-wrap items-baseline">
<Text className="text-base font-semibold text-cal-text">{title}</Text>
@@ -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 <Text className="mb-1 mt-0.5 text-sm text-cal-text-secondary">{linkText}</Text>;
}
@@ -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;
}
+34 -21
View File
@@ -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,11 @@ 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(() => {
@@ -1285,11 +1292,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 +1320,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 +1583,7 @@ export default defineContentScript({
duration?: number;
description?: string;
users?: Array<{ username?: string }>;
bookingUrl?: string;
}> = [];
if (isCacheValid && eventTypesCache) {
@@ -1615,6 +1625,7 @@ export default defineContentScript({
duration?: number;
description?: string;
users?: Array<{ username?: string }>;
bookingUrl?: string;
}>;
}
).data
@@ -1630,6 +1641,7 @@ export default defineContentScript({
duration?: number;
description?: string;
users?: Array<{ username?: string }>;
bookingUrl?: string;
}>;
}
).data;
@@ -1815,9 +1827,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 +1869,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 +2037,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);
+5 -1
View File
@@ -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[]) {
+6 -13
View File
@@ -318,18 +318,6 @@ async function getUserProfile(): Promise<UserProfile> {
return _userProfilePromise;
}
// Get cached username or fetch if not available
async function getUsername(): Promise<string> {
const profile = await getUserProfile();
return profile.username;
}
// Build shareable link for event type
async function buildEventTypeLink(eventTypeSlug: string): Promise<string> {
const username = await getUsername();
return `https://cal.com/${username}/${eventTypeSlug}`;
}
// Clear cached profile (useful for logout)
function clearUserProfile(): void {
_userProfile = null;
@@ -1663,6 +1651,12 @@ async function deleteEventTypePrivateLink(eventTypeId: number, linkId: number):
}
}
// Helper to get username
async function getUsername(): Promise<string> {
const profile = await getUserProfile();
return profile.username;
}
// Export as object to satisfy noStaticOnlyClass rule
export const CalComAPIService = {
setAccessToken,
@@ -1673,7 +1667,6 @@ export const CalComAPIService = {
updateUserProfile,
getUserProfile,
getUsername,
buildEventTypeLink,
clearUserProfile,
testRawBookingsAPI,
deleteEventType,
@@ -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 {