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>
This commit is contained in:
Peer Richelsen
2026-02-05 17:39:26 +05:30
committed by GitHub
co-authored by peer@cal.com Devin AI Dhairyashil Shinde
parent 75cd509106
commit 2e9191fcad
6 changed files with 103 additions and 68 deletions
@@ -1,15 +1,15 @@
import * as Clipboard from "expo-clipboard";
import { isLiquidGlassAvailable } from "expo-glass-effect";
import { Stack, useLocalSearchParams } from "expo-router";
import { useCallback, useMemo, useRef } from "react";
import { useColorScheme } from "react-native";
import { BookingDetailScreen } from "@/components/screens/BookingDetailScreen";
import { useAuth } from "@/contexts/AuthContext";
import { useBookingByUid } from "@/hooks/useBookings";
import type { Booking } from "@/services/calcom";
import { showErrorAlert, showInfoAlert, showSuccessAlert } from "@/utils/alerts";
import { type BookingActionsResult, getBookingActions } from "@/utils/booking-actions";
import { openInAppBrowser } from "@/utils/browser";
import { isLiquidGlassAvailable } from "expo-glass-effect";
import { getMeetingUrl } from "@/utils/booking";
import { openInDefaultBrowser } from "@/utils/browser";
// Empty actions result for when no booking is loaded
const EMPTY_ACTIONS: BookingActionsResult = {
@@ -42,25 +42,6 @@ const getMonthName = (dateString: string | undefined): string => {
return date.toLocaleDateString("en-US", { month: "long" });
};
// Get meeting URL from booking
const getMeetingUrl = (booking: Booking | null): string | null => {
if (!booking) return null;
// Check metadata for videoCallUrl first
const videoCallUrl = booking.responses?.videoCallUrl;
if (typeof videoCallUrl === "string" && videoCallUrl.startsWith("http")) {
return videoCallUrl;
}
// Check location
const location = booking.location;
if (typeof location === "string" && location.startsWith("http")) {
return location;
}
return null;
};
export default function BookingDetailIOS() {
const { uid } = useLocalSearchParams<{ uid: string }>();
const { userInfo } = useAuth();
@@ -89,7 +70,7 @@ export default function BookingDetailIOS() {
// Handle join meeting
const handleJoinMeeting = useCallback(() => {
if (meetingUrl) {
openInAppBrowser(meetingUrl, "meeting link");
openInDefaultBrowser(meetingUrl, "meeting link");
}
}, [meetingUrl]);
@@ -1,8 +1,8 @@
import * as Clipboard from "expo-clipboard";
import { Ionicons } from "@expo/vector-icons";
import * as Clipboard from "expo-clipboard";
import { Stack, useLocalSearchParams, useNavigation, useRouter } from "expo-router";
import { useCallback, useEffect, useMemo, useRef } from "react";
import { Platform, Text, View, useColorScheme } from "react-native";
import { Platform, Text, useColorScheme, View } from "react-native";
import { AppPressable } from "@/components/AppPressable";
import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper";
import { BookingDetailScreen } from "@/components/screens/BookingDetailScreen";
@@ -16,10 +16,10 @@ import {
} from "@/components/ui/dropdown-menu";
import { useAuth } from "@/contexts/AuthContext";
import { useBookingByUid } from "@/hooks/useBookings";
import type { Booking } from "@/services/calcom";
import { showErrorAlert, showInfoAlert, showSuccessAlert } from "@/utils/alerts";
import { type BookingActionsResult, getBookingActions } from "@/utils/booking-actions";
import { openInAppBrowser } from "@/utils/browser";
import { getMeetingUrl } from "@/utils/booking";
import { openInDefaultBrowser } from "@/utils/browser";
// Empty actions result for when no booking is loaded
const EMPTY_ACTIONS: BookingActionsResult = {
@@ -33,22 +33,6 @@ const EMPTY_ACTIONS: BookingActionsResult = {
markNoShow: { visible: false, enabled: false },
};
const getMeetingUrl = (booking: Booking | null): string | null => {
if (!booking) return null;
const videoCallUrl = booking.responses?.videoCallUrl;
if (typeof videoCallUrl === "string" && videoCallUrl.startsWith("http")) {
return videoCallUrl;
}
const location = booking.location;
if (typeof location === "string" && location.startsWith("http")) {
return location;
}
return null;
};
// Type for action handlers exposed by BookingDetailScreen
type ActionHandlers = {
openRescheduleModal: () => void;
@@ -280,7 +264,7 @@ export default function BookingDetail() {
const handleJoinMeeting = useCallback(() => {
if (meetingUrl) {
openInAppBrowser(meetingUrl, "meeting link");
openInDefaultBrowser(meetingUrl, "meeting link");
}
}, [meetingUrl]);
@@ -43,6 +43,7 @@ import { useCancelBooking } from "@/hooks/useBookings";
import type { Booking } from "@/services/calcom";
import { showErrorAlert, showInfoAlert, showSuccessAlert } from "@/utils/alerts";
import { type BookingActionsResult, getBookingActions } from "@/utils/booking-actions";
import { getMeetingUrl } from "@/utils/booking";
import { openInAppBrowser } from "@/utils/browser";
import { getColors } from "@/constants/colors";
@@ -136,22 +137,6 @@ const calculateDuration = (startDateString: string, endDateString: string): numb
return Math.round((endDate.getTime() - startDate.getTime()) / (1000 * 60));
};
const getMeetingUrl = (booking: Booking | null): string | null => {
if (!booking) return null;
const videoCallUrl = booking.responses?.videoCallUrl;
if (typeof videoCallUrl === "string" && videoCallUrl.startsWith("http")) {
return videoCallUrl;
}
const location = booking.location;
if (typeof location === "string" && location.startsWith("http")) {
return location;
}
return null;
};
export interface BookingDetailScreenProps {
/**
* The booking data to display. When null/undefined, shows loading or error state.
+42
View File
@@ -0,0 +1,42 @@
/**
* Booking Utilities
*
* Shared utility functions for working with bookings.
*/
import type { Booking } from "@/services/calcom";
/**
* Extract the meeting URL from a booking.
*
* Checks both the booking's responses.videoCallUrl and location fields
* for a valid HTTP(S) URL that can be used to join the meeting.
*
* @param booking - The booking to extract the meeting URL from
* @returns The meeting URL if found, null otherwise
*
* @example
* ```tsx
* const meetingUrl = getMeetingUrl(booking);
* if (meetingUrl) {
* openInDefaultBrowser(meetingUrl, "meeting link");
* }
* ```
*/
export const getMeetingUrl = (booking: Booking | null): string | null => {
if (!booking) return null;
// Check metadata for videoCallUrl first
const videoCallUrl = booking.responses?.videoCallUrl;
if (typeof videoCallUrl === "string" && videoCallUrl.startsWith("http")) {
return videoCallUrl;
}
// Check location
const location = booking.location;
if (typeof location === "string" && location.startsWith("http")) {
return location;
}
return null;
};
+50 -8
View File
@@ -6,10 +6,31 @@
*/
import * as WebBrowser from "expo-web-browser";
import { Platform } from "react-native";
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
*/
@@ -100,12 +121,33 @@ export const openInAppBrowser = async (
await WebBrowser.openBrowserAsync(processedUrl, browserOptions);
} catch (error) {
console.error("Failed to open link");
if (__DEV__) {
const message = error instanceof Error ? error.message : String(error);
const stack = error instanceof Error ? error.stack : undefined;
console.debug("[openInAppBrowser] failed", { message, stack, fallbackMessage });
}
showErrorAlert("Error", `Failed to open ${fallbackMessage || "link"}. Please try again.`);
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);
}
};
+1
View File
@@ -14,6 +14,7 @@
export { setGlobalToastFunction, showErrorAlert, showInfoAlert, showSuccessAlert } from "./alerts";
// Browser utilities
export { openInAppBrowser } from "./browser";
export { openInDefaultBrowser } from "./browser";
// Default location utilities
export {
type DefaultLocation,