Files
Peer RichelsenGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>DhairyashilDhairyashil ShindeVolnei Munhoz
8a7cf24f90 feat(companion): add upcoming bookings widget for iOS and Android (#27199)
* feat(companion): add upcoming bookings widget for iOS and Android

- Add iOS widget using @bacons/apple-targets with SwiftUI
- Add Android widget using react-native-android-widget
- Configure App Groups for iOS data sharing between app and widget
- Create widget sync hook to update widget data when bookings change
- Widget displays up to 5 upcoming bookings with title, time, and attendee
- Widget refreshes every 15 minutes and when app goes to background

Co-Authored-By: [email protected] <[email protected]>

* fix(companion): refactor useWidgetSync to avoid React Compiler limitation

Move conditional logic outside of try/catch block to fix React Compiler
error: 'Support value blocks (conditional, logical, optional chaining,
etc) within a try/catch statement'

Co-Authored-By: [email protected] <[email protected]>

* fix(companion): fix widget data sync issues

- Fix Swift widget to read data as JSON string (format used by react-native-shared-group-preferences)
- Add fallback to read as raw Data for compatibility
- Fix query key mismatch by checking multiple common filter combinations
- Ensure widget finds cached bookings regardless of which filter the app used

Co-Authored-By: [email protected] <[email protected]>

* fix(companion): use ExtensionStorage from @bacons/apple-targets for widget data

- Replace react-native-shared-group-preferences with ExtensionStorage
- ExtensionStorage properly stores data in iOS App Groups
- Add ExtensionStorage.reloadWidget() call to trigger widget refresh
- This ensures the widget picks up new data immediately

Co-Authored-By: [email protected] <[email protected]>

