Skip to content
3 changes: 1 addition & 2 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
cd companion
npx lint-staged
bunx lint-staged
14 changes: 6 additions & 8 deletions apps/extension/entrypoints/background/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// <reference types="chrome" />

import { getCalApiUrl, getCalMarketingAppUrl } from "../../lib/region";
import type { Booking } from "../../types/bookings.types";
import type { OAuthTokens } from "../../types/oauth";

Expand Down Expand Up @@ -325,11 +326,12 @@ function isRestrictedUrl(url: string | undefined): boolean {
return restrictedPatterns.some((pattern) => pattern.test(url));
}

// Open cal.com/app (Framer marketing page) in a new tab with auto-open parameter
// Open the Cal.com Framer marketing landing in a new tab with auto-open parameter.
// The marketing site is intentionally cross-region — see getCalMarketingAppUrl().
function openAppPage(): void {
const tabsAPI = getTabsAPI();
if (tabsAPI) {
tabsAPI.create({ url: "https://cal.com/app?openExtension=true" });
tabsAPI.create({ url: getCalMarketingAppUrl() });
}
}

Expand Down Expand Up @@ -895,13 +897,9 @@ async function getStoredRegion(): Promise<"us" | "eu"> {
}
}

function apiBaseUrlForRegion(region: "us" | "eu"): string {
return region === "eu" ? "https://api.cal.eu/v2" : "https://api.cal.com/v2";
}

async function getApiBaseUrl(): Promise<string> {
const region = await getStoredRegion();
return apiBaseUrlForRegion(region);
return `${getCalApiUrl(region)}/v2`;
}

const tokenOperationTimestamps: number[] = [];
Expand Down Expand Up @@ -934,7 +932,7 @@ async function validateTokens(tokens: OAuthTokens, region?: "us" | "eu"): Promis
}

