* 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: peer@cal.com <peer@cal.com> * 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: peer@cal.com <peer@cal.com> * 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: peer@cal.com <peer@cal.com> * 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: peer@cal.com <peer@cal.com> * 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 <dhairyashil10101010@gmail.com> Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com> Co-authored-by: Volnei Munhoz <volnei@cal.com>
156 lines
5.0 KiB
TypeScript
156 lines
5.0 KiB
TypeScript
import { useEffect, useCallback } from "react";
|
|
import { Platform } from "react-native";
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
import { queryKeys } from "@/config/cache.config";
|
|
import { CalComAPIService, type Booking } from "@/services/calcom";
|
|
import {
|
|
updateWidgetBookings,
|
|
clearWidgetBookings,
|
|
setupWidgetRefreshOnAppStateChange,
|
|
} from "@/utils/widgetStorage";
|
|
|
|
export function useWidgetSync() {
|
|
const queryClient = useQueryClient();
|
|
|
|
const syncBookingsToWidget = useCallback(async () => {
|
|
if (__DEV__) {
|
|
console.log("[Widget Debug] syncBookingsToWidget called, Platform:", Platform.OS);
|
|
}
|
|
|
|
if (Platform.OS === "web") {
|
|
if (__DEV__) {
|
|
console.log("[Widget Debug] Skipping - web platform");
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Try to get cached bookings from any of the common query keys used in the app
|
|
// The app uses different filter combinations, so we check multiple keys
|
|
const possibleFilters = [
|
|
{ status: ["upcoming"], limit: 50 },
|
|
{ status: ["upcoming"] },
|
|
{ status: ["upcoming", "unconfirmed"] },
|
|
{},
|
|
];
|
|
|
|
let cachedBookings: Booking[] | undefined;
|
|
for (const filters of possibleFilters) {
|
|
const cached = queryClient.getQueryData<Booking[]>(queryKeys.bookings.list(filters));
|
|
if (__DEV__) {
|
|
console.log(
|
|
"[Widget Debug] Checking cache for filters:",
|
|
JSON.stringify(filters),
|
|
"found:",
|
|
cached?.length ?? 0
|
|
);
|
|
}
|
|
if (cached !== undefined && cached.length > 0) {
|
|
cachedBookings = cached;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (cachedBookings !== undefined && cachedBookings.length > 0) {
|
|
if (__DEV__) {
|
|
console.log("[Widget Debug] Using cached bookings, count:", cachedBookings.length);
|
|
console.log("[Widget Debug] Current time (UTC):", new Date().toISOString());
|
|
}
|
|
|
|
// Log each booking's times for debugging - check both property name formats
|
|
if (__DEV__) {
|
|
cachedBookings.slice(0, 3).forEach((booking, i) => {
|
|
console.log(
|
|
`[Widget Debug] Booking ${i}: startTime=${booking.startTime}, start=${booking.start}, endTime=${booking.endTime}, end=${booking.end}`
|
|
);
|
|
});
|
|
}
|
|
|
|
try {
|
|
// Filter bookings that haven't ended yet (includes ongoing meetings)
|
|
// Sort by start time to show soonest first
|
|
// Note: API may return either start/end or startTime/endTime
|
|
const upcomingBookings = cachedBookings
|
|
.filter((booking) => {
|
|
const endTimeStr = booking.endTime || booking.end;
|
|
if (!endTimeStr) {
|
|
if (__DEV__) {
|
|
console.log(`[Widget Debug] Booking ${booking.id}: No end time found, skipping`);
|
|
}
|
|
return false;
|
|
}
|
|
const endTime = new Date(endTimeStr);
|
|
const isUpcoming = endTime > new Date();
|
|
if (__DEV__) {
|
|
console.log(
|
|
`[Widget Debug] Booking ${booking.id}: endTime=${endTime.toISOString()}, isUpcoming=${isUpcoming}`
|
|
);
|
|
}
|
|
return isUpcoming;
|
|
})
|
|
.sort((a, b) => {
|
|
const aStart = new Date(a.startTime || a.start || "").getTime();
|
|
const bStart = new Date(b.startTime || b.start || "").getTime();
|
|
return aStart - bStart;
|
|
});
|
|
|
|
if (__DEV__) {
|
|
console.log("[Widget Debug] Filtered upcoming bookings, count:", upcomingBookings.length);
|
|
}
|
|
await updateWidgetBookings(upcomingBookings);
|
|
} catch (error) {
|
|
console.warn("Failed to sync cached bookings to widget:", error);
|
|
}
|
|
} else {
|
|
// No cached data found, fetch from API
|
|
if (__DEV__) {
|
|
console.log("[Widget Debug] No cached data, fetching from API...");
|
|
}
|
|
try {
|
|
const bookings = await CalComAPIService.getBookings({
|
|
status: ["upcoming"],
|
|
limit: 10,
|
|
});
|
|
|
|
if (__DEV__) {
|
|
console.log("[Widget Debug] API returned bookings, count:", bookings.length);
|
|
}
|
|
|
|
// Filter bookings that haven't ended yet (includes ongoing meetings)
|
|
const upcomingBookings = bookings
|
|
.filter((booking) => {
|
|
const endTime = new Date(booking.endTime);
|
|
return endTime > new Date();
|
|
})
|
|
.sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime());
|
|
|
|
if (__DEV__) {
|
|
console.log(
|
|
"[Widget Debug] Filtered upcoming bookings from API, count:",
|
|
upcomingBookings.length
|
|
);
|
|
}
|
|
await updateWidgetBookings(upcomingBookings);
|
|
} catch (error) {
|
|
console.warn("Failed to fetch and sync bookings to widget:", error);
|
|
}
|
|
}
|
|
}, [queryClient]);
|
|
|
|
useEffect(() => {
|
|
if (Platform.OS === "web") {
|
|
return;
|
|
}
|
|
|
|
syncBookingsToWidget();
|
|
|
|
const cleanup = setupWidgetRefreshOnAppStateChange(syncBookingsToWidget);
|
|
|
|
return cleanup;
|
|
}, [syncBookingsToWidget]);
|
|
|
|
return {
|
|
syncBookingsToWidget,
|
|
clearWidgetBookings,
|
|
};
|
|
}
|