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>
This commit is contained in:
authored and GitHub committed 2026-02-06 12:32:46 +00:00
1 parent 617cdea683
commit 8a7cf24f90
37 files changed
+1455 -14

No files matched your search

+35 -6
View File
@@ -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>
+4 -1
View File
@@ -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>
+105 -7
View File
@@ -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}</>;
}
+2
View File
@@ -75,3 +75,5 @@ export {
useUsername,
useUserProfile,
} from "./useUserProfile";
// Widget sync hook
export { useWidgetSync } from "./useWidgetSync";
+155
View File
@@ -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,
};
}
+5
View File
@@ -1 +1,6 @@
import { registerWidgetTaskHandler } from "react-native-android-widget";
import { widgetTaskHandler } from "./widgets/widgetTaskHandler";
registerWidgetTaskHandler(widgetTaskHandler);
import "expo-router/entry";
+3
View File
@@ -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",
Binary file not shown.

After

Width:  |  Height:  |  Size: 583 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

+11
View File
@@ -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>
+9
View File
@@ -0,0 +1,9 @@
import WidgetKit
import SwiftUI
@main
struct exportWidgets: WidgetBundle {
var body: some Widget {
UpcomingBookingsWidget()
}
}
+431
View File
@@ -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)
])
}
+53
View File
@@ -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";
+54
View File
@@ -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";
+106
View File
@@ -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();
};
}
+26
View File
@@ -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>
);
}
+2
View File
@@ -0,0 +1,2 @@
export { UpcomingBookingsWidget } from "./UpcomingBookingsWidget";
export { widgetTaskHandler } from "./widgetTaskHandler";
+55
View File
@@ -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;
}
}