try {
const apiBaseUrl = region ? apiBaseUrlForRegion(region) : await getApiBaseUrl();
const apiBaseUrl = region ? `${getCalApiUrl(region)}/v2` : await getApiBaseUrl();
const response = await fetchWithTimeout(`${apiBaseUrl}/me`, {
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
Expand Down
27 changes: 14 additions & 13 deletions apps/extension/entrypoints/content.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/// <reference types="chrome" />
import { initGoogleCalendarIntegration } from "../lib/google-calendar";
import { initLinkedInIntegration } from "../lib/linkedin";
import { getCalAppUrl, getCalWebUrl } from "../lib/region";
import { escapeHtml } from "../lib/utils";

/**
Expand Down Expand Up @@ -1091,7 +1092,7 @@ export default defineContentScript({
e.stopPropagation();
const bookingUrl =
eventType.bookingUrl ||
`https://cal.com/${
`${getCalWebUrl()}/${
eventType.users?.[0]?.username || "user"
}/${eventType.slug}`;
window.open(bookingUrl, "_blank");
Expand Down Expand Up @@ -1135,7 +1136,7 @@ export default defineContentScript({
// Copy to clipboard
const bookingUrl =
eventType.bookingUrl ||
`https://cal.com/${
`${getCalWebUrl()}/${
eventType.users?.[0]?.username || "user"
}/${eventType.slug}`;
navigator.clipboard
Expand Down Expand Up @@ -1197,7 +1198,7 @@ export default defineContentScript({

editBtn.addEventListener("click", (e) => {
e.stopPropagation();
const editUrl = `https://app.cal.com/event-types/${eventType.id}`;
const editUrl = `${getCalAppUrl()}/event-types/${eventType.id}`;
window.open(editUrl, "_blank");
});
editBtn.addEventListener("mouseenter", () => {
Expand Down Expand Up @@ -1308,7 +1309,7 @@ export default defineContentScript({
// Construct the Cal.com booking link
const bookingUrl =
eventType.bookingUrl ||
`https://cal.com/${eventType.users?.[0]?.username || "user"}/${eventType.slug}`;
`${getCalWebUrl()}/${eventType.users?.[0]?.username || "user"}/${eventType.slug}`;

// Try to insert at cursor position in the compose field
const inserted = insertTextAtCursor(bookingUrl);
Expand Down Expand Up @@ -1336,7 +1337,7 @@ export default defineContentScript({
// Construct the Cal.com booking link
const bookingUrl =
eventType.bookingUrl ||
`https://cal.com/${eventType.users?.[0]?.username || "user"}/${eventType.slug}`;
`${getCalWebUrl()}/${eventType.users?.[0]?.username || "user"}/${eventType.slug}`;

// Try to insert at cursor position in the compose field
const inserted = insertTextAtCursor(bookingUrl);
Expand Down Expand Up @@ -1840,7 +1841,7 @@ export default defineContentScript({
e.stopPropagation();
const bookingUrl =
eventType.bookingUrl ||
`https://cal.com/${eventType.users?.[0]?.username || "user"}/${eventType.slug}`;
`${getCalWebUrl()}/${eventType.users?.[0]?.username || "user"}/${eventType.slug}`;
window.open(bookingUrl, "_blank");
});
previewBtn.addEventListener("mouseenter", () => {
Expand Down Expand Up @@ -1882,7 +1883,7 @@ export default defineContentScript({
// Copy to clipboard
const bookingUrl =
eventType.bookingUrl ||
`https://cal.com/${eventType.users?.[0]?.username || "user"}/${eventType.slug}`;
`${getCalWebUrl()}/${eventType.users?.[0]?.username || "user"}/${eventType.slug}`;
navigator.clipboard
.writeText(bookingUrl)
.then(() => {
Expand Down Expand Up @@ -1942,7 +1943,7 @@ export default defineContentScript({

editBtn.addEventListener("click", (e) => {
e.stopPropagation();
const editUrl = `https://app.cal.com/event-types/${eventType.id}`;
const editUrl = `${getCalAppUrl()}/event-types/${eventType.id}`;
window.open(editUrl, "_blank");
});
editBtn.addEventListener("mouseenter", () => {
Expand Down Expand Up @@ -2053,7 +2054,7 @@ export default defineContentScript({
// Construct the Cal.com booking link
const bookingUrl =
eventType.bookingUrl ||
`https://cal.com/${eventType.users?.[0]?.username || "user"}/${eventType.slug}`;
`${getCalWebUrl()}/${eventType.users?.[0]?.username || "user"}/${eventType.slug}`;

// Try to insert at cursor position in the message field
const inserted = insertTextAtCursor(bookingUrl);
Expand Down Expand Up @@ -2242,7 +2243,7 @@ export default defineContentScript({
.map((slot) => {
// URL-encode the timezone to handle special characters like "/"
const encodedTimezone = encodeURIComponent(timezone);
const bookingURL = `https://cal.com/${username}/${eventType.slug}?duration=${duration}&date=${slot.isoDate}&slot=${slot.isoTimestamp}&cal.tz=${encodedTimezone}`;
const bookingURL = `${getCalWebUrl()}/${username}/${eventType.slug}?duration=${duration}&date=${slot.isoDate}&slot=${slot.isoTimestamp}&cal.tz=${encodedTimezone}`;

return `
<td style="padding: 0px; width: 64px; display: inline-block; margin-right: 4px; margin-bottom: 4px; height: 24px; border: 1px solid #111827; border-radius: 3px;">
Expand Down Expand Up @@ -2306,7 +2307,7 @@ export default defineContentScript({
</div>
${datesHTML}
<div style="margin-top: 13px;">
<a href="https://cal.com/${username}/${eventType.slug}?cal.tz=${encodeURIComponent(
<a href="${getCalWebUrl()}/${username}/${eventType.slug}?cal.tz=${encodeURIComponent(
timezone
)}" style="text-decoration: none; cursor: pointer; color: #0B57D0; font-size: 14px;">
See all available times →
Expand Down Expand Up @@ -3416,7 +3417,7 @@ export default defineContentScript({
const username =
eventTypes.length > 0 ? eventTypes[0].users?.[0]?.username || "user" : "user";

const createUrl = `https://app.cal.com/event-types?dialog=new&eventPage=${username}`;
const createUrl = `${getCalAppUrl()}/event-types?dialog=new&eventPage=${username}`;
window.open(createUrl, "_blank");
showGmailNotification(
`Opening Cal.com to create ${parsedData.detectedDuration}min event type`,
Expand Down Expand Up @@ -3694,7 +3695,7 @@ export default defineContentScript({
const selectedUsername = selectedEventType.users?.[0]?.username || "user";

// Generate Cal.com URL with slot parameters
const baseUrl = `https://cal.com/${selectedUsername}/${selectedSlug}`;
const baseUrl = `${getCalWebUrl()}/${selectedUsername}/${selectedSlug}`;
const params = new URLSearchParams({
overlayCalendar: "true",
date: slot.isoDate,
Expand Down
20 changes: 11 additions & 9 deletions apps/extension/lib/google-calendar.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/// <reference types="chrome" />
import { CAL_WEB_HOSTNAMES } from "./region";

// Google Calendar integration: inject a no-show toggle next to attendees in event popups.
const bookingStatusCache = new Map<string, { data: Map<string, boolean>; timestamp: number }>();
Expand Down Expand Up @@ -266,11 +267,12 @@ function injectStyles(): void {
}

/**
* Extract booking UID from event description
* Looks for patterns like: https://cal.com/booking/{uid} or https://app.cal.com/booking/{uid}
* Extract booking UID from event description.
* Matches both regions: https://cal.{com|eu}/booking/{uid} and
* https://app.cal.{com|eu}/booking/{uid}.
*/
function extractBookingUid(text: string): string | null {
const regex = /https:\/\/(?:app\.)?cal\.com\/booking\/([a-zA-Z0-9]+)/;
const regex = /https:\/\/(?:app\.)?cal\.(?:com|eu)\/booking\/([a-zA-Z0-9]+)/;
const match = text.match(regex);
return match ? match[1] : null;
}
Expand Down Expand Up @@ -1199,10 +1201,10 @@ function observeEventPopups(): void {
}

const text = element.textContent || "";
// Check if this looks like a Cal.com event
const lowerText = text.toLowerCase();
// Check if this looks like a Cal.com event (case-insensitive across both regions)
const isCalComEvent =
text.includes("cal.com") ||
text.includes("Cal.com") ||
CAL_WEB_HOSTNAMES.some((host) => lowerText.includes(host)) ||
text.includes("booking") ||
extractBookingUid(text) !== null;

Expand Down Expand Up @@ -1354,11 +1356,11 @@ function observeEventPopups(): void {
return;
}

// Check if this is a Cal.com event
// Check if this is a Cal.com event (case-insensitive across both regions)
const text = popup.textContent || "";
const lowerText = text.toLowerCase();
const isCalComEvent =
text.includes("cal.com") ||
text.includes("Cal.com") ||
CAL_WEB_HOSTNAMES.some((host) => lowerText.includes(host)) ||
text.includes("booking") ||
extractBookingUid(text) !== null;

Expand Down
5 changes: 3 additions & 2 deletions apps/extension/lib/linkedin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/// <reference types="chrome" />
import { getCalAppUrl, getCalWebUrl } from "./region";
import { escapeHtml } from "./utils";

// LinkedIn integration: inject a Cal.com scheduling button in LinkedIn messaging
Expand Down Expand Up @@ -617,7 +618,7 @@ export function initLinkedInIntegration() {
const editBtn = createActionButton(SVG_ICONS.EDIT, "Edit", "0 6px 6px 0", tooltipsToCleanup);
editBtn.addEventListener("click", (e) => {
e.stopPropagation();
const editUrl = `https://app.cal.com/event-types/${eventType.id}`;
const editUrl = `${getCalAppUrl()}/event-types/${eventType.id}`;
window.open(editUrl, "_blank");
});

Expand Down Expand Up @@ -668,7 +669,7 @@ export function initLinkedInIntegration() {
function buildBookingUrl(eventType: EventType): string {
return (
eventType.bookingUrl ||
`https://cal.com/${eventType.users?.[0]?.username || "user"}/${eventType.slug}`
`${getCalWebUrl()}/${eventType.users?.[0]?.username || "user"}/${eventType.slug}`
);
}

Expand Down
138 changes: 138 additions & 0 deletions apps/extension/lib/region.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/**
* Cal.com data region helpers for the Chrome extension.
*
* Mirrors `apps/mobile/utils/region.ts` but reads from
* `chrome.storage.local["cal_region"]` — the same key the background worker
* writes via the `sync-oauth-tokens` handler
* (`apps/extension/entrypoints/background/index.ts:26`).
*
* Region is cached synchronously in a module-level variable so content-script
* callsites can build URLs without an `await`. The cache is initialized on
* module import via the fire-and-forget `preloadRegion()` and kept in sync
* thereafter by a `chrome.storage.onChanged` listener.
*
* NEVER inline literal `cal.com` / `cal.eu` hostnames elsewhere in
* `apps/extension/**`. The CI grep `check:no-cal-hostnames` enforces this.
*/

export type CalRegion = "us" | "eu";

const REGION_STORAGE_KEY = "cal_region";
const DEFAULT_REGION: CalRegion = "us";

let currentRegion: CalRegion = DEFAULT_REGION;
let preloaded = false;

function isValidRegion(value: unknown): value is CalRegion {
return value === "us" || value === "eu";
}

function isChromeStorageAvailable(): boolean {
return (
typeof chrome !== "undefined" &&
typeof chrome.storage !== "undefined" &&
typeof chrome.storage.local !== "undefined"
);
}

/**
* Load the persisted region from chrome.storage.local. Idempotent — subsequent
* calls re-read storage but do not re-register listeners. Best-effort: on
* storage failure the cache stays at its current value (default US for the
* first call, last-known otherwise).
*/
export async function preloadRegion(): Promise<CalRegion> {
if (!isChromeStorageAvailable()) {
preloaded = true;
return currentRegion;
}
try {
const result = await chrome.storage.local.get([REGION_STORAGE_KEY]);
if (isValidRegion(result[REGION_STORAGE_KEY])) {
currentRegion = result[REGION_STORAGE_KEY];
}
} catch {
// Best-effort; stay on default.
}
preloaded = true;
return currentRegion;
}

if (
typeof chrome !== "undefined" &&
chrome.storage?.onChanged &&
typeof chrome.storage.onChanged.addListener === "function"
) {
chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName !== "local") return;
if (!changes[REGION_STORAGE_KEY]) return;
const next = changes[REGION_STORAGE_KEY].newValue;
if (isValidRegion(next)) {
currentRegion = next;
} else if (next === undefined) {
currentRegion = DEFAULT_REGION;
}
});
}

// Kick off the initial read on import. Fire-and-forget — the helpers below are
// safe to call before this resolves (they fall back to the default).
preloadRegion();

export function getRegion(): CalRegion {
return currentRegion;
}

export function isRegionPreloaded(): boolean {
return preloaded;
}

/**
* `https://app.cal.{com|eu}` — Cal.com web app origin for the current region.
*
* Note: the very first synchronous call after extension load may return the
* US default before `preloadRegion()` resolves. In practice this is benign:
* content scripts only construct URLs in user-event handlers, by which time
* the async preload has completed.
*/
export function getCalAppUrl(region: CalRegion = currentRegion): string {
return region === "eu" ? "https://app.cal.eu" : "https://app.cal.com";
}

/**
* `https://api.cal.{com|eu}` — Cal.com API origin for the current region.
* Same first-call caveat as `getCalAppUrl()`.
*/
export function getCalApiUrl(region: CalRegion = currentRegion): string {
return region === "eu" ? "https://api.cal.eu" : "https://api.cal.com";
}

/**
* `https://cal.{com|eu}` — Cal.com booking-page origin for the current region.
* Same first-call caveat as `getCalAppUrl()`.
*/
export function getCalWebUrl(region: CalRegion = currentRegion): string {
return region === "eu" ? "https://cal.eu" : "https://cal.com";
}

/**
* Cal.com Framer marketing landing for the "open extension" deep link. Stays
* global — the Framer-hosted marketing site does not have an EU mirror.
* Same precedent as `getCalSupportUrl()` / `getCalHelpUrl()` in mobile's
* region module.
*/
export function getCalMarketingAppUrl(): string {
return "https://cal.com/app?openExtension=true";
}

/**
* Hostnames the extension recognizes as Cal.com booking surfaces across both
* regions. Use for regex / `text.includes(...)` host detection. Do NOT use to
* build outbound URLs (use the getters above).
*/
export const CAL_WEB_HOSTNAMES: ReadonlyArray<string> = [
"cal.com",
"cal.eu",
"app.cal.com",
"app.cal.eu",
];
Loading
Loading