Skip to content

Commit 2e9191f

Browse files
PeerRichdevin-ai-integration[bot]dhairyashiil
authored
fix: open Join button in default browser instead of in-app browser (calcom#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 (calcom#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>
1 parent 75cd509 commit 2e9191f

6 files changed

Lines changed: 103 additions & 68 deletions

File tree

companion/app/(tabs)/(bookings)/booking-detail.ios.tsx

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import * as Clipboard from "expo-clipboard";
2+
import { isLiquidGlassAvailable } from "expo-glass-effect";
23
import { Stack, useLocalSearchParams } from "expo-router";
34
import { useCallback, useMemo, useRef } from "react";
45
import { useColorScheme } from "react-native";
56
import { BookingDetailScreen } from "@/components/screens/BookingDetailScreen";
67
import { useAuth } from "@/contexts/AuthContext";
78
import { useBookingByUid } from "@/hooks/useBookings";
8-
import type { Booking } from "@/services/calcom";
99
import { showErrorAlert, showInfoAlert, showSuccessAlert } from "@/utils/alerts";
1010
import { type BookingActionsResult, getBookingActions } from "@/utils/booking-actions";
11-
import { openInAppBrowser } from "@/utils/browser";
12-
import { isLiquidGlassAvailable } from "expo-glass-effect";
11+
import { getMeetingUrl } from "@/utils/booking";
12+
import { openInDefaultBrowser } from "@/utils/browser";
1313

1414
// Empty actions result for when no booking is loaded
1515
const EMPTY_ACTIONS: BookingActionsResult = {
@@ -42,25 +42,6 @@ const getMonthName = (dateString: string | undefined): string => {
4242
return date.toLocaleDateString("en-US", { month: "long" });
4343
};
4444

45-
// Get meeting URL from booking
46-
const getMeetingUrl = (booking: Booking | null): string | null => {
47-
if (!booking) return null;
48-
49-
// Check metadata for videoCallUrl first
50-
const videoCallUrl = booking.responses?.videoCallUrl;
51-
if (typeof videoCallUrl === "string" && videoCallUrl.startsWith("http")) {
52-
return videoCallUrl;
53-
}
54-
55-
// Check location
56-
const location = booking.location;
57-
if (typeof location === "string" && location.startsWith("http")) {
58-
return location;
59-
}
60-
61-
return null;
62-
};
63-
6445
export default function BookingDetailIOS() {
6546
const { uid } = useLocalSearchParams<{ uid: string }>();
6647
const { userInfo } = useAuth();
@@ -89,7 +70,7 @@ export default function BookingDetailIOS() {
8970
// Handle join meeting
9071
const handleJoinMeeting = useCallback(() => {
9172
if (meetingUrl) {
92-
openInAppBrowser(meetingUrl, "meeting link");
73+
openInDefaultBrowser(meetingUrl, "meeting link");
9374
}
9475
}, [meetingUrl]);
9576

companion/app/(tabs)/(bookings)/booking-detail.tsx

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import * as Clipboard from "expo-clipboard";
21
import { Ionicons } from "@expo/vector-icons";
2+
import * as Clipboard from "expo-clipboard";
33
import { Stack, useLocalSearchParams, useNavigation, useRouter } from "expo-router";
44
import { useCallback, useEffect, useMemo, useRef } from "react";
5-
import { Platform, Text, View, useColorScheme } from "react-native";
5+
import { Platform, Text, useColorScheme, View } from "react-native";
66
import { AppPressable } from "@/components/AppPressable";
77
import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper";
88
import { BookingDetailScreen } from "@/components/screens/BookingDetailScreen";
@@ -16,10 +16,10 @@ import {
1616
} from "@/components/ui/dropdown-menu";
1717
import { useAuth } from "@/contexts/AuthContext";
1818
import { useBookingByUid } from "@/hooks/useBookings";
19-
import type { Booking } from "@/services/calcom";
2019
import { showErrorAlert, showInfoAlert, showSuccessAlert } from "@/utils/alerts";
2120
import { type BookingActionsResult, getBookingActions } from "@/utils/booking-actions";
22-
import { openInAppBrowser } from "@/utils/browser";
21+
import { getMeetingUrl } from "@/utils/booking";
22+
import { openInDefaultBrowser } from "@/utils/browser";
2323

2424
// Empty actions result for when no booking is loaded
2525
const EMPTY_ACTIONS: BookingActionsResult = {
@@ -33,22 +33,6 @@ const EMPTY_ACTIONS: BookingActionsResult = {
3333
markNoShow: { visible: false, enabled: false },
3434
};
3535

36-
const getMeetingUrl = (booking: Booking | null): string | null => {
37-
if (!booking) return null;
38-
39-
const videoCallUrl = booking.responses?.videoCallUrl;
40-
if (typeof videoCallUrl === "string" && videoCallUrl.startsWith("http")) {
41-
return videoCallUrl;
42-
}
43-
44-
const location = booking.location;
45-
if (typeof location === "string" && location.startsWith("http")) {
46-
return location;
47-
}
48-
49-
return null;
50-
};
51-
5236
// Type for action handlers exposed by BookingDetailScreen
5337
type ActionHandlers = {
5438
openRescheduleModal: () => void;
@@ -280,7 +264,7 @@ export default function BookingDetail() {
280264

281265
const handleJoinMeeting = useCallback(() => {
282266
if (meetingUrl) {
283-
openInAppBrowser(meetingUrl, "meeting link");
267+
openInDefaultBrowser(meetingUrl, "meeting link");
284268
}
285269
}, [meetingUrl]);
286270

companion/components/screens/BookingDetailScreen.tsx

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import { useCancelBooking } from "@/hooks/useBookings";
4343
import type { Booking } from "@/services/calcom";
4444
import { showErrorAlert, showInfoAlert, showSuccessAlert } from "@/utils/alerts";
4545
import { type BookingActionsResult, getBookingActions } from "@/utils/booking-actions";
46+
import { getMeetingUrl } from "@/utils/booking";
4647
import { openInAppBrowser } from "@/utils/browser";
4748
import { getColors } from "@/constants/colors";
4849

@@ -136,22 +137,6 @@ const calculateDuration = (startDateString: string, endDateString: string): numb
136137
return Math.round((endDate.getTime() - startDate.getTime()) / (1000 * 60));
137138
};
138139

139-
const getMeetingUrl = (booking: Booking | null): string | null => {
140-
if (!booking) return null;
141-
142-
const videoCallUrl = booking.responses?.videoCallUrl;
143-
if (typeof videoCallUrl === "string" && videoCallUrl.startsWith("http")) {
144-
return videoCallUrl;
145-
}
146-
147-
const location = booking.location;
148-
if (typeof location === "string" && location.startsWith("http")) {
149-
return location;
150-
}
151-
152-
return null;
153-
};
154-
155140
export interface BookingDetailScreenProps {
156141
/**
157142
* The booking data to display. When null/undefined, shows loading or error state.

companion/utils/booking.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Booking Utilities
3+
*
4+
* Shared utility functions for working with bookings.
5+
*/
6+
7+
import type { Booking } from "@/services/calcom";
8+
9+
/**
10+
* Extract the meeting URL from a booking.
11+
*
12+
* Checks both the booking's responses.videoCallUrl and location fields
13+
* for a valid HTTP(S) URL that can be used to join the meeting.
14+
*
15+
* @param booking - The booking to extract the meeting URL from
16+
* @returns The meeting URL if found, null otherwise
17+
*
18+
* @example
19+
* ```tsx
20+
* const meetingUrl = getMeetingUrl(booking);
21+
* if (meetingUrl) {
22+
* openInDefaultBrowser(meetingUrl, "meeting link");
23+
* }
24+
* ```
25+
*/
26+
export const getMeetingUrl = (booking: Booking | null): string | null => {
27+
if (!booking) return null;
28+
29+
// Check metadata for videoCallUrl first
30+
const videoCallUrl = booking.responses?.videoCallUrl;
31+
if (typeof videoCallUrl === "string" && videoCallUrl.startsWith("http")) {
32+
return videoCallUrl;
33+
}
34+
35+
// Check location
36+
const location = booking.location;
37+
if (typeof location === "string" && location.startsWith("http")) {
38+
return location;
39+
}
40+
41+
return null;
42+
};

companion/utils/browser.ts

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,31 @@
66
*/
77

88
import * as WebBrowser from "expo-web-browser";
9-
import { Platform } from "react-native";
9+
import { Linking, Platform } from "react-native";
1010

1111
import { showErrorAlert } from "./alerts";
1212

13+
/**
14+
* Handle errors from browser functions in a consistent way.
15+
*
16+
* @param error - The error that occurred
17+
* @param functionName - Name of the function for debug logging
18+
* @param fallbackMessage - Optional message to show in error alert (defaults to "link")
19+
*/
20+
const handleBrowserError = (
21+
error: unknown,
22+
functionName: string,
23+
fallbackMessage?: string
24+
): void => {
25+
console.error(`Failed to open link in ${functionName}`);
26+
if (__DEV__) {
27+
const message = error instanceof Error ? error.message : String(error);
28+
const stack = error instanceof Error ? error.stack : undefined;
29+
console.debug(`[${functionName}] failed`, { message, stack, fallbackMessage });
30+
}
31+
showErrorAlert("Error", `Failed to open ${fallbackMessage || "link"}. Please try again.`);
32+
};
33+
1334
/**
1435
* Configuration options for in-app browser
1536
*/
@@ -100,12 +121,33 @@ export const openInAppBrowser = async (
100121

101122
await WebBrowser.openBrowserAsync(processedUrl, browserOptions);
102123
} catch (error) {
103-
console.error("Failed to open link");
104-
if (__DEV__) {
105-
const message = error instanceof Error ? error.message : String(error);
106-
const stack = error instanceof Error ? error.stack : undefined;
107-
console.debug("[openInAppBrowser] failed", { message, stack, fallbackMessage });
108-
}
109-
showErrorAlert("Error", `Failed to open ${fallbackMessage || "link"}. Please try again.`);
124+
handleBrowserError(error, "openInAppBrowser", fallbackMessage);
125+
}
126+
};
127+
128+
/**
129+
* Open a URL in the device's default browser (Safari on iOS, Chrome on Android).
130+
*
131+
* Unlike openInAppBrowser, this opens the URL in the actual browser app
132+
* rather than an in-app browser view. This is useful for video meeting links
133+
* where users may want to use their browser's native features or extensions.
134+
*
135+
* @param url - The URL to open
136+
* @param fallbackMessage - Optional message to show in error alert (defaults to "link")
137+
*
138+
* @example
139+
* ```tsx
140+
* // Open a meeting link in the default browser
141+
* await openInDefaultBrowser("https://meet.cal.com/abc123", "meeting link");
142+
* ```
143+
*/
144+
export const openInDefaultBrowser = async (
145+
url: string,
146+
fallbackMessage?: string
147+
): Promise<void> => {
148+
try {
149+
await Linking.openURL(url);
150+
} catch (error) {
151+
handleBrowserError(error, "openInDefaultBrowser", fallbackMessage);
110152
}
111153
};

companion/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
export { setGlobalToastFunction, showErrorAlert, showInfoAlert, showSuccessAlert } from "./alerts";
1515
// Browser utilities
1616
export { openInAppBrowser } from "./browser";
17+
export { openInDefaultBrowser } from "./browser";
1718
// Default location utilities
1819
export {
1920
type DefaultLocation,

0 commit comments

Comments
 (0)