Files
calendar/companion/utils/browser.ts
T
Peer RichelsenGitHubpeer@cal.com <peer@cal.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Dhairyashil Shinde
2e9191fcad fix: open Join button in default browser instead of in-app browser (#27455)
* fix: open Join button in default browser instead of in-app browser

Co-Authored-By: peer@cal.com <peer@cal.com>

* feat(companion): open join in default browser (#27655)

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
2026-02-05 17:39:26 +05:30

154 lines
4.7 KiB
TypeScript

/**
* Browser Utilities
*
* Centralized utility for opening links in the in-app browser.
* Configured for session sharing with Safari/Chrome to maintain login state.
*/
import * as WebBrowser from "expo-web-browser";
import { Linking, Platform } from "react-native";
import { showErrorAlert } from "./alerts";
/**
* Handle errors from browser functions in a consistent way.
*
* @param error - The error that occurred
* @param functionName - Name of the function for debug logging
* @param fallbackMessage - Optional message to show in error alert (defaults to "link")
*/
const handleBrowserError = (
error: unknown,
functionName: string,
fallbackMessage?: string
): void => {
console.error(`Failed to open link in ${functionName}`);
if (__DEV__) {
const message = error instanceof Error ? error.message : String(error);
const stack = error instanceof Error ? error.stack : undefined;
console.debug(`[${functionName}] failed`, { message, stack, fallbackMessage });
}
showErrorAlert("Error", `Failed to open ${fallbackMessage || "link"}. Please try again.`);
};
/**
* Configuration options for in-app browser
*/
export interface BrowserOptions {
/** iOS: Toolbar color (hex string) */
toolbarColor?: string;
/** iOS: Controls color (hex string) */
controlsColor?: string;
}
/**
* Appends ?standalone=true to app.cal.com URLs on iOS.
* This hides navigation elements when pages are opened in the in-app browser,
* which is required for Apple App Store compliance.
*
* @param url - The URL to process
* @returns The URL with standalone=true appended if it's an app.cal.com URL on iOS
*/
const appendStandaloneParam = (url: string): string => {
// Only apply to iOS
if (Platform.OS !== "ios") {
return url;
}
try {
const urlObj = new URL(url);
// Only apply to app.cal.com URLs
if (urlObj.hostname !== "app.cal.com") {
return url;
}
// Don't add if already present
if (urlObj.searchParams.has("standalone")) {
return url;
}
urlObj.searchParams.set("standalone", "true");
return urlObj.toString();
} catch {
// If URL parsing fails, return original
return url;
}
};
/**
* Open a URL in the in-app browser with session sharing enabled.
*
* Session sharing allows cookies to be shared between the in-app browser
* and Safari (iOS) or Chrome (Android). This means users who authenticate
* via OAuth will remain logged in when opening Cal.com links.
*
* On iOS, app.cal.com URLs automatically get ?standalone=true appended
* to hide navigation elements for Apple App Store compliance.
*
* @param url - The URL to open
* @param fallbackMessage - Optional message to show in error alert (defaults to "link")
* @param options - Optional browser customization options
*
* @example
* ```tsx
* // Open a link with session sharing
* await openInAppBrowser("https://app.cal.com");
*
* // With custom error message
* await openInAppBrowser("https://app.cal.com/settings", "Settings page");
*
* // With custom toolbar color
* await openInAppBrowser("https://app.cal.com", "Cal.com", { toolbarColor: "#111827" });
* ```
*/
export const openInAppBrowser = async (
url: string,
fallbackMessage?: string,
options?: BrowserOptions
): Promise<void> => {
try {
// Append standalone=true for app.cal.com URLs on iOS
const processedUrl = appendStandaloneParam(url);
// Configure browser options
// Session sharing happens automatically when using Safari View Controller (iOS)
// or Chrome Custom Tabs (Android) - no special configuration needed
const browserOptions: WebBrowser.WebBrowserOpenOptions = {
...(options?.toolbarColor && { toolbarColor: options.toolbarColor }),
...(options?.controlsColor && { controlsColor: options.controlsColor }),
};
await WebBrowser.openBrowserAsync(processedUrl, browserOptions);
} catch (error) {
handleBrowserError(error, "openInAppBrowser", fallbackMessage);
}
};
/**
* Open a URL in the device's default browser (Safari on iOS, Chrome on Android).
*
* Unlike openInAppBrowser, this opens the URL in the actual browser app
* rather than an in-app browser view. This is useful for video meeting links
* where users may want to use their browser's native features or extensions.
*
* @param url - The URL to open
* @param fallbackMessage - Optional message to show in error alert (defaults to "link")
*
* @example
* ```tsx
* // Open a meeting link in the default browser
* await openInDefaultBrowser("https://meet.cal.com/abc123", "meeting link");
* ```
*/
export const openInDefaultBrowser = async (
url: string,
fallbackMessage?: string
): Promise<void> => {
try {
await Linking.openURL(url);
} catch (error) {
handleBrowserError(error, "openInDefaultBrowser", fallbackMessage);
}
};