Error displaying event types
@@ -1164,7 +1213,9 @@ export default defineContentScript({
if (isAuthError) {
// Not logged in - open sidebar directly
- tooltipsToCleanup.forEach((tooltip) => tooltip.remove());
+ for (const tooltip of tooltipsToCleanup) {
+ tooltip.remove();
+ }
menu.remove();
openSidebar();
return;
@@ -1208,7 +1259,9 @@ export default defineContentScript({
closeBtn.style.background = "#F3F4F6";
});
closeBtn.addEventListener("click", () => {
- tooltipsToCleanup.forEach((tooltip) => tooltip.remove());
+ for (const tooltip of tooltipsToCleanup) {
+ tooltip.remove();
+ }
menu.remove();
});
}
@@ -1243,7 +1296,7 @@ export default defineContentScript({
}
}
- function copyEventTypeLink(eventType: {
+ function _copyEventTypeLink(eventType: {
slug: string;
users?: Array<{ username?: string }>;
}): void {
@@ -1289,7 +1342,7 @@ export default defineContentScript({
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
// If no selection, append to the end
- const textNode = document.createTextNode(" " + text + " ");
+ const textNode = document.createTextNode(` ${text} `);
composeBody.appendChild(textNode);
// Move cursor after inserted text
@@ -1307,7 +1360,7 @@ export default defineContentScript({
range.deleteContents();
// Create a text node with the link (with spaces around it)
- const textNode = document.createTextNode(" " + text + " ");
+ const textNode = document.createTextNode(` ${text} `);
range.insertNode(textNode);
// Move cursor after inserted text
@@ -1492,18 +1545,27 @@ export default defineContentScript({
}
}
- async function fetchEventTypes(menu: HTMLElement, tooltipsToCleanup: HTMLElement[]) {
+ async function _fetchEventTypes(menu: HTMLElement, tooltipsToCleanup: HTMLElement[]) {
try {
// Check cache first
const now = Date.now();
const isCacheValid =
eventTypesCache && cacheTimestamp && now - cacheTimestamp < CACHE_DURATION;
- let eventTypes: any[] = [];
+ let eventTypes: Array<{
+ id: number;
+ title: string;
+ slug: string;
+ lengthInMinutes?: number;
+ length?: number;
+ duration?: number;
+ description?: string;
+ users?: Array<{ username?: string }>;
+ }> = [];
- if (isCacheValid) {
+ if (isCacheValid && eventTypesCache) {
// Use cached data
- eventTypes = eventTypesCache!;
+ eventTypes = eventTypesCache;
} else {
// Check if extension context is valid
if (!chrome.runtime?.id) {
@@ -1516,7 +1578,7 @@ export default defineContentScript({
chrome.runtime.sendMessage({ action: "fetch-event-types" }, (response) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
- } else if (response && response.error) {
+ } else if (response?.error) {
reject(new Error(response.error));
} else {
resolve(response);
@@ -1527,8 +1589,37 @@ export default defineContentScript({
}
});
- if (response && (response as any).data) {
- eventTypes = (response as any).data;
+ if (
+ response &&
+ (
+ response as {
+ data?: Array<{
+ id: number;
+ title: string;
+ slug: string;
+ lengthInMinutes?: number;
+ length?: number;
+ duration?: number;
+ description?: string;
+ users?: Array<{ username?: string }>;
+ }>;
+ }
+ ).data
+ ) {
+ eventTypes = (
+ response as {
+ data: Array<{
+ id: number;
+ title: string;
+ slug: string;
+ lengthInMinutes?: number;
+ length?: number;
+ duration?: number;
+ description?: string;
+ users?: Array<{ username?: string }>;
+ }>;
+ }
+ ).data;
} else if (Array.isArray(response)) {
eventTypes = response;
} else {
@@ -1644,7 +1735,7 @@ export default defineContentScript({
tooltipsToCleanup.push(contentTooltip);
// Show/hide tooltip
- contentWrapper.addEventListener("mouseenter", (e) => {
+ contentWrapper.addEventListener("mouseenter", (_e) => {
const rect = contentWrapper.getBoundingClientRect();
contentTooltip.style.left = `${rect.left + rect.width / 2 + 80}px`;
contentTooltip.style.top = `${rect.top + 35}px`;
@@ -1845,7 +1936,7 @@ export default defineContentScript({
menu.appendChild(menuItem);
});
- } catch (forEachError) {
+ } catch (_forEachError) {
menu.innerHTML = `
Error displaying event types
@@ -1862,7 +1953,9 @@ export default defineContentScript({
if (isAuthError) {
// Not logged in - open sidebar directly
- tooltipsToCleanup.forEach((tooltip) => tooltip.remove());
+ for (const tooltip of tooltipsToCleanup) {
+ tooltip.remove();
+ }
menu.remove();
openCalSidebar();
return;
@@ -1906,7 +1999,9 @@ export default defineContentScript({
closeBtn.style.background = "#F3F4F6";
});
closeBtn.addEventListener("click", () => {
- tooltipsToCleanup.forEach((tooltip) => tooltip.remove());
+ for (const tooltip of tooltipsToCleanup) {
+ tooltip.remove();
+ }
menu.remove();
});
}
@@ -1914,7 +2009,10 @@ export default defineContentScript({
}
}
- function insertEventTypeLink(eventType: any) {
+ function insertEventTypeLink(eventType: {
+ slug: string;
+ users?: Array<{ username?: string }>;
+ }) {
// Construct the Cal.com booking link
const bookingUrl = `https://cal.com/${eventType.users?.[0]?.username || "user"}/${
eventType.slug
@@ -1959,7 +2057,7 @@ export default defineContentScript({
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
// If no selection, append to the end
- const textNode = document.createTextNode(" " + text + " ");
+ const textNode = document.createTextNode(` ${text} `);
messageInput.appendChild(textNode);
// Move cursor after inserted text
@@ -1977,7 +2075,7 @@ export default defineContentScript({
range.deleteContents();
// Create a text node with the link (with spaces around it)
- const textNode = document.createTextNode(" " + text + " ");
+ const textNode = document.createTextNode(` ${text} `);
range.insertNode(textNode);
// Move cursor after inserted text
@@ -2062,9 +2160,16 @@ export default defineContentScript({
* Based on the email embed feature in main Cal.com codebase
*/
function generateEmailEmbedHTML(params: {
- eventType: any;
+ eventType: { slug: string; title?: string };
username: string;
- slots: any[];
+ slots: Array<{
+ isoDate: string;
+ date: string;
+ isoTimestamp: string;
+ time?: string;
+ startTime?: string;
+ endTime?: string;
+ }>;
duration: number;
timezone: string;
timezoneOffset: string;
@@ -2072,7 +2177,16 @@ export default defineContentScript({
const { eventType, username, slots, duration, timezone, timezoneOffset } = params;
// Group slots by date
- const slotsByDate: { [date: string]: any[] } = {};
+ const slotsByDate: {
+ [date: string]: Array<{
+ isoDate: string;
+ date: string;
+ isoTimestamp: string;
+ time?: string;
+ startTime?: string;
+ endTime?: string;
+ }>;
+ } = {};
slots.forEach((slot) => {
if (!slotsByDate[slot.isoDate]) {
slotsByDate[slot.isoDate] = [];
@@ -2261,7 +2375,7 @@ export default defineContentScript({
console.warn("Cal.com: Failed to insert HTML at cursor:", insertError);
return false;
}
- } catch (error) {
+ } catch (_error) {
console.error("Cal.com: Critical error inserting HTML");
return false;
}
@@ -2324,7 +2438,7 @@ export default defineContentScript({
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
try {
- const textNode = document.createTextNode(" " + text + " ");
+ const textNode = document.createTextNode(` ${text} `);
composeBody.appendChild(textNode);
const range = document.createRange();
range.setStartAfter(textNode);
@@ -2341,7 +2455,7 @@ export default defineContentScript({
try {
const range = selection.getRangeAt(0);
range.deleteContents();
- const textNode = document.createTextNode(" " + text + " ");
+ const textNode = document.createTextNode(` ${text} `);
range.insertNode(textNode);
range.setStartAfter(textNode);
range.collapse(true);
@@ -2414,7 +2528,7 @@ export default defineContentScript({
try {
notification.style.opacity = "1";
notification.style.transform = "translateY(0)";
- } catch (e) {
+ } catch (_e) {
// Ignore animation errors
}
});
@@ -2426,11 +2540,11 @@ export default defineContentScript({
setTimeout(() => {
try {
notification.remove();
- } catch (e) {
+ } catch (_e) {
// Ignore removal errors
}
}, 200);
- } catch (e) {
+ } catch (_e) {
// Ignore animation errors
}
}, 3000);
@@ -2445,7 +2559,7 @@ export default defineContentScript({
*/
function watchForGoogleChips() {
try {
- const observer = new MutationObserver((mutations) => {
+ const observer = new MutationObserver((_mutations) => {
try {
const chips = document.querySelectorAll(
".gmail_chip.gmail_ad_hoc_v2_content:not([data-calcom-chip-processed])"
@@ -2489,7 +2603,7 @@ export default defineContentScript({
console.warn("Cal.com: Failed to find existing chips:", error);
}
}, 1000);
- } catch (error) {
+ } catch (_error) {
// Critical failure - log but don't break Gmail
console.error("Cal.com: Failed to initialize chip watcher");
}
@@ -2557,7 +2671,7 @@ export default defineContentScript({
console.log(`Cal.com: 🔄 Chip updated - ${parsedData.detectedDuration}min`);
try {
existingActionBar.remove();
- } catch (e) {
+ } catch (_e) {
// Silently ignore removal errors
}
}
@@ -2578,11 +2692,11 @@ export default defineContentScript({
actionBar?.remove();
// Recreate action bar with new data
handleGoogleChipDetected(chipElement);
- } catch (e) {
+ } catch (_e) {
// Silently ignore recreation errors
}
}
- } catch (error) {
+ } catch (_error) {
// Silently ignore observer errors - expected during DOM updates
}
});
@@ -2594,7 +2708,7 @@ export default defineContentScript({
attributes: true,
attributeFilter: ["data-ad-hoc-v2-params"],
});
- } catch (error) {
+ } catch (_error) {
// Silently ignore observer setup errors
}
@@ -2758,11 +2872,28 @@ export default defineContentScript({
embedButton.style.opacity = "0.6";
embedButton.style.cursor = "not-allowed";
- const response: any = await new Promise((resolve, reject) => {
+ const response = await new Promise<
+ | {
+ data?: Array<{
+ id: number;
+ title: string;
+ slug: string;
+ lengthInMinutes?: number;
+ users?: Array<{ username?: string }>;
+ }>;
+ }
+ | Array<{
+ id: number;
+ title: string;
+ slug: string;
+ lengthInMinutes?: number;
+ users?: Array<{ username?: string }>;
+ }>
+ >((resolve, reject) => {
chrome.runtime.sendMessage({ action: "fetch-event-types" }, (result) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
- } else if (result && result.error) {
+ } else if (result?.error) {
reject(new Error(result.error));
} else {
resolve(result);
@@ -2770,9 +2901,14 @@ export default defineContentScript({
});
});
- const eventTypes = response?.data || (Array.isArray(response) ? response : []);
+ const eventTypes =
+ (response && "data" in response
+ ? response.data
+ : Array.isArray(response)
+ ? response
+ : []) || [];
const matchingEventType = eventTypes.find(
- (et: any) => et.lengthInMinutes === freshParsedData.detectedDuration
+ (et) => et.lengthInMinutes === freshParsedData.detectedDuration
);
if (!matchingEventType) {
@@ -2808,9 +2944,7 @@ export default defineContentScript({
// Immediately remove the Google chip and action bar
try {
chipElement.remove();
- if ((actionBar as any).__cleanup) {
- (actionBar as any).__cleanup();
- }
+ (actionBar as HTMLElement & { __cleanup?: () => void }).__cleanup?.();
actionBar.remove();
} catch (removeError) {
console.warn("Cal.com: Failed to remove chip/action bar:", removeError);
@@ -2882,11 +3016,9 @@ export default defineContentScript({
// Remove the action bar completely
try {
// Call cleanup function if it exists
- if ((actionBar as any).__cleanup) {
- (actionBar as any).__cleanup();
- }
+ (actionBar as HTMLElement & { __cleanup?: () => void }).__cleanup?.();
actionBar.remove();
- } catch (error) {
+ } catch (_error) {
// Silently ignore removal errors
}
});
@@ -2916,7 +3048,7 @@ export default defineContentScript({
actionBar.style.top = `${top}px`;
actionBar.style.left = `${left}px`;
- } catch (error) {
+ } catch (_error) {
// Silently ignore positioning errors
}
};
@@ -2933,11 +3065,9 @@ export default defineContentScript({
if (barScheduleId === scheduleId) {
// This bar belongs to the current chip - remove it (we'll create a fresh one)
try {
- if ((bar as any).__cleanup) {
- (bar as any).__cleanup();
- }
+ (bar as Element & { __cleanup?: () => void }).__cleanup?.();
bar.remove();
- } catch (e) {
+ } catch (_e) {
// Ignore removal errors
}
} else {
@@ -2948,13 +3078,11 @@ export default defineContentScript({
);
if (!chipForBar) {
// Chip is gone, this is an orphaned action bar - remove it
- if ((bar as any).__cleanup) {
- (bar as any).__cleanup();
- }
+ (bar as Element & { __cleanup?: () => void }).__cleanup?.();
bar.remove();
}
// Else: chip exists, keep this action bar (belongs to another chip)
- } catch (e) {
+ } catch (_e) {
// Ignore errors when checking
}
}
@@ -2976,7 +3104,7 @@ export default defineContentScript({
// Chip removed, clean up
try {
actionBar.remove();
- } catch (e) {
+ } catch (_e) {
// Ignore
}
}
@@ -2986,20 +3114,20 @@ export default defineContentScript({
window.addEventListener("resize", updatePosition);
// Store cleanup function
- (actionBar as any).__cleanup = () => {
+ (actionBar as HTMLElement & { __cleanup?: () => void }).__cleanup = () => {
window.removeEventListener("scroll", updatePosition, true);
window.removeEventListener("resize", updatePosition);
};
- } catch (error) {
+ } catch (_error) {
console.error("Cal.com: Failed to create or insert action bar");
// Clean up if something failed
try {
actionBar.remove();
- } catch (e) {
+ } catch (_e) {
// Ignore cleanup errors
}
}
- } catch (error) {
+ } catch (_error) {
// Catch-all to prevent breaking Gmail UI
console.error("Cal.com: Error handling Google chip");
return;
@@ -3009,7 +3137,22 @@ export default defineContentScript({
/**
* Show Cal.com suggestion menu for Google Calendar chip - CENTERED FULL-SCREEN MODAL
*/
- async function showCalcomSuggestionMenu(chipElement: HTMLElement, parsedData: any) {
+ async function showCalcomSuggestionMenu(
+ chipElement: HTMLElement,
+ parsedData: {
+ detectedDuration: number;
+ slots: Array<{
+ isoDate: string;
+ date: string;
+ isoTimestamp: string;
+ time?: string;
+ startTime?: string;
+ endTime?: string;
+ }>;
+ timezone: string;
+ timezoneOffset: string;
+ }
+ ) {
console.log(
"Cal.com: Opening menu for",
parsedData.detectedDuration,
@@ -3125,22 +3268,48 @@ export default defineContentScript({
try {
// Fetch event types (will use cache if available)
- let eventTypes: any[] = [];
+ let eventTypes: Array<{
+ id: number;
+ title: string;
+ slug: string;
+ lengthInMinutes?: number;
+ description?: string;
+ users?: Array<{ username?: string }>;
+ }> = [];
// Check cache first
const now = Date.now();
const isCacheValid =
eventTypesCache && cacheTimestamp && now - cacheTimestamp < CACHE_DURATION;
- if (isCacheValid) {
- eventTypes = eventTypesCache!;
+ if (isCacheValid && eventTypesCache) {
+ eventTypes = eventTypesCache;
} else {
// Fetch from background script
- const response: any = await new Promise((resolve, reject) => {
+ const response = await new Promise<
+ | {
+ data?: Array<{
+ id: number;
+ title: string;
+ slug: string;
+ lengthInMinutes?: number;
+ description?: string;
+ users?: Array<{ username?: string }>;
+ }>;
+ }
+ | Array<{
+ id: number;
+ title: string;
+ slug: string;
+ lengthInMinutes?: number;
+ description?: string;
+ users?: Array<{ username?: string }>;
+ }>
+ >((resolve, reject) => {
chrome.runtime.sendMessage({ action: "fetch-event-types" }, (response) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
- } else if (response && response.error) {
+ } else if (response?.error) {
reject(new Error(response.error));
} else {
resolve(response);
@@ -3148,7 +3317,7 @@ export default defineContentScript({
});
});
- if (response && response.data) {
+ if (response && "data" in response && response.data) {
eventTypes = response.data;
} else if (Array.isArray(response)) {
eventTypes = response;
@@ -3168,7 +3337,7 @@ export default defineContentScript({
// Filter event types by matching duration
const matchingEventTypes = eventTypes.filter(
- (et: any) => et.lengthInMinutes === parsedData.detectedDuration
+ (et) => et.lengthInMinutes === parsedData.detectedDuration
);
// If no matching event types, show create prompt
@@ -3322,7 +3491,7 @@ export default defineContentScript({
`;
// Create options
- matchingEventTypes.forEach((et: any, index: number) => {
+ matchingEventTypes.forEach((et, index: number) => {
const option = document.createElement("div");
option.className = "dropdown-option";
option.style.cssText = `
@@ -3409,7 +3578,7 @@ export default defineContentScript({
const slotsContainer = document.createElement("div");
slotsContainer.style.cssText = "padding: 12px;";
- parsedData.slots.forEach((slot: any, index: number) => {
+ parsedData.slots.forEach((slot, index: number) => {
const slotItem = document.createElement("div");
slotItem.style.cssText = `
padding: 14px;
@@ -3479,7 +3648,9 @@ export default defineContentScript({
console.log("Cal.com: Insert button clicked!");
e.preventDefault();
e.stopPropagation();
- const slotIndex = parseInt(btn.getAttribute("data-slot-index")!);
+ const slotIndexAttr = btn.getAttribute("data-slot-index");
+ if (!slotIndexAttr) return;
+ const slotIndex = parseInt(slotIndexAttr, 10);
const slot = parsedData.slots[slotIndex];
console.log("Cal.com: Inserting link for slot", slotIndex, slot);
@@ -3518,9 +3689,7 @@ export default defineContentScript({
chipElement.remove();
if (actionBar) {
- if ((actionBar as any).__cleanup) {
- (actionBar as any).__cleanup();
- }
+ (actionBar as Element & { __cleanup?: () => void }).__cleanup?.();
actionBar.remove();
}
} catch (removeError) {
@@ -3623,7 +3792,21 @@ export default defineContentScript({
* Parse Google Calendar scheduling chip
* Returns null if structure has changed or parsing fails (fail gracefully)
*/
- function parseGoogleChip(chipElement: HTMLElement): any {
+ function parseGoogleChip(chipElement: HTMLElement): {
+ scheduleId: string;
+ timezone: string;
+ timezoneOffset: string;
+ slots: Array<{
+ date: string;
+ startTime: string;
+ endTime: string;
+ durationMinutes: number;
+ isoDate: string;
+ isoTimestamp: string;
+ googleUrl: string;
+ }>;
+ detectedDuration: number;
+ } | null {
try {
// Validate input
if (!chipElement || typeof chipElement.getAttribute !== "function") {
@@ -3642,7 +3825,7 @@ export default defineContentScript({
console.log(
"Cal.com: Valid chip detected - Schedule ID:",
- scheduleId?.slice(0, 20) + "..."
+ `${scheduleId?.slice(0, 20)}...`
);
// Parse timezone (non-critical - fallback to UTC if structure changed)
@@ -3669,7 +3852,7 @@ export default defineContentScript({
tzMatch = paramsAttr.match(/\\"([^\\]+)\\"\]/);
}
- if (tzMatch && tzMatch[1]) {
+ if (tzMatch?.[1]) {
timezone = tzMatch[1]; // e.g., "Asia/Kolkata"
console.log("Cal.com: ✅ Parsed IANA timezone from data attribute:", timezone);
} else {
@@ -3706,7 +3889,15 @@ export default defineContentScript({
return null;
}
- const slots: any[] = [];
+ const slots: Array<{
+ date: string;
+ startTime: string;
+ endTime: string;
+ durationMinutes: number;
+ isoDate: string;
+ isoTimestamp: string;
+ googleUrl: string;
+ }> = [];
let detectedDuration = 60;
slotLinks.forEach((link) => {
@@ -3722,18 +3913,18 @@ export default defineContentScript({
if (!slotStartTime || !slotDurationMinutes) return;
const durationMinutes = parseInt(slotDurationMinutes, 10);
- if (isNaN(durationMinutes) || durationMinutes <= 0) return;
+ if (Number.isNaN(durationMinutes) || durationMinutes <= 0) return;
detectedDuration = durationMinutes;
const startTimestamp = parseInt(slotStartTime, 10);
- if (isNaN(startTimestamp)) return;
+ if (Number.isNaN(startTimestamp)) return;
const startDate = new Date(startTimestamp);
const endDate = new Date(startDate.getTime() + durationMinutes * 60 * 1000);
// Validate dates
- if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) return;
+ if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) return;
const dateOptions: Intl.DateTimeFormatOptions = {
weekday: "short",
diff --git a/companion/extension/lib/google-calendar.ts b/companion/extension/lib/google-calendar.ts
index 9e56ed5e73..c528cb1027 100644
--- a/companion/extension/lib/google-calendar.ts
+++ b/companion/extension/lib/google-calendar.ts
@@ -289,7 +289,7 @@ function isEventInPast(eventElement: Element): boolean {
// Short date format: "Dec 20, 2024"
/(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2}),?\s+(\d{4})/i,
// Numeric format: "12/20/2024" or "20/12/2024"
- /(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/,
+ /(\d{1,2})[/-](\d{1,2})[/-](\d{4})/,
];
const months: Record
= {
@@ -374,7 +374,7 @@ function isEventInPast(eventElement: Element): boolean {
if (eventStartAttr) {
try {
const eventDate = new Date(eventStartAttr);
- if (!isNaN(eventDate.getTime())) {
+ if (!Number.isNaN(eventDate.getTime())) {
return eventDate < now;
}
} catch {
@@ -443,7 +443,7 @@ function parseAttendees(eventPopup: Element): AttendeeInfo[] {
// Extract name from the SDqFWd span (Google Calendar's name display)
let name = email.split("@")[0];
const nameSpan = el.querySelector(".SDqFWd span");
- if (nameSpan && nameSpan.textContent) {
+ if (nameSpan?.textContent) {
const nameText = nameSpan.textContent.trim();
// Only use if it's not just the email
if (nameText !== email && nameText.includes("@") === false) {
@@ -689,7 +689,9 @@ function showConfirmDialog(
*/
function showAlert(message: string, type: "error" | "success" | "info" = "info"): void {
// Remove existing alerts
- document.querySelectorAll(".cal-noshow-alert").forEach((el) => el.remove());
+ for (const el of document.querySelectorAll(".cal-noshow-alert")) {
+ el.remove();
+ }
const alert = document.createElement("div");
alert.className = `cal-noshow-alert ${type}`;
@@ -1092,7 +1094,7 @@ async function injectNoShowButtons(eventPopup: Element): Promise {
? nameContainer.querySelector(".SDqFWd span, .SDqFWd")
: attendee.element.querySelector(".SDqFWd span, .SDqFWd");
- if (nameSpan && nameSpan.parentElement) {
+ if (nameSpan?.parentElement) {
// Insert button AFTER the name span (on the right side)
// Check if button already exists at this location
const parent = nameSpan.parentElement;
@@ -1162,7 +1164,7 @@ async function injectNoShowButtons(eventPopup: Element): Promise {
const emailElement = eventPopup.querySelector(`[data-email="${attendee.email}"]`);
if (emailElement && emailElement !== eventPopup) {
const nameSpan = emailElement.querySelector(".SDqFWd span, .SDqFWd");
- if (nameSpan && nameSpan.parentElement) {
+ if (nameSpan?.parentElement) {
nameSpan.parentElement.insertBefore(buttonContainer, nameSpan.nextSibling);
inserted = true;
} else if (emailElement.parentElement) {
diff --git a/companion/extension/lib/linkedin.ts b/companion/extension/lib/linkedin.ts
index 007bc89246..f3daf7a906 100644
--- a/companion/extension/lib/linkedin.ts
+++ b/companion/extension/lib/linkedin.ts
@@ -300,7 +300,9 @@ export function initLinkedInIntegration() {
setTimeout(() => {
const closeMenu = (evt: MouseEvent) => {
if (!menu.contains(evt.target as Node) && evt.target !== button) {
- tooltipsToCleanup.forEach((tooltip) => tooltip.remove());
+ for (const tooltip of tooltipsToCleanup) {
+ tooltip.remove();
+ }
menu.remove();
document.removeEventListener("click", closeMenu);
}
@@ -438,7 +440,7 @@ export function initLinkedInIntegration() {
);
menu.appendChild(menuItem);
});
- } catch (error) {
+ } catch (_error) {
menu.innerHTML = `
Error displaying event types
@@ -674,7 +676,9 @@ export function initLinkedInIntegration() {
errorMessage.includes("authentication");
if (isAuthError) {
- tooltipsToCleanup.forEach((tooltip) => tooltip.remove());
+ for (const tooltip of tooltipsToCleanup) {
+ tooltip.remove();
+ }
menu.remove();
openCalSidebar();
return;
@@ -714,7 +718,9 @@ export function initLinkedInIntegration() {
closeBtn.style.background = "#F3F4F6";
});
closeBtn.addEventListener("click", () => {
- tooltipsToCleanup.forEach((tooltip) => tooltip.remove());
+ for (const tooltip of tooltipsToCleanup) {
+ tooltip.remove();
+ }
menu.remove();
});
}
diff --git a/companion/hooks/index.ts b/companion/hooks/index.ts
index 9fdd78cad4..11758a7a74 100644
--- a/companion/hooks/index.ts
+++ b/companion/hooks/index.ts
@@ -10,73 +10,66 @@
* ```
*/
-// Bookings hooks
-export {
- useBookings,
- useBookingByUid,
- useCancelBooking,
- useRescheduleBooking,
- useConfirmBooking,
- useDeclineBooking,
- usePrefetchBookings,
- useInvalidateBookings,
- type BookingFilters,
- type Booking,
-} from "./useBookings";
-
-// Event Types hooks
-export {
- useEventTypes,
- useEventTypeById,
- useCreateEventType,
- useUpdateEventType,
- useDeleteEventType,
- useDuplicateEventType,
- usePrefetchEventTypes,
- useInvalidateEventTypes,
- type EventType,
- type CreateEventTypeInput,
-} from "./useEventTypes";
-
-// Schedules (Availability) hooks
-export {
- useSchedules,
- useScheduleById,
- useCreateSchedule,
- useUpdateSchedule,
- useSetScheduleAsDefault,
- useDeleteSchedule,
- useDuplicateSchedule,
- usePrefetchSchedules,
- useInvalidateSchedules,
- type Schedule,
- type CreateScheduleInput,
- type UpdateScheduleInput,
-} from "./useSchedules";
-
-// User Profile hooks
-export {
- useUserProfile,
- useUsername,
- useUpdateUserProfile,
- usePrefetchUserProfile,
- useInvalidateUserProfile,
- type UserProfile,
- type UpdateUserProfileInput,
-} from "./useUserProfile";
-
-// UI State Management hooks
-export {
- useActiveBookingFilter,
- type BookingFilter,
- type BookingFilterOption,
-} from "./useActiveBookingFilter";
-
-export { useBookingActions } from "./useBookingActions";
-export { useBookingActionModals } from "./useBookingActionModals";
-
// Re-export query keys for advanced use cases
export { queryKeys } from "../config/cache.config";
-
// Re-export query context utilities
export { useQueryContext } from "../contexts/QueryContext";
+// UI State Management hooks
+export {
+ type BookingFilter,
+ type BookingFilterOption,
+ useActiveBookingFilter,
+} from "./useActiveBookingFilter";
+export { useBookingActionModals } from "./useBookingActionModals";
+export { useBookingActions } from "./useBookingActions";
+// Bookings hooks
+export {
+ type Booking,
+ type BookingFilters,
+ useBookingByUid,
+ useBookings,
+ useCancelBooking,
+ useConfirmBooking,
+ useDeclineBooking,
+ useInvalidateBookings,
+ usePrefetchBookings,
+ useRescheduleBooking,
+} from "./useBookings";
+// Event Types hooks
+export {
+ type CreateEventTypeInput,
+ type EventType,
+ useCreateEventType,
+ useDeleteEventType,
+ useDuplicateEventType,
+ useEventTypeById,
+ useEventTypes,
+ useInvalidateEventTypes,
+ usePrefetchEventTypes,
+ useUpdateEventType,
+} from "./useEventTypes";
+// Schedules (Availability) hooks
+export {
+ type CreateScheduleInput,
+ type Schedule,
+ type UpdateScheduleInput,
+ useCreateSchedule,
+ useDeleteSchedule,
+ useDuplicateSchedule,
+ useInvalidateSchedules,
+ usePrefetchSchedules,
+ useScheduleById,
+ useSchedules,
+ useSetScheduleAsDefault,
+ useUpdateSchedule,
+} from "./useSchedules";
+// User Profile hooks
+export {
+ type UpdateUserProfileInput,
+ type UserProfile,
+ useInvalidateUserProfile,
+ usePrefetchUserProfile,
+ useUpdateUserProfile,
+ useUsername,
+ useUserProfile,
+} from "./useUserProfile";
diff --git a/companion/hooks/useActiveBookingFilter.tsx b/companion/hooks/useActiveBookingFilter.tsx
index 2613861505..d2600276d4 100644
--- a/companion/hooks/useActiveBookingFilter.tsx
+++ b/companion/hooks/useActiveBookingFilter.tsx
@@ -1,4 +1,4 @@
-import { useState, useMemo, useCallback } from "react";
+import { useCallback, useMemo, useState } from "react";
import type { NativeSyntheticEvent } from "react-native";
import type { BookingFilters } from "./useBookings";
diff --git a/companion/hooks/useBookingActionModals.ts b/companion/hooks/useBookingActionModals.ts
index ed8e7396c8..c02e368537 100644
--- a/companion/hooks/useBookingActionModals.ts
+++ b/companion/hooks/useBookingActionModals.ts
@@ -4,17 +4,18 @@
* Centralized hook for managing all booking action modals and their API calls.
* This hook is designed to be used across iOS, Android, and extension platforms.
*/
-import { useState, useCallback } from "react";
+
+import { useQueryClient } from "@tanstack/react-query";
+import { useCallback, useState } from "react";
import { Alert, Linking } from "react-native";
-import { CalComAPIService, type Booking } from "../services/calcom";
+import { queryKeys } from "../config/cache.config";
+import { type Booking, CalComAPIService } from "../services/calcom";
import type {
+ AddGuestInput,
BookingRecording,
ConferencingSession,
- AddGuestInput,
} from "../services/types/bookings.types";
import { showErrorAlert } from "../utils/alerts";
-import { useQueryClient } from "@tanstack/react-query";
-import { queryKeys } from "../config/cache.config";
interface UseBookingActionModalsReturn {
// Selected booking for actions
diff --git a/companion/hooks/useBookingActions.ts b/companion/hooks/useBookingActions.ts
index 4b46451da8..34c6ad7e00 100644
--- a/companion/hooks/useBookingActions.ts
+++ b/companion/hooks/useBookingActions.ts
@@ -1,8 +1,8 @@
-import type { Booking } from "../services/calcom";
-import { showErrorAlert } from "../utils/alerts";
import type { useRouter } from "expo-router";
import { useState } from "react";
import { Alert } from "react-native";
+import type { Booking } from "../services/calcom";
+import { showErrorAlert } from "../utils/alerts";
interface UseBookingActionsParams {
router: ReturnType
;
@@ -90,7 +90,7 @@ export const useBookingActions = ({
const currentDate = new Date(startTimeValue);
// Check if the date is valid
- if (isNaN(currentDate.getTime())) {
+ if (Number.isNaN(currentDate.getTime())) {
showErrorAlert("Error", "Unable to reschedule: invalid booking date");
return;
}
@@ -126,7 +126,7 @@ export const useBookingActions = ({
const newDateTime = new Date(dateTimeStr);
// Validate the date
- if (isNaN(newDateTime.getTime())) {
+ if (Number.isNaN(newDateTime.getTime())) {
showErrorAlert(
"Error",
"Invalid date or time format. Please use YYYY-MM-DD for date and HH:MM for time."
@@ -179,7 +179,7 @@ export const useBookingActions = ({
const newDateTime = new Date(dateTimeStr);
// Validate the date
- if (isNaN(newDateTime.getTime())) {
+ if (Number.isNaN(newDateTime.getTime())) {
throw new Error(
"Invalid date or time format. Please use YYYY-MM-DD for date and HH:MM for time."
);
@@ -250,7 +250,7 @@ export const useBookingActions = ({
onSuccess: () => {
Alert.alert("Success", "Event cancelled successfully");
},
- onError: (error) => {
+ onError: (_error) => {
console.error("Failed to cancel booking");
showErrorAlert("Error", "Failed to cancel event. Please try again.");
},
@@ -359,7 +359,7 @@ export const useBookingActions = ({
setRejectReason("");
Alert.alert("Success", "Booking rejected successfully");
},
- onError: (error) => {
+ onError: (_error) => {
showErrorAlert("Error", "Failed to reject booking. Please try again.");
},
}
@@ -385,7 +385,7 @@ export const useBookingActions = ({
onSuccess: () => {
Alert.alert("Success", "Booking confirmed successfully");
},
- onError: (error) => {
+ onError: (_error) => {
showErrorAlert("Error", "Failed to confirm booking. Please try again.");
},
}
diff --git a/companion/hooks/useBookingActionsGating.ts b/companion/hooks/useBookingActionsGating.ts
index 7e1c2c77fa..c2c69c62d0 100644
--- a/companion/hooks/useBookingActionsGating.ts
+++ b/companion/hooks/useBookingActionsGating.ts
@@ -4,15 +4,16 @@
* This hook provides the booking actions state using the centralized gating utility.
* It computes which actions are visible and enabled for a given booking.
*/
+
+import { useMemo } from "react";
import type { Booking } from "../services/calcom";
import type { EventType } from "../services/types/event-types.types";
import {
- normalizeBooking,
- getBookingActions,
type BookingActionsResult,
+ getBookingActions,
type NormalizedBooking,
+ normalizeBooking,
} from "../utils/booking-actions";
-import { useMemo } from "react";
interface UseBookingActionsGatingParams {
booking: Booking | null;
diff --git a/companion/hooks/useBookings.ts b/companion/hooks/useBookings.ts
index 9fee0be6b4..49a07e59b9 100644
--- a/companion/hooks/useBookings.ts
+++ b/companion/hooks/useBookings.ts
@@ -8,9 +8,10 @@
* - Optimistic updates for mutations
* - Cache invalidation on mutations
*/
+
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { CACHE_CONFIG, queryKeys } from "../config/cache.config";
-import { CalComAPIService, Booking } from "../services/calcom";
-import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { type Booking, CalComAPIService } from "../services/calcom";
/**
* Filter options for fetching bookings
@@ -73,7 +74,10 @@ export function useBookings(filters?: BookingFilters) {
export function useBookingByUid(uid: string | undefined) {
return useQuery({
queryKey: queryKeys.bookings.detail(uid || ""),
- queryFn: () => CalComAPIService.getBookingByUid(uid!),
+ queryFn: () => {
+ if (!uid) throw new Error("uid is required");
+ return CalComAPIService.getBookingByUid(uid);
+ },
enabled: !!uid, // Only fetch when uid is provided
staleTime: CACHE_CONFIG.bookings.staleTime,
});
@@ -106,7 +110,7 @@ export function useCancelBooking() {
queryKey: queryKeys.bookings.detail(variables.uid),
});
},
- onError: (error) => {
+ onError: (_error) => {
console.error("Failed to cancel booking");
},
});
@@ -150,7 +154,7 @@ export function useRescheduleBooking() {
queryKey: queryKeys.bookings.detail(variables.uid),
});
},
- onError: (error) => {
+ onError: (_error) => {
console.error("Failed to reschedule booking");
},
});
@@ -182,7 +186,7 @@ export function useConfirmBooking() {
queryKey: queryKeys.bookings.detail(variables.uid),
});
},
- onError: (error) => {
+ onError: (_error) => {
console.error("Failed to confirm booking");
},
});
@@ -215,7 +219,7 @@ export function useDeclineBooking() {
queryKey: queryKeys.bookings.detail(variables.uid),
});
},
- onError: (error) => {
+ onError: (_error) => {
console.error("Failed to decline booking");
},
});
diff --git a/companion/hooks/useEventTypes.ts b/companion/hooks/useEventTypes.ts
index 60b1b695e0..658397aa9e 100644
--- a/companion/hooks/useEventTypes.ts
+++ b/companion/hooks/useEventTypes.ts
@@ -8,9 +8,10 @@
* - Optimistic updates for mutations
* - Cache invalidation on create/update/delete
*/
+
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { CACHE_CONFIG, queryKeys } from "../config/cache.config";
-import { CalComAPIService, EventType, CreateEventTypeInput } from "../services/calcom";
-import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { CalComAPIService, type CreateEventTypeInput, type EventType } from "../services/calcom";
/**
* Hook to fetch all event types
@@ -57,7 +58,10 @@ export function useEventTypes() {
export function useEventTypeById(id: number | undefined) {
return useQuery({
queryKey: queryKeys.eventTypes.detail(id || 0),
- queryFn: () => CalComAPIService.getEventTypeById(id!),
+ queryFn: () => {
+ if (!id) throw new Error("id is required");
+ return CalComAPIService.getEventTypeById(id);
+ },
enabled: !!id, // Only fetch when id is provided
staleTime: CACHE_CONFIG.eventTypes.staleTime,
});
diff --git a/companion/hooks/useNetworkStatus.ts b/companion/hooks/useNetworkStatus.ts
index 56dca19cfe..af2f3c3ba2 100644
--- a/companion/hooks/useNetworkStatus.ts
+++ b/companion/hooks/useNetworkStatus.ts
@@ -4,8 +4,8 @@
* This hook provides real-time network status for components.
* Used to disable mutations when offline.
*/
-import NetInfo, { NetInfoState } from "@react-native-community/netinfo";
-import { useState, useEffect } from "react";
+import NetInfo, { type NetInfoState } from "@react-native-community/netinfo";
+import { useEffect, useState } from "react";
interface NetworkStatus {
isOnline: boolean;
diff --git a/companion/hooks/useSchedules.ts b/companion/hooks/useSchedules.ts
index e614dcab31..d040d08873 100644
--- a/companion/hooks/useSchedules.ts
+++ b/companion/hooks/useSchedules.ts
@@ -8,9 +8,10 @@
* - Optimistic updates for mutations
* - Cache invalidation on create/update/delete
*/
+
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { CACHE_CONFIG, queryKeys } from "../config/cache.config";
-import { CalComAPIService, Schedule } from "../services/calcom";
-import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { CalComAPIService, type Schedule } from "../services/calcom";
/**
* Sort schedules: default first, then alphabetically by name
@@ -109,7 +110,10 @@ export function useSchedules() {
export function useScheduleById(id: number | undefined) {
return useQuery({
queryKey: queryKeys.schedules.detail(id || 0),
- queryFn: () => CalComAPIService.getScheduleById(id!),
+ queryFn: () => {
+ if (!id) throw new Error("id is required");
+ return CalComAPIService.getScheduleById(id);
+ },
enabled: !!id, // Only fetch when id is provided
staleTime: CACHE_CONFIG.schedules.staleTime,
});
@@ -145,7 +149,7 @@ export function useCreateSchedule() {
// Optionally, add the new schedule to cache immediately
queryClient.setQueryData(queryKeys.schedules.detail(newSchedule.id), newSchedule);
},
- onError: (error) => {
+ onError: (_error) => {
console.error("Failed to create schedule");
},
});
@@ -179,7 +183,7 @@ export function useUpdateSchedule() {
// Update the specific schedule in cache
queryClient.setQueryData(queryKeys.schedules.detail(variables.id), updatedSchedule);
},
- onError: (error) => {
+ onError: (_error) => {
console.error("Failed to update schedule");
},
});
@@ -206,7 +210,7 @@ export function useSetScheduleAsDefault() {
// Invalidate all schedules to update the default flag
queryClient.invalidateQueries({ queryKey: queryKeys.schedules.all });
},
- onError: (error) => {
+ onError: (_error) => {
console.error("Failed to set schedule as default");
},
});
@@ -246,7 +250,7 @@ export function useDeleteSchedule() {
return { previousSchedules };
},
- onError: (error, _deletedId, context) => {
+ onError: (_error, _deletedId, context) => {
// Rollback on error
if (context?.previousSchedules) {
queryClient.setQueryData(queryKeys.schedules.lists(), context.previousSchedules);
@@ -281,7 +285,7 @@ export function useDuplicateSchedule() {
// Invalidate the list to include the duplicated schedule
queryClient.invalidateQueries({ queryKey: queryKeys.schedules.lists() });
},
- onError: (error) => {
+ onError: (_error) => {
console.error("Failed to duplicate schedule");
},
});
diff --git a/companion/hooks/useUserProfile.ts b/companion/hooks/useUserProfile.ts
index a7b6005e5b..9b921143d6 100644
--- a/companion/hooks/useUserProfile.ts
+++ b/companion/hooks/useUserProfile.ts
@@ -6,9 +6,10 @@
* - Automatic caching with configurable stale times
* - Profile update mutations with cache invalidation
*/
+
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { CACHE_CONFIG, queryKeys } from "../config/cache.config";
-import { CalComAPIService, UserProfile } from "../services/calcom";
-import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { CalComAPIService, type UserProfile } from "../services/calcom";
/**
* User profile update input type
diff --git a/companion/lib/queryPersister.ts b/companion/lib/queryPersister.ts
index ad927df5b9..cc45bedda1 100644
--- a/companion/lib/queryPersister.ts
+++ b/companion/lib/queryPersister.ts
@@ -4,8 +4,8 @@
*/
export {
- createQueryPersister,
clearQueryCache,
+ createQueryPersister,
getCacheMetadata,
storage,
} from "../utils/queryPersister";
diff --git a/companion/lib/storage.ts b/companion/lib/storage.ts
index a8046ae0f6..aaa9a6dd39 100644
--- a/companion/lib/storage.ts
+++ b/companion/lib/storage.ts
@@ -4,8 +4,8 @@
*/
export {
- secureStorage,
generalStorage,
isChromeStorageAvailable,
type StorageAdapter,
+ secureStorage,
} from "../utils/storage";
diff --git a/companion/package.json b/companion/package.json
index 728b570db4..5e153ebfb7 100644
--- a/companion/package.json
+++ b/companion/package.json
@@ -40,8 +40,11 @@
"ext:zip-edge-prod": "BUILD_FOR_STORE=true BROWSER_TARGET=edge wxt zip -b edge",
"ext:build-all": "npm run ext:build && npm run ext:build-firefox && npm run ext:build-safari && npm run ext:build-edge",
"ext:build-all-prod": "npm run ext:build-prod && npm run ext:build-firefox-prod && npm run ext:build-safari-prod && npm run ext:build-edge-prod",
- "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,md}\"",
- "format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,css,md}\"",
+ "lint": "biome lint .",
+ "format": "biome format --write .",
+ "format:check": "biome format .",
+ "check": "biome check --write .",
+ "check:ci": "biome ci .",
"prepare": "cd .. && husky companion/.husky"
},
"dependencies": {
@@ -81,21 +84,20 @@
},
"private": true,
"devDependencies": {
+ "@biomejs/biome": "2.3.10",
"@types/chrome": "^0.1.31",
"@types/react": "19.2.3",
"@types/react-dom": "19.2.3",
"babel-preset-expo": "^54.0.7",
"husky": "^9.0.11",
"lint-staged": "^15.2.0",
- "prettier": "^3.2.5",
- "prettier-plugin-tailwindcss": "^0.5.11",
"tailwindcss": "^3.4.17",
"typescript": "^5.9.3",
"wxt": "^0.20.11"
},
"lint-staged": {
- "**/*.{js,jsx,ts,tsx,json,css,md}": [
- "prettier --write"
+ "**/*.{js,jsx,ts,tsx,json}": [
+ "biome check --write --error-on-warnings --no-errors-on-unmatched"
]
}
}
diff --git a/companion/services/calcom.ts b/companion/services/calcom.ts
index 18e4d349b8..5d2f4bdf23 100644
--- a/companion/services/calcom.ts
+++ b/companion/services/calcom.ts
@@ -1,32 +1,31 @@
import type {
- EventType,
- CreateEventTypeInput,
- GetEventTypesResponse,
- Booking,
- GetBookingsResponse,
- BookingParticipationResult,
- Schedule,
- GetSchedulesResponse,
- GetScheduleResponse,
- UserProfile,
- ConferencingOption,
- Webhook,
- CreateWebhookInput,
- UpdateWebhookInput,
- PrivateLink,
- CreatePrivateLinkInput,
- UpdatePrivateLinkInput,
- MarkAbsentRequest,
- MarkAbsentResponse,
AddGuestInput,
AddGuestsResponse,
- UpdateLocationResponse,
+ Booking,
+ BookingLimitsCount,
+ BookingLimitsDuration,
+ BookingParticipationResult,
BookingRecording,
- GetRecordingsResponse,
- ConferencingSession,
- GetConferencingSessionsResponse,
BookingTranscript,
+ ConferencingOption,
+ ConferencingSession,
+ ConfirmationPolicy,
+ CreateEventTypeInput,
+ CreatePrivateLinkInput,
+ CreateWebhookInput,
+ EventType,
+ GetConferencingSessionsResponse,
+ GetRecordingsResponse,
GetTranscriptsResponse,
+ MarkAbsentRequest,
+ MarkAbsentResponse,
+ PrivateLink,
+ Schedule,
+ UpdateLocationResponse,
+ UpdatePrivateLinkInput,
+ UpdateWebhookInput,
+ UserProfile,
+ Webhook,
} from "./types";
const API_BASE_URL = "https://api.cal.com/v2";
@@ -64,6 +63,9 @@ export type {
PrivateLink,
CreatePrivateLinkInput,
UpdatePrivateLinkInput,
+ BookingLimitsCount,
+ BookingLimitsDuration,
+ ConfirmationPolicy,
};
export const getBookingParticipation = (
@@ -95,1460 +97,1496 @@ export const getBookingParticipation = (
return { isOrganizer, isHost, isAttendee, isParticipating };
};
-export class CalComAPIService {
- private static userProfile: UserProfile | null = null;
+// Module-level state (previously private static)
+let _userProfile: UserProfile | null = null;
- /**
- * Set OAuth access token for authentication
- */
- static setAccessToken(accessToken: string, refreshToken?: string): void {
- authConfig.accessToken = accessToken;
- if (refreshToken) {
- authConfig.refreshToken = refreshToken;
- }
+/**
+ * Set OAuth access token for authentication
+ */
+function setAccessToken(accessToken: string, refreshToken?: string): void {
+ authConfig.accessToken = accessToken;
+ if (refreshToken) {
+ authConfig.refreshToken = refreshToken;
}
+}
- /**
- * Set refresh token function for automatic token refresh
- */
- static setRefreshTokenFunction(
- refreshFn: (refreshToken: string) => Promise<{ accessToken: string; refreshToken?: string }>
- ): void {
- refreshTokenFunction = refreshFn;
+/**
+ * Set refresh token function for automatic token refresh
+ */
+function setRefreshTokenFunction(
+ refreshFn: (refreshToken: string) => Promise<{ accessToken: string; refreshToken?: string }>
+): void {
+ refreshTokenFunction = refreshFn;
+}
+
+/**
+ * Clear all authentication
+ */
+function clearAuth(): void {
+ authConfig.accessToken = undefined;
+ authConfig.refreshToken = undefined;
+ tokenRefreshCallback = null;
+ refreshTokenFunction = null;
+}
+
+/**
+ * Set token refresh callback for OAuth token refresh
+ */
+function setTokenRefreshCallback(
+ callback: (accessToken: string, refreshToken?: string) => Promise
+): void {
+ tokenRefreshCallback = callback;
+}
+
+/**
+ * Get current authentication header
+ */
+function getAuthHeader(): string {
+ if (authConfig.accessToken) {
+ return `Bearer ${authConfig.accessToken}`;
+ } else {
+ throw new Error("No authentication configured. Please sign in with OAuth.");
}
+}
- /**
- * Clear all authentication
- */
- static clearAuth(): void {
- authConfig.accessToken = undefined;
- authConfig.refreshToken = undefined;
- tokenRefreshCallback = null;
- refreshTokenFunction = null;
- }
+// Get current user profile
+async function getCurrentUser(): Promise {
+ const response = await makeRequest<{ status: string; data: UserProfile }>(
+ "/me",
+ {
+ headers: {
+ "cal-api-version": "2024-06-11",
+ },
+ },
+ "2024-06-11"
+ );
+ return response.data;
+}
- /**
- * Set token refresh callback for OAuth token refresh
- */
- static setTokenRefreshCallback(
- callback: (accessToken: string, refreshToken?: string) => Promise
- ): void {
- tokenRefreshCallback = callback;
- }
-
- /**
- * Get current authentication header
- */
- private static getAuthHeader(): string {
- if (authConfig.accessToken) {
- return `Bearer ${authConfig.accessToken}`;
- } else {
- throw new Error("No authentication configured. Please sign in with OAuth.");
- }
- }
-
- // Get current user profile
- static async getCurrentUser(): Promise {
- try {
- const response = await this.makeRequest<{ status: string; data: UserProfile }>(
- "/me",
- {
- headers: {
- "cal-api-version": "2024-06-11",
- },
- },
- "2024-06-11"
- );
- return response.data;
- } catch (error) {
- throw error;
- }
- }
-
- // Update current user profile
- static async updateUserProfile(updates: {
- email?: string;
- name?: string;
- timeFormat?: number;
- defaultScheduleId?: number;
- weekStart?: string;
- timeZone?: string;
- locale?: string;
- avatarUrl?: string;
- bio?: string;
- metadata?: Record;
- }): Promise {
- try {
- const response = await this.makeRequest<{ status: string; data: UserProfile }>(
- "/me",
- {
- method: "PATCH",
- headers: {
- "Content-Type": "application/json",
- "cal-api-version": "2024-06-11",
- },
- body: JSON.stringify(updates),
- },
- "2024-06-11"
- );
-
- if (response && response.data) {
- // Update cached profile
- this.userProfile = response.data;
- return response.data;
- }
-
- throw new Error("Invalid response from update user profile API");
- } catch (error) {
- console.error("updateUserProfile error");
- throw error;
- }
- }
-
- // Get and cache user profile
- static async getUserProfile(): Promise {
- if (this.userProfile) {
- return this.userProfile;
- }
-
- this.userProfile = await this.getCurrentUser();
- return this.userProfile;
- }
-
- // Get cached username or fetch if not available
- static async getUsername(): Promise {
- const profile = await this.getUserProfile();
- return profile.username;
- }
-
- // Build shareable link for event type
- static async buildEventTypeLink(eventTypeSlug: string): Promise {
- const username = await this.getUsername();
- return `https://cal.com/${username}/${eventTypeSlug}`;
- }
-
- // Clear cached profile (useful for logout)
- static clearUserProfile(): void {
- this.userProfile = null;
- }
-
- // Test function for bookings API specifically
- static async testRawBookingsAPI(): Promise {
- try {
- const url = `${API_BASE_URL}/bookings?status=upcoming&status=unconfirmed&limit=50`;
-
- const response = await fetch(url, {
+// Update current user profile
+async function updateUserProfile(updates: {
+ email?: string;
+ name?: string;
+ timeFormat?: number;
+ defaultScheduleId?: number;
+ weekStart?: string;
+ timeZone?: string;
+ locale?: string;
+ avatarUrl?: string;
+ bio?: string;
+ metadata?: Record;
+}): Promise {
+ try {
+ const response = await makeRequest<{ status: string; data: UserProfile }>(
+ "/me",
+ {
+ method: "PATCH",
headers: {
- Authorization: this.getAuthHeader(),
"Content-Type": "application/json",
- "cal-api-version": "2024-08-13",
+ "cal-api-version": "2024-06-11",
},
- });
+ body: JSON.stringify(updates),
+ },
+ "2024-06-11"
+ );
- const responseText = await response.text();
+ if (response?.data) {
+ // Update cached profile
+ _userProfile = response.data;
+ return response.data;
+ }
- try {
- const responseJson = JSON.parse(responseText);
- } catch (parseError) {}
- } catch (error) {}
+ throw new Error("Invalid response from update user profile API");
+ } catch (error) {
+ console.error("updateUserProfile error");
+ throw error;
+ }
+}
+
+// Get and cache user profile
+async function getUserProfile(): Promise {
+ if (_userProfile) {
+ return _userProfile;
}
- private static async makeRequest(
- endpoint: string,
- options: RequestInit = {},
- apiVersion: string = "2024-08-13",
- isRetry: boolean = false
- ): Promise {
- const url = `${API_BASE_URL}${endpoint}`;
+ _userProfile = await getCurrentUser();
+ return _userProfile;
+}
+
+// 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;
+}
+
+// Test function for bookings API specifically
+async function testRawBookingsAPI(): Promise {
+ try {
+ const url = `${API_BASE_URL}/bookings?status=upcoming&status=unconfirmed&limit=50`;
const response = await fetch(url, {
- ...options,
headers: {
- Authorization: this.getAuthHeader(),
+ Authorization: getAuthHeader(),
"Content-Type": "application/json",
- "cal-api-version": apiVersion,
- ...options.headers,
+ "cal-api-version": "2024-08-13",
},
});
- if (!response.ok) {
- const errorBody = await response.text();
+ const responseText = await response.text();
- // Parse error for better user messages
- let errorMessage = response.statusText;
+ try {
+ const _responseJson = JSON.parse(responseText);
+ } catch (_parseError) {}
+ } catch (_error) {}
+}
- try {
- const errorJson = JSON.parse(errorBody);
- errorMessage = errorJson?.error?.message || errorJson?.message || response.statusText;
- } catch (parseError) {
- // If JSON parsing fails, use the raw error body
- errorMessage = errorBody || response.statusText;
- }
+async function makeRequest(
+ endpoint: string,
+ options: RequestInit = {},
+ apiVersion: string = "2024-08-13",
+ isRetry: boolean = false
+): Promise {
+ const url = `${API_BASE_URL}${endpoint}`;
- // Handle specific error cases
- if (response.status === 401) {
- if (!isRetry && authConfig.refreshToken && refreshTokenFunction && tokenRefreshCallback) {
- try {
- const newTokens = await refreshTokenFunction(authConfig.refreshToken);
+ const response = await fetch(url, {
+ ...options,
+ headers: {
+ Authorization: getAuthHeader(),
+ "Content-Type": "application/json",
+ "cal-api-version": apiVersion,
+ ...options.headers,
+ },
+ });
- authConfig.accessToken = newTokens.accessToken;
- if (newTokens.refreshToken) {
- authConfig.refreshToken = newTokens.refreshToken;
- }
+ if (!response.ok) {
+ const errorBody = await response.text();
- // Notify AuthContext to update stored tokens
- await tokenRefreshCallback(newTokens.accessToken, newTokens.refreshToken);
+ // Parse error for better user messages
+ let errorMessage = response.statusText;
- // Retry the original request with the new token
- return this.makeRequest(endpoint, options, apiVersion, true);
- } catch (refreshError) {
- console.error("Token refresh failed:", refreshError);
- this.clearAuth();
- throw new Error("Authentication failed. Please sign in again.");
+ try {
+ const errorJson = JSON.parse(errorBody);
+ errorMessage = errorJson?.error?.message || errorJson?.message || response.statusText;
+ } catch (_parseError) {
+ // If JSON parsing fails, use the raw error body
+ errorMessage = errorBody || response.statusText;
+ }
+
+ // Handle specific error cases
+ if (response.status === 401) {
+ if (!isRetry && authConfig.refreshToken && refreshTokenFunction && tokenRefreshCallback) {
+ try {
+ const newTokens = await refreshTokenFunction(authConfig.refreshToken);
+
+ authConfig.accessToken = newTokens.accessToken;
+ if (newTokens.refreshToken) {
+ authConfig.refreshToken = newTokens.refreshToken;
}
- }
- if (errorMessage.includes("expired")) {
- throw new Error("Your authentication has expired. Please sign in again.");
- }
- throw new Error("Authentication failed. Please check your credentials.");
- }
+ // Notify AuthContext to update stored tokens
+ await tokenRefreshCallback(newTokens.accessToken, newTokens.refreshToken);
- // Include status code in error message for graceful error handling downstream
- throw new Error(`API Error: ${response.status} ${errorMessage}`);
- }
-
- return response.json();
- }
-
- static async deleteEventType(eventTypeId: number): Promise {
- try {
- await this.makeRequest(
- `/event-types/${eventTypeId}`,
- {
- method: "DELETE",
- },
- "2024-06-14"
- );
- } catch (error) {
- console.error("Delete API error");
- throw error;
- }
- }
-
- static async createEventType(input: CreateEventTypeInput): Promise {
- try {
- const sanitizedInput = this.sanitizePayload(input as Record);
-
- const response = await this.makeRequest<{ status: string; data: EventType }>(
- "/event-types",
- {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "cal-api-version": "2024-06-14",
- },
- body: JSON.stringify(sanitizedInput),
- },
- "2024-06-14"
- );
-
- if (response && response.data) {
- return response.data;
- }
-
- throw new Error("Invalid response from create event type API");
- } catch (error) {
- console.error("createEventType error");
- throw error;
- }
- }
-
- // Cancel a booking
- static async cancelBooking(bookingUid: string, cancellationReason?: string): Promise {
- try {
- const body: { cancellationReason?: string } = {};
- if (cancellationReason) {
- body.cancellationReason = cancellationReason;
- }
-
- await this.makeRequest(
- `/bookings/${bookingUid}/cancel`,
- {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "cal-api-version": "2024-08-13",
- },
- body: JSON.stringify(body),
- },
- "2024-08-13"
- );
- } catch (error) {
- throw error;
- }
- }
-
- static async rescheduleBooking(
- bookingUid: string,
- input: {
- start: string;
- reschedulingReason?: string;
- }
- ): Promise {
- try {
- console.log("[CalComAPIService] rescheduleBooking request:", {
- bookingUid,
- input,
- });
-
- const response = await this.makeRequest<{ status: string; data: Booking }>(
- `/bookings/${bookingUid}/reschedule`,
- {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "cal-api-version": "2024-08-13",
- },
- body: JSON.stringify(input),
- },
- "2024-08-13"
- );
-
- if (response && response.data) {
- return response.data;
- }
-
- throw new Error("Invalid response from reschedule booking API");
- } catch (error) {
- console.error("[CalComAPIService] rescheduleBooking error:", error);
- if (error instanceof Error) {
- console.error("[CalComAPIService] Error message:", error.message);
- console.error("[CalComAPIService] Error stack:", error.stack);
- }
- throw error;
- }
- }
-
- static async confirmBooking(bookingUid: string): Promise {
- try {
- const response = await this.makeRequest<{ status: string; data: Booking }>(
- `/bookings/${bookingUid}/confirm`,
- {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "cal-api-version": "2024-08-13",
- },
- },
- "2024-08-13"
- );
-
- if (response && response.data) {
- return response.data;
- }
-
- throw new Error("Invalid response from confirm booking API");
- } catch (error) {
- console.error("confirmBooking error");
- throw error;
- }
- }
-
- static async declineBooking(bookingUid: string, reason?: string): Promise {
- try {
- const body: { reason?: string } = {};
- if (reason) {
- body.reason = reason;
- }
-
- const response = await this.makeRequest<{ status: string; data: Booking }>(
- `/bookings/${bookingUid}/decline`,
- {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "cal-api-version": "2024-08-13",
- },
- body: JSON.stringify(body),
- },
- "2024-08-13"
- );
-
- if (response && response.data) {
- return response.data;
- }
-
- throw new Error("Invalid response from decline booking API");
- } catch (error) {
- console.error("declineBooking error");
- throw error;
- }
- }
-
- /**
- * Mark an attendee as absent (no-show) for a booking
- * @param bookingUid - The unique identifier of the booking
- * @param attendeeEmail - The email of the attendee to mark as absent
- * @param absent - Whether to mark as absent (true) or undo (false)
- */
- static async markAbsent(
- bookingUid: string,
- attendeeEmail: string,
- absent: boolean = true
- ): Promise {
- try {
- const body: MarkAbsentRequest = {
- attendees: [{ email: attendeeEmail, absent }],
- };
-
- const response = await this.makeRequest(
- `/bookings/${bookingUid}/mark-absent`,
- {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "cal-api-version": "2024-08-13",
- },
- body: JSON.stringify(body),
- },
- "2024-08-13"
- );
-
- if (response && response.data) {
- return response.data;
- }
-
- throw new Error("Invalid response from mark absent API");
- } catch (error) {
- console.error("markAbsent error");
- throw error;
- }
- }
-
- // ============================================================================
- // Booking Action API Methods (API v2 2024-08-13)
- // ============================================================================
-
- /**
- * Add guests to a booking
- * @param bookingUid - The unique identifier of the booking
- * @param guests - Array of guests to add (email required, name optional)
- * @returns Updated booking with new guests
- */
- static async addGuests(bookingUid: string, guests: AddGuestInput[]): Promise {
- try {
- const response = await this.makeRequest(
- `/bookings/${bookingUid}/guests`,
- {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "cal-api-version": "2024-08-13",
- },
- body: JSON.stringify({ guests }),
- },
- "2024-08-13"
- );
-
- if (response && response.data) {
- return response.data;
- }
-
- throw new Error("Invalid response from add guests API");
- } catch (error) {
- console.error("addGuests error");
- throw error;
- }
- }
-
- /**
- * Update the location of a booking (legacy - sends location as string)
- * Note: This updates the booking location but may not update the calendar event
- * @param bookingUid - The unique identifier of the booking
- * @param location - The new location string
- * @returns Updated booking with new location
- * @deprecated Use updateLocationV2 instead for proper typed location updates
- */
- static async updateLocation(bookingUid: string, location: string): Promise {
- try {
- const response = await this.makeRequest(
- `/bookings/${bookingUid}/location`,
- {
- method: "PATCH",
- headers: {
- "Content-Type": "application/json",
- "cal-api-version": "2024-08-13",
- },
- body: JSON.stringify({ location }),
- },
- "2024-08-13"
- );
-
- if (response && response.data) {
- return response.data;
- }
-
- throw new Error("Invalid response from update location API");
- } catch (error) {
- console.error("updateLocation error");
- throw error;
- }
- }
-
- /**
- * Update the location of a booking with typed location object
- * Supports: address, link, phone, attendeePhone, attendeeAddress, attendeeDefined
- * Note: Integration-based locations (Cal Video, Google Meet, Zoom) are NOT supported
- * @param bookingUid - The unique identifier of the booking
- * @param location - The location object with type and value
- * @returns Updated booking with new location
- */
- static async updateLocationV2(
- bookingUid: string,
- location: { type: string; [key: string]: string }
- ): Promise {
- try {
- const response = await this.makeRequest(
- `/bookings/${bookingUid}/location`,
- {
- method: "PATCH",
- headers: {
- "Content-Type": "application/json",
- "cal-api-version": "2024-08-13",
- },
- body: JSON.stringify({ location }),
- },
- "2024-08-13"
- );
-
- if (response && response.data) {
- return response.data;
- }
-
- throw new Error("Invalid response from update location API");
- } catch (error) {
- console.error("[CalComAPIService] updateLocationV2 error:", error);
- throw error;
- }
- }
-
- /**
- * Get recordings for a booking (Cal Video only)
- * Note: Only available for past Cal Video bookings
- * @param bookingUid - The unique identifier of the booking
- * @returns Array of recording objects with download URLs
- */
- static async getRecordings(bookingUid: string): Promise {
- try {
- const response = await this.makeRequest(
- `/bookings/${bookingUid}/recordings`,
- {
- headers: {
- "cal-api-version": "2024-08-13",
- },
- },
- "2024-08-13"
- );
-
- if (response && response.data) {
- return response.data;
- }
-
- return [];
- } catch (error) {
- // Handle 401/403 gracefully - endpoint may require auth in future
- if (error instanceof Error) {
- const statusMatch = error.message.match(/API Error: (\d+)/);
- if (statusMatch && (statusMatch[1] === "401" || statusMatch[1] === "403")) {
- console.warn(`Recordings access denied for booking ${bookingUid}`);
- return [];
- }
- if (statusMatch && statusMatch[1] === "404") {
- console.warn(`No recordings found for booking ${bookingUid}`);
- return [];
- }
- }
- console.error("getRecordings error");
- throw error;
- }
- }
-
- /**
- * Get conferencing session details for a booking (Cal Video only)
- * Note: This endpoint is PBAC-guarded and requires booking.readRecordings permission
- * @param bookingUid - The unique identifier of the booking
- * @returns Array of conferencing session objects with participant info
- */
- static async getConferencingSessions(bookingUid: string): Promise {
- try {
- const response = await this.makeRequest(
- `/bookings/${bookingUid}/conferencing-sessions`,
- {
- headers: {
- "cal-api-version": "2024-08-13",
- },
- },
- "2024-08-13"
- );
-
- if (response && response.data) {
- return response.data;
- }
-
- return [];
- } catch (error) {
- // Handle 401/403 gracefully - endpoint is PBAC-guarded
- if (error instanceof Error) {
- const statusMatch = error.message.match(/API Error: (\d+)/);
- if (statusMatch && (statusMatch[1] === "401" || statusMatch[1] === "403")) {
- console.warn(`Conferencing sessions access denied for booking ${bookingUid}`);
- return [];
- }
- if (statusMatch && statusMatch[1] === "404") {
- console.warn(`No conferencing sessions found for booking ${bookingUid}`);
- return [];
- }
- }
- console.error("getConferencingSessions error");
- throw error;
- }
- }
-
- /**
- * Get transcripts for a booking (Cal Video only)
- * Note: This endpoint may require authentication in future releases
- * @param bookingUid - The unique identifier of the booking
- * @returns Array of transcript objects with download URLs
- */
- static async getTranscripts(bookingUid: string): Promise {
- try {
- const response = await this.makeRequest(
- `/bookings/${bookingUid}/transcripts`,
- {
- headers: {
- "cal-api-version": "2024-08-13",
- },
- },
- "2024-08-13"
- );
-
- if (response && response.data) {
- return response.data;
- }
-
- return [];
- } catch (error) {
- // Handle 401/403 gracefully - endpoint may require auth in future
- if (error instanceof Error) {
- const statusMatch = error.message.match(/API Error: (\d+)/);
- if (statusMatch && (statusMatch[1] === "401" || statusMatch[1] === "403")) {
- console.warn(`Transcripts access denied for booking ${bookingUid}`);
- return [];
- }
- if (statusMatch && statusMatch[1] === "404") {
- console.warn(`No transcripts found for booking ${bookingUid}`);
- return [];
- }
- }
- console.error("getTranscripts error");
- throw error;
- }
- }
-
- static async getEventTypes(): Promise {
- try {
- // Get current user to extract username
- let username: string | undefined;
- try {
- const currentUser = await this.getCurrentUser();
- // Extract username from response
- if (currentUser?.username) {
- username = currentUser.username;
- }
- } catch (error) {}
-
- // Build query string with username and sorting
- const params = new URLSearchParams();
- if (username) {
- params.append("username", username);
- }
- // Sort by creation date descending (newer first) to match main codebase behavior
- // Main codebase uses position: "desc", id: "desc" - since API doesn't expose position,
- // we use sortCreatedAt: "desc" for similar behavior (newer event types first)
- params.append("sortCreatedAt", "desc");
-
- const queryString = params.toString();
- const endpoint = `/event-types${queryString ? `?${queryString}` : ""}`;
-
- const response = await this.makeRequest(endpoint, {}, "2024-06-14");
-
- // Handle different possible response structures
- let eventTypesArray: EventType[] = [];
-
- if (Array.isArray(response)) {
- eventTypesArray = response;
- } else if (response && typeof response === "object") {
- if (response.data && Array.isArray(response.data)) {
- eventTypesArray = response.data;
- } else if (response.eventTypes && Array.isArray(response.eventTypes)) {
- eventTypesArray = response.eventTypes;
- } else if (response.items && Array.isArray(response.items)) {
- eventTypesArray = response.items;
- } else if (response.data && typeof response.data === "object") {
- if (response.data.eventTypes && Array.isArray(response.data.eventTypes)) {
- eventTypesArray = response.data.eventTypes;
- } else if (response.data.items && Array.isArray(response.data.items)) {
- eventTypesArray = response.data.items;
- } else {
- const keys = Object.keys(response.data);
- if (keys.length > 0) {
- eventTypesArray = Object.values(response.data).filter((item): item is EventType =>
- Boolean(item && typeof item === "object" && "id" in item)
- ) as EventType[];
- }
- }
- } else {
- const possibleArrays = Object.values(response).filter((val) => Array.isArray(val));
- if (possibleArrays.length > 0) {
- eventTypesArray = possibleArrays[0] as EventType[];
- }
+ // Retry the original request with the new token
+ return makeRequest(endpoint, options, apiVersion, true);
+ } catch (refreshError) {
+ console.error("Token refresh failed:", refreshError);
+ clearAuth();
+ throw new Error("Authentication failed. Please sign in again.");
}
}
- return eventTypesArray;
- } catch (error) {
- throw error;
+ if (errorMessage.includes("expired")) {
+ throw new Error("Your authentication has expired. Please sign in again.");
+ }
+ throw new Error("Authentication failed. Please check your credentials.");
}
+
+ // Include status code in error message for graceful error handling downstream
+ throw new Error(`API Error: ${response.status} ${errorMessage}`);
}
- static async getBookingByUid(bookingUid: string): Promise {
- try {
- const response = await this.makeRequest<{ status: string; data: Booking }>(
- `/bookings/${bookingUid}`,
- {
- headers: {
- "cal-api-version": "2024-08-13",
- },
+ return response.json();
+}
+
+async function deleteEventType(eventTypeId: number): Promise {
+ try {
+ await makeRequest(
+ `/event-types/${eventTypeId}`,
+ {
+ method: "DELETE",
+ },
+ "2024-06-14"
+ );
+ } catch (error) {
+ console.error("Delete API error");
+ throw error;
+ }
+}
+
+async function createEventType(input: CreateEventTypeInput): Promise {
+ try {
+ const sanitizedInput = sanitizePayload(input as unknown as Record);
+
+ const response = await makeRequest<{ status: string; data: EventType }>(
+ "/event-types",
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "cal-api-version": "2024-06-14",
},
- "2024-08-13"
- );
- if (response && response.data) {
- return response.data;
- }
- throw new Error("Invalid response from get booking API");
- } catch (error) {
- console.error("getBookingByUid error");
- throw error;
+ body: JSON.stringify(sanitizedInput),
+ },
+ "2024-06-14"
+ );
+
+ if (response?.data) {
+ return response.data;
}
+
+ throw new Error("Invalid response from create event type API");
+ } catch (error) {
+ console.error("createEventType error");
+ throw error;
+ }
+}
+
+// Cancel a booking
+async function cancelBooking(bookingUid: string, cancellationReason?: string): Promise {
+ const body: { cancellationReason?: string } = {};
+ if (cancellationReason) {
+ body.cancellationReason = cancellationReason;
}
- static async getBookings(filters?: {
- status?: string[];
- fromDate?: string;
- toDate?: string;
- eventTypeId?: number;
- limit?: number;
- offset?: number;
- }): Promise {
- try {
- // Build query parameters
- const params = new URLSearchParams();
+ await makeRequest(
+ `/bookings/${bookingUid}/cancel`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "cal-api-version": "2024-08-13",
+ },
+ body: JSON.stringify(body),
+ },
+ "2024-08-13"
+ );
+}
- if (filters?.status?.length) {
- filters.status.forEach((status) => params.append("status", status));
- }
- if (filters?.fromDate) {
- params.append("fromDate", filters.fromDate);
- }
- if (filters?.toDate) {
- params.append("toDate", filters.toDate);
- }
- if (filters?.eventTypeId) {
- params.append("eventTypeId", filters.eventTypeId.toString());
- }
- if (filters?.limit) {
- params.append("limit", filters.limit.toString());
- }
- if (filters?.offset) {
- params.append("offset", filters.offset.toString());
- }
+async function rescheduleBooking(
+ bookingUid: string,
+ input: {
+ start: string;
+ reschedulingReason?: string;
+ }
+): Promise {
+ try {
+ console.log("[CalComAPIService] rescheduleBooking request:", {
+ bookingUid,
+ input,
+ });
- const queryString = params.toString();
- const endpoint = `/bookings${queryString ? `?${queryString}` : ""}`;
+ const response = await makeRequest<{ status: string; data: Booking }>(
+ `/bookings/${bookingUid}/reschedule`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "cal-api-version": "2024-08-13",
+ },
+ body: JSON.stringify(input),
+ },
+ "2024-08-13"
+ );
- const response = await this.makeRequest(endpoint);
+ if (response?.data) {
+ return response.data;
+ }
- // Handle different possible response structures (same logic as event types)
- let bookingsArray: Booking[] = [];
+ throw new Error("Invalid response from reschedule booking API");
+ } catch (error) {
+ console.error("[CalComAPIService] rescheduleBooking error:", error);
+ if (error instanceof Error) {
+ console.error("[CalComAPIService] Error message:", error.message);
+ console.error("[CalComAPIService] Error stack:", error.stack);
+ }
+ throw error;
+ }
+}
- if (Array.isArray(response)) {
- bookingsArray = response;
- } else if (response && typeof response === "object") {
- if (response.data && Array.isArray(response.data)) {
- bookingsArray = response.data;
- } else if (response.bookings && Array.isArray(response.bookings)) {
- bookingsArray = response.bookings;
- } else if (response.items && Array.isArray(response.items)) {
- bookingsArray = response.items;
- } else if (response.data && typeof response.data === "object") {
- if (response.data.bookings && Array.isArray(response.data.bookings)) {
- bookingsArray = response.data.bookings;
- } else if (response.data.items && Array.isArray(response.data.items)) {
- bookingsArray = response.data.items;
- } else {
- // Convert object values to array as last resort
- const keys = Object.keys(response.data);
- if (keys.length > 0) {
- bookingsArray = Object.values(response.data).filter((item): item is Booking =>
- Boolean(item && typeof item === "object" && ("id" in item || "uid" in item))
- ) as Booking[];
- }
- }
- } else {
- // Try to extract any arrays from the response
- const possibleArrays = Object.values(response).filter((val) => Array.isArray(val));
- if (possibleArrays.length > 0) {
- bookingsArray = possibleArrays[0] as Booking[];
- }
+async function confirmBooking(bookingUid: string): Promise {
+ try {
+ const response = await makeRequest<{ status: string; data: Booking }>(
+ `/bookings/${bookingUid}/confirm`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "cal-api-version": "2024-08-13",
+ },
+ },
+ "2024-08-13"
+ );
+
+ if (response?.data) {
+ return response.data;
+ }
+
+ throw new Error("Invalid response from confirm booking API");
+ } catch (error) {
+ console.error("confirmBooking error");
+ throw error;
+ }
+}
+
+async function declineBooking(bookingUid: string, reason?: string): Promise {
+ try {
+ const body: { reason?: string } = {};
+ if (reason) {
+ body.reason = reason;
+ }
+
+ const response = await makeRequest<{ status: string; data: Booking }>(
+ `/bookings/${bookingUid}/decline`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "cal-api-version": "2024-08-13",
+ },
+ body: JSON.stringify(body),
+ },
+ "2024-08-13"
+ );
+
+ if (response?.data) {
+ return response.data;
+ }
+
+ throw new Error("Invalid response from decline booking API");
+ } catch (error) {
+ console.error("declineBooking error");
+ throw error;
+ }
+}
+
+/**
+ * Mark an attendee as absent (no-show) for a booking
+ * @param bookingUid - The unique identifier of the booking
+ * @param attendeeEmail - The email of the attendee to mark as absent
+ * @param absent - Whether to mark as absent (true) or undo (false)
+ */
+async function markAbsent(
+ bookingUid: string,
+ attendeeEmail: string,
+ absent: boolean = true
+): Promise {
+ try {
+ const body: MarkAbsentRequest = {
+ attendees: [{ email: attendeeEmail, absent }],
+ };
+
+ const response = await makeRequest(
+ `/bookings/${bookingUid}/mark-absent`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "cal-api-version": "2024-08-13",
+ },
+ body: JSON.stringify(body),
+ },
+ "2024-08-13"
+ );
+
+ if (response?.data) {
+ return response.data;
+ }
+
+ throw new Error("Invalid response from mark absent API");
+ } catch (error) {
+ console.error("markAbsent error");
+ throw error;
+ }
+}
+
+// ============================================================================
+// Booking Action API Methods (API v2 2024-08-13)
+// ============================================================================
+
+/**
+ * Add guests to a booking
+ * @param bookingUid - The unique identifier of the booking
+ * @param guests - Array of guests to add (email required, name optional)
+ * @returns Updated booking with new guests
+ */
+async function addGuests(bookingUid: string, guests: AddGuestInput[]): Promise {
+ try {
+ const response = await makeRequest(
+ `/bookings/${bookingUid}/guests`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "cal-api-version": "2024-08-13",
+ },
+ body: JSON.stringify({ guests }),
+ },
+ "2024-08-13"
+ );
+
+ if (response?.data) {
+ return response.data;
+ }
+
+ throw new Error("Invalid response from add guests API");
+ } catch (error) {
+ console.error("addGuests error");
+ throw error;
+ }
+}
+
+/**
+ * Update the location of a booking (legacy - sends location as string)
+ * Note: This updates the booking location but may not update the calendar event
+ * @param bookingUid - The unique identifier of the booking
+ * @param location - The new location string
+ * @returns Updated booking with new location
+ * @deprecated Use updateLocationV2 instead for proper typed location updates
+ */
+async function updateLocation(bookingUid: string, location: string): Promise {
+ try {
+ const response = await makeRequest(
+ `/bookings/${bookingUid}/location`,
+ {
+ method: "PATCH",
+ headers: {
+ "Content-Type": "application/json",
+ "cal-api-version": "2024-08-13",
+ },
+ body: JSON.stringify({ location }),
+ },
+ "2024-08-13"
+ );
+
+ if (response?.data) {
+ return response.data;
+ }
+
+ throw new Error("Invalid response from update location API");
+ } catch (error) {
+ console.error("updateLocation error");
+ throw error;
+ }
+}
+
+/**
+ * Update the location of a booking with typed location object
+ * Supports: address, link, phone, attendeePhone, attendeeAddress, attendeeDefined
+ * Note: Integration-based locations (Cal Video, Google Meet, Zoom) are NOT supported
+ * @param bookingUid - The unique identifier of the booking
+ * @param location - The location object with type and value
+ * @returns Updated booking with new location
+ */
+async function updateLocationV2(
+ bookingUid: string,
+ location: { type: string; [key: string]: string }
+): Promise {
+ try {
+ const response = await makeRequest(
+ `/bookings/${bookingUid}/location`,
+ {
+ method: "PATCH",
+ headers: {
+ "Content-Type": "application/json",
+ "cal-api-version": "2024-08-13",
+ },
+ body: JSON.stringify({ location }),
+ },
+ "2024-08-13"
+ );
+
+ if (response?.data) {
+ return response.data;
+ }
+
+ throw new Error("Invalid response from update location API");
+ } catch (error) {
+ console.error("[CalComAPIService] updateLocationV2 error:", error);
+ throw error;
+ }
+}
+
+/**
+ * Get recordings for a booking (Cal Video only)
+ * Note: Only available for past Cal Video bookings
+ * @param bookingUid - The unique identifier of the booking
+ * @returns Array of recording objects with download URLs
+ */
+async function getRecordings(bookingUid: string): Promise {
+ try {
+ const response = await makeRequest(
+ `/bookings/${bookingUid}/recordings`,
+ {
+ headers: {
+ "cal-api-version": "2024-08-13",
+ },
+ },
+ "2024-08-13"
+ );
+
+ if (response?.data) {
+ return response.data;
+ }
+
+ return [];
+ } catch (error) {
+ // Handle 401/403 gracefully - endpoint may require auth in future
+ if (error instanceof Error) {
+ const statusMatch = error.message.match(/API Error: (\d+)/);
+ if (statusMatch && (statusMatch[1] === "401" || statusMatch[1] === "403")) {
+ console.warn(`Recordings access denied for booking ${bookingUid}`);
+ return [];
+ }
+ if (statusMatch && statusMatch[1] === "404") {
+ console.warn(`No recordings found for booking ${bookingUid}`);
+ return [];
+ }
+ }
+ console.error("getRecordings error");
+ throw error;
+ }
+}
+
+/**
+ * Get conferencing session details for a booking (Cal Video only)
+ * Note: This endpoint is PBAC-guarded and requires booking.readRecordings permission
+ * @param bookingUid - The unique identifier of the booking
+ * @returns Array of conferencing session objects with participant info
+ */
+async function getConferencingSessions(bookingUid: string): Promise {
+ try {
+ const response = await makeRequest(
+ `/bookings/${bookingUid}/conferencing-sessions`,
+ {
+ headers: {
+ "cal-api-version": "2024-08-13",
+ },
+ },
+ "2024-08-13"
+ );
+
+ if (response?.data) {
+ return response.data;
+ }
+
+ return [];
+ } catch (error) {
+ // Handle 401/403 gracefully - endpoint is PBAC-guarded
+ if (error instanceof Error) {
+ const statusMatch = error.message.match(/API Error: (\d+)/);
+ if (statusMatch && (statusMatch[1] === "401" || statusMatch[1] === "403")) {
+ console.warn(`Conferencing sessions access denied for booking ${bookingUid}`);
+ return [];
+ }
+ if (statusMatch && statusMatch[1] === "404") {
+ console.warn(`No conferencing sessions found for booking ${bookingUid}`);
+ return [];
+ }
+ }
+ console.error("getConferencingSessions error");
+ throw error;
+ }
+}
+
+/**
+ * Get transcripts for a booking (Cal Video only)
+ * Note: This endpoint may require authentication in future releases
+ * @param bookingUid - The unique identifier of the booking
+ * @returns Array of transcript objects with download URLs
+ */
+async function getTranscripts(bookingUid: string): Promise {
+ try {
+ const response = await makeRequest(
+ `/bookings/${bookingUid}/transcripts`,
+ {
+ headers: {
+ "cal-api-version": "2024-08-13",
+ },
+ },
+ "2024-08-13"
+ );
+
+ if (response?.data) {
+ return response.data;
+ }
+
+ return [];
+ } catch (error) {
+ // Handle 401/403 gracefully - endpoint may require auth in future
+ if (error instanceof Error) {
+ const statusMatch = error.message.match(/API Error: (\d+)/);
+ if (statusMatch && (statusMatch[1] === "401" || statusMatch[1] === "403")) {
+ console.warn(`Transcripts access denied for booking ${bookingUid}`);
+ return [];
+ }
+ if (statusMatch && statusMatch[1] === "404") {
+ console.warn(`No transcripts found for booking ${bookingUid}`);
+ return [];
+ }
+ }
+ console.error("getTranscripts error");
+ throw error;
+ }
+}
+
+async function getEventTypes(): Promise {
+ // Get current user to extract username
+ let username: string | undefined;
+ try {
+ const currentUser = await getCurrentUser();
+ // Extract username from response
+ if (currentUser?.username) {
+ username = currentUser.username;
+ }
+ } catch (_error) {}
+
+ // Build query string with username and sorting
+ const params = new URLSearchParams();
+ if (username) {
+ params.append("username", username);
+ }
+ // Sort by creation date descending (newer first) to match main codebase behavior
+ // Main codebase uses position: "desc", id: "desc" - since API doesn't expose position,
+ // we use sortCreatedAt: "desc" for similar behavior (newer event types first)
+ params.append("sortCreatedAt", "desc");
+
+ const queryString = params.toString();
+ const endpoint = `/event-types${queryString ? `?${queryString}` : ""}`;
+
+ const response = await makeRequest(endpoint, {}, "2024-06-14");
+
+ // Handle different possible response structures
+ let eventTypesArray: EventType[] = [];
+
+ if (Array.isArray(response)) {
+ eventTypesArray = response as EventType[];
+ } else if (response && typeof response === "object") {
+ const resp = response as Record;
+ if (resp.data && Array.isArray(resp.data)) {
+ eventTypesArray = resp.data as EventType[];
+ } else if (resp.eventTypes && Array.isArray(resp.eventTypes)) {
+ eventTypesArray = resp.eventTypes as EventType[];
+ } else if (resp.items && Array.isArray(resp.items)) {
+ eventTypesArray = resp.items as EventType[];
+ } else if (resp.data && typeof resp.data === "object") {
+ const dataObj = resp.data as Record;
+ if (dataObj.eventTypes && Array.isArray(dataObj.eventTypes)) {
+ eventTypesArray = dataObj.eventTypes as EventType[];
+ } else if (dataObj.items && Array.isArray(dataObj.items)) {
+ eventTypesArray = dataObj.items as EventType[];
+ } else {
+ const keys = Object.keys(dataObj);
+ if (keys.length > 0) {
+ eventTypesArray = Object.values(dataObj).filter((item): item is EventType =>
+ Boolean(item && typeof item === "object" && "id" in item)
+ );
}
}
-
- // Get current user to filter bookings
- let currentUser;
- try {
- currentUser = await this.getCurrentUser();
- } catch (error) {
- return bookingsArray;
+ } else {
+ const possibleArrays = Object.values(resp).filter((val) => Array.isArray(val));
+ if (possibleArrays.length > 0) {
+ eventTypesArray = possibleArrays[0] as EventType[];
}
-
- // Extract user info from response
- let userId: number | undefined;
- let userEmail: string | undefined;
-
- if (currentUser) {
- userId = currentUser.id;
- userEmail = currentUser.email;
- }
-
- // Filter bookings to only show ones where the current user is participating
- const userBookings = bookingsArray.filter((booking) => {
- const { isOrganizer, isHost, isAttendee, isParticipating } = getBookingParticipation(
- booking,
- userId,
- userEmail
- );
-
- return isParticipating;
- });
-
- return userBookings;
- } catch (error) {
- throw error;
}
}
- // Get all schedules
- static async getSchedules(): Promise {
- try {
- const response = await this.makeRequest<{ status: string; data: Schedule[] }>(
- "/schedules",
- {
- headers: {
- "cal-api-version": "2024-06-11", // Override version for schedules
- },
+ return eventTypesArray;
+}
+
+async function getBookingByUid(bookingUid: string): Promise {
+ try {
+ const response = await makeRequest<{ status: string; data: Booking }>(
+ `/bookings/${bookingUid}`,
+ {
+ headers: {
+ "cal-api-version": "2024-08-13",
},
- "2024-06-11"
- );
+ },
+ "2024-08-13"
+ );
+ if (response?.data) {
+ return response.data;
+ }
+ throw new Error("Invalid response from get booking API");
+ } catch (error) {
+ console.error("getBookingByUid error");
+ throw error;
+ }
+}
- let schedulesArray: Schedule[] = [];
+async function getBookings(filters?: {
+ status?: string[];
+ fromDate?: string;
+ toDate?: string;
+ eventTypeId?: number;
+ limit?: number;
+ offset?: number;
+}): Promise {
+ // Build query parameters
+ const params = new URLSearchParams();
- // Handle response structure: { status: "success", data: [...] }
- if (
- response &&
- response.status === "success" &&
- response.data &&
- Array.isArray(response.data)
- ) {
- schedulesArray = response.data;
- } else if (Array.isArray(response)) {
- // Fallback: response might be array directly
- schedulesArray = response;
- } else if (response && response.data && Array.isArray(response.data)) {
- // Fallback: response.data might be array
- schedulesArray = response.data;
- }
-
- return schedulesArray;
- } catch (error) {
- throw error;
+ if (filters?.status?.length) {
+ for (const status of filters.status) {
+ params.append("status", status);
}
}
-
- // Create a new schedule
- static async createSchedule(input: {
- name: string;
- timeZone: string;
- isDefault?: boolean;
- availability?: Array<{
- days: string[]; // e.g., ["Monday", "Tuesday"]
- startTime: string; // e.g., "09:00"
- endTime: string; // e.g., "17:00"
- }>;
- overrides?: Array<{
- date: string; // e.g., "2024-05-20"
- startTime: string; // e.g., "12:00"
- endTime: string; // e.g., "14:00"
- }>;
- }): Promise {
- try {
- const sanitizedInput = this.sanitizePayload(input as Record);
-
- const response = await this.makeRequest<{ status: string; data: Schedule }>(
- "/schedules",
- {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "cal-api-version": "2024-06-11",
- },
- body: JSON.stringify(sanitizedInput),
- },
- "2024-06-11"
- );
-
- if (response && response.data) {
- return response.data;
- }
-
- throw new Error("Invalid response from create schedule API");
- } catch (error) {
- console.error("createSchedule error");
- throw error;
- }
+ if (filters?.fromDate) {
+ params.append("fromDate", filters.fromDate);
+ }
+ if (filters?.toDate) {
+ params.append("toDate", filters.toDate);
+ }
+ if (filters?.eventTypeId) {
+ params.append("eventTypeId", filters.eventTypeId.toString());
+ }
+ if (filters?.limit) {
+ params.append("limit", filters.limit.toString());
+ }
+ if (filters?.offset) {
+ params.append("offset", filters.offset.toString());
}
- static async getScheduleById(scheduleId: number): Promise {
- try {
- const response = await this.makeRequest(
- `/schedules/${scheduleId}`,
- {
- headers: {
- "cal-api-version": "2024-06-11",
- },
- },
- "2024-06-11"
- );
+ const queryString = params.toString();
+ const endpoint = `/bookings${queryString ? `?${queryString}` : ""}`;
- if (response && response.data) {
- return response.data;
- }
+ const response = await makeRequest(endpoint);
- if (response && response.id) {
- return response;
- }
+ // Handle different possible response structures (same logic as event types)
+ let bookingsArray: Booking[] = [];
- return null;
- } catch (error) {
- console.error("getScheduleById error");
- throw error;
- }
- }
-
- // Get conferencing options
- static async getConferencingOptions(): Promise {
- try {
- const response = await this.makeRequest<{ status: string; data: ConferencingOption[] }>(
- "/conferencing"
- );
-
- if (response && response.data && Array.isArray(response.data)) {
- return response.data;
- }
-
- return [];
- } catch (error) {
- console.error("getConferencingOptions error");
- throw error;
- }
- }
-
- // Get a single event type by ID
- static async getEventTypeById(eventTypeId: number): Promise {
- try {
- const response = await this.makeRequest<{ status: string; data: EventType }>(
- `/event-types/${eventTypeId}`,
- {},
- "2024-06-14"
- );
-
- if (response && response.data) {
- return response.data;
- }
-
- return null;
- } catch (error) {
- // Handle 404 errors gracefully - resource doesn't exist or user doesn't have access
- if (error instanceof Error) {
- // Check if error message contains 404 status code
- const statusMatch = error.message.match(/API Error: (\d+)/);
- if (statusMatch && statusMatch[1] === "404") {
- console.warn(`Event type ${eventTypeId} not found`);
- return null;
+ if (Array.isArray(response)) {
+ bookingsArray = response as Booking[];
+ } else if (response && typeof response === "object") {
+ const resp = response as Record;
+ if (resp.data && Array.isArray(resp.data)) {
+ bookingsArray = resp.data as Booking[];
+ } else if (resp.bookings && Array.isArray(resp.bookings)) {
+ bookingsArray = resp.bookings as Booking[];
+ } else if (resp.items && Array.isArray(resp.items)) {
+ bookingsArray = resp.items as Booking[];
+ } else if (resp.data && typeof resp.data === "object") {
+ const dataObj = resp.data as Record;
+ if (dataObj.bookings && Array.isArray(dataObj.bookings)) {
+ bookingsArray = dataObj.bookings as Booking[];
+ } else if (dataObj.items && Array.isArray(dataObj.items)) {
+ bookingsArray = dataObj.items as Booking[];
+ } else {
+ // Convert object values to array as last resort
+ const keys = Object.keys(dataObj);
+ if (keys.length > 0) {
+ bookingsArray = Object.values(dataObj).filter((item): item is Booking =>
+ Boolean(item && typeof item === "object" && ("id" in item || "uid" in item))
+ );
}
}
- console.error("getEventTypeById error");
- throw error;
+ } else {
+ // Try to extract any arrays from the response
+ const possibleArrays = Object.values(resp).filter((val) => Array.isArray(val));
+ if (possibleArrays.length > 0) {
+ bookingsArray = possibleArrays[0] as Booking[];
+ }
}
}
- // Update an event type
- /**
- * Sanitizes a payload before sending to the API.
- * - Removes keys with null values for array fields (API expects arrays or field to be omitted)
- * - Removes keys with undefined values
- * - Recursively sanitizes nested objects
- */
- private static sanitizePayload(payload: Record): Record {
- const sanitized: Record = {};
+ // Get current user to filter bookings
+ let currentUser;
+ try {
+ currentUser = await getCurrentUser();
+ } catch (_error) {
+ return bookingsArray;
+ }
- // Fields that should NEVER be sent as null - API expects array or omit entirely
- const arrayFields = [
- "lengthInMinutesOptions",
- "multipleDuration",
- "locations",
- "bookingFields",
- "hosts",
- "children",
- "customInputs",
- ];
+ // Extract user info from response
+ let userId: number | undefined;
+ let userEmail: string | undefined;
- // Fields that can be null (to clear the value)
- const nullableFields = [
- "description",
- "successRedirectUrl",
- "slotInterval",
- "eventName",
- "timeZone",
- ];
+ if (currentUser) {
+ userId = currentUser.id;
+ userEmail = currentUser.email;
+ }
- for (const [key, value] of Object.entries(payload)) {
- // Skip undefined values
- if (value === undefined) continue;
+ // Filter bookings to only show ones where the current user is participating
+ const userBookings = bookingsArray.filter((booking) => {
+ const {
+ isOrganizer: _isOrganizer,
+ isHost: _isHost,
+ isAttendee: _isAttendee,
+ isParticipating,
+ } = getBookingParticipation(booking, userId, userEmail);
- // Handle null values
- if (value === null) {
- // For array fields, skip entirely (don't send null)
- if (arrayFields.includes(key)) {
- console.warn(`Skipping null value for array field: ${key}`);
- continue;
- }
- // For nullable fields, allow null
- if (nullableFields.includes(key)) {
- sanitized[key] = null;
- continue;
- }
- // For other fields, skip null to be safe
- console.warn(`Skipping null value for field: ${key}`);
+ return isParticipating;
+ });
+
+ return userBookings;
+}
+
+// Get all schedules
+async function getSchedules(): Promise {
+ const response = await makeRequest<{ status: string; data: Schedule[] }>(
+ "/schedules",
+ {
+ headers: {
+ "cal-api-version": "2024-06-11", // Override version for schedules
+ },
+ },
+ "2024-06-11"
+ );
+
+ let schedulesArray: Schedule[] = [];
+
+ // Handle response structure: { status: "success", data: [...] }
+ if (response && response.status === "success" && response.data && Array.isArray(response.data)) {
+ schedulesArray = response.data;
+ } else if (Array.isArray(response)) {
+ // Fallback: response might be array directly
+ schedulesArray = response;
+ } else if (response?.data && Array.isArray(response.data)) {
+ // Fallback: response.data might be array
+ schedulesArray = response.data;
+ }
+
+ return schedulesArray;
+}
+
+// Create a new schedule
+async function createSchedule(input: {
+ name: string;
+ timeZone: string;
+ isDefault?: boolean;
+ availability?: Array<{
+ days: string[]; // e.g., ["Monday", "Tuesday"]
+ startTime: string; // e.g., "09:00"
+ endTime: string; // e.g., "17:00"
+ }>;
+ overrides?: Array<{
+ date: string; // e.g., "2024-05-20"
+ startTime: string; // e.g., "12:00"
+ endTime: string; // e.g., "14:00"
+ }>;
+}): Promise {
+ try {
+ const sanitizedInput = sanitizePayload(input as Record);
+
+ const response = await makeRequest<{ status: string; data: Schedule }>(
+ "/schedules",
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "cal-api-version": "2024-06-11",
+ },
+ body: JSON.stringify(sanitizedInput),
+ },
+ "2024-06-11"
+ );
+
+ if (response?.data) {
+ return response.data;
+ }
+
+ throw new Error("Invalid response from create schedule API");
+ } catch (error) {
+ console.error("createSchedule error");
+ throw error;
+ }
+}
+
+async function getScheduleById(scheduleId: number): Promise {
+ try {
+ const response = await makeRequest(
+ `/schedules/${scheduleId}`,
+ {
+ headers: {
+ "cal-api-version": "2024-06-11",
+ },
+ },
+ "2024-06-11"
+ );
+
+ if (response && typeof response === "object") {
+ const resp = response as Record;
+ if (resp.data && typeof resp.data === "object") {
+ return resp.data as Schedule;
+ }
+ if (resp.id) {
+ return response as Schedule;
+ }
+ }
+
+ return null;
+ } catch (error) {
+ console.error("getScheduleById error");
+ throw error;
+ }
+}
+
+// Get conferencing options
+async function getConferencingOptions(): Promise {
+ try {
+ const response = await makeRequest<{
+ status: string;
+ data: ConferencingOption[];
+ }>("/conferencing");
+
+ if (response?.data && Array.isArray(response.data)) {
+ return response.data;
+ }
+
+ return [];
+ } catch (error) {
+ console.error("getConferencingOptions error");
+ throw error;
+ }
+}
+
+// Get a single event type by ID
+async function getEventTypeById(eventTypeId: number): Promise {
+ try {
+ const response = await makeRequest<{ status: string; data: EventType }>(
+ `/event-types/${eventTypeId}`,
+ {},
+ "2024-06-14"
+ );
+
+ if (response?.data) {
+ return response.data;
+ }
+
+ return null;
+ } catch (error) {
+ // Handle 404 errors gracefully - resource doesn't exist or user doesn't have access
+ if (error instanceof Error) {
+ // Check if error message contains 404 status code
+ const statusMatch = error.message.match(/API Error: (\d+)/);
+ if (statusMatch && statusMatch[1] === "404") {
+ console.warn(`Event type ${eventTypeId} not found`);
+ return null;
+ }
+ }
+ console.error("getEventTypeById error");
+ throw error;
+ }
+}
+
+// Update an event type
+/**
+ * Sanitizes a payload before sending to the API.
+ * - Removes keys with null values for array fields (API expects arrays or field to be omitted)
+ * - Removes keys with undefined values
+ * - Recursively sanitizes nested objects
+ */
+function sanitizePayload(payload: Record): Record {
+ const sanitized: Record = {};
+
+ // Fields that should NEVER be sent as null - API expects array or omit entirely
+ const arrayFields = [
+ "lengthInMinutesOptions",
+ "multipleDuration",
+ "locations",
+ "bookingFields",
+ "hosts",
+ "children",
+ "customInputs",
+ ];
+
+ // Fields that can be null (to clear the value)
+ const nullableFields = [
+ "description",
+ "successRedirectUrl",
+ "slotInterval",
+ "eventName",
+ "timeZone",
+ ];
+
+ for (const [key, value] of Object.entries(payload)) {
+ // Skip undefined values
+ if (value === undefined) continue;
+
+ // Handle null values
+ if (value === null) {
+ // For array fields, skip entirely (don't send null)
+ if (arrayFields.includes(key)) {
+ console.warn(`Skipping null value for array field: ${key}`);
continue;
}
-
- // Recursively sanitize nested objects (but not arrays)
- if (typeof value === "object" && !Array.isArray(value)) {
- const sanitizedNested = this.sanitizePayload(value);
- // Only include if the nested object has values
- if (Object.keys(sanitizedNested).length > 0) {
- sanitized[key] = sanitizedNested;
- }
- } else {
- sanitized[key] = value;
+ // For nullable fields, allow null
+ if (nullableFields.includes(key)) {
+ sanitized[key] = null;
+ continue;
}
+ // For other fields, skip null to be safe
+ console.warn(`Skipping null value for field: ${key}`);
+ continue;
}
- return sanitized;
+ // Recursively sanitize nested objects (but not arrays)
+ if (typeof value === "object" && !Array.isArray(value)) {
+ const sanitizedNested = sanitizePayload(value as Record);
+ // Only include if the nested object has values
+ if (Object.keys(sanitizedNested).length > 0) {
+ sanitized[key] = sanitizedNested;
+ }
+ } else {
+ sanitized[key] = value;
+ }
}
- static async updateEventType(
- eventTypeId: number,
- updates: Partial
- ): Promise {
- try {
- const sanitizedUpdates = this.sanitizePayload(updates as Record);
+ return sanitized;
+}
- const response = await this.makeRequest<{ status: string; data: EventType }>(
- `/event-types/${eventTypeId}`,
- {
- method: "PATCH",
- headers: {
- "Content-Type": "application/json",
- "cal-api-version": "2024-06-14",
- },
- body: JSON.stringify(sanitizedUpdates),
+async function updateEventType(
+ eventTypeId: number,
+ updates: Partial
+): Promise {
+ try {
+ const sanitizedUpdates = sanitizePayload(updates as Record);
+
+ const response = await makeRequest<{ status: string; data: EventType }>(
+ `/event-types/${eventTypeId}`,
+ {
+ method: "PATCH",
+ headers: {
+ "Content-Type": "application/json",
+ "cal-api-version": "2024-06-14",
},
- "2024-06-14"
- );
+ body: JSON.stringify(sanitizedUpdates),
+ },
+ "2024-06-14"
+ );
- if (response && response.data) {
- return response.data;
- }
-
- throw new Error("Invalid response from update event type API");
- } catch (error) {
- console.error("updateEventType error");
- throw error;
+ if (response?.data) {
+ return response.data;
}
+
+ throw new Error("Invalid response from update event type API");
+ } catch (error) {
+ console.error("updateEventType error");
+ throw error;
}
+}
- // Update a schedule
- static async updateSchedule(
- scheduleId: number,
- updates: {
- isDefault?: boolean;
- name?: string;
- timeZone?: string;
- availability?: Array<{
- days: string[]; // Day names like "Monday", "Tuesday"
- startTime: string; // Format: "09:00"
- endTime: string; // Format: "10:00"
- }>;
- overrides?: Array<{
- date: string; // Format: "2024-05-20"
- startTime: string; // Format: "12:00"
- endTime: string; // Format: "14:00"
- }>;
- }
- ): Promise {
- try {
- // Sanitize the updates to remove null values
- const sanitizedUpdates = this.sanitizePayload(updates as Record);
- const response = await this.makeRequest<{ status: string; data: Schedule }>(
- `/schedules/${scheduleId}`,
- {
- method: "PATCH",
- headers: {
- "Content-Type": "application/json",
- "cal-api-version": "2024-06-11",
- },
- body: JSON.stringify(sanitizedUpdates),
+// Update a schedule
+async function updateSchedule(
+ scheduleId: number,
+ updates: {
+ isDefault?: boolean;
+ name?: string;
+ timeZone?: string;
+ availability?: Array<{
+ days: string[]; // Day names like "Monday", "Tuesday"
+ startTime: string; // Format: "09:00"
+ endTime: string; // Format: "10:00"
+ }>;
+ overrides?: Array<{
+ date: string; // Format: "2024-05-20"
+ startTime: string; // Format: "12:00"
+ endTime: string; // Format: "14:00"
+ }>;
+ }
+): Promise {
+ try {
+ // Sanitize the updates to remove null values
+ const sanitizedUpdates = sanitizePayload(updates as Record);
+ const response = await makeRequest<{ status: string; data: Schedule }>(
+ `/schedules/${scheduleId}`,
+ {
+ method: "PATCH",
+ headers: {
+ "Content-Type": "application/json",
+ "cal-api-version": "2024-06-11",
},
- "2024-06-11"
- );
+ body: JSON.stringify(sanitizedUpdates),
+ },
+ "2024-06-11"
+ );
- if (response && response.data) {
- return response.data;
- }
-
- throw new Error("Invalid response from update schedule API");
- } catch (error) {
- console.error("updateSchedule error");
- throw error;
+ if (response?.data) {
+ return response.data;
}
- }
- // Duplicate a schedule
- static async duplicateSchedule(scheduleId: number): Promise {
- try {
- const response = await this.makeRequest<{ status: string; data: Schedule }>(
- `/atoms/schedules/${scheduleId}/duplicate`,
- {
- method: "POST",
- headers: {
- "cal-api-version": "2024-06-11",
- },
+ throw new Error("Invalid response from update schedule API");
+ } catch (error) {
+ console.error("updateSchedule error");
+ throw error;
+ }
+}
+
+// Duplicate a schedule
+async function duplicateSchedule(scheduleId: number): Promise {
+ try {
+ const response = await makeRequest<{ status: string; data: Schedule }>(
+ `/atoms/schedules/${scheduleId}/duplicate`,
+ {
+ method: "POST",
+ headers: {
+ "cal-api-version": "2024-06-11",
},
- "2024-06-11"
- );
+ },
+ "2024-06-11"
+ );
- if (response && response.data) {
- return response.data;
- }
-
- throw new Error("Invalid response from duplicate schedule API");
- } catch (error) {
- console.error("duplicateSchedule error");
- throw error;
+ if (response?.data) {
+ return response.data;
}
- }
- // Delete a schedule
- static async deleteSchedule(scheduleId: number): Promise {
- try {
- await this.makeRequest<{ status: string }>(
- `/schedules/${scheduleId}`,
- {
- method: "DELETE",
- headers: {
- "cal-api-version": "2024-06-11",
- },
+ throw new Error("Invalid response from duplicate schedule API");
+ } catch (error) {
+ console.error("duplicateSchedule error");
+ throw error;
+ }
+}
+
+// Delete a schedule
+async function deleteSchedule(scheduleId: number): Promise {
+ try {
+ await makeRequest<{ status: string }>(
+ `/schedules/${scheduleId}`,
+ {
+ method: "DELETE",
+ headers: {
+ "cal-api-version": "2024-06-11",
},
- "2024-06-11"
- );
- } catch (error) {
- console.error("deleteSchedule error");
- throw error;
- }
+ },
+ "2024-06-11"
+ );
+ } catch (error) {
+ console.error("deleteSchedule error");
+ throw error;
}
+}
- // ============================================
- // WEBHOOKS
- // ============================================
+// ============================================
+// WEBHOOKS
+// ============================================
- // Get all global webhooks
- static async getWebhooks(): Promise {
- try {
- const response = await this.makeRequest<{ status: string; data: Webhook[] }>("/webhooks");
+// Get all global webhooks
+async function getWebhooks(): Promise {
+ try {
+ const response = await makeRequest<{ status: string; data: Webhook[] }>("/webhooks");
- if (response && response.data && Array.isArray(response.data)) {
- return response.data;
+ if (response?.data && Array.isArray(response.data)) {
+ return response.data;
+ }
+
+ return [];
+ } catch (error) {
+ console.error("getWebhooks error");
+ throw error;
+ }
+}
+
+// Create a global webhook
+async function createWebhook(input: CreateWebhookInput): Promise {
+ try {
+ const sanitizedInput = sanitizePayload(input as unknown as Record);
+ const response = await makeRequest<{ status: string; data: Webhook }>("/webhooks", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(sanitizedInput),
+ });
+
+ if (response?.data) {
+ return response.data;
+ }
+
+ throw new Error("Invalid response from create webhook API");
+ } catch (error) {
+ console.error("createWebhook error");
+ throw error;
+ }
+}
+
+// Update a global webhook
+async function updateWebhook(webhookId: string, updates: UpdateWebhookInput): Promise {
+ try {
+ const sanitizedUpdates = sanitizePayload(updates as Record);
+ const response = await makeRequest<{ status: string; data: Webhook }>(
+ `/webhooks/${webhookId}`,
+ {
+ method: "PATCH",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(sanitizedUpdates),
}
+ );
- return [];
- } catch (error) {
- console.error("getWebhooks error");
- throw error;
+ if (response?.data) {
+ return response.data;
}
- }
- // Create a global webhook
- static async createWebhook(input: CreateWebhookInput): Promise {
- try {
- const sanitizedInput = this.sanitizePayload(input as Record);
- const response = await this.makeRequest<{ status: string; data: Webhook }>("/webhooks", {
+ throw new Error("Invalid response from update webhook API");
+ } catch (error) {
+ console.error("updateWebhook error");
+ throw error;
+ }
+}
+
+// Delete a global webhook
+async function deleteWebhook(webhookId: string): Promise {
+ try {
+ await makeRequest(`/webhooks/${webhookId}`, {
+ method: "DELETE",
+ });
+ } catch (error) {
+ console.error("deleteWebhook error");
+ throw error;
+ }
+}
+
+// Get webhooks for a specific event type
+async function getEventTypeWebhooks(eventTypeId: number): Promise {
+ try {
+ const response = await makeRequest<{ status: string; data: Webhook[] }>(
+ `/event-types/${eventTypeId}/webhooks`
+ );
+
+ if (response?.data && Array.isArray(response.data)) {
+ return response.data;
+ }
+
+ return [];
+ } catch (error) {
+ console.error("getEventTypeWebhooks error");
+ throw error;
+ }
+}
+
+// Create a webhook for a specific event type
+async function createEventTypeWebhook(
+ eventTypeId: number,
+ input: CreateWebhookInput
+): Promise {
+ try {
+ const sanitizedInput = sanitizePayload(input as unknown as Record);
+ const response = await makeRequest<{ status: string; data: Webhook }>(
+ `/event-types/${eventTypeId}/webhooks`,
+ {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(sanitizedInput),
- });
-
- if (response && response.data) {
- return response.data;
}
+ );
- throw new Error("Invalid response from create webhook API");
- } catch (error) {
- console.error("createWebhook error");
- throw error;
+ if (response?.data) {
+ return response.data;
}
- }
- // Update a global webhook
- static async updateWebhook(webhookId: string, updates: UpdateWebhookInput): Promise {
- try {
- const sanitizedUpdates = this.sanitizePayload(updates as Record);
- const response = await this.makeRequest<{ status: string; data: Webhook }>(
- `/webhooks/${webhookId}`,
- {
- method: "PATCH",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(sanitizedUpdates),
- }
- );
-
- if (response && response.data) {
- return response.data;
- }
-
- throw new Error("Invalid response from update webhook API");
- } catch (error) {
- console.error("updateWebhook error");
- throw error;
- }
- }
-
- // Delete a global webhook
- static async deleteWebhook(webhookId: string): Promise {
- try {
- await this.makeRequest(`/webhooks/${webhookId}`, {
- method: "DELETE",
- });
- } catch (error) {
- console.error("deleteWebhook error");
- throw error;
- }
- }
-
- // Get webhooks for a specific event type
- static async getEventTypeWebhooks(eventTypeId: number): Promise {
- try {
- const response = await this.makeRequest<{ status: string; data: Webhook[] }>(
- `/event-types/${eventTypeId}/webhooks`
- );
-
- if (response && response.data && Array.isArray(response.data)) {
- return response.data;
- }
-
- return [];
- } catch (error) {
- console.error("getEventTypeWebhooks error");
- throw error;
- }
- }
-
- // Create a webhook for a specific event type
- static async createEventTypeWebhook(
- eventTypeId: number,
- input: CreateWebhookInput
- ): Promise {
- try {
- const sanitizedInput = this.sanitizePayload(input as Record);
- const response = await this.makeRequest<{ status: string; data: Webhook }>(
- `/event-types/${eventTypeId}/webhooks`,
- {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(sanitizedInput),
- }
- );
-
- if (response && response.data) {
- return response.data;
- }
-
- throw new Error("Invalid response from create event type webhook API");
- } catch (error) {
- console.error("createEventTypeWebhook error");
- throw error;
- }
- }
-
- // Update an event type webhook
- static async updateEventTypeWebhook(
- eventTypeId: number,
- webhookId: string,
- updates: UpdateWebhookInput
- ): Promise {
- try {
- const sanitizedUpdates = this.sanitizePayload(updates as Record);
- const response = await this.makeRequest<{ status: string; data: Webhook }>(
- `/event-types/${eventTypeId}/webhooks/${webhookId}`,
- {
- method: "PATCH",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(sanitizedUpdates),
- }
- );
-
- if (response && response.data) {
- return response.data;
- }
-
- throw new Error("Invalid response from update event type webhook API");
- } catch (error) {
- console.error("updateEventTypeWebhook error");
- throw error;
- }
- }
-
- // Delete an event type webhook
- static async deleteEventTypeWebhook(eventTypeId: number, webhookId: string): Promise {
- try {
- await this.makeRequest(`/event-types/${eventTypeId}/webhooks/${webhookId}`, {
- method: "DELETE",
- });
- } catch (error) {
- console.error("deleteEventTypeWebhook error");
- throw error;
- }
- }
-
- // ============================================
- // PRIVATE LINKS
- // ============================================
-
- // Get all private links for an event type
- static async getEventTypePrivateLinks(eventTypeId: number): Promise {
- try {
- const response = await this.makeRequest<{ status: string; data: PrivateLink[] }>(
- `/event-types/${eventTypeId}/private-links`
- );
-
- if (response && response.data && Array.isArray(response.data)) {
- return response.data;
- }
-
- return [];
- } catch (error) {
- console.error("getEventTypePrivateLinks error");
- throw error;
- }
- }
-
- // Create a private link for an event type
- static async createEventTypePrivateLink(
- eventTypeId: number,
- input: CreatePrivateLinkInput = {}
- ): Promise {
- try {
- const sanitizedInput = this.sanitizePayload(input as Record);
- const response = await this.makeRequest<{ status: string; data: PrivateLink }>(
- `/event-types/${eventTypeId}/private-links`,
- {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(sanitizedInput),
- }
- );
-
- if (response && response.data) {
- return response.data;
- }
-
- throw new Error("Invalid response from create private link API");
- } catch (error) {
- console.error("createEventTypePrivateLink error");
- throw error;
- }
- }
-
- // Update a private link
- static async updateEventTypePrivateLink(
- eventTypeId: number,
- linkId: number,
- updates: UpdatePrivateLinkInput
- ): Promise {
- try {
- const sanitizedUpdates = this.sanitizePayload(updates as Record);
- const response = await this.makeRequest<{ status: string; data: PrivateLink }>(
- `/event-types/${eventTypeId}/private-links/${linkId}`,
- {
- method: "PATCH",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(sanitizedUpdates),
- }
- );
-
- if (response && response.data) {
- return response.data;
- }
-
- throw new Error("Invalid response from update private link API");
- } catch (error) {
- console.error("updateEventTypePrivateLink error");
- throw error;
- }
- }
-
- // Delete a private link
- static async deleteEventTypePrivateLink(eventTypeId: number, linkId: number): Promise {
- try {
- await this.makeRequest(`/event-types/${eventTypeId}/private-links/${linkId}`, {
- method: "DELETE",
- });
- } catch (error) {
- console.error("deleteEventTypePrivateLink error");
- throw error;
- }
+ throw new Error("Invalid response from create event type webhook API");
+ } catch (error) {
+ console.error("createEventTypeWebhook error");
+ throw error;
}
}
+
+// Update an event type webhook
+async function updateEventTypeWebhook(
+ eventTypeId: number,
+ webhookId: string,
+ updates: UpdateWebhookInput
+): Promise {
+ try {
+ const sanitizedUpdates = sanitizePayload(updates as Record);
+ const response = await makeRequest<{ status: string; data: Webhook }>(
+ `/event-types/${eventTypeId}/webhooks/${webhookId}`,
+ {
+ method: "PATCH",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(sanitizedUpdates),
+ }
+ );
+
+ if (response?.data) {
+ return response.data;
+ }
+
+ throw new Error("Invalid response from update event type webhook API");
+ } catch (error) {
+ console.error("updateEventTypeWebhook error");
+ throw error;
+ }
+}
+
+// Delete an event type webhook
+async function deleteEventTypeWebhook(eventTypeId: number, webhookId: string): Promise {
+ try {
+ await makeRequest(`/event-types/${eventTypeId}/webhooks/${webhookId}`, {
+ method: "DELETE",
+ });
+ } catch (error) {
+ console.error("deleteEventTypeWebhook error");
+ throw error;
+ }
+}
+
+// ============================================
+// PRIVATE LINKS
+// ============================================
+
+// Get all private links for an event type
+async function getEventTypePrivateLinks(eventTypeId: number): Promise {
+ try {
+ const response = await makeRequest<{ status: string; data: PrivateLink[] }>(
+ `/event-types/${eventTypeId}/private-links`
+ );
+
+ if (response?.data && Array.isArray(response.data)) {
+ return response.data;
+ }
+
+ return [];
+ } catch (error) {
+ console.error("getEventTypePrivateLinks error");
+ throw error;
+ }
+}
+
+// Create a private link for an event type
+async function createEventTypePrivateLink(
+ eventTypeId: number,
+ input: CreatePrivateLinkInput = {}
+): Promise {
+ try {
+ const sanitizedInput = sanitizePayload(input as Record);
+ const response = await makeRequest<{ status: string; data: PrivateLink }>(
+ `/event-types/${eventTypeId}/private-links`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(sanitizedInput),
+ }
+ );
+
+ if (response?.data) {
+ return response.data;
+ }
+
+ throw new Error("Invalid response from create private link API");
+ } catch (error) {
+ console.error("createEventTypePrivateLink error");
+ throw error;
+ }
+}
+
+// Update a private link
+async function updateEventTypePrivateLink(
+ eventTypeId: number,
+ linkId: number,
+ updates: UpdatePrivateLinkInput
+): Promise {
+ try {
+ const sanitizedUpdates = sanitizePayload(updates as Record);
+ const response = await makeRequest<{ status: string; data: PrivateLink }>(
+ `/event-types/${eventTypeId}/private-links/${linkId}`,
+ {
+ method: "PATCH",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(sanitizedUpdates),
+ }
+ );
+
+ if (response?.data) {
+ return response.data;
+ }
+
+ throw new Error("Invalid response from update private link API");
+ } catch (error) {
+ console.error("updateEventTypePrivateLink error");
+ throw error;
+ }
+}
+
+// Delete a private link
+async function deleteEventTypePrivateLink(eventTypeId: number, linkId: number): Promise {
+ try {
+ await makeRequest(`/event-types/${eventTypeId}/private-links/${linkId}`, {
+ method: "DELETE",
+ });
+ } catch (error) {
+ console.error("deleteEventTypePrivateLink error");
+ throw error;
+ }
+}
+
+// Export as object to satisfy noStaticOnlyClass rule
+export const CalComAPIService = {
+ setAccessToken,
+ setRefreshTokenFunction,
+ clearAuth,
+ setTokenRefreshCallback,
+ getCurrentUser,
+ updateUserProfile,
+ getUserProfile,
+ getUsername,
+ buildEventTypeLink,
+ clearUserProfile,
+ testRawBookingsAPI,
+ deleteEventType,
+ createEventType,
+ cancelBooking,
+ rescheduleBooking,
+ confirmBooking,
+ declineBooking,
+ markAbsent,
+ addGuests,
+ updateLocation,
+ updateLocationV2,
+ getRecordings,
+ getConferencingSessions,
+ getTranscripts,
+ getEventTypes,
+ getBookingByUid,
+ getBookings,
+ getSchedules,
+ createSchedule,
+ getScheduleById,
+ getConferencingOptions,
+ getEventTypeById,
+ updateEventType,
+ updateSchedule,
+ duplicateSchedule,
+ deleteSchedule,
+ getWebhooks,
+ createWebhook,
+ updateWebhook,
+ deleteWebhook,
+ getEventTypeWebhooks,
+ createEventTypeWebhook,
+ updateEventTypeWebhook,
+ deleteEventTypeWebhook,
+ getEventTypePrivateLinks,
+ createEventTypePrivateLink,
+ updateEventTypePrivateLink,
+ deleteEventTypePrivateLink,
+};
diff --git a/companion/services/oauthService.ts b/companion/services/oauthService.ts
index df19ff8bc4..4dfa029a2f 100644
--- a/companion/services/oauthService.ts
+++ b/companion/services/oauthService.ts
@@ -89,7 +89,6 @@ export interface OAuthConfig {
export class CalComOAuthService {
private config: OAuthConfig;
private codeVerifier: string | null = null;
- private state: string | null = null;
constructor(config: OAuthConfig) {
this.config = config;
@@ -101,7 +100,6 @@ export class CalComOAuthService {
const state = this.generateRandomBase64Url();
this.codeVerifier = codeVerifier;
- this.state = state;
return { codeVerifier, codeChallenge, state };
}
@@ -427,7 +425,6 @@ export class CalComOAuthService {
clearPKCEParams(): void {
this.codeVerifier = null;
- this.state = null;
}
async syncTokensToExtension(tokens: OAuthTokens): Promise {
@@ -618,10 +615,6 @@ function getBrowserSpecificOAuthConfig(): { clientId: string; redirectUri: strin
clientId: process.env.EXPO_PUBLIC_CALCOM_OAUTH_CLIENT_ID_EDGE || defaultClientId,
redirectUri: process.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI_EDGE || defaultRedirectUri,
};
-
- case "chrome":
- case "brave":
- case "unknown":
default:
// Chrome, Brave, and unknown browsers use the default configuration
return { clientId: defaultClientId, redirectUri: defaultRedirectUri };
diff --git a/companion/services/types/event-types.types.ts b/companion/services/types/event-types.types.ts
index c0e38eef0d..e92e847b93 100644
--- a/companion/services/types/event-types.types.ts
+++ b/companion/services/types/event-types.types.ts
@@ -225,6 +225,18 @@ export interface EventType {
disableCancelling?: boolean;
minimumRescheduleNotice?: number;
allowReschedulingPastBookings?: boolean;
+
+ // Additional properties from API responses
+ hideCalendarEventDetails?: boolean;
+ hideOrganizerEmail?: boolean;
+ allowReschedulingCancelledBookings?: boolean;
+ customReplyToEmail?: string;
+ color?: {
+ lightThemeHex?: string;
+ darkThemeHex?: string;
+ };
+ sendCalVideoTranscription?: boolean;
+ autoTranslate?: boolean;
}
export interface CreateEventTypeInput {
diff --git a/companion/services/types/index.ts b/companion/services/types/index.ts
index 29169f1296..dad7218772 100644
--- a/companion/services/types/index.ts
+++ b/companion/services/types/index.ts
@@ -1,7 +1,8 @@
// Re-export all types for easy importing
-export * from "./event-types.types";
+
export * from "./bookings.types";
+export * from "./event-types.types";
+export * from "./private-links.types";
export * from "./schedules.types";
export * from "./users.types";
export * from "./webhooks.types";
-export * from "./private-links.types";
diff --git a/companion/services/webAuth.ts b/companion/services/webAuth.ts
index a99f7bc58b..a38bd9fd6e 100644
--- a/companion/services/webAuth.ts
+++ b/companion/services/webAuth.ts
@@ -9,186 +9,194 @@ export interface WebSessionInfo {
userInfo?: UserProfile;
}
-export class WebAuthService {
- // Check if running on web
- static isWeb(): boolean {
- return Platform.OS === "web";
+// Check if running on web
+function isWeb(): boolean {
+ return Platform.OS === "web";
+}
+
+// Get cookies from the browser
+function getCookies(): { [key: string]: string } {
+ if (!isWeb()) return {};
+
+ const cookies: { [key: string]: string } = {};
+ if (typeof document !== "undefined") {
+ document.cookie.split(";").forEach((cookie) => {
+ const [name, value] = cookie.trim().split("=");
+ if (name && value) {
+ cookies[name] = decodeURIComponent(value);
+ }
+ });
+ }
+ return cookies;
+}
+
+// Check for Cal.com session cookies
+async function checkCalComSession(): Promise {
+ if (!isWeb()) {
+ return { isLoggedIn: false };
}
- // Get cookies from the browser
- static getCookies(): { [key: string]: string } {
- if (!this.isWeb()) return {};
+ try {
+ // Check for common Cal.com session cookies
+ const cookies = getCookies();
- const cookies: { [key: string]: string } = {};
- if (typeof document !== "undefined") {
- document.cookie.split(";").forEach((cookie) => {
- const [name, value] = cookie.trim().split("=");
- if (name && value) {
- cookies[name] = decodeURIComponent(value);
- }
- });
- }
- return cookies;
- }
+ // Look for Cal.com authentication cookies
+ // Common patterns: next-auth.session-token, __Secure-next-auth.session-token, etc.
+ const sessionCookieNames = [
+ "next-auth.session-token",
+ "__Secure-next-auth.session-token",
+ "__Host-next-auth.session-token",
+ "next-auth.csrf-token",
+ "cal-session",
+ "session-token",
+ ];
- // Check for Cal.com session cookies
- static async checkCalComSession(): Promise {
- if (!this.isWeb()) {
- return { isLoggedIn: false };
+ const foundSessionCookie = sessionCookieNames.find((name) => cookies[name]);
+
+ if (foundSessionCookie) {
+ // Try to validate the session with Cal.com API
+ const sessionInfo = await validateWebSession();
+ return sessionInfo;
}
- try {
- // Check for common Cal.com session cookies
- const cookies = this.getCookies();
-
- // Look for Cal.com authentication cookies
- // Common patterns: next-auth.session-token, __Secure-next-auth.session-token, etc.
- const sessionCookieNames = [
- "next-auth.session-token",
- "__Secure-next-auth.session-token",
- "__Host-next-auth.session-token",
- "next-auth.csrf-token",
- "cal-session",
- "session-token",
- ];
-
- const foundSessionCookie = sessionCookieNames.find((name) => cookies[name]);
-
- if (foundSessionCookie) {
- // Try to validate the session with Cal.com API
- const sessionInfo = await this.validateWebSession();
- return sessionInfo;
- }
-
- return { isLoggedIn: false };
- } catch (error) {
- return { isLoggedIn: false };
- }
- }
-
- // Validate web session by calling Cal.com API with cookies
- static async validateWebSession(): Promise {
- if (!this.isWeb()) {
- return { isLoggedIn: false };
- }
-
- try {
- // First try to call the NextAuth session endpoint
- const sessionResponse = await fetch("https://app.cal.com/api/auth/session", {
- method: "GET",
- credentials: "include", // Include cookies
- headers: {
- "Content-Type": "application/json",
- },
- });
-
- if (sessionResponse.ok) {
- const sessionData = await sessionResponse.json();
-
- if (sessionData && sessionData.user) {
- // User is logged in via web session
- return {
- isLoggedIn: true,
- userInfo: sessionData.user,
- };
- }
- }
-
- // Try the internal Cal.com me endpoint (this might work with cookies)
- const meResponse = await fetch("https://app.cal.com/api/me", {
- method: "GET",
- credentials: "include",
- headers: {
- "Content-Type": "application/json",
- },
- });
-
- if (meResponse.ok) {
- const userData = await meResponse.json();
-
- if (userData && (userData.id || userData.user || userData.data)) {
- return {
- isLoggedIn: true,
- userInfo: userData.user || userData.data || userData,
- };
- }
- }
-
- // Try to check if user is logged in by attempting to access a protected page
- const dashboardResponse = await fetch("https://app.cal.com/api/trpc/viewer.me", {
- method: "GET",
- credentials: "include",
- headers: {
- "Content-Type": "application/json",
- },
- });
-
- if (dashboardResponse.ok) {
- const dashboardData = await dashboardResponse.json();
-
- if (dashboardData && dashboardData.result && dashboardData.result.data) {
- return {
- isLoggedIn: true,
- userInfo: dashboardData.result.data,
- };
- }
- }
-
- return { isLoggedIn: false };
- } catch (error) {
- return { isLoggedIn: false };
- }
- }
-
- // Try to get API tokens from web session
- static async getTokensFromWebSession(): Promise<{ accessToken?: string; refreshToken?: string }> {
- if (!this.isWeb()) {
- return {};
- }
-
- try {
- // Some Cal.com implementations might expose tokens via specific endpoints
- // This is a fallback approach - in practice, web sessions might not expose tokens directly
-
- // Check localStorage/sessionStorage for any stored tokens
- if (typeof window !== "undefined") {
- const localStorage = window.localStorage;
- const sessionStorage = window.sessionStorage;
-
- const possibleTokenKeys = [
- "cal-access-token",
- "cal-auth-token",
- "next-auth.token",
- "auth-token",
- "access-token",
- ];
-
- for (const key of possibleTokenKeys) {
- const localToken = localStorage.getItem(key);
- const sessionToken = sessionStorage.getItem(key);
-
- if (localToken || sessionToken) {
- return {
- accessToken: localToken || sessionToken || undefined,
- };
- }
- }
- }
-
- return {};
- } catch (error) {
- return {};
- }
- }
-
- // Redirect to Cal.com web login instead of using WebView
- static redirectToWebLogin(): void {
- if (!this.isWeb()) return;
-
- // For web, redirect directly to Cal.com login
- const currentUrl = window.location.href;
- const loginUrl = `https://app.cal.com/auth/signin?callbackUrl=${encodeURIComponent(currentUrl)}`;
-
- window.location.href = loginUrl;
+ return { isLoggedIn: false };
+ } catch (_error) {
+ return { isLoggedIn: false };
}
}
+
+// Validate web session by calling Cal.com API with cookies
+async function validateWebSession(): Promise {
+ if (!isWeb()) {
+ return { isLoggedIn: false };
+ }
+
+ try {
+ // First try to call the NextAuth session endpoint
+ const sessionResponse = await fetch("https://app.cal.com/api/auth/session", {
+ method: "GET",
+ credentials: "include", // Include cookies
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (sessionResponse.ok) {
+ const sessionData = await sessionResponse.json();
+
+ if (sessionData?.user) {
+ // User is logged in via web session
+ return {
+ isLoggedIn: true,
+ userInfo: sessionData.user,
+ };
+ }
+ }
+
+ // Try the internal Cal.com me endpoint (this might work with cookies)
+ const meResponse = await fetch("https://app.cal.com/api/me", {
+ method: "GET",
+ credentials: "include",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (meResponse.ok) {
+ const userData = await meResponse.json();
+
+ if (userData && (userData.id || userData.user || userData.data)) {
+ return {
+ isLoggedIn: true,
+ userInfo: userData.user || userData.data || userData,
+ };
+ }
+ }
+
+ // Try to check if user is logged in by attempting to access a protected page
+ const dashboardResponse = await fetch("https://app.cal.com/api/trpc/viewer.me", {
+ method: "GET",
+ credentials: "include",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (dashboardResponse.ok) {
+ const dashboardData = await dashboardResponse.json();
+
+ if (dashboardData?.result?.data) {
+ return {
+ isLoggedIn: true,
+ userInfo: dashboardData.result.data,
+ };
+ }
+ }
+
+ return { isLoggedIn: false };
+ } catch (_error) {
+ return { isLoggedIn: false };
+ }
+}
+
+// Try to get API tokens from web session
+async function getTokensFromWebSession(): Promise<{ accessToken?: string; refreshToken?: string }> {
+ if (!isWeb()) {
+ return {};
+ }
+
+ try {
+ // Some Cal.com implementations might expose tokens via specific endpoints
+ // This is a fallback approach - in practice, web sessions might not expose tokens directly
+
+ // Check localStorage/sessionStorage for any stored tokens
+ if (typeof window !== "undefined") {
+ const localStorage = window.localStorage;
+ const sessionStorage = window.sessionStorage;
+
+ const possibleTokenKeys = [
+ "cal-access-token",
+ "cal-auth-token",
+ "next-auth.token",
+ "auth-token",
+ "access-token",
+ ];
+
+ for (const key of possibleTokenKeys) {
+ const localToken = localStorage.getItem(key);
+ const sessionToken = sessionStorage.getItem(key);
+
+ if (localToken || sessionToken) {
+ return {
+ accessToken: localToken || sessionToken || undefined,
+ };
+ }
+ }
+ }
+
+ return {};
+ } catch (_error) {
+ return {};
+ }
+}
+
+// Redirect to Cal.com web login instead of using WebView
+function redirectToWebLogin(): void {
+ if (!isWeb()) return;
+
+ // For web, redirect directly to Cal.com login
+ const currentUrl = window.location.href;
+ const loginUrl = `https://app.cal.com/auth/signin?callbackUrl=${encodeURIComponent(currentUrl)}`;
+
+ window.location.href = loginUrl;
+}
+
+// Export as object to satisfy noStaticOnlyClass rule
+export const WebAuthService = {
+ isWeb,
+ getCookies,
+ checkCalComSession,
+ validateWebSession,
+ getTokensFromWebSession,
+ redirectToWebLogin,
+};
diff --git a/companion/types/index.ts b/companion/types/index.ts
index a34ce07c1a..a91fe02076 100644
--- a/companion/types/index.ts
+++ b/companion/types/index.ts
@@ -3,4 +3,3 @@
*/
export * from "./locations";
-
diff --git a/companion/utils/booking-actions.ts b/companion/utils/booking-actions.ts
index 15de1a100b..a5eb99ff8d 100644
--- a/companion/utils/booking-actions.ts
+++ b/companion/utils/booking-actions.ts
@@ -130,7 +130,7 @@ export function normalizeBooking(booking: Booking): NormalizedBooking {
startTime: new Date(startTimeStr),
endTime: new Date(endTimeStr),
location: booking.location,
- isRecorded: (booking as any).isRecorded,
+ isRecorded: (booking as { isRecorded?: boolean }).isRecorded,
rescheduled: booking.rescheduled,
fromReschedule: booking.fromReschedule,
recurringEventId: booking.recurringEventId,
@@ -138,7 +138,9 @@ export function normalizeBooking(booking: Booking): NormalizedBooking {
user: booking.user,
hosts: booking.hosts,
attendees: booking.attendees,
- seatsReferences: (booking as any).seatsReferences,
+ seatsReferences: (
+ booking as { seatsReferences?: Array<{ referenceUid: string; attendee: { email: string } }> }
+ ).seatsReferences,
payment: booking.payment,
};
}
diff --git a/companion/utils/bookings-utils.ts b/companion/utils/bookings-utils.ts
index e7f2555475..5b2bc24a96 100644
--- a/companion/utils/bookings-utils.ts
+++ b/companion/utils/bookings-utils.ts
@@ -47,7 +47,7 @@ export const formatTime = (dateString: string): string => {
}
try {
const date = new Date(dateString);
- if (isNaN(date.getTime())) {
+ if (Number.isNaN(date.getTime())) {
console.warn("Invalid date string:", dateString);
return "";
}
@@ -74,7 +74,7 @@ export const formatDate = (dateString: string, isUpcoming: boolean): string => {
}
try {
const date = new Date(dateString);
- if (isNaN(date.getTime())) {
+ if (Number.isNaN(date.getTime())) {
console.warn("Invalid date string:", dateString);
return "";
}
@@ -114,7 +114,7 @@ export const formatMonthYear = (dateString: string): string => {
if (!dateString) return "";
try {
const date = new Date(dateString);
- if (isNaN(date.getTime())) {
+ if (Number.isNaN(date.getTime())) {
return "";
}
const now = new Date();
@@ -129,7 +129,7 @@ export const formatMonthYear = (dateString: string): string => {
month: "long",
year: "numeric",
});
- } catch (error) {
+ } catch (_error) {
return "";
}
};
@@ -143,11 +143,11 @@ export const getMonthYearKey = (dateString: string): string => {
if (!dateString) return "";
try {
const date = new Date(dateString);
- if (isNaN(date.getTime())) {
+ if (Number.isNaN(date.getTime())) {
return "";
}
return `${date.getFullYear()}-${date.getMonth()}`;
- } catch (error) {
+ } catch (_error) {
return "";
}
};
@@ -211,23 +211,29 @@ export const searchBookings = (bookings: Booking[], searchQuery: string): Bookin
// Search in booking title
booking.title?.toLowerCase().includes(searchLower) ||
// Search in booking description
- booking.description?.toLowerCase().includes(searchLower) ||
+ booking.description
+ ?.toLowerCase()
+ .includes(searchLower) ||
// Search in event type title
- booking.eventType?.title?.toLowerCase().includes(searchLower) ||
+ booking.eventType?.title
+ ?.toLowerCase()
+ .includes(searchLower) ||
// Search in attendee names
- (booking.attendees &&
- booking.attendees.some((attendee) => attendee.name?.toLowerCase().includes(searchLower))) ||
+ booking.attendees?.some((attendee) => attendee.name?.toLowerCase().includes(searchLower)) ||
// Search in attendee emails
- (booking.attendees &&
- booking.attendees.some((attendee) =>
- attendee.email?.toLowerCase().includes(searchLower)
- )) ||
+ booking.attendees?.some((attendee) => attendee.email?.toLowerCase().includes(searchLower)) ||
// Search in location
- booking.location?.toLowerCase().includes(searchLower) ||
+ booking.location
+ ?.toLowerCase()
+ .includes(searchLower) ||
// Search in user name
- booking.user?.name?.toLowerCase().includes(searchLower) ||
+ booking.user?.name
+ ?.toLowerCase()
+ .includes(searchLower) ||
// Search in user email
- booking.user?.email?.toLowerCase().includes(searchLower)
+ booking.user?.email
+ ?.toLowerCase()
+ .includes(searchLower)
);
};
diff --git a/companion/utils/browser.ts b/companion/utils/browser.ts
index 80a99a5bf6..eecfddff97 100644
--- a/companion/utils/browser.ts
+++ b/companion/utils/browser.ts
@@ -4,8 +4,9 @@
* Centralized utility for opening links in the in-app browser.
* Configured for session sharing with Safari/Chrome to maintain login state.
*/
-import { showErrorAlert } from "./alerts";
+
import * as WebBrowser from "expo-web-browser";
+import { showErrorAlert } from "./alerts";
/**
* Configuration options for in-app browser
diff --git a/companion/utils/eventTypeParsers.ts b/companion/utils/eventTypeParsers.ts
index e83882b5d7..ff7cf86d03 100644
--- a/companion/utils/eventTypeParsers.ts
+++ b/companion/utils/eventTypeParsers.ts
@@ -10,7 +10,7 @@
*/
export const parseBufferTime = (buffer: string): number => {
const match = buffer.match(/(\d+)/);
- return match ? parseInt(match[1]) : 0;
+ return match ? parseInt(match[1], 10) : 0;
};
/**
@@ -20,7 +20,7 @@ export const parseBufferTime = (buffer: string): number => {
* @returns Total minutes
*/
export const parseMinimumNotice = (value: string, unit: string): number => {
- const val = parseInt(value) || 0;
+ const val = parseInt(value, 10) || 0;
if (unit === "Hours") return val * 60;
if (unit === "Days") return val * 24 * 60;
return val; // Minutes
@@ -47,5 +47,5 @@ export const parseFrequencyUnit = (unit: string): string | null => {
*/
export const parseSlotInterval = (interval: string): number => {
const match = interval.match(/(\d+)/);
- return match ? parseInt(match[1]) : 0;
+ return match ? parseInt(match[1], 10) : 0;
};
diff --git a/companion/utils/formatters.ts b/companion/utils/formatters.ts
index 020a787cb2..29c97bfb0b 100644
--- a/companion/utils/formatters.ts
+++ b/companion/utils/formatters.ts
@@ -14,7 +14,7 @@
* formatDuration(90) // "1h 30m"
*/
export const formatDuration = (minutes: number | string | undefined): string => {
- const mins = typeof minutes === "string" ? parseInt(minutes) || 0 : minutes || 0;
+ const mins = typeof minutes === "string" ? parseInt(minutes, 10) || 0 : minutes || 0;
if (mins <= 0) return "0m";
if (mins < 60) return `${mins}m`;
const hours = Math.floor(mins / 60);
diff --git a/companion/utils/getEventDuration.ts b/companion/utils/getEventDuration.ts
index 8ea961a230..e67955232c 100644
--- a/companion/utils/getEventDuration.ts
+++ b/companion/utils/getEventDuration.ts
@@ -1,4 +1,4 @@
-import { EventType } from "../hooks";
+import type { EventType } from "../hooks";
export const getEventDuration = (eventType: EventType): number => {
// Prefer lengthInMinutes (API field), fallback to length for backwards compatibility
diff --git a/companion/utils/gmailGoogleChipParser.ts b/companion/utils/gmailGoogleChipParser.ts
index 7d550cbfce..c829cfdd7b 100644
--- a/companion/utils/gmailGoogleChipParser.ts
+++ b/companion/utils/gmailGoogleChipParser.ts
@@ -54,7 +54,7 @@ export function parseGoogleChip(chipElement: HTMLElement): ParsedGoogleChip | nu
tzMatch = paramsAttr.match(/\\"([^\\]+)\\"\]/);
}
- if (tzMatch && tzMatch[1]) {
+ if (tzMatch?.[1]) {
timezone = tzMatch[1]; // e.g., "Asia/Kolkata"
}
}
diff --git a/companion/utils/index.ts b/companion/utils/index.ts
index 8ab29c45ce..6f0d34caf8 100644
--- a/companion/utils/index.ts
+++ b/companion/utils/index.ts
@@ -10,76 +10,64 @@
* ```
*/
-// Formatting utilities
-export { formatDuration, truncateTitle, formatAppIdToDisplayName } from "./formatters";
-
-// Storage utilities
-export {
- secureStorage,
- generalStorage,
- isChromeStorageAvailable,
- type StorageAdapter,
-} from "./storage";
-
-// Query persistence utilities
-export { createQueryPersister, clearQueryCache, getCacheMetadata } from "./queryPersister";
-
// Alert utilities
export { showErrorAlert } from "./alerts";
-
// Browser utilities
export { openInAppBrowser } from "./browser";
-
-// Network utilities
-export { isOnline } from "./network";
-
-// Slug utilities
-export { slugify } from "./slugify";
-
-// App icon utilities
-export { getAppIconUrl } from "./getAppIconUrl";
-
-// Avatar utilities
-export { getAvatarUrl } from "./getAvatarUrl";
-
// Default location utilities
export {
+ type DefaultLocation,
+ DefaultLocationType,
defaultLocations,
getDefaultLocationIconUrl,
isDefaultLocation,
- DefaultLocationType,
- type DefaultLocation,
} from "./defaultLocations";
-
-// Location helper utilities
-export {
- formatAppIdToDisplayName as formatAppId, // Alias for backward compatibility
- generateLocationId,
- mapApiLocationToItem,
- mapItemToApiLocation,
- getLocationDisplayName,
- getLocationIconUrl,
- createLocationItemFromOption,
- locationRequiresInput,
- getLocationInputType,
- getLocationInputPlaceholder,
- getLocationInputLabel,
- validateLocationItem,
- buildLocationOptions,
- displayNameToLocationValue,
-} from "./locationHelpers";
-
// Event type parser utilities
export {
parseBufferTime,
- parseMinimumNotice,
parseFrequencyUnit,
+ parseMinimumNotice,
parseSlotInterval,
} from "./eventTypeParsers";
-
+// Formatting utilities
+export { formatAppIdToDisplayName, formatDuration, truncateTitle } from "./formatters";
+// App icon utilities
+export { getAppIconUrl } from "./getAppIconUrl";
+// Avatar utilities
+export { getAvatarUrl } from "./getAvatarUrl";
// Gmail Google Chip parser (for extension)
export {
- parseGoogleChip,
type GoogleTimeSlot,
type ParsedGoogleChip,
+ parseGoogleChip,
} from "./gmailGoogleChipParser";
+// Location helper utilities
+export {
+ buildLocationOptions,
+ createLocationItemFromOption,
+ displayNameToLocationValue,
+ formatAppIdToDisplayName as formatAppId, // Alias for backward compatibility
+ generateLocationId,
+ getLocationDisplayName,
+ getLocationIconUrl,
+ getLocationInputLabel,
+ getLocationInputPlaceholder,
+ getLocationInputType,
+ locationRequiresInput,
+ mapApiLocationToItem,
+ mapItemToApiLocation,
+ validateLocationItem,
+} from "./locationHelpers";
+// Network utilities
+export { isOnline } from "./network";
+// Query persistence utilities
+export { clearQueryCache, createQueryPersister, getCacheMetadata } from "./queryPersister";
+// Slug utilities
+export { slugify } from "./slugify";
+// Storage utilities
+export {
+ generalStorage,
+ isChromeStorageAvailable,
+ type StorageAdapter,
+ secureStorage,
+} from "./storage";
diff --git a/companion/utils/locationHelpers.ts b/companion/utils/locationHelpers.ts
index 0f9779bee6..537ae3a096 100644
--- a/companion/utils/locationHelpers.ts
+++ b/companion/utils/locationHelpers.ts
@@ -3,20 +3,16 @@
* Provides utilities for converting between API format and UI format
*/
-import { getAppIconUrl } from "./getAppIconUrl";
-import {
- defaultLocations,
- getDefaultLocationIconUrl,
- DefaultLocationType,
-} from "./defaultLocations";
-import { formatAppIdToDisplayName } from "./formatters";
-import {
- LocationItem,
+import type {
ApiLocation,
ApiLocationInput,
+ LocationItem,
LocationOption,
LocationOptionGroup,
} from "../types/locations";
+import { DefaultLocationType, defaultLocations } from "./defaultLocations";
+import { formatAppIdToDisplayName } from "./formatters";
+import { getAppIconUrl } from "./getAppIconUrl";
// Re-export formatAppIdToDisplayName for backward compatibility
export { formatAppIdToDisplayName } from "./formatters";
diff --git a/companion/utils/network.ts b/companion/utils/network.ts
index 5ed8194357..3ef97b0820 100644
--- a/companion/utils/network.ts
+++ b/companion/utils/network.ts
@@ -4,8 +4,8 @@
* Helper functions for network-aware operations.
*/
-import { Alert } from "react-native";
import NetInfo from "@react-native-community/netinfo";
+import { Alert } from "react-native";
/**
* Check if the device is currently online
diff --git a/companion/utils/queryPersister.ts b/companion/utils/queryPersister.ts
index bfdb47b237..ce5b77fcab 100644
--- a/companion/utils/queryPersister.ts
+++ b/companion/utils/queryPersister.ts
@@ -4,7 +4,7 @@
* Uses the shared storage adapter from utils/storage.ts for cross-platform support.
*/
-import type { Persister, PersistedClient } from "@tanstack/react-query-persist-client";
+import type { PersistedClient, Persister } from "@tanstack/react-query-persist-client";
import { CACHE_CONFIG } from "../config/cache.config";
import { generalStorage } from "./storage";
@@ -51,7 +51,7 @@ export const createQueryPersister = (): Persister => {
const client = JSON.parse(serialized) as PersistedClient;
// Validate timestamp exists and is a valid number
- if (typeof client.timestamp !== "number" || isNaN(client.timestamp)) {
+ if (typeof client.timestamp !== "number" || Number.isNaN(client.timestamp)) {
console.warn("[QueryPersister] Invalid or missing timestamp, discarding cache");
await storage.removeItem(storageKey);
return undefined;
@@ -122,7 +122,7 @@ export const getCacheMetadata = async (): Promise<{
const client = JSON.parse(serialized) as PersistedClient;
// Validate timestamp exists and is a valid number
- if (typeof client.timestamp !== "number" || isNaN(client.timestamp)) {
+ if (typeof client.timestamp !== "number" || Number.isNaN(client.timestamp)) {
return { exists: true, isExpired: true }; // Treat invalid timestamp as expired
}
diff --git a/companion/utils/shadows.ts b/companion/utils/shadows.ts
index c116b8a0a6..06c549d6da 100644
--- a/companion/utils/shadows.ts
+++ b/companion/utils/shadows.ts
@@ -1,4 +1,4 @@
-import { Platform, ViewStyle } from "react-native";
+import { Platform, type ViewStyle } from "react-native";
interface ShadowConfig {
color?: string;
@@ -20,7 +20,7 @@ export function createShadow(config: ShadowConfig = {}): ViewStyle {
} = config;
if (Platform.OS === "web") {
- const alpha = Math.round(opacity * 255)
+ const _alpha = Math.round(opacity * 255)
.toString(16)
.padStart(2, "0");
return {
diff --git a/companion/utils/storage.ts b/companion/utils/storage.ts
index ef4f297be7..f15e1ee676 100644
--- a/companion/utils/storage.ts
+++ b/companion/utils/storage.ts
@@ -12,9 +12,9 @@
* This is the single source of truth for storage operations across the app.
*/
-import { Platform } from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import * as SecureStore from "expo-secure-store";
+import { Platform } from "react-native";
/**
* Check if chrome.storage is available (browser extension context)
@@ -111,7 +111,9 @@ export const secureStorage = {
});
}
if (Platform.OS === "web") {
- keys.forEach((key) => localStorage.removeItem(key));
+ for (const key of keys) {
+ localStorage.removeItem(key);
+ }
return;
}
await Promise.all(keys.map((key) => SecureStore.deleteItemAsync(key)));
diff --git a/companion/wxt.config.ts b/companion/wxt.config.ts
index 3459780282..a79b13b472 100644
--- a/companion/wxt.config.ts
+++ b/companion/wxt.config.ts
@@ -33,7 +33,6 @@ function getOAuthConfig() {
clientId: process.env.EXPO_PUBLIC_CALCOM_OAUTH_CLIENT_ID_EDGE || defaultClientId,
redirectUri: process.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI_EDGE || defaultRedirectUri,
};
- case "chrome":
default:
return {
clientId: defaultClientId,
@@ -92,7 +91,7 @@ export default defineConfig({
vite: () => {
// Determine companion URL based on build type
const devUrl = isBuildForStore ? "" : process.env.EXPO_PUBLIC_COMPANION_DEV_URL || "";
- const isLocalDev = Boolean(devUrl && devUrl.includes("localhost"));
+ const isLocalDev = Boolean(devUrl?.includes("localhost"));
// Get OAuth config for the target browser
const oauthConfig = getOAuthConfig();