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: 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>
No files matched your search
@@ -2,7 +2,7 @@
|
||||
"expo": {
|
||||
"name": "Calcom",
|
||||
"slug": "calcom-companion",
|
||||
"scheme": "expo-wxt-app",
|
||||
"scheme": "calcom",
|
||||
"version": "1.0.3",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/icon.png",
|
||||
@@ -15,6 +15,7 @@
|
||||
},
|
||||
"ios": {
|
||||
"supportsTablet": false,
|
||||
"appleTeamId": "ADWKXDJV75",
|
||||
"deploymentTarget": "18.0",
|
||||
"bundleIdentifier": "com.cal.companion",
|
||||
"userInterfaceStyle": "automatic",
|
||||
@@ -22,7 +23,8 @@
|
||||
"ITSAppUsesNonExemptEncryption": false
|
||||
},
|
||||
"entitlements": {
|
||||
"com.apple.developer.default-data-protection": "NSFileProtectionComplete"
|
||||
"com.apple.developer.default-data-protection": "NSFileProtectionComplete",
|
||||
"com.apple.security.application-groups": ["group.com.cal.companion"]
|
||||
},
|
||||
"icon": "./assets/cal-logo.icon"
|
||||
},
|
||||
@@ -73,13 +75,40 @@
|
||||
}
|
||||
},
|
||||
"timePicker": {
|
||||
"background": { "light": "#ffffff" },
|
||||
"numbersBackgroundColor": { "light": "#ffffff" },
|
||||
"headerBackground": { "light": "#000000" },
|
||||
"numbersSelectorColor": { "light": "#000000" }
|
||||
"background": {
|
||||
"light": "#ffffff"
|
||||
},
|
||||
"numbersBackgroundColor": {
|
||||
"light": "#ffffff"
|
||||
},
|
||||
"headerBackground": {
|
||||
"light": "#000000"
|
||||
},
|
||||
"numbersSelectorColor": {
|
||||
"light": "#000000"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"@bacons/apple-targets",
|
||||
[
|
||||
"react-native-android-widget",
|
||||
{
|
||||
"widgets": [
|
||||
{
|
||||
"name": "UpcomingBookingsWidget",
|
||||
"label": "Upcoming Bookings",
|
||||
"minWidth": "180dp",
|
||||
"minHeight": "110dp",
|
||||
"description": "View your upcoming Cal.com bookings",
|
||||
"previewImage": "./assets/icon.png",
|
||||
"resizeMode": "horizontal|vertical",
|
||||
"targetCellWidth": 3,
|
||||
"targetCellHeight": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"experiments": {
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Stack } from "expo-router";
|
||||
import { Platform } from "react-native";
|
||||
|
||||
// Define initial route for deep linking
|
||||
// This ensures booking-detail has index as parent in the stack
|
||||
export const unstable_settings = {
|
||||
initialRouteName: "index",
|
||||
};
|
||||
|
||||
export default function BookingsLayout() {
|
||||
return (
|
||||
<Stack>
|
||||
|
||||
@@ -10,6 +10,7 @@ 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";
|
||||
@@ -454,7 +455,9 @@ export default function RootLayout() {
|
||||
<QueryProvider>
|
||||
<AuthProvider>
|
||||
<ToastProvider>
|
||||
<RootLayoutContent />
|
||||
<WidgetSyncProvider>
|
||||
<RootLayoutContent />
|
||||
</WidgetSyncProvider>
|
||||
<GlobalToast />
|
||||
</ToastProvider>
|
||||
</AuthProvider>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"": {
|
||||
"name": "cal-companion",
|
||||
"dependencies": {
|
||||
"@bacons/apple-targets": "^3.0.7",
|
||||
"@expo/ui": "0.2.0-canary-20251230-fc48ddc",
|
||||
"@expo/vector-icons": "15.0.3",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
@@ -40,6 +41,7 @@
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"react-native": "0.83.1",
|
||||
"react-native-android-widget": "^0.20.1",
|
||||
"react-native-gesture-handler": "2.28.0",
|
||||
"react-native-reanimated": "4.2.0",
|
||||
"react-native-safe-area-context": "5.6.2",
|
||||
@@ -268,6 +270,10 @@
|
||||
|
||||
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||
|
||||
"@bacons/apple-targets": ["@bacons/apple-targets@3.0.7", "", { "dependencies": { "@bacons/xcode": "1.0.0-alpha.27", "@react-native/normalize-colors": "^0.79.2", "debug": "^4.3.4", "glob": "^10.4.2" } }, "sha512-RpzofKGvll8BPrvgf1Xj5raoHffJRW2qfWl7Q9F/25eo6lZeRa9R5NRIZCpUYoVnsL7whCQV9NIp0/ai4O+Ejw=="],
|
||||
|
||||
"@bacons/xcode": ["@bacons/xcode@1.0.0-alpha.27", "", { "dependencies": { "@expo/plist": "^0.0.18", "debug": "^4.3.4", "uuid": "^8.3.2" } }, "sha512-WlemTiwpwwVH8IMvg4VBsyxGUuFWUfzeWKzxhU+dRxd8MHvnX7F9PlWCf7T0ZXmtjbl39iEgQB+/Tf3wuGJbyg=="],
|
||||
|
||||
"@biomejs/biome": ["@biomejs/biome@2.3.10", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.10", "@biomejs/cli-darwin-x64": "2.3.10", "@biomejs/cli-linux-arm64": "2.3.10", "@biomejs/cli-linux-arm64-musl": "2.3.10", "@biomejs/cli-linux-x64": "2.3.10", "@biomejs/cli-linux-x64-musl": "2.3.10", "@biomejs/cli-win32-arm64": "2.3.10", "@biomejs/cli-win32-x64": "2.3.10" }, "bin": { "biome": "bin/biome" } }, "sha512-/uWSUd1MHX2fjqNLHNL6zLYWBbrJeG412/8H7ESuK8ewoRoMPUgHDebqKrPTx/5n6f17Xzqc9hdg3MEqA5hXnQ=="],
|
||||
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-M6xUjtCVnNGFfK7HMNKa593nb7fwNm43fq1Mt71kpLpb+4mE7odO8W/oWVDyBVO4ackhresy1ZYO7OJcVo/B7w=="],
|
||||
@@ -404,7 +410,7 @@
|
||||
|
||||
"@expo/package-manager": ["@expo/package-manager@1.9.10-canary-20251230-fc48ddc", "", { "dependencies": { "@expo/json-file": "10.0.9-canary-20251230-fc48ddc", "@expo/spawn-async": "^1.7.2", "chalk": "^4.0.0", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "resolve-workspace-root": "^2.0.0" } }, "sha512-Yy74eMzqJKlk1H3B7w1geOyXOz15aOvjgUeRdgBikMj9lgBA6iPGER+cRObGiI5e2nE5eZYQxFOecV0kJawntg=="],
|
||||
|
||||
"@expo/plist": ["@expo/plist@0.4.9-canary-20251230-fc48ddc", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-I/x0ULbAtHRHgfW5oZ+rx5+qe9/Y6vA7yQZYZ3ip9e31g1kxpwxhcmHDhFBS62jJLrZ/PWIJ5N5LWQOlL66uYw=="],
|
||||
"@expo/plist": ["@expo/plist@0.0.18", "", { "dependencies": { "@xmldom/xmldom": "~0.7.0", "base64-js": "^1.2.3", "xmlbuilder": "^14.0.0" } }, "sha512-+48gRqUiz65R21CZ/IXa7RNBXgAI/uPSdvJqoN9x1hfL44DNbUoWHgHiEXTx7XelcATpDwNTz6sHLfy0iNqf+w=="],
|
||||
|
||||
"@expo/prebuild-config": ["@expo/prebuild-config@54.0.9-canary-20251230-fc48ddc", "", { "dependencies": { "@expo/config": "12.0.14-canary-20251230-fc48ddc", "@expo/config-plugins": "54.0.5-canary-20251230-fc48ddc", "@expo/config-types": "55.0.0-canary-20251230-fc48ddc", "@expo/image-utils": "0.8.9-canary-20251230-fc48ddc", "@expo/json-file": "10.0.9-canary-20251230-fc48ddc", "@react-native/normalize-colors": "0.83.1", "debug": "^4.3.1", "resolve-from": "^5.0.0", "semver": "^7.6.0", "xml2js": "0.6.0" }, "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc" } }, "sha512-fQTz858QMQqlx7v9rf75FOS+nJJ5EgFIVJVFfn4TVPYi9wPlLtOieEKVh3RK8ZKg9VTt81kp/dFpgcLS12AiGA=="],
|
||||
|
||||
@@ -446,6 +452,8 @@
|
||||
|
||||
"@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="],
|
||||
|
||||
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
||||
|
||||
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
|
||||
|
||||
"@isaacs/ttlcache": ["@isaacs/ttlcache@1.4.1", "", {}, "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA=="],
|
||||
@@ -484,6 +492,8 @@
|
||||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||
|
||||
"@pnpm/config.env-replace": ["@pnpm/config.env-replace@1.1.0", "", {}, "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w=="],
|
||||
|
||||
"@pnpm/network.ca-file": ["@pnpm/network.ca-file@1.0.2", "", { "dependencies": { "graceful-fs": "4.2.10" } }, "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA=="],
|
||||
@@ -576,7 +586,7 @@
|
||||
|
||||
"@react-native/js-polyfills": ["@react-native/js-polyfills@0.83.1", "", {}, "sha512-qgPpdWn/c5laA+3WoJ6Fak8uOm7CG50nBsLlPsF8kbT7rUHIVB9WaP6+GPsoKV/H15koW7jKuLRoNVT7c3Ht3w=="],
|
||||
|
||||
"@react-native/normalize-colors": ["@react-native/normalize-colors@0.83.1", "", {}, "sha512-84feABbmeWo1kg81726UOlMKAhcQyFXYz2SjRKYkS78QmfhVDhJ2o/ps1VjhFfBz0i/scDwT1XNv9GwmRIghkg=="],
|
||||
"@react-native/normalize-colors": ["@react-native/normalize-colors@0.79.7", "", {}, "sha512-RrvewhdanEWhlyrHNWGXGZCc6MY0JGpNgRzA8y6OomDz0JmlnlIsbBHbNpPnIrt9Jh2KaV10KTscD1Ry8xU9gQ=="],
|
||||
|
||||
"@react-native/virtualized-lists": ["@react-native/virtualized-lists@0.83.1", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^19.2.0", "react": "*", "react-native": "*" }, "optionalPeers": ["@types/react"] }, "sha512-MdmoAbQUTOdicCocm5XAFDJWsswxk7hxa6ALnm6Y88p01HFML0W593hAn6qOt9q6IM1KbAcebtH6oOd4gcQy8w=="],
|
||||
|
||||
@@ -744,7 +754,7 @@
|
||||
|
||||
"@wxt-dev/storage": ["@wxt-dev/storage@1.2.6", "", { "dependencies": { "@wxt-dev/browser": "^0.1.4", "async-mutex": "^0.5.0", "dequal": "^2.0.3" } }, "sha512-f6AknnpJvhNHW4s0WqwSGCuZAj0fjP3EVNPBO5kB30pY+3Zt/nqZGqJN6FgBLCSkYjPJ8VL1hNX5LMVmvxQoDw=="],
|
||||
|
||||
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="],
|
||||
"@xmldom/xmldom": ["@xmldom/xmldom@0.7.13", "", {}, "sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g=="],
|
||||
|
||||
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
||||
|
||||
@@ -1014,6 +1024,8 @@
|
||||
|
||||
"dotenv-expand": ["dotenv-expand@12.0.3", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA=="],
|
||||
|
||||
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="],
|
||||
@@ -1192,6 +1204,8 @@
|
||||
|
||||
"fontfaceobserver": ["fontfaceobserver@2.3.0", "", {}, "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg=="],
|
||||
|
||||
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||
|
||||
"form-data-encoder": ["form-data-encoder@4.1.0", "", {}, "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw=="],
|
||||
|
||||
"formdata-node": ["formdata-node@6.0.3", "", {}, "sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg=="],
|
||||
@@ -1228,7 +1242,7 @@
|
||||
|
||||
"giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
|
||||
|
||||
"glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||
"glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
@@ -1356,6 +1370,8 @@
|
||||
|
||||
"istanbul-lib-instrument": ["istanbul-lib-instrument@5.2.1", "", { "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.2.0", "semver": "^6.3.0" } }, "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg=="],
|
||||
|
||||
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
||||
|
||||
"jest-environment-node": ["jest-environment-node@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw=="],
|
||||
|
||||
"jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="],
|
||||
@@ -1624,6 +1640,8 @@
|
||||
|
||||
"package-json": ["package-json@10.0.1", "", { "dependencies": { "ky": "^1.2.0", "registry-auth-token": "^5.0.2", "registry-url": "^6.0.1", "semver": "^7.6.0" } }, "sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg=="],
|
||||
|
||||
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
||||
|
||||
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
||||
|
||||
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||
@@ -1642,7 +1660,7 @@
|
||||
|
||||
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||
|
||||
"path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="],
|
||||
"path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
@@ -1744,6 +1762,8 @@
|
||||
|
||||
"react-native": ["react-native@0.83.1", "", { "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.83.1", "@react-native/codegen": "0.83.1", "@react-native/community-cli-plugin": "0.83.1", "@react-native/gradle-plugin": "0.83.1", "@react-native/js-polyfills": "0.83.1", "@react-native/normalize-colors": "0.83.1", "@react-native/virtualized-lists": "0.83.1", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-jest": "^29.7.0", "babel-plugin-syntax-hermes-parser": "0.32.0", "base64-js": "^1.5.1", "commander": "^12.0.0", "flow-enums-runtime": "^0.0.6", "glob": "^7.1.1", "hermes-compiler": "0.14.0", "invariant": "^2.2.4", "jest-environment-node": "^29.7.0", "memoize-one": "^5.0.0", "metro-runtime": "^0.83.3", "metro-source-map": "^0.83.3", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.5", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.27.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "peerDependencies": { "@types/react": "^19.1.1", "react": "^19.2.0" }, "optionalPeers": ["@types/react"], "bin": { "react-native": "cli.js" } }, "sha512-mL1q5HPq5cWseVhWRLl+Fwvi5z1UO+3vGOpjr+sHFwcUletPRZ5Kv+d0tUfqHmvi73/53NjlQqX1Pyn4GguUfA=="],
|
||||
|
||||
"react-native-android-widget": ["react-native-android-widget@0.20.1", "", { "peerDependencies": { "expo": ">=54.0.0", "react": "*", "react-native": "*" }, "optionalPeers": ["expo"] }, "sha512-m3akQCyoG6XUH8OLHOyL3/Xxx/XXTXf9ZyFYmWvSQrv1YUaZ7kVjmeLIYnaNz+qQ9VrTAS9OygCxZQptqCGzjQ=="],
|
||||
|
||||
"react-native-css-interop": ["react-native-css-interop@0.2.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.22.15", "@babel/traverse": "^7.23.0", "@babel/types": "^7.23.0", "debug": "^4.3.7", "lightningcss": "~1.27.0", "semver": "^7.6.3" }, "peerDependencies": { "react": ">=18", "react-native": "*", "react-native-reanimated": ">=3.6.2", "tailwindcss": "~3" } }, "sha512-B88f5rIymJXmy1sNC/MhTkb3xxBej1KkuAt7TiT9iM7oXz3RM8Bn+7GUrfR02TvSgKm4cg2XiSuLEKYfKwNsjA=="],
|
||||
|
||||
"react-native-gesture-handler": ["react-native-gesture-handler@2.28.0", "", { "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A=="],
|
||||
@@ -1910,10 +1930,14 @@
|
||||
|
||||
"string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||
|
||||
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-bom": ["strip-bom@5.0.0", "", {}, "sha512-p+byADHF7SzEcVnLvc/r3uognM1hUhObuHXxJcgLCfD194XAkaLbjq3Wzb0N5G2tgIjH0dgT708Z51QxMeu60A=="],
|
||||
|
||||
"strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="],
|
||||
@@ -2094,6 +2118,8 @@
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
|
||||
|
||||
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"write-file-atomic": ["write-file-atomic@4.0.2", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" } }, "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg=="],
|
||||
@@ -2110,7 +2136,7 @@
|
||||
|
||||
"xml2js": ["xml2js@0.6.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w=="],
|
||||
|
||||
"xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="],
|
||||
"xmlbuilder": ["xmlbuilder@14.0.0", "", {}, "sha512-ts+B2rSe4fIckR6iquDjsKbQFK2NlUk6iG5nf14mDEyldgoc2nEKZ3jZWMPTxGQwVgToSjt6VGIho1H8/fNFTg=="],
|
||||
|
||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||
|
||||
@@ -2154,6 +2180,8 @@
|
||||
|
||||
"@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.15.2", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg=="],
|
||||
|
||||
"@expo/cli/@expo/plist": ["@expo/plist@0.4.9-canary-20251230-fc48ddc", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-I/x0ULbAtHRHgfW5oZ+rx5+qe9/Y6vA7yQZYZ3ip9e31g1kxpwxhcmHDhFBS62jJLrZ/PWIJ5N5LWQOlL66uYw=="],
|
||||
|
||||
"@expo/cli/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="],
|
||||
|
||||
"@expo/cli/glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="],
|
||||
@@ -2176,6 +2204,8 @@
|
||||
|
||||
"@expo/config/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"@expo/config-plugins/@expo/plist": ["@expo/plist@0.4.9-canary-20251230-fc48ddc", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-I/x0ULbAtHRHgfW5oZ+rx5+qe9/Y6vA7yQZYZ3ip9e31g1kxpwxhcmHDhFBS62jJLrZ/PWIJ5N5LWQOlL66uYw=="],
|
||||
|
||||
"@expo/config-plugins/glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="],
|
||||
|
||||
"@expo/config-plugins/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
@@ -2212,12 +2242,18 @@
|
||||
|
||||
"@expo/package-manager/ora": ["ora@3.4.0", "", { "dependencies": { "chalk": "^2.4.2", "cli-cursor": "^2.1.0", "cli-spinners": "^2.0.0", "log-symbols": "^2.2.0", "strip-ansi": "^5.2.0", "wcwidth": "^1.0.1" } }, "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg=="],
|
||||
|
||||
"@expo/prebuild-config/@react-native/normalize-colors": ["@react-native/normalize-colors@0.83.1", "", {}, "sha512-84feABbmeWo1kg81726UOlMKAhcQyFXYz2SjRKYkS78QmfhVDhJ2o/ps1VjhFfBz0i/scDwT1XNv9GwmRIghkg=="],
|
||||
|
||||
"@expo/prebuild-config/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"@expo/router-server/expo-font": ["expo-font@14.1.0-canary-20251230-fc48ddc", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc", "react": "*", "react-native": "*" } }, "sha512-OQdE9hB4y1N/Aub5S9c8GOlWGaxB6FIIbwxiCg74yMr5c61QlV9RN7a5QAWdIB8ghHlJMZ8HDH/R6taR5AOA2g=="],
|
||||
|
||||
"@expo/xcpretty/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="],
|
||||
|
||||
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||
|
||||
"@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
||||
|
||||
"@istanbuljs/load-nyc-config/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
|
||||
|
||||
"@istanbuljs/load-nyc-config/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
||||
@@ -2248,6 +2284,8 @@
|
||||
|
||||
"@react-native/babel-preset/@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ=="],
|
||||
|
||||
"@react-native/codegen/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||
|
||||
"@react-native/codegen/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="],
|
||||
|
||||
"@react-native/community-cli-plugin/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
@@ -2356,6 +2394,8 @@
|
||||
|
||||
"fx-runner/which": ["which@1.2.4", "", { "dependencies": { "is-absolute": "^0.1.7", "isexe": "^1.1.1" }, "bin": { "which": "./bin/which" } }, "sha512-zDRAqDSBudazdfM9zpiI30Fu9ve47htYXcGi3ln0wfKu2a7SmrT6F3VDoYONu//48V8Vz4TdCRNPjtvyRO3yBA=="],
|
||||
|
||||
"glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"global-directory/ini": ["ini@4.1.1", "", {}, "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g=="],
|
||||
|
||||
"global-dirs/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
|
||||
@@ -2436,7 +2476,11 @@
|
||||
|
||||
"parse-json/type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="],
|
||||
|
||||
"path-scurry/lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="],
|
||||
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"plist/@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="],
|
||||
|
||||
"plist/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="],
|
||||
|
||||
"postcss-load-config/lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
|
||||
|
||||
@@ -2452,10 +2496,14 @@
|
||||
|
||||
"rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
|
||||
|
||||
"react-native/@react-native/normalize-colors": ["@react-native/normalize-colors@0.83.1", "", {}, "sha512-84feABbmeWo1kg81726UOlMKAhcQyFXYz2SjRKYkS78QmfhVDhJ2o/ps1VjhFfBz0i/scDwT1XNv9GwmRIghkg=="],
|
||||
|
||||
"react-native/babel-plugin-syntax-hermes-parser": ["babel-plugin-syntax-hermes-parser@0.32.0", "", { "dependencies": { "hermes-parser": "0.32.0" } }, "sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg=="],
|
||||
|
||||
"react-native/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
|
||||
|
||||
"react-native/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||
|
||||
"react-native/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"react-native-css-interop/lightningcss": ["lightningcss@1.27.0", "", { "dependencies": { "detect-libc": "^1.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.27.0", "lightningcss-darwin-x64": "1.27.0", "lightningcss-freebsd-x64": "1.27.0", "lightningcss-linux-arm-gnueabihf": "1.27.0", "lightningcss-linux-arm64-gnu": "1.27.0", "lightningcss-linux-arm64-musl": "1.27.0", "lightningcss-linux-x64-gnu": "1.27.0", "lightningcss-linux-x64-musl": "1.27.0", "lightningcss-win32-arm64-msvc": "1.27.0", "lightningcss-win32-x64-msvc": "1.27.0" } }, "sha512-8f7aNmS1+etYSLHht0fQApPc2kNO8qGRutifN5rVIc6Xo6ABsEbqOr758UwI7ALVbTt4x1fllKt0PYgzD9S3yQ=="],
|
||||
@@ -2476,6 +2524,8 @@
|
||||
|
||||
"restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
||||
|
||||
"rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||
|
||||
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"serve-static/send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="],
|
||||
@@ -2492,6 +2542,10 @@
|
||||
|
||||
"stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
|
||||
|
||||
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
"strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
|
||||
@@ -2506,6 +2560,8 @@
|
||||
|
||||
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||
|
||||
"test-exclude/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||
|
||||
"tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"unimport/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||
@@ -2532,6 +2588,10 @@
|
||||
|
||||
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
|
||||
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"write-file-atomic/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"wxt/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||
@@ -2554,8 +2614,14 @@
|
||||
|
||||
"@babel/highlight/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="],
|
||||
|
||||
"@expo/cli/@expo/plist/@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="],
|
||||
|
||||
"@expo/cli/@expo/plist/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="],
|
||||
|
||||
"@expo/cli/glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
|
||||
|
||||
"@expo/cli/glob/path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="],
|
||||
|
||||
"@expo/cli/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"@expo/cli/ora/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="],
|
||||
@@ -2570,16 +2636,28 @@
|
||||
|
||||
"@expo/cli/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"@expo/config-plugins/@expo/plist/@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="],
|
||||
|
||||
"@expo/config-plugins/@expo/plist/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="],
|
||||
|
||||
"@expo/config-plugins/glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
|
||||
|
||||
"@expo/config-plugins/glob/path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="],
|
||||
|
||||
"@expo/config/glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
|
||||
|
||||
"@expo/config/glob/path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="],
|
||||
|
||||
"@expo/fingerprint/glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
|
||||
|
||||
"@expo/fingerprint/glob/path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="],
|
||||
|
||||
"@expo/fingerprint/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"@expo/metro-config/glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
|
||||
|
||||
"@expo/metro-config/glob/path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="],
|
||||
|
||||
"@expo/metro-config/hermes-parser/hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="],
|
||||
|
||||
"@expo/metro-config/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
@@ -2594,6 +2672,10 @@
|
||||
|
||||
"@expo/package-manager/ora/strip-ansi": ["strip-ansi@5.2.0", "", { "dependencies": { "ansi-regex": "^4.1.0" } }, "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA=="],
|
||||
|
||||
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
|
||||
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
|
||||
"@istanbuljs/load-nyc-config/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
||||
|
||||
"@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
|
||||
@@ -2604,6 +2686,8 @@
|
||||
|
||||
"@jest/types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"@react-native/babel-plugin-codegen/@react-native/codegen/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||
|
||||
"@react-native/babel-plugin-codegen/@react-native/codegen/hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="],
|
||||
|
||||
"@react-native/codegen/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="],
|
||||
@@ -2656,6 +2740,8 @@
|
||||
|
||||
"fx-runner/which/isexe": ["isexe@1.1.2", "", {}, "sha512-d2eJzK691yZwPHcv1LbeAOa91yMJ9QmfTgSO1oXB65ezVhXQsxBac2vEB4bMVms9cGzaA99n6V2viHMq82VLDw=="],
|
||||
|
||||
"glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"jest-environment-node/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"jest-haste-map/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
@@ -2710,6 +2796,8 @@
|
||||
|
||||
"terminal-link/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
|
||||
|
||||
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"wxt/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
"yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
@@ -2720,6 +2808,8 @@
|
||||
|
||||
"@babel/highlight/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
|
||||
|
||||
"@expo/cli/glob/path-scurry/lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="],
|
||||
|
||||
"@expo/cli/ora/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
|
||||
|
||||
"@expo/cli/ora/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
|
||||
@@ -2732,6 +2822,14 @@
|
||||
|
||||
"@expo/cli/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"@expo/config-plugins/glob/path-scurry/lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="],
|
||||
|
||||
"@expo/config/glob/path-scurry/lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="],
|
||||
|
||||
"@expo/fingerprint/glob/path-scurry/lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="],
|
||||
|
||||
"@expo/metro-config/glob/path-scurry/lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="],
|
||||
|
||||
"@expo/package-manager/ora/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
|
||||
|
||||
"@expo/package-manager/ora/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { useWidgetSync } from "@/hooks/useWidgetSync";
|
||||
|
||||
export function WidgetSyncProvider({ children }: { children: React.ReactNode }) {
|
||||
useWidgetSync();
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -75,3 +75,5 @@ export {
|
||||
useUsername,
|
||||
useUserProfile,
|
||||
} from "./useUserProfile";
|
||||
// Widget sync hook
|
||||
export { useWidgetSync } from "./useWidgetSync";
|
||||
@@ -0,0 +1,155 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -1 +1,6 @@
|
||||
import { registerWidgetTaskHandler } from "react-native-android-widget";
|
||||
import { widgetTaskHandler } from "./widgets/widgetTaskHandler";
|
||||
|
||||
registerWidgetTaskHandler(widgetTaskHandler);
|
||||
|
||||
import "expo-router/entry";
|
||||
@@ -8,6 +8,7 @@
|
||||
"android": "expo run:android",
|
||||
"ios": "expo run:ios",
|
||||
"web": "expo start --web",
|
||||
"prewidget": "EXPO_NO_GIT_STATUS=1 npx expo prebuild --template ./node_modules/@bacons/apple-targets/prebuild-blank.tgz --clean",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"typecheck:extension": "tsc -p tsconfig.extension.json --noEmit",
|
||||
"typecheck:all": "npm run typecheck && npm run typecheck:extension",
|
||||
@@ -49,6 +50,7 @@
|
||||
"check:ci": "biome ci ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@bacons/apple-targets": "^3.0.7",
|
||||
"@expo/ui": "0.2.0-canary-20251230-fc48ddc",
|
||||
"@expo/vector-icons": "15.0.3",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
@@ -85,6 +87,7 @@
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"react-native": "0.83.1",
|
||||
"react-native-android-widget": "^0.20.1",
|
||||
"react-native-gesture-handler": "2.28.0",
|
||||
"react-native-reanimated": "4.2.0",
|
||||
"react-native-safe-area-context": "5.6.2",
|
||||
|
||||
|
After Width: | Height: | Size: 583 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"size": "20x20",
|
||||
"scale": "2x",
|
||||
"filename": "App-Icon-20x20@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"size": "20x20",
|
||||
"scale": "3x",
|
||||
"filename": "App-Icon-20x20@3x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"size": "29x29",
|
||||
"scale": "1x",
|
||||
"filename": "App-Icon-29x29@1x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"size": "29x29",
|
||||
"scale": "2x",
|
||||
"filename": "App-Icon-29x29@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"size": "29x29",
|
||||
"scale": "3x",
|
||||
"filename": "App-Icon-29x29@3x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"size": "40x40",
|
||||
"scale": "2x",
|
||||
"filename": "App-Icon-40x40@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"size": "40x40",
|
||||
"scale": "3x",
|
||||
"filename": "App-Icon-40x40@3x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"size": "60x60",
|
||||
"scale": "2x",
|
||||
"filename": "App-Icon-60x60@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"size": "60x60",
|
||||
"scale": "3x",
|
||||
"filename": "App-Icon-60x60@3x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ipad",
|
||||
"size": "20x20",
|
||||
"scale": "1x",
|
||||
"filename": "App-Icon-20x20@1x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ipad",
|
||||
"size": "20x20",
|
||||
"scale": "2x",
|
||||
"filename": "App-Icon-20x20@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ipad",
|
||||
"size": "29x29",
|
||||
"scale": "1x",
|
||||
"filename": "App-Icon-29x29@1x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ipad",
|
||||
"size": "29x29",
|
||||
"scale": "2x",
|
||||
"filename": "App-Icon-29x29@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ipad",
|
||||
"size": "40x40",
|
||||
"scale": "1x",
|
||||
"filename": "App-Icon-40x40@1x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ipad",
|
||||
"size": "40x40",
|
||||
"scale": "2x",
|
||||
"filename": "App-Icon-40x40@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ipad",
|
||||
"size": "76x76",
|
||||
"scale": "1x",
|
||||
"filename": "App-Icon-76x76@1x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ipad",
|
||||
"size": "76x76",
|
||||
"scale": "2x",
|
||||
"filename": "App-Icon-76x76@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ipad",
|
||||
"size": "83.5x83.5",
|
||||
"scale": "2x",
|
||||
"filename": "App-Icon-83.5x83.5@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ios-marketing",
|
||||
"size": "1024x1024",
|
||||
"scale": "1x",
|
||||
"filename": "ItunesArtwork@2x.png"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "expo"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 58 KiB |
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,9 @@
|
||||
/** @type {import('@bacons/apple-targets/app.plugin').ConfigFunction} */
|
||||
module.exports = () => ({
|
||||
type: "widget",
|
||||
name: "UpcomingBookings",
|
||||
icon: "../../assets/icon.png",
|
||||
entitlements: {
|
||||
"com.apple.security.application-groups": ["group.com.cal.companion"],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.cal.companion</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,9 @@
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct exportWidgets: WidgetBundle {
|
||||
var body: some Widget {
|
||||
UpcomingBookingsWidget()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Data Models
|
||||
|
||||
struct BookingData: Codable, Identifiable {
|
||||
let id: String
|
||||
let title: String
|
||||
let date: String
|
||||
let startTime: String
|
||||
let endTime: String
|
||||
let startTimeISO: String? // For countdown calculation
|
||||
let attendeeName: String?
|
||||
let hostName: String?
|
||||
let location: String?
|
||||
let hasVideoCall: Bool?
|
||||
|
||||
// Calculate minutes until meeting starts
|
||||
var minutesUntilStart: Int? {
|
||||
guard let isoString = startTimeISO else { return nil }
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
|
||||
// Try with fractional seconds first, then without
|
||||
var startDate = formatter.date(from: isoString)
|
||||
if startDate == nil {
|
||||
formatter.formatOptions = [.withInternetDateTime]
|
||||
startDate = formatter.date(from: isoString)
|
||||
}
|
||||
|
||||
guard let start = startDate else { return nil }
|
||||
let minutes = Int(start.timeIntervalSinceNow / 60)
|
||||
return max(0, minutes)
|
||||
}
|
||||
|
||||
// Check if meeting is happening now
|
||||
var isHappeningNow: Bool {
|
||||
guard let minutes = minutesUntilStart else { return false }
|
||||
return minutes <= 0
|
||||
}
|
||||
|
||||
// Check if meeting is starting soon (within 30 min)
|
||||
var isStartingSoon: Bool {
|
||||
guard let minutes = minutesUntilStart else { return false }
|
||||
return minutes > 0 && minutes <= 30
|
||||
}
|
||||
}
|
||||
|
||||
struct WidgetData: Codable {
|
||||
let bookings: [BookingData]
|
||||
let lastUpdated: String?
|
||||
}
|
||||
|
||||
// MARK: - Timeline Provider
|
||||
|
||||
struct Provider: TimelineProvider {
|
||||
func placeholder(in context: Context) -> BookingEntry {
|
||||
BookingEntry(date: Date(), bookings: [
|
||||
BookingData(id: "1", title: "Team Meeting", date: "Mon, Feb 3", startTime: "10:00 AM", endTime: "11:00 AM", startTimeISO: nil, attendeeName: "John Doe", hostName: "You", location: nil, hasVideoCall: true)
|
||||
])
|
||||
}
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (BookingEntry) -> Void) {
|
||||
let entry = BookingEntry(date: Date(), bookings: loadBookings())
|
||||
completion(entry)
|
||||
}
|
||||
|
||||
func getTimeline(in context: Context, completion: @escaping (Timeline<BookingEntry>) -> Void) {
|
||||
let bookings = loadBookings()
|
||||
let entry = BookingEntry(date: Date(), bookings: bookings)
|
||||
// Refresh every 5 minutes for more accurate countdowns
|
||||
let nextUpdate = Calendar.current.date(byAdding: .minute, value: 5, to: Date())!
|
||||
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
|
||||
completion(timeline)
|
||||
}
|
||||
|
||||
private func loadBookings() -> [BookingData] {
|
||||
guard let userDefaults = UserDefaults(suiteName: "group.com.cal.companion") else {
|
||||
return []
|
||||
}
|
||||
|
||||
if let jsonString = userDefaults.string(forKey: "widgetBookings"),
|
||||
let data = jsonString.data(using: .utf8),
|
||||
let widgetData = try? JSONDecoder().decode(WidgetData.self, from: data) {
|
||||
return widgetData.bookings
|
||||
}
|
||||
|
||||
if let data = userDefaults.data(forKey: "widgetBookings"),
|
||||
let widgetData = try? JSONDecoder().decode(WidgetData.self, from: data) {
|
||||
return widgetData.bookings
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
struct BookingEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let bookings: [BookingData]
|
||||
}
|
||||
|
||||
// MARK: - Booking Row View
|
||||
|
||||
struct BookingRowView: View {
|
||||
let booking: BookingData
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
// Left accent bar (color based on urgency)
|
||||
RoundedRectangle(cornerRadius: 2)
|
||||
.fill(accentColor)
|
||||
.frame(width: 3)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
// Title row with video icon
|
||||
HStack(spacing: 4) {
|
||||
Text(booking.title)
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(1)
|
||||
|
||||
if booking.hasVideoCall == true {
|
||||
Image(systemName: "video.fill")
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
|
||||
// Date and time with countdown
|
||||
HStack(spacing: 4) {
|
||||
Text("\(booking.date) • \(booking.startTime)")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
if let countdown = countdownText {
|
||||
Text("•")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
Text(countdown)
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.foregroundColor(countdownColor)
|
||||
}
|
||||
}
|
||||
|
||||
// Attendee info
|
||||
if let attendee = booking.attendeeName {
|
||||
HStack(spacing: 4) {
|
||||
// Attendee initial circle
|
||||
Circle()
|
||||
.fill(Color.accentColor.opacity(0.2))
|
||||
.frame(width: 16, height: 16)
|
||||
.overlay(
|
||||
Text(String(attendee.prefix(1)).uppercased())
|
||||
.font(.system(size: 9, weight: .semibold))
|
||||
.foregroundColor(.accentColor)
|
||||
)
|
||||
Text(attendee)
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
|
||||
private var accentColor: Color {
|
||||
if booking.isHappeningNow {
|
||||
return .green
|
||||
} else if booking.isStartingSoon {
|
||||
return .orange
|
||||
}
|
||||
return .accentColor
|
||||
}
|
||||
|
||||
private var countdownText: String? {
|
||||
guard let minutes = booking.minutesUntilStart else { return nil }
|
||||
if minutes <= 0 {
|
||||
return "Now"
|
||||
} else if minutes < 60 {
|
||||
return "In \(minutes)m"
|
||||
} else if minutes < 1440 {
|
||||
let hours = minutes / 60
|
||||
return "In \(hours)h"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private var countdownColor: Color {
|
||||
guard let minutes = booking.minutesUntilStart else { return .secondary }
|
||||
if minutes <= 0 {
|
||||
return .green
|
||||
} else if minutes <= 30 {
|
||||
return .orange
|
||||
}
|
||||
return .secondary
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Widget Entry View
|
||||
|
||||
struct UpcomingBookingsWidgetEntryView: View {
|
||||
var entry: Provider.Entry
|
||||
@Environment(\.widgetFamily) var family
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// Subtle gradient background
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [
|
||||
Color.accentColor.opacity(0.05),
|
||||
Color.clear
|
||||
]),
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
// Header
|
||||
HStack {
|
||||
Image(systemName: "calendar")
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.foregroundColor(.accentColor)
|
||||
Text("Upcoming")
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.bottom, 8)
|
||||
|
||||
if entry.bookings.isEmpty {
|
||||
Spacer()
|
||||
VStack(spacing: 4) {
|
||||
Image(systemName: "calendar.badge.checkmark")
|
||||
.font(.system(size: 24))
|
||||
.foregroundColor(.secondary)
|
||||
Text("No upcoming bookings")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
Spacer()
|
||||
} else {
|
||||
if family == .systemMedium {
|
||||
// Horizontal layout for medium widget
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
ForEach(entry.bookings.prefix(2)) { booking in
|
||||
MediumBookingCard(booking: booking)
|
||||
}
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
} else {
|
||||
// Vertical layout for small and large widgets
|
||||
let maxBookings = family == .systemSmall ? 2 : 6
|
||||
ForEach(entry.bookings.prefix(maxBookings)) { booking in
|
||||
Link(destination: URL(string: "calcom://(tabs)/(bookings)/booking-detail?uid=\(booking.id)")!) {
|
||||
BookingRowView(booking: booking)
|
||||
}
|
||||
if booking.id != entry.bookings.prefix(maxBookings).last?.id {
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
}
|
||||
.widgetURL(URL(string: "calcom://(tabs)/(bookings)"))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Medium Widget Card
|
||||
|
||||
struct MediumBookingCard: View {
|
||||
let booking: BookingData
|
||||
|
||||
var body: some View {
|
||||
Link(destination: URL(string: "calcom://(tabs)/(bookings)/booking-detail?uid=\(booking.id)")!) {
|
||||
HStack(spacing: 6) {
|
||||
// Left accent bar
|
||||
RoundedRectangle(cornerRadius: 2)
|
||||
.fill(accentColor)
|
||||
.frame(width: 3)
|
||||
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
// Title with video icon
|
||||
HStack(spacing: 3) {
|
||||
Text(booking.title)
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(1)
|
||||
|
||||
if booking.hasVideoCall == true {
|
||||
Image(systemName: "video.fill")
|
||||
.font(.system(size: 8))
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
|
||||
// Date
|
||||
HStack(spacing: 3) {
|
||||
Image(systemName: "calendar")
|
||||
.font(.system(size: 9))
|
||||
.foregroundColor(.secondary)
|
||||
Text(booking.date)
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
// Time with countdown
|
||||
HStack(spacing: 3) {
|
||||
Image(systemName: "clock")
|
||||
.font(.system(size: 9))
|
||||
.foregroundColor(.secondary)
|
||||
Text(booking.startTime)
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
if let countdown = countdownText {
|
||||
Text("• \(countdown)")
|
||||
.font(.system(size: 10, weight: .medium))
|
||||
.foregroundColor(countdownColor)
|
||||
}
|
||||
}
|
||||
|
||||
// Attendee
|
||||
if let attendee = booking.attendeeName {
|
||||
HStack(spacing: 3) {
|
||||
Circle()
|
||||
.fill(Color.accentColor.opacity(0.2))
|
||||
.frame(width: 14, height: 14)
|
||||
.overlay(
|
||||
Text(String(attendee.prefix(1)).uppercased())
|
||||
.font(.system(size: 8, weight: .semibold))
|
||||
.foregroundColor(.accentColor)
|
||||
)
|
||||
Text(attendee)
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
private var accentColor: Color {
|
||||
if booking.isHappeningNow {
|
||||
return .green
|
||||
} else if booking.isStartingSoon {
|
||||
return .orange
|
||||
}
|
||||
return .accentColor
|
||||
}
|
||||
|
||||
private var countdownText: String? {
|
||||
guard let minutes = booking.minutesUntilStart else { return nil }
|
||||
if minutes <= 0 {
|
||||
return "Now"
|
||||
} else if minutes < 60 {
|
||||
return "In \(minutes)m"
|
||||
} else if minutes < 1440 {
|
||||
let hours = minutes / 60
|
||||
return "In \(hours)h"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private var countdownColor: Color {
|
||||
guard let minutes = booking.minutesUntilStart else { return .secondary }
|
||||
if minutes <= 0 {
|
||||
return .green
|
||||
} else if minutes <= 30 {
|
||||
return .orange
|
||||
}
|
||||
return .secondary
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Widget Configuration
|
||||
|
||||
struct UpcomingBookingsWidget: Widget {
|
||||
let kind: String = "UpcomingBookingsWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: Provider()) { entry in
|
||||
UpcomingBookingsWidgetEntryView(entry: entry)
|
||||
.containerBackground(.fill.tertiary, for: .widget)
|
||||
}
|
||||
.configurationDisplayName("Upcoming Bookings")
|
||||
.description("View your upcoming Cal.com bookings at a glance.")
|
||||
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview(as: .systemSmall) {
|
||||
UpcomingBookingsWidget()
|
||||
} timeline: {
|
||||
BookingEntry(date: .now, bookings: [
|
||||
BookingData(id: "1", title: "Team Standup", date: "Mon, Feb 3", startTime: "10:00 AM", endTime: "10:30 AM", startTimeISO: ISO8601DateFormatter().string(from: Date().addingTimeInterval(1800)), attendeeName: "John Doe", hostName: "You", location: nil, hasVideoCall: true),
|
||||
BookingData(id: "2", title: "Product Review", date: "Mon, Feb 3", startTime: "2:00 PM", endTime: "3:00 PM", startTimeISO: nil, attendeeName: "Jane Smith", hostName: "You", location: nil, hasVideoCall: false)
|
||||
])
|
||||
BookingEntry(date: .now, bookings: [])
|
||||
}
|
||||
|
||||
#Preview(as: .systemMedium) {
|
||||
UpcomingBookingsWidget()
|
||||
} timeline: {
|
||||
BookingEntry(date: .now, bookings: [
|
||||
BookingData(id: "1", title: "Team Standup", date: "Mon, Feb 3", startTime: "10:00 AM", endTime: "10:30 AM", startTimeISO: ISO8601DateFormatter().string(from: Date().addingTimeInterval(600)), attendeeName: "John Doe", hostName: "You", location: nil, hasVideoCall: true),
|
||||
BookingData(id: "2", title: "Product Review", date: "Mon, Feb 3", startTime: "2:00 PM", endTime: "3:00 PM", startTimeISO: nil, attendeeName: "Jane Smith", hostName: "You", location: "Zoom", hasVideoCall: true)
|
||||
])
|
||||
}
|
||||
|
||||
#Preview(as: .systemLarge) {
|
||||
UpcomingBookingsWidget()
|
||||
} timeline: {
|
||||
BookingEntry(date: .now, bookings: [
|
||||
BookingData(id: "1", title: "Team Standup", date: "Mon, Feb 3", startTime: "10:00 AM", endTime: "10:30 AM", startTimeISO: nil, attendeeName: "John Doe", hostName: "You", location: nil, hasVideoCall: true),
|
||||
BookingData(id: "2", title: "Product Review", date: "Mon, Feb 3", startTime: "2:00 PM", endTime: "3:00 PM", startTimeISO: nil, attendeeName: "Jane Smith", hostName: "You", location: nil, hasVideoCall: false),
|
||||
BookingData(id: "3", title: "Client Call", date: "Tue, Feb 4", startTime: "4:00 PM", endTime: "5:00 PM", startTimeISO: nil, attendeeName: "Bob Wilson", hostName: "You", location: nil, hasVideoCall: true),
|
||||
BookingData(id: "4", title: "Interview", date: "Tue, Feb 4", startTime: "5:30 PM", endTime: "6:00 PM", startTimeISO: nil, attendeeName: "Alice Johnson", hostName: "You", location: nil, hasVideoCall: false),
|
||||
BookingData(id: "5", title: "Wrap Up", date: "Wed, Feb 5", startTime: "6:00 PM", endTime: "6:15 PM", startTimeISO: nil, attendeeName: "Team", hostName: "You", location: nil, hasVideoCall: false)
|
||||
])
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { requestWidgetUpdate } from "react-native-android-widget";
|
||||
import { UpcomingBookingsWidget } from "@/widgets/UpcomingBookingsWidget";
|
||||
import {
|
||||
type BookingInput,
|
||||
type WidgetData,
|
||||
ANDROID_WIDGET_STORAGE_KEY,
|
||||
transformBookingsToWidgetData,
|
||||
} from "./widgetStorage.shared";
|
||||
|
||||
async function updateAndroidWidget(widgetData: WidgetData): Promise<void> {
|
||||
await AsyncStorage.setItem(ANDROID_WIDGET_STORAGE_KEY, JSON.stringify(widgetData));
|
||||
|
||||
try {
|
||||
await requestWidgetUpdate({
|
||||
widgetName: "UpcomingBookingsWidget",
|
||||
renderWidget: () => UpcomingBookingsWidget({ bookings: widgetData.bookings }),
|
||||
widgetNotFound: () => {
|
||||
console.warn("Widget not found on home screen");
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("Failed to request Android widget update:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateWidgetBookings(bookings: BookingInput[]): Promise<void> {
|
||||
if (__DEV__) {
|
||||
console.log("[Widget Debug] updateWidgetBookings called with", bookings.length, "bookings");
|
||||
}
|
||||
|
||||
try {
|
||||
const widgetData = transformBookingsToWidgetData(bookings);
|
||||
await updateAndroidWidget(widgetData);
|
||||
} catch (error) {
|
||||
console.warn("Failed to update widget bookings:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearWidgetBookings(): Promise<void> {
|
||||
try {
|
||||
const emptyData: WidgetData = {
|
||||
bookings: [],
|
||||
lastUpdated: new Date().toISOString(),
|
||||
};
|
||||
await updateAndroidWidget(emptyData);
|
||||
} catch (error) {
|
||||
console.warn("Failed to clear widget bookings:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-export shared utilities
|
||||
export * from "./widgetStorage.shared";
|
||||
@@ -0,0 +1,54 @@
|
||||
import { ExtensionStorage } from "@bacons/apple-targets";
|
||||
import {
|
||||
type BookingInput,
|
||||
type WidgetData,
|
||||
APP_GROUP_IDENTIFIER,
|
||||
WIDGET_BOOKINGS_KEY,
|
||||
transformBookingsToWidgetData,
|
||||
} from "./widgetStorage.shared";
|
||||
|
||||
const iosStorage = new ExtensionStorage(APP_GROUP_IDENTIFIER);
|
||||
|
||||
async function updateIOSWidget(widgetData: WidgetData): Promise<void> {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: ExtensionStorage.set() expects any for the value parameter
|
||||
iosStorage.set(WIDGET_BOOKINGS_KEY, widgetData as any);
|
||||
if (__DEV__) {
|
||||
console.log("[Widget Debug] Data written to ExtensionStorage with key:", WIDGET_BOOKINGS_KEY);
|
||||
}
|
||||
|
||||
ExtensionStorage.reloadWidget();
|
||||
if (__DEV__) {
|
||||
console.log("[Widget Debug] ExtensionStorage.reloadWidget() called");
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateWidgetBookings(bookings: BookingInput[]): Promise<void> {
|
||||
if (__DEV__) {
|
||||
console.log("[Widget Debug] updateWidgetBookings called with", bookings.length, "bookings");
|
||||
}
|
||||
|
||||
try {
|
||||
const widgetData = transformBookingsToWidgetData(bookings);
|
||||
if (__DEV__) {
|
||||
console.log("[Widget Debug] Platform is iOS, calling updateIOSWidget");
|
||||
}
|
||||
await updateIOSWidget(widgetData);
|
||||
} catch (error) {
|
||||
console.warn("Failed to update widget bookings:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearWidgetBookings(): Promise<void> {
|
||||
try {
|
||||
const emptyData: WidgetData = {
|
||||
bookings: [],
|
||||
lastUpdated: new Date().toISOString(),
|
||||
};
|
||||
await updateIOSWidget(emptyData);
|
||||
} catch (error) {
|
||||
console.warn("Failed to clear widget bookings:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-export shared utilities
|
||||
export * from "./widgetStorage.shared";
|
||||
@@ -0,0 +1,106 @@
|
||||
import { AppState, type AppStateStatus } from "react-native";
|
||||
|
||||
export const APP_GROUP_IDENTIFIER = "group.com.cal.companion";
|
||||
export const WIDGET_BOOKINGS_KEY = "widgetBookings";
|
||||
export const ANDROID_WIDGET_STORAGE_KEY = "android_widget_bookings";
|
||||
|
||||
export interface WidgetBookingData {
|
||||
id: string;
|
||||
title: string;
|
||||
date: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
startTimeISO: string;
|
||||
attendeeName: string | null;
|
||||
hostName: string | null;
|
||||
location: string | null;
|
||||
hasVideoCall: boolean;
|
||||
}
|
||||
|
||||
export interface WidgetData {
|
||||
bookings: WidgetBookingData[];
|
||||
lastUpdated: string | null;
|
||||
}
|
||||
|
||||
export function formatTime(dateString: string): string {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function formatDate(dateString: string): string {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString("en-US", {
|
||||
weekday: "short",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
export interface BookingInput {
|
||||
id: number;
|
||||
uid: string;
|
||||
title: string;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
start?: string;
|
||||
end?: string;
|
||||
attendees?: Array<{ name: string; email: string }>;
|
||||
hosts?: Array<{ name?: string; email?: string }>;
|
||||
user?: { name?: string; email?: string };
|
||||
location?: string;
|
||||
}
|
||||
|
||||
export function transformBookingsToWidgetData(bookings: BookingInput[]): WidgetData {
|
||||
const widgetBookings: WidgetBookingData[] = bookings.slice(0, 5).map((booking) => {
|
||||
const startTimeStr = booking.startTime || booking.start || "";
|
||||
const endTimeStr = booking.endTime || booking.end || "";
|
||||
const hostName = booking.hosts?.[0]?.name || booking.user?.name || null;
|
||||
const locationLower = (booking.location || "").toLowerCase();
|
||||
const hasVideoCall =
|
||||
locationLower.includes("zoom") ||
|
||||
locationLower.includes("meet") ||
|
||||
locationLower.includes("teams") ||
|
||||
locationLower.includes("webex") ||
|
||||
locationLower.includes("cal video");
|
||||
|
||||
return {
|
||||
id: booking.uid || String(booking.id),
|
||||
title: booking.title,
|
||||
date: formatDate(startTimeStr),
|
||||
startTime: formatTime(startTimeStr),
|
||||
endTime: formatTime(endTimeStr),
|
||||
startTimeISO: startTimeStr,
|
||||
attendeeName: booking.attendees?.[0]?.name || null,
|
||||
hostName: hostName,
|
||||
location: booking.location || null,
|
||||
hasVideoCall: hasVideoCall,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
bookings: widgetBookings,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
export function setupWidgetRefreshOnAppStateChange(
|
||||
fetchAndUpdateBookings: () => Promise<void>
|
||||
): () => void {
|
||||
const handleAppStateChange = (nextAppState: AppStateStatus) => {
|
||||
if (nextAppState === "background" || nextAppState === "inactive") {
|
||||
fetchAndUpdateBookings().catch((error) => {
|
||||
console.warn("Failed to refresh widget on app state change:", error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const subscription = AppState.addEventListener("change", handleAppStateChange);
|
||||
|
||||
return () => {
|
||||
subscription.remove();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// This file serves as the base implementation for platforms without specific extensions.
|
||||
// Metro bundler will prefer widgetStorage.ios.ts or widgetStorage.android.ts when available.
|
||||
|
||||
import type { BookingInput } from "./widgetStorage.shared";
|
||||
|
||||
// Re-export shared types and utilities for consumers
|
||||
export {
|
||||
type WidgetBookingData,
|
||||
type WidgetData,
|
||||
type BookingInput,
|
||||
setupWidgetRefreshOnAppStateChange,
|
||||
formatTime,
|
||||
formatDate,
|
||||
transformBookingsToWidgetData,
|
||||
APP_GROUP_IDENTIFIER,
|
||||
WIDGET_BOOKINGS_KEY,
|
||||
ANDROID_WIDGET_STORAGE_KEY,
|
||||
} from "./widgetStorage.shared";
|
||||
|
||||
export async function updateWidgetBookings(_bookings: BookingInput[]): Promise<void> {
|
||||
console.warn("Widget updates are not supported on this platform");
|
||||
}
|
||||
|
||||
export async function clearWidgetBookings(): Promise<void> {
|
||||
console.warn("Widget clearing is not supported on this platform");
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
"use no memo";
|
||||
import { FlexWidget, TextWidget } from "react-native-android-widget";
|
||||
|
||||
interface BookingData {
|
||||
id: string;
|
||||
title: string;
|
||||
date: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
startTimeISO?: string;
|
||||
attendeeName: string | null;
|
||||
hostName?: string | null;
|
||||
location?: string | null;
|
||||
hasVideoCall?: boolean;
|
||||
}
|
||||
|
||||
interface WidgetProps {
|
||||
bookings: BookingData[];
|
||||
}
|
||||
|
||||
// Get accent color based on start time proximity - returns HexColor type
|
||||
function getAccentColor(startTimeISO?: string): `#${string}` {
|
||||
if (!startTimeISO) return "#007AFF";
|
||||
try {
|
||||
const startDate = new Date(startTimeISO);
|
||||
const now = new Date();
|
||||
const minutes = Math.floor((startDate.getTime() - now.getTime()) / 60000);
|
||||
if (minutes <= 0) return "#007AFF"; // Blue - already started or past
|
||||
if (minutes <= 30) return "#FF9500"; // Orange - starting soon
|
||||
return "#007AFF";
|
||||
} catch {
|
||||
return "#007AFF";
|
||||
}
|
||||
}
|
||||
|
||||
// Get first initial from name
|
||||
function getInitial(name: string | null): string {
|
||||
if (!name) return "";
|
||||
return name.charAt(0).toUpperCase();
|
||||
}
|
||||
|
||||
function BookingItem({ booking }: { booking: BookingData }) {
|
||||
const accentColor = getAccentColor(booking.startTimeISO);
|
||||
|
||||
return (
|
||||
<FlexWidget
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 12,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#E5E5EA",
|
||||
}}
|
||||
clickAction="OPEN_URI"
|
||||
clickActionData={{
|
||||
uri: `calcom://(tabs)/(bookings)/booking-detail?uid=${booking.id}`,
|
||||
}}
|
||||
>
|
||||
{/* Left accent bar */}
|
||||
<FlexWidget
|
||||
style={{
|
||||
width: 3,
|
||||
borderRadius: 2,
|
||||
backgroundColor: accentColor,
|
||||
marginRight: 8,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<FlexWidget
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{/* Title row with video icon */}
|
||||
<FlexWidget
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<TextWidget
|
||||
text={booking.title}
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
color: "#000000",
|
||||
}}
|
||||
maxLines={1}
|
||||
/>
|
||||
{booking.hasVideoCall && (
|
||||
<TextWidget
|
||||
text=" 📹"
|
||||
style={{
|
||||
fontSize: 10,
|
||||
color: "#007AFF",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</FlexWidget>
|
||||
|
||||
{/* Date and time with countdown */}
|
||||
<FlexWidget
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginTop: 2,
|
||||
}}
|
||||
>
|
||||
<TextWidget
|
||||
text={`${booking.date} • ${booking.startTime}`}
|
||||
style={{
|
||||
fontSize: 11,
|
||||
color: "#8E8E93",
|
||||
}}
|
||||
/>
|
||||
</FlexWidget>
|
||||
|
||||
{/* Attendee info with initial circle */}
|
||||
{booking.attendeeName && (
|
||||
<FlexWidget
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
{/* Initial circle */}
|
||||
<FlexWidget
|
||||
style={{
|
||||
width: 16,
|
||||
height: 16,
|
||||
borderRadius: 8,
|
||||
backgroundColor: "#007AFF20",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginRight: 4,
|
||||
}}
|
||||
>
|
||||
<TextWidget
|
||||
text={getInitial(booking.attendeeName)}
|
||||
style={{
|
||||
fontSize: 9,
|
||||
fontWeight: "600",
|
||||
color: "#007AFF",
|
||||
}}
|
||||
/>
|
||||
</FlexWidget>
|
||||
<TextWidget
|
||||
text={booking.attendeeName}
|
||||
style={{
|
||||
fontSize: 11,
|
||||
color: "#8E8E93",
|
||||
}}
|
||||
maxLines={1}
|
||||
/>
|
||||
</FlexWidget>
|
||||
)}
|
||||
</FlexWidget>
|
||||
</FlexWidget>
|
||||
);
|
||||
}
|
||||
|
||||
export function UpcomingBookingsWidget({ bookings }: WidgetProps) {
|
||||
return (
|
||||
<FlexWidget
|
||||
style={{
|
||||
height: "match_parent",
|
||||
width: "match_parent",
|
||||
backgroundColor: "#FFFFFF",
|
||||
borderRadius: 16,
|
||||
flexDirection: "column",
|
||||
}}
|
||||
clickAction="OPEN_URI"
|
||||
clickActionData={{
|
||||
uri: "calcom://(tabs)/(bookings)",
|
||||
}}
|
||||
>
|
||||
{/* Header with calendar icon */}
|
||||
<FlexWidget
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
padding: 12,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#E5E5EA",
|
||||
}}
|
||||
>
|
||||
<TextWidget
|
||||
text="📅"
|
||||
style={{
|
||||
fontSize: 12,
|
||||
marginRight: 4,
|
||||
}}
|
||||
/>
|
||||
<TextWidget
|
||||
text="Upcoming"
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontWeight: "600",
|
||||
color: "#000000",
|
||||
}}
|
||||
/>
|
||||
</FlexWidget>
|
||||
|
||||
{bookings.length === 0 ? (
|
||||
<FlexWidget
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
padding: 16,
|
||||
}}
|
||||
>
|
||||
<TextWidget
|
||||
text="✓"
|
||||
style={{
|
||||
fontSize: 24,
|
||||
color: "#8E8E93",
|
||||
}}
|
||||
/>
|
||||
<TextWidget
|
||||
text="No upcoming bookings"
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: "#8E8E93",
|
||||
marginTop: 4,
|
||||
}}
|
||||
/>
|
||||
</FlexWidget>
|
||||
) : (
|
||||
<FlexWidget
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{bookings.slice(0, 4).map((booking) => (
|
||||
<BookingItem key={booking.id} booking={booking} />
|
||||
))}
|
||||
</FlexWidget>
|
||||
)}
|
||||
</FlexWidget>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { UpcomingBookingsWidget } from "./UpcomingBookingsWidget";
|
||||
export { widgetTaskHandler } from "./widgetTaskHandler";
|
||||
@@ -0,0 +1,55 @@
|
||||
"use no memo";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import type { WidgetTaskHandlerProps } from "react-native-android-widget";
|
||||
import { UpcomingBookingsWidget } from "./UpcomingBookingsWidget";
|
||||
|
||||
const WIDGET_STORAGE_KEY = "android_widget_bookings";
|
||||
|
||||
interface BookingData {
|
||||
id: string;
|
||||
title: string;
|
||||
date: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
startTimeISO?: string;
|
||||
attendeeName: string | null;
|
||||
hostName?: string | null;
|
||||
location?: string | null;
|
||||
hasVideoCall?: boolean;
|
||||
}
|
||||
|
||||
async function getStoredBookings(): Promise<BookingData[]> {
|
||||
try {
|
||||
const data = await AsyncStorage.getItem(WIDGET_STORAGE_KEY);
|
||||
if (data) {
|
||||
const parsed = JSON.parse(data);
|
||||
return parsed.bookings || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to get stored bookings for widget:", error);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function widgetTaskHandler(props: WidgetTaskHandlerProps) {
|
||||
switch (props.widgetAction) {
|
||||
case "WIDGET_ADDED":
|
||||
case "WIDGET_UPDATE":
|
||||
case "WIDGET_RESIZED": {
|
||||
const bookings = await getStoredBookings();
|
||||
props.renderWidget(<UpcomingBookingsWidget bookings={bookings} />);
|
||||
break;
|
||||
}
|
||||
|
||||
case "WIDGET_CLICK": {
|
||||
break;
|
||||
}
|
||||
|
||||
case "WIDGET_DELETED": {
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||