diff --git a/companion/app.json b/companion/app.json index 33da374fd2..9cb1e75570 100644 --- a/companion/app.json +++ b/companion/app.json @@ -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": { diff --git a/companion/app/(tabs)/(bookings)/_layout.tsx b/companion/app/(tabs)/(bookings)/_layout.tsx index 972e613628..47b53ec36e 100644 --- a/companion/app/(tabs)/(bookings)/_layout.tsx +++ b/companion/app/(tabs)/(bookings)/_layout.tsx @@ -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 ( diff --git a/companion/app/_layout.tsx b/companion/app/_layout.tsx index 5de4af4865..3bbee33423 100644 --- a/companion/app/_layout.tsx +++ b/companion/app/_layout.tsx @@ -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() { - + + + diff --git a/companion/bun.lock b/companion/bun.lock index 859c330275..b4bb2fba63 100644 --- a/companion/bun.lock +++ b/companion/bun.lock @@ -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=="], diff --git a/companion/components/WidgetSyncProvider.tsx b/companion/components/WidgetSyncProvider.tsx new file mode 100644 index 0000000000..864ccb4ede --- /dev/null +++ b/companion/components/WidgetSyncProvider.tsx @@ -0,0 +1,6 @@ +import { useWidgetSync } from "@/hooks/useWidgetSync"; + +export function WidgetSyncProvider({ children }: { children: React.ReactNode }) { + useWidgetSync(); + return <>{children}; +} diff --git a/companion/hooks/index.ts b/companion/hooks/index.ts index 25657f8d65..f40491509b 100644 --- a/companion/hooks/index.ts +++ b/companion/hooks/index.ts @@ -75,3 +75,5 @@ export { useUsername, useUserProfile, } from "./useUserProfile"; +// Widget sync hook +export { useWidgetSync } from "./useWidgetSync"; diff --git a/companion/hooks/useWidgetSync.ts b/companion/hooks/useWidgetSync.ts new file mode 100644 index 0000000000..c7747dc393 --- /dev/null +++ b/companion/hooks/useWidgetSync.ts @@ -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(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, + }; +} diff --git a/companion/index.js b/companion/index.js index 80d3d998f1..a84621eb20 100644 --- a/companion/index.js +++ b/companion/index.js @@ -1 +1,6 @@ +import { registerWidgetTaskHandler } from "react-native-android-widget"; +import { widgetTaskHandler } from "./widgets/widgetTaskHandler"; + +registerWidgetTaskHandler(widgetTaskHandler); + import "expo-router/entry"; diff --git a/companion/package.json b/companion/package.json index ad07557749..8277dbe773 100644 --- a/companion/package.json +++ b/companion/package.json @@ -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", diff --git a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@1x.png b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@1x.png new file mode 100644 index 0000000000..ae598628b0 Binary files /dev/null and b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@1x.png differ diff --git a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@2x.png b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@2x.png new file mode 100644 index 0000000000..ff9d54105a Binary files /dev/null and b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@2x.png differ diff --git a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@3x.png b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@3x.png new file mode 100644 index 0000000000..ba7913561e Binary files /dev/null and b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@3x.png differ diff --git a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@1x.png b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@1x.png new file mode 100644 index 0000000000..d2b59a92be Binary files /dev/null and b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@1x.png differ diff --git a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@2x.png b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@2x.png new file mode 100644 index 0000000000..597bdb4bd4 Binary files /dev/null and b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@2x.png differ diff --git a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@3x.png b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@3x.png new file mode 100644 index 0000000000..2f0e2b6213 Binary files /dev/null and b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@3x.png differ diff --git a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@1x.png b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@1x.png new file mode 100644 index 0000000000..ff9d54105a Binary files /dev/null and b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@1x.png differ diff --git a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@2x.png b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@2x.png new file mode 100644 index 0000000000..3b831397fe Binary files /dev/null and b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@2x.png differ diff --git a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@3x.png b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@3x.png new file mode 100644 index 0000000000..6cd063f2dc Binary files /dev/null and b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@3x.png differ diff --git a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-60x60@2x.png b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-60x60@2x.png new file mode 100644 index 0000000000..6cd063f2dc Binary files /dev/null and b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-60x60@2x.png differ diff --git a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-60x60@3x.png b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-60x60@3x.png new file mode 100644 index 0000000000..523900936e Binary files /dev/null and b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-60x60@3x.png differ diff --git a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-76x76@1x.png b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-76x76@1x.png new file mode 100644 index 0000000000..69908aedf3 Binary files /dev/null and b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-76x76@1x.png differ diff --git a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-76x76@2x.png b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-76x76@2x.png new file mode 100644 index 0000000000..04580c4483 Binary files /dev/null and b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-76x76@2x.png differ diff --git a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-83.5x83.5@2x.png b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-83.5x83.5@2x.png new file mode 100644 index 0000000000..74e8e26bb2 Binary files /dev/null and b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-83.5x83.5@2x.png differ diff --git a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/Contents.json b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..6188260aba --- /dev/null +++ b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -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" + } +} diff --git a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png new file mode 100644 index 0000000000..2732229faf Binary files /dev/null and b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png differ diff --git a/companion/targets/widget/Info.plist b/companion/targets/widget/Info.plist new file mode 100644 index 0000000000..5510804d0e --- /dev/null +++ b/companion/targets/widget/Info.plist @@ -0,0 +1,11 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + \ No newline at end of file diff --git a/companion/targets/widget/expo-target.config.js b/companion/targets/widget/expo-target.config.js new file mode 100644 index 0000000000..290dd3e41a --- /dev/null +++ b/companion/targets/widget/expo-target.config.js @@ -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"], + }, +}); diff --git a/companion/targets/widget/generated.entitlements b/companion/targets/widget/generated.entitlements new file mode 100644 index 0000000000..935884dc88 --- /dev/null +++ b/companion/targets/widget/generated.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.cal.companion + + + \ No newline at end of file diff --git a/companion/targets/widget/index.swift b/companion/targets/widget/index.swift new file mode 100644 index 0000000000..93caa87500 --- /dev/null +++ b/companion/targets/widget/index.swift @@ -0,0 +1,9 @@ +import WidgetKit +import SwiftUI + +@main +struct exportWidgets: WidgetBundle { + var body: some Widget { + UpcomingBookingsWidget() + } +} diff --git a/companion/targets/widget/widgets.swift b/companion/targets/widget/widgets.swift new file mode 100644 index 0000000000..56e00d58fa --- /dev/null +++ b/companion/targets/widget/widgets.swift @@ -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) -> 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) + ]) +} diff --git a/companion/utils/widgetStorage.android.ts b/companion/utils/widgetStorage.android.ts new file mode 100644 index 0000000000..f346e6f1f9 --- /dev/null +++ b/companion/utils/widgetStorage.android.ts @@ -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 { + 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 { + 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 { + 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"; diff --git a/companion/utils/widgetStorage.ios.ts b/companion/utils/widgetStorage.ios.ts new file mode 100644 index 0000000000..d145cb2892 --- /dev/null +++ b/companion/utils/widgetStorage.ios.ts @@ -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 { + // 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 { + 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 { + 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"; diff --git a/companion/utils/widgetStorage.shared.ts b/companion/utils/widgetStorage.shared.ts new file mode 100644 index 0000000000..3ba9f656ae --- /dev/null +++ b/companion/utils/widgetStorage.shared.ts @@ -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 { + 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(); + }; +} diff --git a/companion/utils/widgetStorage.ts b/companion/utils/widgetStorage.ts new file mode 100644 index 0000000000..1dc6d7f12b --- /dev/null +++ b/companion/utils/widgetStorage.ts @@ -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 { + console.warn("Widget updates are not supported on this platform"); +} + +export async function clearWidgetBookings(): Promise { + console.warn("Widget clearing is not supported on this platform"); +} diff --git a/companion/widgets/UpcomingBookingsWidget.tsx b/companion/widgets/UpcomingBookingsWidget.tsx new file mode 100644 index 0000000000..0df344eb95 --- /dev/null +++ b/companion/widgets/UpcomingBookingsWidget.tsx @@ -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 ( + + {/* Left accent bar */} + + + {/* Content */} + + {/* Title row with video icon */} + + + {booking.hasVideoCall && ( + + )} + + + {/* Date and time with countdown */} + + + + + {/* Attendee info with initial circle */} + {booking.attendeeName && ( + + {/* Initial circle */} + + + + + + )} + + + ); +} + +export function UpcomingBookingsWidget({ bookings }: WidgetProps) { + return ( + + {/* Header with calendar icon */} + + + + + + {bookings.length === 0 ? ( + + + + + ) : ( + + {bookings.slice(0, 4).map((booking) => ( + + ))} + + )} + + ); +} diff --git a/companion/widgets/index.ts b/companion/widgets/index.ts new file mode 100644 index 0000000000..63f3b9b6b5 --- /dev/null +++ b/companion/widgets/index.ts @@ -0,0 +1,2 @@ +export { UpcomingBookingsWidget } from "./UpcomingBookingsWidget"; +export { widgetTaskHandler } from "./widgetTaskHandler"; diff --git a/companion/widgets/widgetTaskHandler.tsx b/companion/widgets/widgetTaskHandler.tsx new file mode 100644 index 0000000000..85d227c64b --- /dev/null +++ b/companion/widgets/widgetTaskHandler.tsx @@ -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 { + 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(); + break; + } + + case "WIDGET_CLICK": { + break; + } + + case "WIDGET_DELETED": { + break; + } + + default: + break; + } +}