diff --git a/apps/web/pagesAndRewritePaths.js b/apps/web/pagesAndRewritePaths.js index 72048e8273..b92901b0e9 100644 --- a/apps/web/pagesAndRewritePaths.js +++ b/apps/web/pagesAndRewritePaths.js @@ -5,22 +5,22 @@ const { nextJsOrgRewriteConfig } = require("./getNextjsOrgRewriteConfig"); // Top-level route names that are explicitly allowed for org rewrite (whitelist) -const topLevelRouteNamesWhitelistedForRewrite = exports.topLevelRouteNamesWhitelistedForRewrite = [ +const topLevelRouteNamesWhitelistedForRewrite = (exports.topLevelRouteNamesWhitelistedForRewrite = [ // We don't allow all dashboard route names to be used as slug because people are probably accustomed to access links like acme.cal.com/workflows, acme.cal.com/event-types etc. // So, we carefully allow, what is absolutely needed. // Allowed to be a team/user slug in organization because onboarding is a common team name - 'onboarding', -] + "onboarding", +]); /** * Extracts top-level route names from all pages/app files and excludes them from org rewrite. * For example: /abc/def/ghi -> 'abc' - * + * * These top-level route names are excluded from rewrites in beforeFiles in next.config.js * to prevent conflicts with organization slug rewrites. */ /* eslint-disable no-undef */ -let topLevelRoutesExcludedFromOrgRewrite = exports.topLevelRoutesExcludedFromOrgRewrite = glob +let topLevelRoutesExcludedFromOrgRewrite = (exports.topLevelRoutesExcludedFromOrgRewrite = glob .sync( "{pages,app,app/(booking-page-wrapper),app/(use-page-wrapper),app/(use-page-wrapper)/(main-nav)}/**/[^_]*.{tsx,js,ts}", { @@ -58,7 +58,7 @@ let topLevelRoutesExcludedFromOrgRewrite = exports.topLevelRoutesExcludedFromOrg ) .filter((page) => { return !topLevelRouteNamesWhitelistedForRewrite.includes(page); - }); + })); // .* matches / as well(Note: *(i.e wildcard) doesn't match / but .*(i.e. RegExp) does) // It would match /free/30min but not /bookings/upcoming because 'bookings' is an item in pages @@ -76,7 +76,10 @@ exports.nextJsOrgRewriteConfig = nextJsOrgRewriteConfig; function getRegExpMatchingAllReservedRoutes(suffix) { // Following routes don't exist but they work by doing rewrite. Thus they need to be excluded from matching the orgRewrite patterns // Make sure to keep it upto date as more nonExistingRouteRewrites are added. - const otherNonExistingRoutePrefixes = ["forms", "router", "success", "cancel"]; + // "app" is reserved for the Cal.com Companion landing page served by Framer at cal.com/app. + // The browser extension redirects users to cal.com/app when clicked on restricted pages (like chrome://newtab). + // Without this reservation, /app would be treated as a username lookup and show "username available" error. + const otherNonExistingRoutePrefixes = ["forms", "router", "success", "cancel", "app"]; // Most files/dirs in public dir must not be rewritten to org pages. Ideally it should be all the content of public dir, but that can be done later // It is important to exclude the embed pages separately here because with SINGLE_ORG_SLUG enabled, the entire domain is eligible for rewrite vs just the org subdomain otherwise diff --git a/companion/app/(tabs)/(event-types)/index.tsx b/companion/app/(tabs)/(event-types)/index.tsx index 946a25335d..d24f104e64 100644 --- a/companion/app/(tabs)/(event-types)/index.tsx +++ b/companion/app/(tabs)/(event-types)/index.tsx @@ -471,7 +471,7 @@ export default function EventTypes() { <> ( diff --git a/companion/app/(tabs)/more.tsx b/companion/app/(tabs)/more.tsx index 473ac7c6f1..31da2a6c64 100644 --- a/companion/app/(tabs)/more.tsx +++ b/companion/app/(tabs)/more.tsx @@ -1,8 +1,9 @@ -import React from "react"; -import { View, Text, TouchableOpacity, ScrollView, Alert } from "react-native"; +import React, { useState } from "react"; +import { View, Text, TouchableOpacity, ScrollView, Alert, Platform } from "react-native"; import { Ionicons } from "@expo/vector-icons"; import { useRouter } from "expo-router"; import { Header } from "../../components/Header"; +import { LogoutConfirmModal } from "../../components/LogoutConfirmModal"; import { useAuth } from "../../contexts/AuthContext"; import { showErrorAlert } from "../../utils/alerts"; import { openInAppBrowser } from "../../utils/browser"; @@ -18,23 +19,32 @@ interface MoreMenuItem { export default function More() { const router = useRouter(); const { logout } = useAuth(); + const [showLogoutModal, setShowLogoutModal] = useState(false); + + const performLogout = async () => { + try { + await logout(); + } catch (error) { + console.error("Logout error:", error); + showErrorAlert("Error", "Failed to sign out. Please try again."); + } + }; const handleSignOut = () => { - Alert.alert("Sign Out", "Are you sure you want to sign out?", [ - { text: "Cancel", style: "cancel" }, - { - text: "Sign Out", - style: "destructive", - onPress: async () => { - try { - await logout(); - } catch (error) { - console.error("Logout error:", error); - showErrorAlert("Error", "Failed to sign out. Please try again."); - } + if (Platform.OS === "web") { + // Use modal for web/extension since Alert.alert doesn't work + setShowLogoutModal(true); + } else { + // Use native Alert for iOS/Android + Alert.alert("Sign Out", "Are you sure you want to sign out?", [ + { text: "Cancel", style: "cancel" }, + { + text: "Sign Out", + style: "destructive", + onPress: performLogout, }, - }, - ]); + ]); + } }; const menuItems: MoreMenuItem[] = [ @@ -129,6 +139,16 @@ export default function More() { + + {/* Logout Confirmation Modal for Web */} + { + setShowLogoutModal(false); + performLogout(); + }} + onCancel={() => setShowLogoutModal(false)} + /> ); } diff --git a/companion/extension/entrypoints/background/index.ts b/companion/extension/entrypoints/background/index.ts index d62fd15d75..abcfde8b7a 100644 --- a/companion/extension/entrypoints/background/index.ts +++ b/companion/extension/entrypoints/background/index.ts @@ -11,6 +11,34 @@ const devLog = { error: (...args: unknown[]) => console.error("[Cal.com]", ...args), }; +// Check if the URL is a restricted page where content scripts can't run +function isRestrictedUrl(url: string | undefined): boolean { + if (!url) return true; + + // List of restricted URL patterns + const restrictedPatterns = [ + /^chrome:\/\//i, // Chrome internal pages (newtab, settings, extensions, etc.) + /^chrome-extension:\/\//i, // Other extension pages + /^edge:\/\//i, // Edge internal pages + /^about:/i, // about:blank, about:newtab, etc. + /^brave:\/\//i, // Brave internal pages + /^opera:\/\//i, // Opera internal pages + /^vivaldi:\/\//i, // Vivaldi internal pages + /^file:\/\//i, // Local files (content scripts often blocked) + /^view-source:/i, // View source pages + /^devtools:\/\//i, // DevTools pages + /^data:/i, // Data URLs + /^blob:/i, // Blob URLs + ]; + + return restrictedPatterns.some((pattern) => pattern.test(url)); +} + +// Open cal.com/app (Framer marketing page) in a new tab with auto-open parameter +function openAppPage(): void { + chrome.tabs.create({ url: "https://cal.com/app?openExtension=true" }); +} + // @ts-ignore - WXT provides this globally export default defineBackground(() => { if (IS_DEV_MODE) { @@ -18,9 +46,17 @@ export default defineBackground(() => { } chrome.action.onClicked.addListener((tab) => { + // Check if this is a restricted URL where content scripts can't run + if (isRestrictedUrl(tab.url)) { + devLog.log("Restricted URL detected, opening app page:", tab.url); + openAppPage(); + return; + } + if (tab.id) { chrome.tabs.sendMessage(tab.id, { action: "icon-clicked" }, () => { - // Ignore errors - expected on pages where content script isn't loaded + // Ignore errors - expected on pages where content script hasn't loaded yet + // The restricted URL check above handles pages where content scripts can't run void chrome.runtime.lastError; }); } diff --git a/companion/extension/entrypoints/content.ts b/companion/extension/entrypoints/content.ts index 61a8403c47..1cbe21cacb 100644 --- a/companion/extension/entrypoints/content.ts +++ b/companion/extension/entrypoints/content.ts @@ -362,44 +362,96 @@ export default defineContentScript({ document.body.appendChild(sidebarContainer); document.body.appendChild(buttonsContainer); + // Function to open the sidebar + function openSidebar() { + if (isClosed) { + isClosed = false; + isVisible = true; + sidebarContainer.style.display = "block"; + buttonsContainer.style.display = "flex"; + sidebarContainer.style.transform = "translateX(0)"; + buttonsContainer.style.right = "420px"; + toggleButton.innerHTML = ` + + +`; + } else if (!isVisible) { + isVisible = true; + sidebarContainer.style.transform = "translateX(0)"; + buttonsContainer.style.right = "420px"; + toggleButton.innerHTML = ` + + +`; + } + } + + // Function to close/hide the sidebar + function hideSidebar() { + if (isVisible) { + isVisible = false; + sidebarContainer.style.transform = "translateX(100%)"; + buttonsContainer.style.right = "20px"; + toggleButton.innerHTML = ` + + +`; + } + } + // Listen for extension icon click chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.action === "icon-clicked") { if (isClosed) { - // Reopen closed sidebar - isClosed = false; - isVisible = true; - sidebarContainer.style.display = "block"; - buttonsContainer.style.display = "flex"; - sidebarContainer.style.transform = "translateX(0)"; - buttonsContainer.style.right = "420px"; - toggleButton.innerHTML = ` - - -`; + openSidebar(); } else { // Toggle visible sidebar - isVisible = !isVisible; if (isVisible) { - sidebarContainer.style.transform = "translateX(0)"; - buttonsContainer.style.right = "420px"; - toggleButton.innerHTML = ` - - -`; + hideSidebar(); } else { - sidebarContainer.style.transform = "translateX(100%)"; - buttonsContainer.style.right = "20px"; - toggleButton.innerHTML = ` - - -`; + openSidebar(); } } sendResponse({ success: true }); // Send response to acknowledge } }); + // Auto-open sidebar when redirected from restricted pages (like new tab) + // Detects ?openExtension=true parameter on cal.com/app or companion.cal.com + const urlParams = new URLSearchParams(window.location.search); + const shouldAutoOpen = + urlParams.get("openExtension") === "true" || window.location.hostname === "companion.cal.com"; + + if (shouldAutoOpen) { + // Function to open sidebar and clean up URL + const autoOpenAndCleanup = () => { + openSidebar(); + // Clean up the URL parameter without triggering a reload + if (urlParams.get("openExtension")) { + const url = new URL(window.location.href); + url.searchParams.delete("openExtension"); + window.history.replaceState({}, document.title, url.toString()); + } + }; + + // Wait for page to fully load before auto-opening + // This handles Framer pages which load dynamically + if (document.readyState === "complete") { + // Page already loaded (cached), use small delay + setTimeout(autoOpenAndCleanup, 300); + } else { + // Page still loading (first visit), wait for load event + window.addEventListener( + "load", + () => { + // Additional delay after load for Framer's JS to initialize + setTimeout(autoOpenAndCleanup, 500); + }, + { once: true } + ); + } + } + // Gmail integration function function initGmailIntegration() { // Cache for event types (refreshed on page reload) diff --git a/companion/wxt.config.ts b/companion/wxt.config.ts index 3b1f1c4a71..74fa753d4e 100644 --- a/companion/wxt.config.ts +++ b/companion/wxt.config.ts @@ -67,7 +67,7 @@ export default defineConfig({ process.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI ), "import.meta.env.EXPO_PUBLIC_COMPANION_DEV_URL": JSON.stringify( - process.env.EXPO_PUBLIC_COMPANION_DEV_URL + process.env.EXPO_PUBLIC_COMPANION_DEV_URL || "" ), "import.meta.env.EXPO_PUBLIC_CACHE_STALE_TIME_MINUTES": JSON.stringify( process.env.EXPO_PUBLIC_CACHE_STALE_TIME_MINUTES