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:
co-authored by
peer@cal.com
Devin AI
Dhairyashil Shinde
parent
75cd509106
commit
2e9191fcad
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user