* feat(companion): add upcoming bookings widget for ios - all shapes (#27564)

* ios working widget

* fix(companion): remove sensitive data logging from widget storage

Remove console.log statements that were logging full widgetData payload
and formatted widget bookings containing attendee names and booking
details. This addresses security concerns identified by Cubic AI review
(confidence 9/10).

Co-Authored-By: unknown <>

* dark mode

* new look

* fix deeplink

* fix biome lint

* fix(companion): remove booking titles from console logs to avoid PII exposure

Co-Authored-By: unknown <>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* lock the versions and dev mode logging

* remove dynamic imports

* feat(companion): upcoming bookings widget android (#27689)

* companion-upcoming-bookings-widget-android

* fix(companion): remove stale relative countdowns and fix past-time clamping in Android widget

- Remove relative countdown text (In 15m, In 1h, Now) that becomes stale
  due to Android widget 30+ min update interval (Cubic violation 2)
- Remove Math.max(0, minutes) clamping that caused ended meetings to
  display as 'Now' indefinitely (Cubic violation 1)
- Simplify accent color logic to use startTimeISO directly
- Keep absolute time display (date + startTime) which remains accurate

Co-Authored-By: unknown <>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Dhairyashil <[email protected]>
Co-authored-by: Dhairyashil Shinde <[email protected]>
Co-authored-by: Volnei Munhoz <[email protected]>
2026-02-06 12:32:46 +00:00

467 lines
16 KiB
TypeScript

import "./_webRuntimeGuards";
import "../global.css";
import { DarkTheme, DefaultTheme, ThemeProvider } from "@react-navigation/native";
import { PortalHost } from "@rn-primitives/portal";
import { isLiquidGlassAvailable } from "expo-glass-effect";
import { Stack } from "expo-router";
import { Platform, StatusBar, useColorScheme, View } from "react-native";
import { CalComLogo } from "@/components/CalComLogo";
import LoginScreenComponent from "@/components/LoginScreen";
import { NetworkStatusBanner } from "@/components/NetworkStatusBanner";
import { GlobalToast } from "@/components/ui/GlobalToast";
import { WidgetSyncProvider } from "@/components/WidgetSyncProvider";
import { getColors, colors as semanticColors } from "@/constants/colors";
import { AuthProvider, useAuth } from "@/contexts/AuthContext";
import { QueryProvider } from "@/contexts/QueryContext";
import { ToastProvider } from "@/contexts/ToastContext";
function RootLayoutContent() {
const { isAuthenticated, loading } = useAuth();
const colorScheme = useColorScheme();
const isDark = colorScheme === "dark";
const colors = getColors(isDark);
// Show Cal.com logo while checking auth state to prevent login flash
if (loading) {
return (
<View
style={{
flex: 1,
backgroundColor: colors.background,
justifyContent: "center",
alignItems: "center",
}}
>
<StatusBar
barStyle={isDark ? "light-content" : "dark-content"}
backgroundColor={colors.background}
/>
<CalComLogo
width={120}
height={26}
color={isDark ? semanticColors.dark.text : semanticColors.light.text}
/>
</View>
);
}
const content = isAuthenticated ? (
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="profile-sheet"
options={{
headerShown: true,
headerTransparent: Platform.OS === "ios",
headerLargeTitle: false,
title: Platform.OS === "ios" ? "You" : "Profile",
presentation:
Platform.OS === "ios"
? isLiquidGlassAvailable()
? "formSheet"
: "modal"
: "containedModal",
// iOS-specific sheet options (ignored on Android)
sheetGrabberVisible: Platform.OS === "ios",
sheetAllowedDetents: Platform.OS === "ios" ? [0.6, 0.9] : undefined,
sheetInitialDetentIndex: Platform.OS === "ios" ? 0 : undefined,
contentStyle: {
backgroundColor:
Platform.OS === "ios" && isLiquidGlassAvailable() ? "transparent" : colors.background,
},
headerStyle: {
backgroundColor: Platform.OS === "ios" ? "transparent" : colors.background,
},
headerBlurEffect:
Platform.OS === "ios" && isLiquidGlassAvailable()
? undefined
: isDark
? "dark"
: "light",
}}
/>
<Stack.Screen
name="meeting-session-details"
options={{
headerShown: true,
headerTransparent: Platform.OS === "ios",
headerLargeTitle: false,
title: "Session Details",
presentation:
Platform.OS === "ios"
? isLiquidGlassAvailable()
? "formSheet"
: "modal"
: "containedModal",
// iOS-specific sheet options (ignored on Android)
sheetGrabberVisible: Platform.OS === "ios",
sheetAllowedDetents: Platform.OS === "ios" ? [0.7, 0.9] : undefined,
sheetInitialDetentIndex: Platform.OS === "ios" ? 0 : undefined,
contentStyle: {
backgroundColor:
Platform.OS === "ios" && isLiquidGlassAvailable()
? "transparent"
: colors.backgroundSecondary,
},
headerStyle: {
backgroundColor: Platform.OS === "ios" ? "transparent" : colors.backgroundSecondary,
},
headerBlurEffect:
Platform.OS === "ios" && isLiquidGlassAvailable()
? undefined
: isDark
? "dark"
: "light",
}}
/>
<Stack.Screen
name="edit-location"
options={{
headerShown: true,
headerTransparent: Platform.OS === "ios",
headerLargeTitle: false,
title: "Edit Location",
presentation:
Platform.OS === "ios"
? isLiquidGlassAvailable()
? "formSheet"
: "modal"
: "containedModal",
sheetGrabberVisible: Platform.OS === "ios",
sheetAllowedDetents: Platform.OS === "ios" ? [0.5, 0.7] : undefined,
sheetInitialDetentIndex: Platform.OS === "ios" ? 0 : undefined,
contentStyle: {
backgroundColor:
Platform.OS === "ios" && isLiquidGlassAvailable()
? "transparent"
: colors.backgroundSecondary,
},
headerStyle: {
backgroundColor: Platform.OS === "ios" ? "transparent" : colors.backgroundSecondary,
},
headerBlurEffect:
Platform.OS === "ios" && isLiquidGlassAvailable()
? undefined
: isDark
? "dark"
: "light",
}}
/>
<Stack.Screen
name="mark-no-show"
options={{
headerShown: true,
headerTransparent: Platform.OS === "ios",
headerLargeTitle: false,
title: "Mark No-Show",
presentation:
Platform.OS === "ios"
? isLiquidGlassAvailable()
? "formSheet"
: "modal"
: "containedModal",
sheetGrabberVisible: Platform.OS === "ios",
sheetAllowedDetents: Platform.OS === "ios" ? [0.4, 0.6] : undefined,
sheetInitialDetentIndex: Platform.OS === "ios" ? 0 : undefined,
contentStyle: {
backgroundColor:
Platform.OS === "ios" && isLiquidGlassAvailable()
? "transparent"
: colors.backgroundSecondary,
},
headerStyle: {
backgroundColor: Platform.OS === "ios" ? "transparent" : colors.backgroundSecondary,
},
headerBlurEffect:
Platform.OS === "ios" && isLiquidGlassAvailable()
? undefined
: isDark
? "dark"
: "light",
}}
/>
<Stack.Screen
name="view-recordings"
options={{
headerShown: true,
headerTransparent: Platform.OS === "ios",
headerLargeTitle: false,
title: "Recordings",
presentation:
Platform.OS === "ios"
? isLiquidGlassAvailable()
? "formSheet"
: "modal"
: "containedModal",
sheetGrabberVisible: Platform.OS === "ios",
sheetAllowedDetents: Platform.OS === "ios" ? [0.7, 0.9] : undefined,
sheetInitialDetentIndex: Platform.OS === "ios" ? 0 : undefined,
contentStyle: {
backgroundColor:
Platform.OS === "ios" && isLiquidGlassAvailable()
? "transparent"
: colors.backgroundSecondary,
},
headerStyle: {
backgroundColor: Platform.OS === "ios" ? "transparent" : colors.backgroundSecondary,
},
headerBlurEffect:
Platform.OS === "ios" && isLiquidGlassAvailable()
? undefined
: isDark
? "dark"
: "light",
}}
/>
<Stack.Screen
name="add-guests"
options={{
headerShown: true,
headerTransparent: Platform.OS === "ios",
headerLargeTitle: false,
title: "Add Guests",
presentation:
Platform.OS === "ios"
? isLiquidGlassAvailable()
? "formSheet"
: "modal"
: "containedModal",
sheetGrabberVisible: Platform.OS === "ios",
sheetAllowedDetents: Platform.OS === "ios" ? [0.6, 0.8] : undefined,
sheetInitialDetentIndex: Platform.OS === "ios" ? 0 : undefined,
contentStyle: {
backgroundColor:
Platform.OS === "ios" && isLiquidGlassAvailable()
? "transparent"
: colors.backgroundSecondary,
},
headerStyle: {
backgroundColor: Platform.OS === "ios" ? "transparent" : colors.backgroundSecondary,
},
headerBlurEffect:
Platform.OS === "ios" && isLiquidGlassAvailable()
? undefined
: isDark
? "dark"
: "light",
}}
/>
<Stack.Screen
name="reschedule"
options={{
headerShown: true,
headerTransparent: Platform.OS === "ios",
headerLargeTitle: false,
title: "Reschedule",
presentation:
Platform.OS === "ios"
? isLiquidGlassAvailable()
? "formSheet"
: "modal"
: "containedModal",
sheetGrabberVisible: Platform.OS === "ios",
sheetAllowedDetents: Platform.OS === "ios" ? [0.8, 0.95] : undefined,
sheetInitialDetentIndex: Platform.OS === "ios" ? 0 : undefined,
contentStyle: {
backgroundColor:
Platform.OS === "ios" && isLiquidGlassAvailable()
? "transparent"
: colors.backgroundSecondary,
},
headerStyle: {
backgroundColor: Platform.OS === "ios" ? "transparent" : colors.backgroundSecondary,
},
headerBlurEffect:
Platform.OS === "ios" && isLiquidGlassAvailable()
? undefined
: isDark
? "dark"
: "light",
}}
/>
<Stack.Screen
name="edit-availability-name"
options={{
headerShown: true,
headerTransparent: Platform.OS === "ios",
headerLargeTitle: false,
title: "Edit Name & Timezone",
presentation:
Platform.OS === "ios"
? isLiquidGlassAvailable()
? "formSheet"
: "modal"
: "containedModal",
sheetGrabberVisible: Platform.OS === "ios",
sheetAllowedDetents: Platform.OS === "ios" ? [0.5, 0.7] : undefined,
sheetInitialDetentIndex: Platform.OS === "ios" ? 0 : undefined,
contentStyle: {
backgroundColor:
Platform.OS === "ios" && isLiquidGlassAvailable()
? "transparent"
: colors.backgroundSecondary,
},
headerStyle: {
backgroundColor: Platform.OS === "ios" ? "transparent" : colors.backgroundSecondary,
},
headerBlurEffect:
Platform.OS === "ios" && isLiquidGlassAvailable()
? undefined
: isDark
? "dark"
: "light",
}}
/>
<Stack.Screen
name="edit-availability-hours"
options={{
headerShown: true,
headerTransparent: Platform.OS === "ios",
headerLargeTitle: false,
title: "Working Hours",
presentation:
Platform.OS === "ios"
? isLiquidGlassAvailable()
? "formSheet"
: "modal"
: "containedModal",
sheetGrabberVisible: Platform.OS === "ios",
sheetAllowedDetents: Platform.OS === "ios" ? [0.7, 1] : undefined,
sheetInitialDetentIndex: Platform.OS === "ios" ? 0 : undefined,
contentStyle: {
backgroundColor:
Platform.OS === "ios" && isLiquidGlassAvailable()
? "transparent"
: colors.backgroundSecondary,
},
headerStyle: {
backgroundColor: Platform.OS === "ios" ? "transparent" : colors.backgroundSecondary,
},
headerBlurEffect:
Platform.OS === "ios" && isLiquidGlassAvailable()
? undefined
: isDark
? "dark"
: "light",
}}
/>
<Stack.Screen
name="edit-availability-day"
options={{
headerShown: true,
headerTransparent: Platform.OS === "ios",
headerLargeTitle: false,
title: "Edit Day",
presentation:
Platform.OS === "ios"
? isLiquidGlassAvailable()
? "formSheet"
: "modal"
: "containedModal",
sheetGrabberVisible: Platform.OS === "ios",
sheetAllowedDetents: Platform.OS === "ios" ? [0.6, 0.9] : undefined,
sheetInitialDetentIndex: Platform.OS === "ios" ? 0 : undefined,
contentStyle: {
backgroundColor:
Platform.OS === "ios" && isLiquidGlassAvailable()
? "transparent"
: colors.backgroundSecondary,
},
headerStyle: {
backgroundColor: Platform.OS === "ios" ? "transparent" : colors.backgroundSecondary,
},
headerBlurEffect:
Platform.OS === "ios" && isLiquidGlassAvailable()
? undefined
: isDark
? "dark"
: "light",
}}
/>
<Stack.Screen
name="edit-availability-override"
options={{
headerShown: true,
headerTransparent: Platform.OS === "ios",
headerLargeTitle: false,
title: "Date Override",
presentation:
Platform.OS === "ios"
? isLiquidGlassAvailable()
? "formSheet"
: "modal"
: "containedModal",
sheetGrabberVisible: Platform.OS === "ios",
sheetAllowedDetents: Platform.OS === "ios" ? [0.7, 0.9] : undefined,
sheetInitialDetentIndex: Platform.OS === "ios" ? 0 : undefined,
contentStyle: {
backgroundColor:
Platform.OS === "ios" && isLiquidGlassAvailable()
? "transparent"
: colors.backgroundSecondary,
},
headerStyle: {
backgroundColor: Platform.OS === "ios" ? "transparent" : colors.backgroundSecondary,
},
headerBlurEffect:
Platform.OS === "ios" && isLiquidGlassAvailable()
? undefined
: isDark
? "dark"
: "light",
}}
/>
</Stack>
) : (
<LoginScreenComponent />
);
const containerClass =
Platform.OS === "web"
? "w-[400px] flex-1 flex-col self-end border-l border-gray-200 dark:border-gray-700 bg-white dark:bg-black"
: "bg-white dark:bg-black";
const containerStyle =
Platform.OS === "web"
? {
width: 400,
flex: 1,
display: "flex" as const,
flexDirection: "column" as const,
}
: { flex: 1 };
return (
<ThemeProvider value={isDark ? DarkTheme : DefaultTheme}>
<View
style={[containerStyle, Platform.OS === "web" && { pointerEvents: "auto" as const }]}
className={containerClass}
>
<StatusBar
barStyle={isDark ? "light-content" : "dark-content"}
backgroundColor={colors.background}
/>
{content}
<NetworkStatusBanner />
<PortalHost />
</View>
</ThemeProvider>
);
}
export default function RootLayout() {
return (
<QueryProvider>
<AuthProvider>
<ToastProvider>
<WidgetSyncProvider>
<RootLayoutContent />
</WidgetSyncProvider>
<GlobalToast />
</ToastProvider>
</AuthProvider>
</QueryProvider>
);
}