diff --git a/.easignore b/.easignore deleted file mode 100644 index fd9cae677a..0000000000 --- a/.easignore +++ /dev/null @@ -1,57 +0,0 @@ -# EAS Build ignore file for monorepo -# Only upload the companion app folder - -# =========================================== -# IGNORE EVERYTHING AT ROOT LEVEL -# =========================================== -/* - -# =========================================== -# EXCEPT THE COMPANION APP -# =========================================== -!companion - -# =========================================== -# BUT IGNORE THESE INSIDE COMPANION -# =========================================== - -# Native folders - EAS generates these during builds (~2.1 GB) -companion/ios -companion/android - -# Dependencies - EAS installs these during builds (~1.6 GB) -companion/node_modules - -# Expo build cache -companion/.expo -companion/dist -companion/web-build - -# Metro -companion/.metro-health-check* - -# WXT Chrome Extension outputs (not needed for mobile builds) -companion/.output -companion/.wxt -companion/dev - -# Kotlin cache -companion/.kotlin - -# Build artifacts -companion/*.jks -companion/*.p8 -companion/*.p12 -companion/*.key -companion/*.mobileprovision - -# Debug logs -companion/*.log - -# TypeScript build info -companion/*.tsbuildinfo - -# Environment files -companion/.env -companion/.env.* -!companion/.env.example diff --git a/.github/ISSUE_TEMPLATE/companion.md b/.github/ISSUE_TEMPLATE/companion.md deleted file mode 100644 index 6980da2d12..0000000000 --- a/.github/ISSUE_TEMPLATE/companion.md +++ /dev/null @@ -1,120 +0,0 @@ ---- -name: Companion (iOS/Android/Extension) -about: Report bugs or request features for mobile apps and browser extension -title: "" -labels: ["companion", "🚨 needs approval"] -assignees: "" ---- - -### Issue Type - - - -- [ ] šŸ› Bug Report -- [ ] ✨ Feature Request - -### Platform - - - -- [ ] šŸ“± iOS App -- [ ] šŸ¤– Android App -- [ ] 🧩 Browser Extension - -### Platform Details - - - -#### iOS -- **iOS Version**: (e.g., 17.2) -- **Device Model**: (e.g., iPhone 15 Pro) -- **App Version**: (e.g., 1.0.0) - -#### Android -- **Android Version**: (e.g., 14) -- **Device Model**: (e.g., Pixel 8) -- **App Version**: (e.g., 1.0.0) - -#### Browser Extension -- **Browser & Version**: (e.g., Chrome 120, Firefox 121) -- **Extension Version**: (e.g., 1.0.0) - ---- - -### Description - - - -(Write your description here.) - -### Steps to Reproduce (Bug Reports Only) - - - -1. Go to... -2. Click on... -3. See error... - -### Expected Behavior - - - -(Describe what you expected.) - -### Actual Behavior (Bug Reports Only) - - - -(Describe what actually happened.) - -### Proposed Solution (Feature Requests Only) - - - -(Describe your proposed solution.) - -### Screenshots / Evidence - - - -(Attach evidence here.) - -### Additional Context - - - -(Add any additional context here.) - ---- - -##### House rules - -- If this issue has a `🚨 needs approval` label, don't start coding yet. Wait until a core member approves the request by removing this label. -- Your ideas are invaluable! However, they undergo review to ensure alignment with the product's direction. -- Follow best practices in our [Contributor Docs](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md) diff --git a/.github/workflows/companion-build.yml b/.github/workflows/companion-build.yml deleted file mode 100644 index c0f10dc328..0000000000 --- a/.github/workflows/companion-build.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Companion Builds - -on: - workflow_call: - -permissions: - contents: read - -env: - EXPO_PUBLIC_CAL_API_KEY: ${{ secrets.CI_EXPO_PUBLIC_CAL_API_KEY || vars.CI_EXPO_PUBLIC_CAL_API_KEY || vars.EXPO_PUBLIC_CAL_API_KEY }} - -jobs: - build: - name: Build Companion App & Extension - runs-on: blacksmith-4vcpu-ubuntu-2404 - timeout-minutes: 30 - - steps: - - uses: actions/checkout@v4 - with: - sparse-checkout: .github - - uses: ./.github/actions/cache-checkout - - - name: Setup Bun - uses: oven-sh/setup-bun@v1 - with: - bun-version: 1.3.0 - cache: true - cache-dependency-path: companion/bun.lock - - - name: Install dependencies - working-directory: companion - run: bun install --frozen-lockfile - - - name: Build Expo web app - working-directory: companion - env: - EXPO_NO_TELEMETRY: 1 - CI: true - run: bunx expo export --platform web - - - name: Build browser extension - working-directory: companion - run: bun run ext:build diff --git a/.github/workflows/companion-lint.yml b/.github/workflows/companion-lint.yml deleted file mode 100644 index 6b4ad4d57f..0000000000 --- a/.github/workflows/companion-lint.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Companion Lint - -on: - workflow_call: - -permissions: - contents: read - -jobs: - lint: - name: Lint Companion App - runs-on: blacksmith-2vcpu-ubuntu-2404 - timeout-minutes: 10 - - steps: - - uses: actions/checkout@v4 - with: - sparse-checkout: .github - - uses: ./.github/actions/cache-checkout - - - name: Setup Bun - uses: oven-sh/setup-bun@v1 - with: - bun-version: 1.3.0 - cache: false - - - name: Install dependencies - working-directory: companion - run: bun install --frozen-lockfile - - - name: Run lint - working-directory: companion - run: bun run lint:all diff --git a/.github/workflows/companion-typecheck.yml b/.github/workflows/companion-typecheck.yml deleted file mode 100644 index 9911a9012c..0000000000 --- a/.github/workflows/companion-typecheck.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Companion Type Check - -on: - workflow_call: - -permissions: - contents: read - -jobs: - typecheck: - name: Type Check Companion App - runs-on: blacksmith-2vcpu-ubuntu-2404 - timeout-minutes: 10 - - steps: - - uses: actions/checkout@v4 - with: - sparse-checkout: .github - - uses: ./.github/actions/cache-checkout - - - name: Setup Bun - uses: oven-sh/setup-bun@v1 - with: - bun-version: 1.3.0 - cache: true - cache-dependency-path: companion/bun.lock - - - name: Install dependencies - working-directory: companion - run: bun install --frozen-lockfile - - - name: Type check - working-directory: companion - run: bun run typecheck:all diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index e6ddda105e..5dbd5aeffd 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -167,7 +167,6 @@ jobs: pull-requests: read outputs: has-files-requiring-all-checks: ${{ steps.filter-exclusions.outputs.has-files-requiring-all-checks }} - has-companion: ${{ steps.filter-inclusions.outputs.has-companion }} has-api-v2-changes: ${{ steps.filter-inclusions.outputs.has-api-v2-changes }} has-prisma-changes: ${{ steps.filter-inclusions.outputs.has-prisma-changes }} has-agents-changes: ${{ steps.filter-inclusions.outputs.has-agents-changes }} @@ -197,7 +196,6 @@ jobs: filters: | has-files-requiring-all-checks: - '**' - - '!companion/**' - '!.vscode/**' - '!**/*.md' - '!**/*.mdx' @@ -213,8 +211,6 @@ jobs: filters: | has-agents-changes: - "agents/**" - has-companion: - - "companion/**" has-api-v2-changes: - "apps/api/v2/**" - "packages/platform-constants/**" @@ -387,27 +383,6 @@ jobs: uses: ./.github/workflows/docs-build.yml secrets: inherit - build-companion: - name: Companion builds - needs: [prepare] - if: needs.prepare.outputs.has-companion == 'true' - uses: ./.github/workflows/companion-build.yml - secrets: inherit - - typecheck-companion: - name: Type Checks - needs: [prepare] - if: needs.prepare.outputs.has-companion == 'true' - uses: ./.github/workflows/companion-typecheck.yml - secrets: inherit - - lint-companion: - name: Linters - needs: [prepare] - if: needs.prepare.outputs.has-companion == 'true' - uses: ./.github/workflows/companion-lint.yml - secrets: inherit - build: name: Production builds needs: [prepare] @@ -489,9 +464,6 @@ jobs: build-api-v2, build-atoms, build-docs, - build-companion, - typecheck-companion, - lint-companion, setup-db, e2e, e2e-api-v2, @@ -540,14 +512,6 @@ jobs: needs.e2e-app-store.result != 'success' ) ) || - ( - needs.prepare.outputs.has-companion == 'true' && - ( - needs.build-companion.result != 'success' || - needs.typecheck-companion.result != 'success' || - needs.lint-companion.result != 'success' - ) - ) || ( needs.prepare.outputs.has-agents-changes == 'true' && needs.validate-agents-format.result != 'success' diff --git a/companion/.env.example b/companion/.env.example deleted file mode 100644 index 1dce561f4e..0000000000 --- a/companion/.env.example +++ /dev/null @@ -1,63 +0,0 @@ -# Cal.com OAuth Configuration -EXPO_PUBLIC_CALCOM_OAUTH_CLIENT_ID=your_oauth_client_id_here - -EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI=your_oauth_redirect_uri_here - -# ============================================================================= -# OAUTH REDIRECT URL FORMATS BY BROWSER -# ============================================================================= -# -# | Browser | Redirect URL Format | Notes | -# |---------|------------------------------------------------------------------|----------------------------------------------------| -# | Chrome | https://.chromiumapp.org/oauth/callback | Get ID from `chrome://extensions` | -# | Brave | https://.chromiumapp.org/oauth/callback | Same extension package as Chrome | -# | Edge | https://.chromiumapp.org/oauth/callback | Different ID from Chrome | -# | Firefox | https://.extensions.allizom.org/oauth/callback | UUID from `browser.identity.getRedirectURL()` | -# | Safari | https://..appextension/oauth/callback | Check Xcode project configuration | -# -# ============================================================================= - -# ============================================================================= -# FIREFOX OAUTH CONFIG -# ============================================================================= -EXPO_PUBLIC_CALCOM_OAUTH_CLIENT_ID_FIREFOX= -EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI_FIREFOX= - -# ============================================================================= -# SAFARI OAUTH CONFIG -# ============================================================================= -EXPO_PUBLIC_CALCOM_OAUTH_CLIENT_ID_SAFARI= -EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI_SAFARI= - -# ============================================================================= -# EDGE OAUTH CONFIG -# ============================================================================= -EXPO_PUBLIC_CALCOM_OAUTH_CLIENT_ID_EDGE= -EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI_EDGE= - -# =========================================== -# CACHE CONFIGURATION (all values in MINUTES) -# =========================================== - -# Default cache stale time for all queries (default: 5 minutes) -EXPO_PUBLIC_CACHE_STALE_TIME_MINUTES=5 - -# Garbage collection time - how long to keep unused cache (default: 1440 minutes - 24 hours) -EXPO_PUBLIC_CACHE_GC_TIME_MINUTES=1440 - -# Bookings cache stale time (default: 5 minutes) -# After this time, data is considered stale and will refetch in background -EXPO_PUBLIC_BOOKINGS_CACHE_STALE_TIME_MINUTES=5 - -# Event Types cache stale time (default: -1 = never stale) -# -1 means data only refreshes on mutations (create/update/delete) or manual pull-to-refresh -EXPO_PUBLIC_EVENT_TYPES_CACHE_STALE_TIME_MINUTES=-1 - -# Schedules (Availability) cache stale time (default: -1 = never stale) -EXPO_PUBLIC_SCHEDULES_CACHE_STALE_TIME_MINUTES=-1 - -# User Profile cache stale time (default: -1 = never stale) -EXPO_PUBLIC_USER_PROFILE_CACHE_STALE_TIME_MINUTES=-1 - -# required for dev/local -EXPO_PUBLIC_COMPANION_DEV_URL=http://localhost:8081 diff --git a/companion/.gitignore b/companion/.gitignore deleted file mode 100644 index 1277e7fc11..0000000000 --- a/companion/.gitignore +++ /dev/null @@ -1,68 +0,0 @@ -# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files - -# dependencies -node_modules/ -.yarn/ -.pnp.* - -# Expo -.expo/ -dist/ -web-build/ -expo-env.d.ts - -# Native -.kotlin/ -*.orig.* -*.jks -*.p8 -*.p12 -*.key -*.mobileprovision - -# Metro -.metro-health-check* - -# debug -npm-debug.* -yarn-debug.* -yarn-error.* -*.log - -# macOS -.DS_Store -*.pem -safari-extension/ - -# local env files -.env*.local -.env - -# typescript -*.tsbuildinfo - -# generated native folders -/ios -/android - -# WXT Chrome Extension build outputs -.output/ -.wxt/ -dev/ - -# Extension builds (generated at build time) -*.zip - -# IDE -.vscode/ -.idea/ -*.swp -*.swo -*~ - -# Testing -coverage/ -.nyc_output/ - -# Misc -*.code-workspace diff --git a/companion/.husky/pre-commit b/companion/.husky/pre-commit deleted file mode 100755 index 0874d0a148..0000000000 --- a/companion/.husky/pre-commit +++ /dev/null @@ -1,2 +0,0 @@ -cd companion -npx lint-staged diff --git a/companion/api/server.ts b/companion/api/server.ts deleted file mode 100644 index 4d07f5f9c7..0000000000 --- a/companion/api/server.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { createReadStream, existsSync, promises as fs } from "node:fs"; -import type { IncomingMessage, ServerResponse } from "node:http"; -import { extname, join, normalize } from "node:path"; - -const distDir = join(process.cwd(), "dist"); - -const mimeTypes: Record = { - ".html": "text/html; charset=utf-8", - ".js": "application/javascript; charset=utf-8", - ".css": "text/css; charset=utf-8", - ".json": "application/json; charset=utf-8", - ".png": "image/png", - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".svg": "image/svg+xml", - ".ico": "image/x-icon", - ".woff": "font/woff", - ".woff2": "font/woff2", - ".ttf": "font/ttf", - ".map": "application/json; charset=utf-8", -}; - -const getSafePath = (urlPath: string): string => { - const decodedPath = decodeURIComponent(urlPath.split("?")[0]); - const normalized = normalize(decodedPath).replace(/^(\.\.(\/|\\|$))+/, ""); - return normalized.startsWith("/") ? normalized : `/${normalized}`; -}; - -const resolveFilePath = async (safePath: string): Promise => { - let candidate = join(distDir, safePath); - try { - const stats = await fs.stat(candidate); - if (stats.isDirectory()) { - candidate = join(candidate, "index.html"); - } - return candidate; - } catch { - const fallback = join(distDir, "index.html"); - if (existsSync(fallback)) { - return fallback; - } - throw new Error("Dist folder missing. Run `npm run build` before deploying."); - } -}; - -const sendFile = (res: ServerResponse, filePath: string) => { - const ext = extname(filePath).toLowerCase(); - const contentType = mimeTypes[ext] || "application/octet-stream"; - res.setHeader("Content-Type", contentType); - res.setHeader( - "Cache-Control", - ext === ".html" ? "no-cache, no-store, must-revalidate" : "public, max-age=31536000, immutable" - ); - - const stream = createReadStream(filePath); - stream.on("error", (error) => { - console.error("Failed to read file"); - if (process.env.NODE_ENV !== "production") { - const message = error instanceof Error ? error.message : String(error); - const stack = error instanceof Error ? error.stack : undefined; - console.debug("[companion/api] Failed to read file", { message, stack }); - } - res.statusCode = 500; - res.end("Internal Server Error"); - }); - stream.pipe(res); -}; - -export default async function handler(req: IncomingMessage, res: ServerResponse) { - if (req.method && !["GET", "HEAD"].includes(req.method)) { - res.statusCode = 405; - res.setHeader("Allow", "GET, HEAD"); - res.end("Method Not Allowed"); - return; - } - - try { - const urlPath = req.url ?? "/"; - const safePath = getSafePath(urlPath); - const filePath = await resolveFilePath(safePath); - sendFile(res, filePath); - } catch (error) { - console.error("Server render error"); - if (process.env.NODE_ENV !== "production") { - const message = error instanceof Error ? error.message : String(error); - const stack = error instanceof Error ? error.stack : undefined; - console.debug("[companion/api] Server render error", { message, stack }); - } - res.statusCode = 500; - res.end("Internal Server Error"); - } -} diff --git a/companion/app.json b/companion/app.json deleted file mode 100644 index 6238a8ef1b..0000000000 --- a/companion/app.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "expo": { - "name": "Calcom", - "slug": "calcom-companion", - "scheme": "calcom", - "version": "1.0.4", - "orientation": "portrait", - "icon": "./assets/icon.png", - "userInterfaceStyle": "automatic", - "newArchEnabled": true, - "splash": { - "image": "./assets/splash-icon.png", - "resizeMode": "contain", - "backgroundColor": "#ffffff" - }, - "ios": { - "supportsTablet": false, - "appleTeamId": "ADWKXDJV75", - "deploymentTarget": "18.0", - "bundleIdentifier": "com.cal.companion", - "userInterfaceStyle": "automatic", - "infoPlist": { - "ITSAppUsesNonExemptEncryption": false - }, - "entitlements": { - "com.apple.developer.default-data-protection": "NSFileProtectionComplete", - "com.apple.security.application-groups": ["group.com.cal.companion"] - }, - "icon": "./assets/cal-logo.icon" - }, - "android": { - "adaptiveIcon": { - "foregroundImage": "./assets/adaptive-icon.png", - "backgroundColor": "#ffffff" - }, - "edgeToEdgeEnabled": true, - "package": "com.calcom.companion", - "softwareKeyboardLayoutMode": "pan", - "usesCleartextTraffic": false, - "userInterfaceStyle": "automatic" - }, - "web": { - "favicon": "./assets/favicon.png", - "bundler": "metro" - }, - "extra": { - "eas": { - "projectId": "f5e97f6f-1e95-44ac-bfa4-f737ef90f198" - } - }, - "owner": "calcoms-organization", - "plugins": [ - "expo-router", - "expo-secure-store", - "expo-web-browser", - "expo-image", - [ - "expo-splash-screen", - { - "backgroundColor": "#ffffff", - "image": "./assets/splash-icon.png", - "imageWidth": 200 - } - ], - [ - "@react-native-community/datetimepicker", - { - "android": { - "datePicker": { - "colorAccent": { - "light": "#000000" - }, - "textColorPrimary": { - "light": "#000000" - } - }, - "timePicker": { - "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/widget-preview.png", - "resizeMode": "horizontal|vertical", - "targetCellWidth": 3, - "targetCellHeight": 2 - } - ] - } - ] - ], - "experiments": { - "typedRoutes": true, - "reactCompiler": true - } - } -} diff --git a/companion/app/(tabs)/(availability)/_layout.tsx b/companion/app/(tabs)/(availability)/_layout.tsx deleted file mode 100644 index f7da32be7a..0000000000 --- a/companion/app/(tabs)/(availability)/_layout.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Stack } from "expo-router"; - -export default function AvailabilityLayout() { - return ( - - - - - ); -} diff --git a/companion/app/(tabs)/(availability)/availability-detail.ios.tsx b/companion/app/(tabs)/(availability)/availability-detail.ios.tsx deleted file mode 100644 index d6ff9ed2a1..0000000000 --- a/companion/app/(tabs)/(availability)/availability-detail.ios.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { useColorScheme } from "react-native"; -import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import { useCallback, useRef } from "react"; -import { - AvailabilityDetailScreen, - type AvailabilityDetailScreenHandle, -} from "@/components/screens/AvailabilityDetailScreen"; -import { getColors } from "@/constants/colors"; - -// Type for action handlers exposed by AvailabilityDetailScreen.ios.tsx -type ActionHandlers = { - handleSetAsDefault: () => void; - handleDelete: () => void; -}; - -export default function AvailabilityDetailIOS() { - const { id } = useLocalSearchParams<{ id: string }>(); - const router = useRouter(); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - - // Ref to store action handlers from AvailabilityDetailScreen - const actionHandlersRef = useRef(null); - const screenRef = useRef(null); - - // Callback to receive action handlers from AvailabilityDetailScreen - const handleActionsReady = useCallback((handlers: ActionHandlers) => { - actionHandlersRef.current = handlers; - }, []); - - // Navigation handlers for edit bottom sheets - const handleEditNameAndTimezone = useCallback(() => { - router.push(`/edit-availability-name?id=${id}` as never); - }, [router, id]); - - const handleEditWorkingHours = useCallback(() => { - router.push(`/edit-availability-hours?id=${id}` as never); - }, [router, id]); - - const handleEditOverride = useCallback(() => { - router.push(`/edit-availability-override?id=${id}` as never); - }, [router, id]); - - // Action handlers for inline actions - const handleSetAsDefault = useCallback(() => { - if (actionHandlersRef.current?.handleSetAsDefault) { - actionHandlersRef.current.handleSetAsDefault(); - } else if (screenRef.current?.setAsDefault) { - screenRef.current.setAsDefault(); - } - }, []); - - const handleDelete = useCallback(() => { - if (actionHandlersRef.current?.handleDelete) { - actionHandlersRef.current.handleDelete(); - } else if (screenRef.current?.delete) { - screenRef.current.delete(); - } - }, []); - - if (!id) { - return null; - } - - return ( - <> - - - - - {/* Edit Menu */} - - Edit - - {/* Name and Timezone */} - - Name and Timezone - - - {/* Working Hours */} - - Working Hours - - - {/* Date Override */} - - Date Override - - - {/* Set as Default */} - - Set as Default - - - {/* Delete */} - - Delete Schedule - - - - - - - - ); -} diff --git a/companion/app/(tabs)/(availability)/availability-detail.tsx b/companion/app/(tabs)/(availability)/availability-detail.tsx deleted file mode 100644 index 81423021eb..0000000000 --- a/companion/app/(tabs)/(availability)/availability-detail.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import { useCallback, useRef } from "react"; -import { Text, useColorScheme } from "react-native"; -import { - AvailabilityDetailScreen, - type AvailabilityDetailScreenHandle, -} from "@/components/screens/AvailabilityDetailScreen"; -import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { getColors } from "@/constants/colors"; -import { AppPressable } from "@/components/AppPressable"; - -// Type for action handlers exposed by AvailabilityDetailScreen -type ActionHandlers = { - handleSetAsDefault: () => void; - handleDelete: () => void; -}; - -export default function AvailabilityDetail() { - const { id } = useLocalSearchParams<{ id: string }>(); - const router = useRouter(); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - - // Ref to store action handlers from AvailabilityDetailScreen - const actionHandlersRef = useRef(null); - const screenRef = useRef(null); - - // Callback to receive action handlers from AvailabilityDetailScreen - const handleActionsReady = useCallback((handlers: ActionHandlers) => { - actionHandlersRef.current = handlers; - }, []); - - // Navigation handlers for edit modal screens - const handleEditNameAndTimezone = useCallback(() => { - router.push(`/edit-availability-name?id=${id}` as never); - }, [router, id]); - - const handleEditWorkingHours = useCallback(() => { - router.push(`/edit-availability-hours?id=${id}` as never); - }, [router, id]); - - const handleEditOverride = useCallback(() => { - router.push(`/edit-availability-override?id=${id}` as never); - }, [router, id]); - - // Action handlers for inline actions - const handleSetAsDefault = useCallback(() => { - if (actionHandlersRef.current?.handleSetAsDefault) { - actionHandlersRef.current.handleSetAsDefault(); - } else if (screenRef.current?.setAsDefault) { - screenRef.current.setAsDefault(); - } - }, []); - - const handleDelete = useCallback(() => { - if (actionHandlersRef.current?.handleDelete) { - actionHandlersRef.current.handleDelete(); - } else if (screenRef.current?.delete) { - screenRef.current.delete(); - } - }, []); - - if (!id) { - return null; - } - - return ( - <> - ( - - router.back()} - className="mr-2 h-10 flex-row items-center justify-center rounded-full border border-[#E5E5E5] bg-white px-3 dark:border-[#262626] dark:bg-[#171717]" - > - - - Availability - - - - ), - headerRight: () => ( - - - - - - Edit - - - - - - - - Name and Timezone - - - - - - Working Hours - - - - - - Date Override - - - - - - - Set as Default - - - - - - - Delete Schedule - - - - - - ), - }} - /> - - - - ); -} diff --git a/companion/app/(tabs)/(availability)/index.tsx b/companion/app/(tabs)/(availability)/index.tsx deleted file mode 100644 index 5d064aabbf..0000000000 --- a/companion/app/(tabs)/(availability)/index.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { isLiquidGlassAvailable } from "expo-glass-effect"; -import { Image } from "expo-image"; -import { Stack, useRouter } from "expo-router"; -import { useState } from "react"; -import { Alert, Platform, Pressable, useColorScheme } from "react-native"; -import { AvailabilityListScreen } from "@/components/screens/AvailabilityListScreen"; -import { useCreateSchedule, useUserProfile } from "@/hooks"; -import { CalComAPIService } from "@/services/calcom"; -import { showErrorAlert } from "@/utils/alerts"; -import { getAvatarUrl } from "@/utils/getAvatarUrl"; - -export default function Availability() { - const router = useRouter(); - const [searchQuery, setSearchQuery] = useState(""); - const [showCreateModal, setShowCreateModal] = useState(false); - const { mutate: createScheduleMutation } = useCreateSchedule(); - const { data: userProfile } = useUserProfile(); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - - const handleCreateNew = () => { - // Use native iOS Alert.prompt for a native look - Alert.prompt( - "Add a new schedule", - "Create a new availability schedule.", - [ - { text: "Cancel", style: "cancel" }, - { - text: "Continue", - onPress: async (name?: string) => { - if (!name?.trim()) { - Alert.alert("Error", "Please enter a schedule name"); - return; - } - - // Get user's timezone (default to America/New_York if not available) - let userTimezone = "America/New_York"; - try { - const userProfile = await CalComAPIService.getUserProfile(); - if (userProfile.timeZone) { - userTimezone = userProfile.timeZone; - } - } catch { - console.log("Could not get user timezone, using default"); - } - - // Create schedule with Monday-Friday 9 AM - 5 PM default - createScheduleMutation( - { - name: name.trim(), - timeZone: userTimezone, - isDefault: false, - availability: [ - { - days: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"], - startTime: "09:00", - endTime: "17:00", - }, - ], - }, - { - onSuccess: (newSchedule) => { - // Navigate to edit the newly created schedule - router.push({ - pathname: "/(tabs)/(availability)/availability-detail", - params: { - id: newSchedule.id.toString(), - }, - }); - }, - onError: (error) => { - const message = error instanceof Error ? error.message : String(error); - console.error("Failed to create schedule", message); - if (__DEV__) { - const stack = error instanceof Error ? error.stack : undefined; - console.debug("[Availability] createSchedule failed", { - message, - stack, - }); - } - showErrorAlert("Error", "Failed to create schedule. Please try again."); - }, - } - ); - }, - }, - ], - "plain-text", - "", - "default" - ); - }; - - return ( - <> - - - - - ); -} diff --git a/companion/app/(tabs)/(bookings)/_layout.tsx b/companion/app/(tabs)/(bookings)/_layout.tsx deleted file mode 100644 index 47b53ec36e..0000000000 --- a/companion/app/(tabs)/(bookings)/_layout.tsx +++ /dev/null @@ -1,17 +0,0 @@ -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/(tabs)/(bookings)/booking-detail.ios.tsx b/companion/app/(tabs)/(bookings)/booking-detail.ios.tsx deleted file mode 100644 index bf1b60c219..0000000000 --- a/companion/app/(tabs)/(bookings)/booking-detail.ios.tsx +++ /dev/null @@ -1,302 +0,0 @@ -import * as Clipboard from "expo-clipboard"; -import { isLiquidGlassAvailable } from "expo-glass-effect"; -import { Stack, useLocalSearchParams } from "expo-router"; -import { useCallback, useMemo, useRef } from "react"; -import { useColorScheme } from "react-native"; -import { BookingDetailScreen } from "@/components/screens/BookingDetailScreen"; -import { useAuth } from "@/contexts/AuthContext"; -import { useBookingByUid } from "@/hooks/useBookings"; -import { showErrorAlert, showInfoAlert, showSuccessAlert } from "@/utils/alerts"; -import { type BookingActionsResult, getBookingActions } from "@/utils/booking-actions"; -import { getMeetingUrl } from "@/utils/booking"; -import { openInDefaultBrowser } from "@/utils/browser"; - -// Empty actions result for when no booking is loaded -const EMPTY_ACTIONS: BookingActionsResult = { - reschedule: { visible: false, enabled: false }, - rescheduleRequest: { visible: false, enabled: false }, - cancel: { visible: false, enabled: false }, - changeLocation: { visible: false, enabled: false }, - addGuests: { visible: false, enabled: false }, - viewRecordings: { visible: false, enabled: false }, - meetingSessionDetails: { visible: false, enabled: false }, - markNoShow: { visible: false, enabled: false }, -}; - -// Type for action handlers exposed by BookingDetailScreen -type ActionHandlers = { - openRescheduleModal: () => void; - openEditLocationModal: () => void; - openAddGuestsModal: () => void; - openViewRecordingsModal: () => void; - openMeetingSessionDetailsModal: () => void; - openMarkNoShowModal: () => void; - handleCancelBooking: () => void; -}; - -// Get month name from date string -const getMonthName = (dateString: string | undefined): string => { - if (!dateString) return "Back"; - const date = new Date(dateString); - if (Number.isNaN(date.getTime())) return "Back"; - return date.toLocaleDateString("en-US", { month: "long" }); -}; - -export default function BookingDetailIOS() { - const { uid } = useLocalSearchParams<{ uid: string }>(); - const { userInfo } = useAuth(); - const colorScheme = useColorScheme(); - - // Use React Query hook for booking data - single source of truth - const { data: booking, isLoading, error, refetch, isRefetching } = useBookingByUid(uid); - - // Ref to store action handlers from BookingDetailScreen - const actionHandlersRef = useRef(null); - - // Callback to receive action handlers from BookingDetailScreen - const handleActionsReady = useCallback((handlers: ActionHandlers) => { - actionHandlersRef.current = handlers; - }, []); - - // Get the month name from booking start date for back button - const monthName = useMemo(() => { - const startTime = booking?.start || booking?.startTime; - return getMonthName(startTime); - }, [booking?.start, booking?.startTime]); - - // Get meeting URL for Join button - const meetingUrl = useMemo(() => getMeetingUrl(booking ?? null), [booking]); - - // Handle join meeting - const handleJoinMeeting = useCallback(() => { - if (meetingUrl) { - openInDefaultBrowser(meetingUrl, "meeting link"); - } - }, [meetingUrl]); - - // Handle copy meeting link - const handleCopyMeetingLink = useCallback(async () => { - if (meetingUrl) { - await Clipboard.setStringAsync(meetingUrl); - showSuccessAlert("Copied", "Meeting link copied to clipboard"); - } - }, [meetingUrl]); - - // Compute actions using centralized gating (same as BookingDetailScreen) - const actions = useMemo(() => { - if (!booking) return EMPTY_ACTIONS; - return getBookingActions({ - booking, - eventType: undefined, // EventType not available in this context - currentUserId: userInfo?.id, - currentUserEmail: userInfo?.email, - isOnline: true, // Assume online for now - }); - }, [booking, userInfo?.id, userInfo?.email]); - - // Invoke a handler by name - only accesses ref at invocation time (event handler) - // This avoids creating closures that capture the ref during render - const invokeHandler = useCallback((handlerName: keyof ActionHandlers, errorMessage: string) => { - const handlers = actionHandlersRef.current; - if (handlers?.[handlerName]) { - (handlers[handlerName] as () => void)(); - } else { - showErrorAlert("Error", errorMessage); - } - }, []); - - // Define and filter booking actions - // Actions store handler metadata instead of closures to avoid capturing ref during render - const bookingActionsSections = useMemo(() => { - // Define all actions with handler metadata (not closures) - const allEditEventActions = [ - { - id: "reschedule", - label: "Reschedule Booking", - icon: "calendar" as const, - handlerName: "openRescheduleModal" as const, - errorMessage: "Unable to reschedule. Please try again.", - gatingKey: "reschedule" as const, - }, - { - id: "edit-location", - label: "Edit Location", - icon: "location" as const, - handlerName: "openEditLocationModal" as const, - errorMessage: "Unable to edit location. Please try again.", - gatingKey: "changeLocation" as const, - }, - { - id: "add-guests", - label: "Add Guests", - icon: "person.badge.plus" as const, - handlerName: "openAddGuestsModal" as const, - errorMessage: "Unable to add guests. Please try again.", - gatingKey: "addGuests" as const, - }, - ]; - - const allAfterEventActions = [ - { - id: "view-recordings", - label: "View Recordings", - icon: "video" as const, - handlerName: "openViewRecordingsModal" as const, - errorMessage: "Unable to view recordings. Please try again.", - gatingKey: "viewRecordings" as const, - }, - { - id: "session-details", - label: "Meeting Session Details", - icon: "info.circle" as const, - handlerName: "openMeetingSessionDetailsModal" as const, - errorMessage: "Unable to view session details. Please try again.", - gatingKey: "meetingSessionDetails" as const, - }, - { - id: "mark-no-show", - label: "Mark as No-Show", - icon: "eye.slash" as const, - handlerName: "openMarkNoShowModal" as const, - errorMessage: "Unable to mark no-show. Please try again.", - gatingKey: "markNoShow" as const, - }, - ]; - - const allStandaloneActions = [ - { - id: "report", - label: "Report Booking", - icon: "flag" as const, - handlerName: null, - errorMessage: null, - customHandler: () => { - showInfoAlert("Report Booking", "Report booking functionality is not yet available"); - }, - destructive: true, - gatingKey: null, - }, - { - id: "cancel", - label: "Cancel Event", - icon: "xmark.circle" as const, - handlerName: "handleCancelBooking" as const, - errorMessage: "Unable to cancel. Please try again.", - destructive: true, - gatingKey: "cancel" as const, - }, - ]; - - // Filter actions based on gating logic - const filterAction = (action: { gatingKey: keyof typeof actions | null }) => { - if (action.gatingKey === null) return true; - const gating = actions[action.gatingKey]; - return gating.visible && gating.enabled; - }; - - return { - editEvent: allEditEventActions.filter(filterAction), - afterEvent: allAfterEventActions.filter(filterAction), - standalone: allStandaloneActions.filter(filterAction), - }; - }, [actions]); - - return ( - <> - - - - - {/* Actions Menu */} - - - - {/* Copy Meeting Link - only show if there's a meeting URL */} - {meetingUrl && ( - - Copy Meeting Link - - )} - - {/* Edit Event Section */} - - {bookingActionsSections.editEvent.map((action) => ( - invokeHandler(action.handlerName, action.errorMessage)} - > - {action.label} - - ))} - - - {/* After Event Section */} - - {bookingActionsSections.afterEvent.map((action) => ( - invokeHandler(action.handlerName, action.errorMessage)} - > - {action.label} - - ))} - - - {/* Danger Zone Submenu */} - - {bookingActionsSections.standalone.map((action) => ( - { - if (action.customHandler) { - action.customHandler(); - } else if (action.handlerName && action.errorMessage) { - invokeHandler(action.handlerName, action.errorMessage); - } - }} - destructive - > - {action.label} - - ))} - - - - {meetingUrl && ( - - Join - - )} - - - - - - ); -} diff --git a/companion/app/(tabs)/(bookings)/booking-detail.tsx b/companion/app/(tabs)/(bookings)/booking-detail.tsx deleted file mode 100644 index a83bf31450..0000000000 --- a/companion/app/(tabs)/(bookings)/booking-detail.tsx +++ /dev/null @@ -1,530 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import * as Clipboard from "expo-clipboard"; -import { Stack, useLocalSearchParams, useNavigation, useRouter } from "expo-router"; -import { useCallback, useEffect, useMemo, useRef } from "react"; -import { Platform, Text, useColorScheme, View } from "react-native"; -import { AppPressable } from "@/components/AppPressable"; -import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; -import { BookingDetailScreen } from "@/components/screens/BookingDetailScreen"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { useAuth } from "@/contexts/AuthContext"; -import { useBookingByUid } from "@/hooks/useBookings"; -import { showErrorAlert, showInfoAlert, showSuccessAlert } from "@/utils/alerts"; -import { type BookingActionsResult, getBookingActions } from "@/utils/booking-actions"; -import { getMeetingUrl } from "@/utils/booking"; -import { openInDefaultBrowser } from "@/utils/browser"; - -// Empty actions result for when no booking is loaded -const EMPTY_ACTIONS: BookingActionsResult = { - reschedule: { visible: false, enabled: false }, - rescheduleRequest: { visible: false, enabled: false }, - cancel: { visible: false, enabled: false }, - changeLocation: { visible: false, enabled: false }, - addGuests: { visible: false, enabled: false }, - viewRecordings: { visible: false, enabled: false }, - meetingSessionDetails: { visible: false, enabled: false }, - markNoShow: { visible: false, enabled: false }, -}; - -// Type for action handlers exposed by BookingDetailScreen -type ActionHandlers = { - openRescheduleModal: () => void; - openEditLocationModal: () => void; - openAddGuestsModal: () => void; - openViewRecordingsModal: () => void; - openMeetingSessionDetailsModal: () => void; - openMarkNoShowModal: () => void; - handleCancelBooking: () => void; -}; - -const getIconName = (sfSymbol: string): keyof typeof Ionicons.glyphMap => { - switch (sfSymbol) { - case "calendar": - return "calendar"; - case "location": - return "location"; - case "person.badge.plus": - return "person-add"; - case "video": - return "videocam"; - case "info.circle": - return "information-circle"; - case "eye.slash": - return "eye-off"; - case "flag": - return "flag"; - case "xmark.circle": - return "close-circle"; - default: - return "ellipsis-horizontal"; - } -}; - -export default function BookingDetail() { - "use no memo"; - const { uid } = useLocalSearchParams<{ uid: string }>(); - const { userInfo } = useAuth(); - - // Use React Query hook for booking data - single source of truth - const { data: booking, isLoading, error, refetch, isRefetching } = useBookingByUid(uid); - - // Ref to store action handlers from BookingDetailScreen - const actionHandlersRef = useRef(null); - - // Callback to receive action handlers from BookingDetailScreen - const handleActionsReady = useCallback((handlers: ActionHandlers) => { - actionHandlersRef.current = handlers; - }, []); - - // Compute actions using centralized gating (same as BookingDetailScreen) - const actions = useMemo(() => { - if (!booking) return EMPTY_ACTIONS; - return getBookingActions({ - booking, - eventType: undefined, // EventType not available in this context - currentUserId: userInfo?.id, - currentUserEmail: userInfo?.email, - isOnline: true, // Assume online for now - }); - }, [booking, userInfo?.id, userInfo?.email]); - - // Action handlers that use the booking data - // Note: These are stable functions that don't change, so we don't memoize them - // They access refs which is safe in event handlers - const handleReschedule = useCallback(() => { - if (actionHandlersRef.current?.openRescheduleModal) { - actionHandlersRef.current.openRescheduleModal(); - } else { - showErrorAlert("Error", "Unable to reschedule. Please try again."); - } - }, []); - - const handleEditLocation = useCallback(() => { - if (actionHandlersRef.current?.openEditLocationModal) { - actionHandlersRef.current.openEditLocationModal(); - } else { - showErrorAlert("Error", "Unable to edit location. Please try again."); - } - }, []); - - const handleAddGuests = useCallback(() => { - if (actionHandlersRef.current?.openAddGuestsModal) { - actionHandlersRef.current.openAddGuestsModal(); - } else { - showErrorAlert("Error", "Unable to add guests. Please try again."); - } - }, []); - - const handleViewRecordings = useCallback(() => { - if (actionHandlersRef.current?.openViewRecordingsModal) { - actionHandlersRef.current.openViewRecordingsModal(); - } else { - showErrorAlert("Error", "Unable to view recordings. Please try again."); - } - }, []); - - const handleSessionDetails = useCallback(() => { - if (actionHandlersRef.current?.openMeetingSessionDetailsModal) { - actionHandlersRef.current.openMeetingSessionDetailsModal(); - } else { - showErrorAlert("Error", "Unable to view session details. Please try again."); - } - }, []); - - const handleMarkNoShow = useCallback(() => { - if (actionHandlersRef.current?.openMarkNoShowModal) { - actionHandlersRef.current.openMarkNoShowModal(); - } else { - showErrorAlert("Error", "Unable to mark no-show. Please try again."); - } - }, []); - - const handleReport = useCallback(() => { - showInfoAlert("Report Booking", "Report booking functionality is not yet available"); - }, []); - - const handleCancel = useCallback(() => { - if (actionHandlersRef.current?.handleCancelBooking) { - actionHandlersRef.current.handleCancelBooking(); - } else { - showErrorAlert("Error", "Unable to cancel. Please try again."); - } - }, []); - - // Define booking actions organized by sections - // For iOS glass UI (header menu), we filter out actions where !visible || !enabled - // This matches the behavior in BookingListItem.ios.tsx context menu - const bookingActionsSections = useMemo(() => { - // All possible actions with their gating keys - const allEditEventActions = [ - { - id: "reschedule", - label: "Reschedule Booking", - icon: "calendar" as const, - onPress: handleReschedule, - gatingKey: "reschedule" as const, - }, - { - id: "edit-location", - label: "Edit Location", - icon: "location" as const, - onPress: handleEditLocation, - gatingKey: "changeLocation" as const, - }, - { - id: "add-guests", - label: "Add Guests", - icon: "person.badge.plus" as const, - onPress: handleAddGuests, - gatingKey: "addGuests" as const, - }, - ]; - - const allAfterEventActions = [ - { - id: "view-recordings", - label: "View Recordings", - icon: "video" as const, - onPress: handleViewRecordings, - gatingKey: "viewRecordings" as const, - }, - { - id: "session-details", - label: "Meeting Session Details", - icon: "info.circle" as const, - onPress: handleSessionDetails, - gatingKey: "meetingSessionDetails" as const, - }, - { - id: "mark-no-show", - label: "Mark as No-Show", - icon: "eye.slash" as const, - onPress: handleMarkNoShow, - gatingKey: "markNoShow" as const, - }, - ]; - - const allStandaloneActions = [ - { - id: "report", - label: "Report Booking", - icon: "flag" as const, - onPress: handleReport, - destructive: true, - // Report is always visible (no gating) - gatingKey: null, - }, - { - id: "cancel", - label: "Cancel Event", - icon: "xmark.circle" as const, - onPress: handleCancel, - destructive: true, - gatingKey: "cancel" as const, - }, - ]; - - // Filter actions based on gating (for glass UI, only show visible && enabled) - const filterAction = (action: { gatingKey: keyof typeof actions | null }) => { - if (action.gatingKey === null) return true; // No gating, always show - const gating = actions[action.gatingKey]; - return gating.visible && gating.enabled; - }; - - return { - editEvent: allEditEventActions.filter(filterAction), - afterEvent: allAfterEventActions.filter(filterAction), - standalone: allStandaloneActions.filter(filterAction), - }; - }, [ - actions, - handleReschedule, - handleEditLocation, - handleAddGuests, - handleViewRecordings, - handleSessionDetails, - handleMarkNoShow, - handleReport, - handleCancel, - ]); - - const router = useRouter(); - const navigation = useNavigation(); - const colorScheme = useColorScheme(); - const isDarkMode = colorScheme === "dark"; - - const meetingUrl = useMemo(() => getMeetingUrl(booking ?? null), [booking]); - - const handleJoinMeeting = useCallback(() => { - if (meetingUrl) { - openInDefaultBrowser(meetingUrl, "meeting link"); - } - }, [meetingUrl]); - - const handleCopyMeetingLink = useCallback(async () => { - if (meetingUrl) { - await Clipboard.setStringAsync(meetingUrl); - showSuccessAlert("Copied", "Meeting link copied to clipboard"); - } - }, [meetingUrl]); - - const renderHeaderLeft = useCallback(() => { - let backButtonLabel = "Back"; - - // Check both start and startTime properties - const startTime = booking?.start || booking?.startTime; - - if (startTime) { - const date = new Date(startTime); - if (!Number.isNaN(date.getTime())) { - backButtonLabel = date.toLocaleDateString("en-US", { month: "long" }); - } - } - - return ( - - router.back()} - className="mr-2 h-10 flex-row items-center justify-center rounded-full border border-[#E5E5E5] bg-white px-3 dark:border-[#262626] dark:bg-[#171717]" - > - - - {backButtonLabel} - - - - ); - }, [booking?.start, booking?.startTime, router, isDarkMode]); - - const renderHeaderRight = useCallback( - () => ( - - - - - - - - - - {meetingUrl && ( - <> - - - - - Copy meeting link - - - - - - )} - - {bookingActionsSections.editEvent.length > 0 && ( - <> - Edit Event - {bookingActionsSections.editEvent.map((action) => ( - - - - - {action.label} - - - - ))} - - - )} - - {bookingActionsSections.afterEvent.length > 0 && ( - <> - After Event - {bookingActionsSections.afterEvent.map((action) => ( - - - - - {action.label} - - - - ))} - - - )} - - Danger Zone - {bookingActionsSections.standalone.map((action) => ( - - - - - {action.label} - - - - ))} - - - {meetingUrl && ( - - - Join - - - )} - - - ), - [isDarkMode, meetingUrl, handleCopyMeetingLink, bookingActionsSections, handleJoinMeeting] - ); - - // Force header update on Android/Web - useEffect(() => { - if (Platform.OS === "android" || Platform.OS === "web") { - navigation.setOptions({ - headerRight: renderHeaderRight, - headerLeft: renderHeaderLeft, - }); - } - }, [navigation, renderHeaderRight, renderHeaderLeft]); - - return ( - <> - - - {Platform.OS === "ios" && ( - - Booking Details - - {/* Header right and left API only works on iOS ATM see: https://docs.expo.dev/versions/unversioned/sdk/router/#stackheaderright */} - - - Actions - - - {/* Edit Event Section */} - - {bookingActionsSections.editEvent.map((action) => ( - - {action.label} - - ))} - - - {/* After Event Section */} - - {bookingActionsSections.afterEvent.map((action) => ( - - {action.label} - - ))} - - - {/* Danger Zone Submenu */} - - {bookingActionsSections.standalone.map((action) => ( - - {action.label} - - ))} - - - - - )} - - - - {/* Action Modals for iOS header menu */} - - ); -} diff --git a/companion/app/(tabs)/(bookings)/index.ios.tsx b/companion/app/(tabs)/(bookings)/index.ios.tsx deleted file mode 100644 index f9bcb4cb4a..0000000000 --- a/companion/app/(tabs)/(bookings)/index.ios.tsx +++ /dev/null @@ -1,216 +0,0 @@ -import type { NativeStackHeaderItemMenuAction } from "@react-navigation/native-stack"; -import { isLiquidGlassAvailable } from "expo-glass-effect"; -import { Stack, useLocalSearchParams } from "expo-router"; -import { useState, useEffect, useRef } from "react"; -import { useColorScheme } from "react-native"; - -import { BookingListScreen } from "@/components/booking-list-screen/BookingListScreen"; -import { useEventTypes } from "@/hooks"; -import { type BookingFilter, useActiveBookingFilter } from "@/hooks/useActiveBookingFilter"; - -const VALID_FILTERS: BookingFilter[] = [ - "upcoming", - "unconfirmed", - "recurring", - "past", - "cancelled", -]; - -function isValidBookingFilter(value: string | undefined): value is BookingFilter { - return value !== undefined && VALID_FILTERS.includes(value as BookingFilter); -} - -export default function Bookings() { - const { filter } = useLocalSearchParams<{ filter?: string }>(); - const initialFilter = isValidBookingFilter(filter) ? filter : "upcoming"; - - const [searchQuery, setSearchQuery] = useState(""); - const [selectedEventTypeId, setSelectedEventTypeId] = useState(null); - const { data: eventTypes } = useEventTypes(); - - // Use the active booking filter hook - const { activeFilter, filterOptions, filterParams, handleFilterChange } = useActiveBookingFilter( - initialFilter, - () => { - // Clear dependent filters when status filter changes - setSearchQuery(""); - setSelectedEventTypeId(null); - } - ); - - // Track if we want to ignore the next activeFilter change (because it came from URL sync) - const lastUrlFilter = useRef(null); - - // Update filter when URL params change (for when component is already mounted) - useEffect(() => { - if ( - isValidBookingFilter(filter) && - filter !== activeFilter && - filter !== lastUrlFilter.current - ) { - lastUrlFilter.current = filter; - handleFilterChange(filter); - } - }, [filter, activeFilter, handleFilterChange]); - - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const textColor = isDark ? "#FFFFFF" : "#000000"; - - const handleSearch = (query: string) => { - setSearchQuery(query); - }; - - const clearEventTypeFilter = () => { - setSelectedEventTypeId(null); - }; - - const buildActiveBookingFilter = () => { - const currentFilterOption = filterOptions.find((option) => option.key === activeFilter); - const statusMenuItems: NativeStackHeaderItemMenuAction[] = filterOptions.map((option) => { - const isSelected = activeFilter === option.key; - return { - type: "action", - label: option.label, - icon: { - name: isSelected - ? "checkmark.circle.fill" - : option.key === "upcoming" - ? "calendar" - : option.key === "unconfirmed" - ? "questionmark.circle" - : option.key === "recurring" - ? "repeat.circle" - : option.key === "past" - ? "checkmark.circle" - : "xmark.circle", - type: "sfSymbol", - }, - onPress: () => { - handleFilterChange(option.key); - }, - } satisfies NativeStackHeaderItemMenuAction; - }); - - return { - type: "menu" as const, - label: currentFilterOption?.label || "Filter", - labelStyle: { - fontWeight: "600", - color: textColor, - }, - menu: { - title: "Filter by Status", - items: statusMenuItems, - }, - }; - }; - - const buildEventTypeFilterMenu = () => { - const eventTypeMenuItems: NativeStackHeaderItemMenuAction[] = (eventTypes || []).map( - (eventType) => { - const isSelected = selectedEventTypeId === eventType.id; - return { - type: "action", - label: eventType.title, - icon: { - name: "calendar.badge.clock", - type: "sfSymbol", - }, - state: isSelected ? "on" : "off", - onPress: () => { - // Toggle behavior: if already selected, clear filter; otherwise, select it - if (isSelected) { - clearEventTypeFilter(); - } else { - setSelectedEventTypeId(eventType.id); - } - }, - }; - } - ); - - const clearFilterItem: NativeStackHeaderItemMenuAction[] = - selectedEventTypeId !== null - ? [ - { - type: "action", - label: "Clear filter", - icon: { - name: "xmark.circle", - type: "sfSymbol", - }, - onPress: () => { - clearEventTypeFilter(); - }, - }, - ] - : []; - - const menuItems = [...eventTypeMenuItems, ...clearFilterItem]; - - return { - type: "menu" as const, - label: "Filter by Event Type", - icon: { - name: "line.3.horizontal.decrease", - type: "sfSymbol", - }, - labelStyle: { - fontWeight: "600", - color: textColor, - }, - menu: { - title: menuItems.length > 0 ? "Filter by Event Type" : "No Event Types", - items: [ - ...menuItems, - // Show message if no event types available - ...(menuItems.length === 0 - ? [ - { - type: "action", - label: "No event types available", - onPress: () => {}, - } satisfies NativeStackHeaderItemMenuAction, - ] - : []), - ], - }, - }; - }; - - return ( - <> - handleSearch(e.nativeEvent.text), - obscureBackground: false, - barTintColor: isDark ? "#171717" : "#fff", - }, - unstable_headerRightItems: () => { - const eventTypeFilterMenu = buildEventTypeFilterMenu(); - const statusFilterMenu = buildActiveBookingFilter(); - - return [eventTypeFilterMenu, statusFilterMenu]; - }, - }} - /> - - - - ); -} diff --git a/companion/app/(tabs)/(bookings)/index.tsx b/companion/app/(tabs)/(bookings)/index.tsx deleted file mode 100644 index 27b11ab34e..0000000000 --- a/companion/app/(tabs)/(bookings)/index.tsx +++ /dev/null @@ -1,221 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import { useLocalSearchParams } from "expo-router"; -import { useState, useEffect, useRef } from "react"; -import { Text, TextInput, useColorScheme, View } from "react-native"; -import { BookingListScreen } from "@/components/booking-list-screen/BookingListScreen"; -import { Header } from "@/components/Header"; -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { AppPressable } from "@/components/AppPressable"; -import { type BookingFilter, useActiveBookingFilter } from "@/hooks/useActiveBookingFilter"; -import { useEventTypes } from "@/hooks"; -import { getColors } from "@/constants/colors"; - -const VALID_FILTERS: BookingFilter[] = [ - "upcoming", - "unconfirmed", - "recurring", - "past", - "cancelled", -]; - -function isValidBookingFilter(value: string | undefined): value is BookingFilter { - return value !== undefined && VALID_FILTERS.includes(value as BookingFilter); -} - -export default function Bookings() { - const { filter } = useLocalSearchParams<{ filter?: string }>(); - const initialFilter = isValidBookingFilter(filter) ? filter : "upcoming"; - - const [searchQuery, setSearchQuery] = useState(""); - const [selectedEventTypeId, setSelectedEventTypeId] = useState(null); - const [selectedEventTypeLabel, setSelectedEventTypeLabel] = useState(null); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - - // Use React Query hook for event types (same as iOS for unified caching) - const { data: eventTypes = [], isLoading: eventTypesLoading } = useEventTypes(); - - // Use the active booking filter hook - const { - activeFilter, - filterOptions, - filterParams, - handleFilterChange: originalHandleFilterChange, - } = useActiveBookingFilter(initialFilter, () => { - // Clear dependent filters when status filter changes - setSearchQuery(""); - setSelectedEventTypeId(null); - setSelectedEventTypeLabel(null); - }); - - // Wrap handleFilterChange with debug logging - const handleFilterChange = (filter: string) => { - originalHandleFilterChange(filter as BookingFilter); - }; - - // Track if we want to ignore the next activeFilter change (because it came from URL sync) - const lastUrlFilter = useRef(null); - - // Reactively update filter when URL params change (e.g., from deep link) - useEffect(() => { - if ( - isValidBookingFilter(filter) && - filter !== activeFilter && - filter !== lastUrlFilter.current - ) { - lastUrlFilter.current = filter; - originalHandleFilterChange(filter); - } - }, [filter, activeFilter, originalHandleFilterChange]); - - const handleSearch = (query: string) => { - setSearchQuery(query); - }; - - const clearEventTypeFilter = () => { - setSelectedEventTypeId(null); - setSelectedEventTypeLabel(null); - }; - - const handleEventTypeSelect = (eventTypeId: number | null, label?: string | null) => { - if (eventTypeId === null) { - clearEventTypeFilter(); - } else { - setSelectedEventTypeId(eventTypeId); - setSelectedEventTypeLabel(label || null); - } - }; - - const renderFilterControls = () => { - const filterLabel = - selectedEventTypeId !== null ? selectedEventTypeLabel || "Event Type" : "Filter"; - - return ( - - - {/* Dropdown menu for event type filter */} - - - - - - {filterLabel} - - - - - - - {/* Clear filter option */} - handleEventTypeSelect(null)} - > - - All Event Types - - - - {/* Event type options */} - {eventTypes.map((eventType) => ( - handleEventTypeSelect(eventType.id, eventType.title)} - > - - {eventType.title} - - - ))} - - {/* Loading state */} - {eventTypesLoading && eventTypes.length === 0 && ( - {}}> - Loading... - - )} - - - - - - - - - ); - }; - - return ( - ( -
void} - /> - )} - renderFilterControls={renderFilterControls} - eventTypes={eventTypes} - eventTypesLoading={eventTypesLoading} - searchQuery={searchQuery} - selectedEventTypeId={selectedEventTypeId} - onEventTypeChange={handleEventTypeSelect} - activeFilter={activeFilter} - filterParams={filterParams} - /> - ); -} diff --git a/companion/app/(tabs)/(event-types)/_layout.tsx b/companion/app/(tabs)/(event-types)/_layout.tsx deleted file mode 100644 index 43cb7019c7..0000000000 --- a/companion/app/(tabs)/(event-types)/_layout.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Stack } from "expo-router"; - -export default function EventTypesLayout() { - return ( - - - - - ); -} diff --git a/companion/app/(tabs)/(event-types)/event-type-detail.tsx b/companion/app/(tabs)/(event-types)/event-type-detail.tsx deleted file mode 100644 index 6c5ba88d43..0000000000 --- a/companion/app/(tabs)/(event-types)/event-type-detail.tsx +++ /dev/null @@ -1,2531 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import * as Clipboard from "expo-clipboard"; -import { Stack, useLocalSearchParams, useNavigation, useRouter } from "expo-router"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { - ActivityIndicator, - Alert, - Animated, - Modal, - Platform, - ScrollView, - Switch, - Text, - TouchableOpacity, - useColorScheme, - View, -} from "react-native"; -import { AppPressable } from "@/components/AppPressable"; -import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; -import { AdvancedTab } from "@/components/event-type-detail/tabs/AdvancedTab"; -import { AvailabilityTab } from "@/components/event-type-detail/tabs/AvailabilityTab"; -import { BasicsTab } from "@/components/event-type-detail/tabs/BasicsTab"; -import { LimitsTab } from "@/components/event-type-detail/tabs/LimitsTab"; -import { RecurringTab } from "@/components/event-type-detail/tabs/RecurringTab"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { truncateTitle } from "@/components/event-type-detail/utils"; -import { buildPartialUpdatePayload } from "@/components/event-type-detail/utils/buildPartialUpdatePayload"; -import { - CalComAPIService, - type ConferencingOption, - type EventType, - type Schedule, -} from "@/services/calcom"; -import { useCreateEventType, useDeleteEventType, useUpdateEventType } from "@/hooks"; -import type { LocationItem, LocationOptionGroup } from "@/types/locations"; -import { - showErrorAlert, - showInfoAlert, - showNotAvailableAlert, - showSuccessAlert, -} from "@/utils/alerts"; -import { openInAppBrowser } from "@/utils/browser"; -import { - buildLocationOptions, - mapApiLocationToItem, - mapItemToApiLocation, - validateLocationItem, -} from "@/utils/locationHelpers"; -import { safeLogError } from "@/utils/safeLogger"; -import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect"; - -// Type definitions for extended EventType fields not in the base type -interface EventTypeExtended { - lengthInMinutesOptions?: number[]; - // API V2 format - these can be objects or booleans from older data - disableCancelling?: boolean | { disabled: boolean }; - disableRescheduling?: boolean | { disabled: boolean; minutesBefore?: number }; - sendCalVideoTranscription?: boolean; - - lockedTimeZone?: string; - hideCalendarEventDetails?: boolean; - hideOrganizerEmail?: boolean; - color?: { - lightThemeHex?: string; - darkThemeHex?: string; - }; - // API V2 new fields - calVideoSettings?: { - sendTranscriptionEmails?: boolean; - }; - interfaceLanguage?: string; - showOptimizedSlots?: boolean; -} - -interface BookerActiveBookingsLimitExtended { - disabled?: boolean; - maximumActiveBookings?: number; - count?: number; - offerReschedule?: boolean; -} - -interface ConfirmationPolicyExtended { - disabled?: boolean; -} - -interface RecurrenceExtended { - disabled?: boolean; - interval?: number; - frequency?: string; - occurrences?: number; -} - -interface SeatsExtended { - disabled?: boolean; - seatsPerTimeSlot?: number; - showAttendeeInfo?: boolean; - showAvailabilityCount?: boolean; -} - -interface ApiLocation { - type: string; - integration?: string; - address?: string; - link?: string; - phone?: string; - public?: boolean; -} - -interface CreateEventTypePayload { - title: string; - slug: string; - lengthInMinutes: number; - description?: string; - locations?: ReturnType[]; - scheduleId?: number; - hidden?: boolean; -} - -type TabIconName = "link" | "calendar" | "time" | "settings" | "refresh" | "ellipsis-horizontal"; - -interface Tab { - id: string; - label: string; - icon: TabIconName; -} - -const tabs: Tab[] = [ - { id: "basics", label: "Basics", icon: "link" }, - { id: "availability", label: "Availability", icon: "calendar" }, - { id: "limits", label: "Limits", icon: "time" }, - { id: "advanced", label: "Advanced", icon: "settings" }, - { id: "recurring", label: "Recurring", icon: "refresh" }, - { id: "other", label: "Other", icon: "ellipsis-horizontal" }, -]; - -export default function EventTypeDetail() { - const router = useRouter(); - const colorScheme = useColorScheme(); - const isDarkMode = colorScheme === "dark"; - const { id, title, description, duration, slug } = useLocalSearchParams<{ - id: string; - title: string; - description?: string; - duration: string; - slug?: string; - }>(); - - // Mutation hooks for optimistic updates - const { mutateAsync: updateEventType, isPending: isUpdating } = useUpdateEventType(); - const { mutateAsync: createEventType, isPending: isCreating } = useCreateEventType(); - const { mutateAsync: deleteEventType } = useDeleteEventType(); - - const [activeTab, setActiveTab] = useState("basics"); - - // Form state - const [eventTitle, setEventTitle] = useState(title || ""); - const [eventDescription, setEventDescription] = useState(description || ""); - const [eventSlug, setEventSlug] = useState(slug || ""); - const [eventDuration, setEventDuration] = useState(duration || "30"); - const [username, setUsername] = useState("username"); - const [allowMultipleDurations, setAllowMultipleDurations] = useState(false); - const [locations, setLocations] = useState([]); - const [_locationAddress, setLocationAddress] = useState(""); - const [_locationLink, setLocationLink] = useState(""); - const [_locationPhone, setLocationPhone] = useState(""); - const [selectedDurations, setSelectedDurations] = useState([]); - const [defaultDuration, setDefaultDuration] = useState(""); - const [showDurationDropdown, setShowDurationDropdown] = useState(false); - const [showDefaultDurationDropdown, setShowDefaultDurationDropdown] = useState(false); - const [schedules, setSchedules] = useState([]); - const [selectedSchedule, setSelectedSchedule] = useState(null); - const [selectedScheduleDetails, setSelectedScheduleDetails] = useState(null); - const [showScheduleDropdown, setShowScheduleDropdown] = useState(false); - const [schedulesLoading, setSchedulesLoading] = useState(false); - const [scheduleDetailsLoading, setScheduleDetailsLoading] = useState(false); - const [initialScheduleId, setInitialScheduleId] = useState(null); - const hasAutoSelectedScheduleRef = useRef(false); - - // Reset auto-selection ref when event type id changes (e.g., navigation reuse) - useEffect(() => { - hasAutoSelectedScheduleRef.current = false; - }, []); - - const [isHidden, setIsHidden] = useState(false); - const [selectedTimezone, setSelectedTimezone] = useState(""); - const [showTimezoneDropdown, setShowTimezoneDropdown] = useState(false); - const [conferencingOptions, setConferencingOptions] = useState([]); - const [conferencingLoading, setConferencingLoading] = useState(false); - const [eventTypeData, setEventTypeData] = useState(null); - const [bookingUrl, setBookingUrl] = useState(""); - // Use mutation hooks' isPending states instead of local saving state - const isSaving = isUpdating || isCreating; - const [beforeEventBuffer, setBeforeEventBuffer] = useState("No buffer time"); - const [afterEventBuffer, setAfterEventBuffer] = useState("No buffer time"); - const [showBeforeBufferDropdown, setShowBeforeBufferDropdown] = useState(false); - const [showAfterBufferDropdown, setShowAfterBufferDropdown] = useState(false); - const [minimumNoticeValue, setMinimumNoticeValue] = useState("1"); - const [minimumNoticeUnit, setMinimumNoticeUnit] = useState("Hours"); - const [showMinimumNoticeUnitDropdown, setShowMinimumNoticeUnitDropdown] = useState(false); - const [limitBookingFrequency, setLimitBookingFrequency] = useState(false); - const [frequencyLimits, setFrequencyLimits] = useState([{ id: 1, value: "1", unit: "Per day" }]); - const [showFrequencyUnitDropdown, setShowFrequencyUnitDropdown] = useState(null); - const [frequencyAnimationValue] = useState(new Animated.Value(0)); - const [limitTotalDuration, setLimitTotalDuration] = useState(false); - const [durationLimits, setDurationLimits] = useState([{ id: 1, value: "60", unit: "Per day" }]); - const [showDurationUnitDropdown, setShowDurationUnitDropdown] = useState(null); - const [durationAnimationValue] = useState(new Animated.Value(0)); - const [slotInterval, setSlotInterval] = useState("Default"); - const [showSlotIntervalDropdown, setShowSlotIntervalDropdown] = useState(false); - const [onlyShowFirstAvailableSlot, setOnlyShowFirstAvailableSlot] = useState(false); - const [maxActiveBookingsPerBooker, setMaxActiveBookingsPerBooker] = useState(false); - const [maxActiveBookingsValue, setMaxActiveBookingsValue] = useState("1"); - const [offerReschedule, setOfferReschedule] = useState(false); - const [limitFutureBookings, setLimitFutureBookings] = useState(false); - const [futureBookingType, setFutureBookingType] = useState<"rolling" | "range">("rolling"); - const [rollingDays, setRollingDays] = useState("30"); - const [rollingCalendarDays, setRollingCalendarDays] = useState(false); - const [rangeStartDate, setRangeStartDate] = useState(""); - const [rangeEndDate, setRangeEndDate] = useState(""); - - // Advanced tab state - const [calendarEventName, setCalendarEventName] = useState(""); - const [addToCalendarEmail, setAddToCalendarEmail] = useState(""); - const [selectedLayouts, setSelectedLayouts] = useState(["MONTH_VIEW"]); - const [defaultLayout, setDefaultLayout] = useState("MONTH_VIEW"); - const [requiresConfirmation, setRequiresConfirmation] = useState(false); - const [disableCancelling, setDisableCancelling] = useState(false); - const [disableRescheduling, setDisableRescheduling] = useState(false); - const [sendCalVideoTranscription, setSendCalVideoTranscription] = useState(true); - - const [requiresBookerEmailVerification, setRequiresBookerEmailVerification] = useState(false); - const [hideCalendarNotes, setHideCalendarNotes] = useState(false); - const [hideCalendarEventDetails, setHideCalendarEventDetails] = useState(false); - const [redirectEnabled, setRedirectEnabled] = useState(false); - const [successRedirectUrl, setSuccessRedirectUrl] = useState(""); - const [forwardParamsSuccessRedirect, setForwardParamsSuccessRedirect] = useState(false); - const [hideOrganizerEmail, setHideOrganizerEmail] = useState(false); - const [lockTimezone, setLockTimezone] = useState(false); - const [lockedTimezone, setLockedTimezone] = useState("Europe/London"); - const [allowReschedulingPastEvents, setAllowReschedulingPastEvents] = useState(false); - const [allowBookingThroughRescheduleLink, setAllowBookingThroughRescheduleLink] = useState(false); - const [disableGuests, setDisableGuests] = useState(false); - const [customReplyToEmail, setCustomReplyToEmail] = useState(""); - const [eventTypeColorLight, setEventTypeColorLight] = useState("#292929"); - const [eventTypeColorDark, setEventTypeColorDark] = useState("#FAFAFA"); - const [interfaceLanguageEnabled, setInterfaceLanguageEnabled] = useState(false); - const [interfaceLanguage, setInterfaceLanguage] = useState(""); - const [showOptimizedSlots, setShowOptimizedSlots] = useState(false); - - const [seatsEnabled, setSeatsEnabled] = useState(false); - const [seatsPerTimeSlot, setSeatsPerTimeSlot] = useState("2"); - const [showAttendeeInfo, setShowAttendeeInfo] = useState(false); - const [showAvailabilityCount, setShowAvailabilityCount] = useState(true); - - const [recurringEnabled, setRecurringEnabled] = useState(false); - const [recurringInterval, setRecurringInterval] = useState("1"); - const [recurringFrequency, setRecurringFrequency] = useState<"weekly" | "monthly" | "yearly">( - "weekly" - ); - const [recurringOccurrences, setRecurringOccurrences] = useState("12"); - const [showRecurringFrequencyDropdown, setShowRecurringFrequencyDropdown] = useState(false); - - const bufferTimeOptions = [ - "No buffer time", - "5 Minutes", - "10 Minutes", - "15 Minutes", - "20 Minutes", - "30 Minutes", - "45 Minutes", - "60 Minutes", - "90 Minutes", - "120 Minutes", - ]; - const timeUnitOptions = ["Minutes", "Hours", "Days"]; - const frequencyUnitOptions = ["Per day", "Per Month", "Per year"]; - const durationUnitOptions = ["Per day", "Per week", "Per month"]; - const slotIntervalOptions = [ - "Default", - "5 Minutes", - "10 Minutes", - "15 Minutes", - "20 Minutes", - "30 Minutes", - "45 Minutes", - "60 Minutes", - "75 Minutes", - "90 Minutes", - "105 Minutes", - "120 Minutes", - ]; - const availableDurations = [ - "5 mins", - "10 mins", - "15 mins", - "20 mins", - "25 mins", - "30 mins", - "45 mins", - "50 mins", - "60 mins", - "75 mins", - "80 mins", - "90 mins", - "120 mins", - "150 mins", - "180 mins", - "240 mins", - "300 mins", - "360 mins", - "420 mins", - "480 mins", - ]; - - const getLocationOptionsForDropdown = (): LocationOptionGroup[] => { - // Filter out conferencing options with null appId - const validOptions = conferencingOptions.filter( - (opt): opt is ConferencingOption & { appId: string } => opt.appId !== null - ); - return buildLocationOptions(validOptions); - }; - - const handleAddLocation = (location: LocationItem) => { - setLocations((prev) => [...prev, location]); - }; - - const handleRemoveLocation = (locationId: string) => { - setLocations((prev) => prev.filter((loc) => loc.id !== locationId)); - }; - - const handleUpdateLocation = (locationId: string, updates: Partial) => { - setLocations((prev) => - prev.map((loc) => (loc.id === locationId ? { ...loc, ...updates } : loc)) - ); - }; - - const toggleDurationSelection = (duration: string) => { - const newSelected = selectedDurations.includes(duration) - ? selectedDurations.filter((d) => d !== duration) - : [...selectedDurations, duration]; - - setSelectedDurations(newSelected); - - // Reset default duration if it's no longer in selected durations - if (!newSelected.includes(defaultDuration)) { - setDefaultDuration(""); - } - }; - - const toggleBookingFrequency = (value: boolean) => { - setLimitBookingFrequency(value); - - Animated.timing(frequencyAnimationValue, { - toValue: value ? 1 : 0, - duration: 300, - useNativeDriver: false, - }).start(); - }; - - const addFrequencyLimit = () => { - const newId = Math.max(...frequencyLimits.map((limit) => limit.id)) + 1; - setFrequencyLimits([...frequencyLimits, { id: newId, value: "1", unit: "Per day" }]); - }; - - const removeFrequencyLimit = (id: number) => { - setFrequencyLimits(frequencyLimits.filter((limit) => limit.id !== id)); - }; - - const updateFrequencyLimit = (id: number, field: "value" | "unit", newValue: string) => { - setFrequencyLimits( - frequencyLimits.map((limit) => (limit.id === id ? { ...limit, [field]: newValue } : limit)) - ); - }; - - const toggleTotalDuration = (value: boolean) => { - setLimitTotalDuration(value); - - Animated.timing(durationAnimationValue, { - toValue: value ? 1 : 0, - duration: 300, - useNativeDriver: false, - }).start(); - }; - - const addDurationLimit = () => { - const newId = Math.max(...durationLimits.map((limit) => limit.id)) + 1; - setDurationLimits([...durationLimits, { id: newId, value: "60", unit: "Per day" }]); - }; - - const removeDurationLimit = (id: number) => { - setDurationLimits(durationLimits.filter((limit) => limit.id !== id)); - }; - - const updateDurationLimit = (id: number, field: "value" | "unit", newValue: string) => { - setDurationLimits( - durationLimits.map((limit) => (limit.id === id ? { ...limit, [field]: newValue } : limit)) - ); - }; - - const fetchScheduleDetails = useCallback(async (scheduleId: number) => { - setScheduleDetailsLoading(true); - let scheduleDetails: Awaited> | null = null; - try { - scheduleDetails = await CalComAPIService.getScheduleById(scheduleId); - } catch (error) { - safeLogError("Failed to fetch schedule details:", error); - setSelectedScheduleDetails(null); - setScheduleDetailsLoading(false); - return; - } - setSelectedScheduleDetails(scheduleDetails); - const timeZone = scheduleDetails?.timeZone; - if (timeZone) { - setSelectedTimezone(timeZone); - } - setScheduleDetailsLoading(false); - }, []); - - const fetchSchedules = useCallback(async () => { - setSchedulesLoading(true); - try { - const schedulesData = await CalComAPIService.getSchedules(); - setSchedules(schedulesData); - - // Only auto-select a schedule if we haven't already done so - // This prevents unnecessary re-fetches when the callback is recreated - if (!hasAutoSelectedScheduleRef.current) { - // Use the event type's schedule if we have it, otherwise use default - let scheduleToSelect: Schedule | undefined; - - if (initialScheduleId) { - scheduleToSelect = schedulesData.find((schedule) => schedule.id === initialScheduleId); - } - - if (!scheduleToSelect) { - scheduleToSelect = schedulesData.find((schedule) => schedule.isDefault); - } - - if (scheduleToSelect) { - hasAutoSelectedScheduleRef.current = true; - setSelectedSchedule(scheduleToSelect); - await fetchScheduleDetails(scheduleToSelect.id); - } - } - setSchedulesLoading(false); - } catch (error) { - safeLogError("Failed to fetch schedules:", error); - setSchedulesLoading(false); - } - }, [fetchScheduleDetails, initialScheduleId]); - - const fetchConferencingOptions = useCallback(async () => { - setConferencingLoading(true); - try { - const options = await CalComAPIService.getConferencingOptions(); - setConferencingOptions(options); - setConferencingLoading(false); - } catch (error) { - safeLogError("Failed to fetch conferencing options:", error); - setConferencingLoading(false); - } - }, []); - - const applyEventTypeData = useCallback((eventType: EventType) => { - setEventTypeData(eventType); - - // Load basic fields - if (eventType.title) setEventTitle(eventType.title); - if (eventType.slug) setEventSlug(eventType.slug); - if (eventType.description) setEventDescription(eventType.description); - if (eventType.lengthInMinutes) setEventDuration(eventType.lengthInMinutes.toString()); - if (eventType.hidden !== undefined) setIsHidden(eventType.hidden); - if (eventType.bookingUrl) setBookingUrl(eventType.bookingUrl); - - // Load schedule ID - this will be used by fetchSchedules to select the correct schedule - if (eventType.scheduleId) { - setInitialScheduleId(eventType.scheduleId); - } - - const eventTypeExt = eventType as EventType & EventTypeExtended; - const lengthOptions = eventTypeExt.lengthInMinutesOptions; - const hasLengthOptions = - lengthOptions && Array.isArray(lengthOptions) && lengthOptions.length > 0; - if (hasLengthOptions) { - setAllowMultipleDurations(true); - const durationStrings = lengthOptions.map((mins: number) => `${mins} mins`); - setSelectedDurations(durationStrings); - if (eventType.lengthInMinutes) { - setDefaultDuration(`${eventType.lengthInMinutes} mins`); - } - } - - // Load buffer times - if (eventType.beforeEventBuffer) { - setBeforeEventBuffer(`${eventType.beforeEventBuffer} Minutes`); - } - if (eventType.afterEventBuffer) { - setAfterEventBuffer(`${eventType.afterEventBuffer} Minutes`); - } - - // Load minimum booking notice - if (eventType.minimumBookingNotice) { - const minutes = eventType.minimumBookingNotice; - if (minutes >= 1440) { - // Days - setMinimumNoticeValue((minutes / 1440).toString()); - setMinimumNoticeUnit("Days"); - } else if (minutes >= 60) { - // Hours - setMinimumNoticeValue((minutes / 60).toString()); - setMinimumNoticeUnit("Hours"); - } else { - // Minutes - setMinimumNoticeValue(minutes.toString()); - setMinimumNoticeUnit("Minutes"); - } - } - - // Load slot interval - if (eventType.slotInterval) { - setSlotInterval(`${eventType.slotInterval} Minutes`); - } - - // Load booking frequency limits - const bookingLimitsCount = eventType.bookingLimitsCount; - const hasBookingLimitsCount = bookingLimitsCount && !("disabled" in bookingLimitsCount); - if (hasBookingLimitsCount) { - setLimitBookingFrequency(true); - const limits: { id: number; value: string; unit: string }[] = []; - let idCounter = 1; - if (bookingLimitsCount.day) { - limits.push({ - id: idCounter++, - value: bookingLimitsCount.day.toString(), - unit: "Per day", - }); - } - if (bookingLimitsCount.week) { - limits.push({ - id: idCounter++, - value: bookingLimitsCount.week.toString(), - unit: "Per week", - }); - } - if (bookingLimitsCount.month) { - limits.push({ - id: idCounter++, - value: bookingLimitsCount.month.toString(), - unit: "Per month", - }); - } - if (bookingLimitsCount.year) { - limits.push({ - id: idCounter++, - value: bookingLimitsCount.year.toString(), - unit: "Per year", - }); - } - if (limits.length > 0) { - setFrequencyLimits(limits); - } - } - - // Load duration limits - const bookingLimitsDuration = eventType.bookingLimitsDuration; - const hasBookingLimitsDuration = - bookingLimitsDuration && !("disabled" in bookingLimitsDuration); - if (hasBookingLimitsDuration) { - setLimitTotalDuration(true); - const limits: { id: number; value: string; unit: string }[] = []; - let idCounter = 1; - if (bookingLimitsDuration.day) { - limits.push({ - id: idCounter++, - value: bookingLimitsDuration.day.toString(), - unit: "Per day", - }); - } - if (bookingLimitsDuration.week) { - limits.push({ - id: idCounter++, - value: bookingLimitsDuration.week.toString(), - unit: "Per week", - }); - } - if (bookingLimitsDuration.month) { - limits.push({ - id: idCounter++, - value: bookingLimitsDuration.month.toString(), - unit: "Per month", - }); - } - if (bookingLimitsDuration.year) { - limits.push({ - id: idCounter++, - value: bookingLimitsDuration.year.toString(), - unit: "Per year", - }); - } - if (limits.length > 0) { - setDurationLimits(limits); - } - } - - // Load only show first slot - if (eventType.onlyShowFirstAvailableSlot !== undefined) { - setOnlyShowFirstAvailableSlot(eventType.onlyShowFirstAvailableSlot); - } - - if (eventType.bookerActiveBookingsLimit) { - const bookingLimit = eventType.bookerActiveBookingsLimit as BookerActiveBookingsLimitExtended; - const isBookingLimitEnabled = !("disabled" in bookingLimit); - if (isBookingLimitEnabled) { - const maxBookings = bookingLimit.maximumActiveBookings ?? bookingLimit.count; - if (maxBookings !== undefined) { - setMaxActiveBookingsPerBooker(true); - setMaxActiveBookingsValue(maxBookings.toString()); - } - if (bookingLimit.offerReschedule !== undefined) { - setOfferReschedule(bookingLimit.offerReschedule); - } - } - } - - const bookingWindow = eventType.bookingWindow; - const hasBookingWindow = bookingWindow && !("disabled" in bookingWindow); - if (hasBookingWindow) { - setLimitFutureBookings(true); - if (bookingWindow.type === "range") { - setFutureBookingType("range"); - const windowValue = bookingWindow.value; - const isValidRange = Array.isArray(windowValue) && windowValue.length === 2; - if (isValidRange) { - setRangeStartDate(windowValue[0]); - setRangeEndDate(windowValue[1]); - } - } else { - setFutureBookingType("rolling"); - if (typeof bookingWindow.value === "number") { - setRollingDays(bookingWindow.value.toString()); - } - setRollingCalendarDays(bookingWindow.type === "calendarDays"); - } - } - - const metadata = eventType.metadata; - - // Handle disableCancelling - can be boolean or object { disabled: boolean } - const disableCancellingValue = eventTypeExt.disableCancelling; - if (disableCancellingValue !== undefined) { - if (typeof disableCancellingValue === "boolean") { - setDisableCancelling(disableCancellingValue); - } else if ( - typeof disableCancellingValue === "object" && - "disabled" in disableCancellingValue - ) { - setDisableCancelling(disableCancellingValue.disabled); - } - } else if (metadata?.disableCancelling) { - setDisableCancelling(true); - } - - // Handle disableRescheduling - can be boolean or object { disabled: boolean, minutesBefore?: number } - const disableReschedulingValue = eventTypeExt.disableRescheduling; - if (disableReschedulingValue !== undefined) { - if (typeof disableReschedulingValue === "boolean") { - setDisableRescheduling(disableReschedulingValue); - } else if ( - typeof disableReschedulingValue === "object" && - "disabled" in disableReschedulingValue - ) { - setDisableRescheduling(disableReschedulingValue.disabled); - } - } else if (metadata?.disableRescheduling) { - setDisableRescheduling(true); - } - - // Handle calVideoSettings.sendTranscriptionEmails (API V2) or sendCalVideoTranscription (legacy) - if (eventTypeExt.calVideoSettings?.sendTranscriptionEmails !== undefined) { - setSendCalVideoTranscription(eventTypeExt.calVideoSettings.sendTranscriptionEmails); - } else if (eventTypeExt.sendCalVideoTranscription !== undefined) { - setSendCalVideoTranscription(eventTypeExt.sendCalVideoTranscription); - } else if (metadata?.sendCalVideoTranscription) { - setSendCalVideoTranscription(true); - } else { - setSendCalVideoTranscription(false); - } - - if (eventTypeExt.interfaceLanguage) { - setInterfaceLanguageEnabled(true); - setInterfaceLanguage(eventTypeExt.interfaceLanguage); - } - - // Load showOptimizedSlots (API V2) - if (eventTypeExt.showOptimizedSlots !== undefined) { - setShowOptimizedSlots(eventTypeExt.showOptimizedSlots); - } - - if (metadata) { - const calendarEventNameValue = metadata.calendarEventName; - if (typeof calendarEventNameValue === "string") { - setCalendarEventName(calendarEventNameValue); - } - const addToCalendarEmailValue = metadata.addToCalendarEmail; - if (typeof addToCalendarEmailValue === "string") { - setAddToCalendarEmail(addToCalendarEmailValue); - } - } - - // Load booker layouts - const bookerLayouts = eventType.bookerLayouts; - if (bookerLayouts) { - const enabledLayouts = bookerLayouts.enabledLayouts; - const hasEnabledLayouts = enabledLayouts && Array.isArray(enabledLayouts); - if (hasEnabledLayouts) { - setSelectedLayouts(enabledLayouts); - } - if (bookerLayouts.defaultLayout) { - setDefaultLayout(bookerLayouts.defaultLayout); - } - } - - if (eventType.confirmationPolicy) { - const policy = eventType.confirmationPolicy as ConfirmationPolicyExtended; - const isPolicyEnabled = !("disabled" in policy) || policy.disabled === false; - if (isPolicyEnabled) { - setRequiresConfirmation(true); - } - } - if (eventType.requiresConfirmation !== undefined) { - setRequiresConfirmation(eventType.requiresConfirmation); - } - - if (eventType.requiresBookerEmailVerification !== undefined) { - setRequiresBookerEmailVerification(eventType.requiresBookerEmailVerification); - } - if (eventType.hideCalendarNotes !== undefined) { - setHideCalendarNotes(eventType.hideCalendarNotes); - } - if (eventType.lockTimeZoneToggleOnBookingPage !== undefined) { - setLockTimezone(eventType.lockTimeZoneToggleOnBookingPage); - } - if (eventTypeExt.lockedTimeZone) { - setLockedTimezone(eventTypeExt.lockedTimeZone); - } - if (eventTypeExt.hideCalendarEventDetails !== undefined) { - setHideCalendarEventDetails(eventTypeExt.hideCalendarEventDetails); - } - if (eventTypeExt.hideOrganizerEmail !== undefined) { - setHideOrganizerEmail(eventTypeExt.hideOrganizerEmail); - } - - if (eventType.successRedirectUrl) { - setRedirectEnabled(true); - setSuccessRedirectUrl(eventType.successRedirectUrl); - } - if (eventType.forwardParamsSuccessRedirect !== undefined) { - setForwardParamsSuccessRedirect(eventType.forwardParamsSuccessRedirect); - } - - const eventTypeExtColor = eventTypeExt.color; - if (eventTypeExtColor) { - if (eventTypeExtColor.lightThemeHex) { - setEventTypeColorLight(eventTypeExtColor.lightThemeHex); - } - if (eventTypeExtColor.darkThemeHex) { - setEventTypeColorDark(eventTypeExtColor.darkThemeHex); - } - } - const eventTypeColor = eventType.eventTypeColor; - if (eventTypeColor) { - if (eventTypeColor.lightEventTypeColor) { - setEventTypeColorLight(eventTypeColor.lightEventTypeColor); - } - if (eventTypeColor.darkEventTypeColor) { - setEventTypeColorDark(eventTypeColor.darkEventTypeColor); - } - } - - if (eventType.recurrence) { - const recurrence = eventType.recurrence as RecurrenceExtended; - const recurrenceInterval = recurrence.interval; - const recurrenceFrequency = recurrence.frequency; - const isRecurrenceEnabled = - recurrence.disabled !== true && recurrenceInterval && recurrenceFrequency; - if (isRecurrenceEnabled) { - setRecurringEnabled(true); - setRecurringInterval(recurrenceInterval.toString()); - const freq = recurrenceFrequency as "weekly" | "monthly" | "yearly"; - if (freq === "weekly" || freq === "monthly" || freq === "yearly") { - setRecurringFrequency(freq); - } - const occurrences = recurrence.occurrences; - setRecurringOccurrences(occurrences?.toString() || "12"); - } - } - - const locations = eventType.locations; - const hasLocations = locations && locations.length > 0; - if (hasLocations) { - const mappedLocations = locations.map((loc: ApiLocation) => mapApiLocationToItem(loc)); - setLocations(mappedLocations); - - const firstLocation = locations[0]; - if (firstLocation.address) { - setLocationAddress(firstLocation.address); - } - if (firstLocation.link) { - setLocationLink(firstLocation.link); - } - if (firstLocation.phone) { - setLocationPhone(firstLocation.phone); - } - } - - if (eventType.disableGuests !== undefined) { - setDisableGuests(eventType.disableGuests); - } - - if (eventType.seats) { - const seats = eventType.seats as SeatsExtended; - const seatsAreEnabled = - seats.disabled === false || (!("disabled" in seats) && seats.seatsPerTimeSlot); - - if (seatsAreEnabled) { - setSeatsEnabled(true); - if (seats.seatsPerTimeSlot) { - setSeatsPerTimeSlot(seats.seatsPerTimeSlot.toString()); - } - if (seats.showAttendeeInfo !== undefined) { - setShowAttendeeInfo(seats.showAttendeeInfo); - } - if (seats.showAvailabilityCount !== undefined) { - setShowAvailabilityCount(seats.showAvailabilityCount); - } - } - } - }, []); - - const fetchEventTypeData = useCallback(async () => { - if (!id) return; - - let eventType: EventType | null = null; - try { - eventType = await CalComAPIService.getEventTypeById(parseInt(id, 10)); - } catch (error) { - safeLogError("Failed to fetch event type data:", error); - return; - } - - if (eventType) { - applyEventTypeData(eventType); - } - }, [id, applyEventTypeData]); - - useEffect(() => { - if (activeTab === "availability") { - fetchSchedules(); - } - if (activeTab === "basics") { - fetchConferencingOptions(); - } - }, [activeTab, fetchConferencingOptions, fetchSchedules]); - - useEffect(() => { - // Fetch event type data and conferencing options on initial load - fetchEventTypeData(); - fetchConferencingOptions(); - }, [fetchConferencingOptions, fetchEventTypeData]); - - useEffect(() => { - const fetchUsername = async () => { - try { - const userUsername = await CalComAPIService.getUsername(); - setUsername(userUsername); - } catch (error) { - safeLogError("Failed to fetch username:", error); - } - }; - fetchUsername(); - }, []); - - const formatTime = (time: string) => { - // Handle different time formats that might come from the API - // Extract conditionals outside try/catch for React Compiler - const isColonFormat = time.includes(":"); - - let date: Date; - - if (isColonFormat) { - // Format like "09:00" or "09:00:00" - const parts = time.split(":").map(Number); - const hours = parts[0]; - const minutes = parts[1] || 0; - date = new Date(); - date.setHours(hours, minutes, 0, 0); - } else { - // Other formats - date = new Date(time); - } - - // Check if the date is valid - const isValidDate = !Number.isNaN(date.getTime()); - if (!isValidDate) { - return time; // Return original if parsing fails - } - - return date.toLocaleTimeString("en-US", { - hour: "numeric", - minute: "2-digit", - hour12: true, - }); - }; - - const getDaySchedule = () => { - if (!selectedScheduleDetails) { - return []; - } - - const daysOfWeek = [ - "Sunday", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", - ]; - - const daySchedule = daysOfWeek.map((day) => { - // Try different possible day formats - const dayLower = day.toLowerCase(); - const dayUpper = day.toUpperCase(); - const dayShort = day.substring(0, 3).toLowerCase(); // mon, tue, etc. - const dayShortUpper = day.substring(0, 3).toUpperCase(); - - // Find ALL matching availability slots for this day (not just the first one) - const matchingSlots = - selectedScheduleDetails.availability?.filter((avail) => { - if (!avail.days || !Array.isArray(avail.days)) return false; - - return avail.days.some( - (d) => - d === dayLower || - d === dayUpper || - d === day || - d === dayShort || - d === dayShortUpper || - d.toLowerCase() === dayLower - ); - }) || []; - - // Map to time slots array - const timeSlots = matchingSlots.map((slot) => ({ - startTime: slot.startTime, - endTime: slot.endTime, - })); - - return { - day, - available: timeSlots.length > 0, - startTime: timeSlots[0]?.startTime, - endTime: timeSlots[0]?.endTime, - timeSlots, // Include all time slots for this day - }; - }); - - return daySchedule; - }; - - const handlePreview = async () => { - if (!bookingUrl) { - showErrorAlert("Error", "Booking URL not available. Please save the event type first."); - return; - } - await openInAppBrowser(bookingUrl, "event type preview"); - }; - - const handleCopyLink = async () => { - if (!bookingUrl) { - showErrorAlert("Error", "Booking URL not available. Please save the event type first."); - return; - } - await Clipboard.setStringAsync(bookingUrl); - showSuccessAlert("Success", "Link copied!"); - }; - - const handleDelete = () => { - Alert.alert("Delete Event Type", `Are you sure you want to delete "${eventTitle}"?`, [ - { text: "Cancel", style: "cancel" }, - { - text: "Delete", - style: "destructive", - onPress: async () => { - const eventTypeId = parseInt(id, 10); - - if (Number.isNaN(eventTypeId)) { - showErrorAlert("Error", "Invalid event type ID"); - return; - } - - try { - await deleteEventType(eventTypeId); - - showSuccessAlert("Success", "Event type deleted successfully"); - router.back(); - } catch (error) { - safeLogError("Failed to delete event type:", error); - showErrorAlert("Error", "Failed to delete event type. Please try again."); - } - }, - }, - ]); - }; - - // Calculate current form state efficiently - const currentFormState = useMemo( - () => ({ - // Basics - eventTitle, - eventSlug, - eventDescription, - eventDuration, - isHidden, - locations, - disableGuests, - - // Multiple durations - allowMultipleDurations, - selectedDurations, - defaultDuration, - - // Availability - selectedScheduleId: selectedSchedule?.id, - - // Limits - beforeEventBuffer, - afterEventBuffer, - minimumNoticeValue, - minimumNoticeUnit, - slotInterval, - limitBookingFrequency, - frequencyLimits, - limitTotalDuration, - durationLimits, - onlyShowFirstAvailableSlot, - maxActiveBookingsPerBooker, - maxActiveBookingsValue, - offerReschedule, - limitFutureBookings, - futureBookingType, - rollingDays, - rollingCalendarDays, - rangeStartDate, - rangeEndDate, - - // Advanced - requiresConfirmation, - requiresBookerEmailVerification, - hideCalendarNotes, - hideCalendarEventDetails, - hideOrganizerEmail, - lockTimezone, - allowReschedulingPastEvents, - allowBookingThroughRescheduleLink, - successRedirectUrl, - forwardParamsSuccessRedirect, - customReplyToEmail, - eventTypeColorLight, - eventTypeColorDark, - calendarEventName, - addToCalendarEmail, - selectedLayouts, - defaultLayout, - disableCancelling, - disableRescheduling, - sendCalVideoTranscription, - - interfaceLanguage, - showOptimizedSlots, - - // Seats - seatsEnabled, - seatsPerTimeSlot, - showAttendeeInfo, - showAvailabilityCount, - - // Recurring - recurringEnabled, - recurringInterval, - recurringFrequency, - recurringOccurrences, - }), - [ - eventTitle, - eventSlug, - eventDescription, - eventDuration, - isHidden, - locations, - disableGuests, - allowMultipleDurations, - selectedDurations, - defaultDuration, - selectedSchedule, - beforeEventBuffer, - afterEventBuffer, - minimumNoticeValue, - minimumNoticeUnit, - slotInterval, - limitBookingFrequency, - frequencyLimits, - limitTotalDuration, - durationLimits, - onlyShowFirstAvailableSlot, - maxActiveBookingsPerBooker, - maxActiveBookingsValue, - offerReschedule, - limitFutureBookings, - futureBookingType, - rollingDays, - rollingCalendarDays, - rangeStartDate, - rangeEndDate, - requiresConfirmation, - requiresBookerEmailVerification, - hideCalendarNotes, - hideCalendarEventDetails, - hideOrganizerEmail, - lockTimezone, - allowReschedulingPastEvents, - allowBookingThroughRescheduleLink, - successRedirectUrl, - forwardParamsSuccessRedirect, - customReplyToEmail, - eventTypeColorLight, - eventTypeColorDark, - calendarEventName, - addToCalendarEmail, - selectedLayouts, - defaultLayout, - disableCancelling, - disableRescheduling, - sendCalVideoTranscription, - interfaceLanguage, - showOptimizedSlots, - seatsEnabled, - seatsPerTimeSlot, - showAttendeeInfo, - showAvailabilityCount, - recurringEnabled, - recurringInterval, - recurringFrequency, - recurringOccurrences, - ] - ); - - // Calculate isDirty state - const isDirty = useMemo(() => { - // For new event types (id="new"), we always want to enable save if required fields are filled - // But for now, let's stick to the dirty check logic which applies mostly to updates - if (id === "new") { - // For create mode, we can consider it "dirty" if title and slug are present - // or just always enabled. The requirement was "when user changes something". - // Usually for "new" forms, the button IS enabled, or disabled until valid. - // Let's assume the requirement "enable/disable based on changes" specifically targets the EDIT flow. - // But to be safe and consistent with "disable until change", we can check if it differs from initial empty state? - // Actually, for "Create", it's usually better to be enabled or validated. - // However, the user request: "we have this save button in event types, so when user changes something then only i want this save button to enable" - // This strongly implies the Edit case. - // For "new", returning true (always dirty/ready) is a safe default to avoid blocking creation. - return true; - } - - if (!eventTypeData) return false; - - const payload = buildPartialUpdatePayload(currentFormState, eventTypeData); - return Object.keys(payload).length > 0; - }, [currentFormState, eventTypeData, id]); - - const handleSave = useCallback(async () => { - if (!id) { - showErrorAlert("Error", "Event type ID is missing"); - return; - } - - // Validate required fields - if (!eventTitle || !eventSlug) { - showErrorAlert("Error", "Title and slug are required"); - return; - } - - const durationNum = parseInt(eventDuration, 10); - if (Number.isNaN(durationNum) || durationNum <= 0) { - showErrorAlert("Error", "Duration must be a positive number"); - return; - } - - // Validate locations before saving - if (locations.length > 0) { - for (const loc of locations) { - const validation = validateLocationItem(loc); - if (!validation.valid) { - showErrorAlert("Error", validation.error || "Invalid location"); - return; - } - } - } - - // Detect create vs update mode - const isCreateMode = id === "new"; - - // Extract values with optional chaining outside try/catch for React Compiler - const selectedScheduleId = selectedSchedule?.id; - - if (isCreateMode) { - // For CREATE mode, build full payload - const payload: CreateEventTypePayload = { - title: eventTitle, - slug: eventSlug, - lengthInMinutes: durationNum, - }; - - if (eventDescription) { - payload.description = eventDescription; - } - - if (locations.length > 0) { - payload.locations = locations.map((loc) => mapItemToApiLocation(loc)); - } - - if (selectedScheduleId !== undefined) { - payload.scheduleId = selectedScheduleId; - } - - payload.hidden = isHidden; - - // Create new event type using mutation hook - try { - await createEventType(payload); - } catch (error) { - safeLogError("Failed to save event type:", error); - showErrorAlert("Error", "Failed to create event type. Please try again."); - return; - } - showSuccessAlert("Success", "Event type created successfully"); - router.back(); - } else { - // For UPDATE mode, use partial update - only send changed fields - // Using the memoized currentFormState - const payload = buildPartialUpdatePayload(currentFormState, eventTypeData); - - if (Object.keys(payload).length === 0) { - // This should theoretically strictly not be reached if button is disabled, - // but it acts as a safeguard. - // return; - } - - // Update event type using mutation hook with optimistic updates - try { - await updateEventType({ id: parseInt(id, 10), updates: payload }); - } catch (error) { - safeLogError("Failed to save event type:", error); - showErrorAlert("Error", "Failed to update event type. Please try again."); - return; - } - showSuccessAlert("Success", "Event type updated successfully"); - // No need to manually refresh - cache is updated by the mutation hook - } - }, [ - id, - eventTitle, - eventSlug, - eventDuration, - locations, - selectedSchedule?.id, - eventDescription, - isHidden, - createEventType, - router, - currentFormState, - eventTypeData, - updateEventType, - ]); - - const headerTitle = id === "new" ? "Create Event Type" : truncateTitle(title); - const saveButtonText = id === "new" ? "Create" : "Save"; - - const renderHeaderLeft = useCallback( - () => ( - - router.back()} - className="mr-2 h-10 w-10 items-center justify-center rounded-full border border-[#E5E5E5] bg-white dark:border-[#262626] dark:bg-[#171717]" - > - - - - ), - [router, isDarkMode] - ); - - const renderHeaderRight = useCallback( - () => ( - - - {/* Tab Navigation Dropdown Menu */} - - - - - {tabs.find((tab) => tab.id === activeTab)?.label ?? "Basics"} - - - - - - {tabs.map((tab) => { - const isSelected = activeTab === tab.id; - return ( - setActiveTab(tab.id)}> - - - - {tab.label} - - - - ); - })} - - - - {/* Save Button */} - - {isSaving ? ( - - ) : ( - - {saveButtonText} - - )} - - - - ), - [activeTab, isDarkMode, handleSave, isSaving, isDirty, saveButtonText] - ); - - // Force header update on Android/Web when state changes - const navigation = useNavigation(); - useEffect(() => { - if (Platform.OS === "android" || Platform.OS === "web") { - navigation.setOptions({ - headerRight: renderHeaderRight, - headerLeft: renderHeaderLeft, - }); - } - }, [navigation, renderHeaderRight, renderHeaderLeft]); - - return ( - <> - - - {Platform.OS === "ios" && ( - - - - - {activeTab.charAt(0).toUpperCase() + activeTab.slice(1)} - - {tabs.map((tab) => ( - setActiveTab(tab.id)} - > - {tab.label} - - ))} - - - {saveButtonText} - - - - )} - - - - {activeTab === "basics" ? ( - - ) : null} - - {/* Duration Multi-Select Modal */} - setShowDurationDropdown(false)} - > - setShowDurationDropdown(false)} - > - - {isLiquidGlassAvailable() && Platform.OS === "ios" ? ( - - - - Select Available Durations - - - {availableDurations.map((duration, index) => ( - toggleDurationSelection(duration)} - > - - {duration} - - {selectedDurations.includes(duration) ? ( - - ) : ( - - )} - - ))} - - setShowDurationDropdown(false)} - > - - Done - - - - - ) : ( - - - Select Available Durations - - - {availableDurations.map((duration, index) => ( - toggleDurationSelection(duration)} - > - - {duration} - - {selectedDurations.includes(duration) ? ( - - ) : ( - - )} - - ))} - - setShowDurationDropdown(false)} - > - Done - - - )} - - - - - {/* Default Duration Dropdown Modal */} - setShowDefaultDurationDropdown(false)} - > - setShowDefaultDurationDropdown(false)} - > - - {isLiquidGlassAvailable() && Platform.OS === "ios" ? ( - - - - Select Default Duration - - - {selectedDurations.map((duration, index) => ( - { - setDefaultDuration(duration); - setShowDefaultDurationDropdown(false); - }} - > - - {duration} - - {defaultDuration === duration ? ( - - ) : null} - - ))} - - - - ) : ( - - - Select Default Duration - - - {selectedDurations.map((duration, index) => ( - { - setDefaultDuration(duration); - setShowDefaultDurationDropdown(false); - }} - > - - {duration} - - {defaultDuration === duration ? ( - - ) : null} - - ))} - - - )} - - - - - {/* Schedule Dropdown Modal */} - setShowScheduleDropdown(false)} - > - setShowScheduleDropdown(false)} - > - - {isLiquidGlassAvailable() && Platform.OS === "ios" ? ( - - - - Select Schedule - - - {schedules.map((schedule, index) => ( - { - setSelectedSchedule(schedule); - fetchScheduleDetails(schedule.id); - setShowScheduleDropdown(false); - }} - > - - - {schedule.name} - - {schedule.isDefault && ( - - Default - - )} - - {selectedSchedule?.id === schedule.id ? ( - - ) : null} - - ))} - - - - ) : ( - - - Select Schedule - - - {schedules.map((schedule, index) => ( - { - setSelectedSchedule(schedule); - fetchScheduleDetails(schedule.id); - setShowScheduleDropdown(false); - }} - > - - - {schedule.name} - - {schedule.isDefault && ( - - Default - - )} - - {selectedSchedule?.id === schedule.id ? ( - - ) : null} - - ))} - - - )} - - - - - {/* Timezone Dropdown Modal */} - setShowTimezoneDropdown(false)} - > - setShowTimezoneDropdown(false)} - > - - - Select Timezone - - - {[ - "America/New_York", - "America/Chicago", - "America/Denver", - "America/Los_Angeles", - "Europe/London", - "Europe/Paris", - "Europe/Berlin", - "Asia/Tokyo", - "Asia/Shanghai", - "Asia/Kolkata", - "Australia/Sydney", - "UTC", - ].map((tz) => ( - { - setSelectedTimezone(tz); - setShowTimezoneDropdown(false); - }} - > - - {tz} - - {selectedTimezone === tz || - (selectedScheduleDetails?.timeZone === tz && !selectedTimezone) ? ( - - ) : null} - - ))} - - - - - - {/* Before Event Buffer Dropdown Modal */} - setShowBeforeBufferDropdown(false)} - > - setShowBeforeBufferDropdown(false)} - > - - - Before event buffer - - {bufferTimeOptions.map((option) => ( - { - setBeforeEventBuffer(option); - setShowBeforeBufferDropdown(false); - }} - > - - {option} - - {beforeEventBuffer === option ? ( - - ) : null} - - ))} - - - - - {/* After Event Buffer Dropdown Modal */} - setShowAfterBufferDropdown(false)} - > - setShowAfterBufferDropdown(false)} - > - - - After event buffer - - {bufferTimeOptions.map((option) => ( - { - setAfterEventBuffer(option); - setShowAfterBufferDropdown(false); - }} - > - - {option} - - {afterEventBuffer === option ? ( - - ) : null} - - ))} - - - - - {/* Minimum Notice Unit Dropdown Modal */} - setShowMinimumNoticeUnitDropdown(false)} - > - setShowMinimumNoticeUnitDropdown(false)} - > - - - Time unit - - {timeUnitOptions.map((option) => ( - { - setMinimumNoticeUnit(option); - setShowMinimumNoticeUnitDropdown(false); - }} - > - - {option} - - {minimumNoticeUnit === option ? ( - - ) : null} - - ))} - - - - - {/* Frequency Unit Dropdown Modal */} - setShowFrequencyUnitDropdown(null)} - > - setShowFrequencyUnitDropdown(null)} - > - - - Frequency unit - - {frequencyUnitOptions.map((option) => { - const selectedLimit = frequencyLimits.find( - (limit) => limit.id === showFrequencyUnitDropdown - ); - const isSelected = selectedLimit?.unit === option; - return ( - { - if (showFrequencyUnitDropdown) { - updateFrequencyLimit(showFrequencyUnitDropdown, "unit", option); - } - setShowFrequencyUnitDropdown(null); - }} - > - - {option} - - {isSelected && } - - ); - })} - - - - - {/* Duration Unit Dropdown Modal */} - setShowDurationUnitDropdown(null)} - > - setShowDurationUnitDropdown(null)} - > - - - Duration unit - - {durationUnitOptions.map((option) => { - const selectedLimit = durationLimits.find( - (limit) => limit.id === showDurationUnitDropdown - ); - const isSelected = selectedLimit?.unit === option; - return ( - { - if (showDurationUnitDropdown) { - updateDurationLimit(showDurationUnitDropdown, "unit", option); - } - setShowDurationUnitDropdown(null); - }} - > - - {option} - - {isSelected && } - - ); - })} - - - - - {/* Slot Interval Dropdown Modal */} - setShowSlotIntervalDropdown(false)} - > - setShowSlotIntervalDropdown(false)} - > - - - Slot interval - - {slotIntervalOptions.map((option) => ( - { - setSlotInterval(option); - setShowSlotIntervalDropdown(false); - }} - > - - {option} - - {slotInterval === option ? ( - - ) : null} - - ))} - - - - - {/* Recurring Frequency Dropdown Modal */} - setShowRecurringFrequencyDropdown(false)} - > - setShowRecurringFrequencyDropdown(false)} - > - - - Repeats every - - {(["weekly", "monthly", "yearly"] as const).map((option) => ( - { - setRecurringFrequency(option); - setShowRecurringFrequencyDropdown(false); - }} - > - - {option === "weekly" ? "week" : option === "monthly" ? "month" : "year"} - - {recurringFrequency === option ? ( - - ) : null} - - ))} - - - - - {activeTab === "availability" ? ( - - ) : null} - - {activeTab === "limits" ? ( - - ) : null} - - {activeTab === "advanced" ? ( - - ) : null} - - {activeTab === "recurring" ? ( - - ) : null} - - {activeTab === "other" ? ( - - {/* Configure on Web */} - - - Configure on Web - - - {/* Apps */} - - { - if (id === "new") { - showInfoAlert( - "Info", - "Save the event type first to configure this setting." - ); - } else if (Platform.OS === "ios") { - showNotAvailableAlert(); - } else { - openInAppBrowser( - `https://app.cal.com/event-types/${id}?tabName=apps`, - "Apps settings" - ); - } - }} - activeOpacity={0.5} - > - - - - - - Apps - Manage integrations - - - - - - - {/* Workflows */} - - { - if (id === "new") { - showInfoAlert( - "Info", - "Save the event type first to configure this setting." - ); - } else if (Platform.OS === "ios") { - showNotAvailableAlert(); - } else { - openInAppBrowser( - `https://app.cal.com/event-types/${id}?tabName=workflows`, - "Workflows settings" - ); - } - }} - activeOpacity={0.5} - > - - - - - - Workflows - Automated actions - - - - - - - {/* Webhooks */} - - { - if (id === "new") { - showInfoAlert( - "Info", - "Save the event type first to configure this setting." - ); - } else if (Platform.OS === "ios") { - showNotAvailableAlert(); - } else { - openInAppBrowser( - `https://app.cal.com/event-types/${id}?tabName=webhooks`, - "Webhooks settings" - ); - } - }} - activeOpacity={0.5} - > - - - - - - Webhooks - Event notifications - - - - - - - - - ) : null} - - {activeTab === "basics" && ( - - {/* Visibility */} - - - Visibility - - - - - Hidden - - - - - - - - - {/* Quick Actions */} - - - Quick Actions - - - - {Platform.OS !== "ios" && ( - - Preview - - - )} - - - - Copy Link - - - - - - - {/* Danger Zone */} - - - - - Delete Event Type - - - - - - - )} - - - - ); -} -// test unused variable diff --git a/companion/app/(tabs)/(event-types)/index.ios.tsx b/companion/app/(tabs)/(event-types)/index.ios.tsx deleted file mode 100644 index 578cdbc449..0000000000 --- a/companion/app/(tabs)/(event-types)/index.ios.tsx +++ /dev/null @@ -1,604 +0,0 @@ -import * as Haptics from "expo-haptics"; -import { Ionicons } from "@expo/vector-icons"; -import * as Clipboard from "expo-clipboard"; -import { isLiquidGlassAvailable } from "expo-glass-effect"; -import { Image } from "expo-image"; -import { Stack, useRouter } from "expo-router"; -import { useMemo, useState } from "react"; -import { - Alert, - Pressable, - RefreshControl, - ScrollView, - Share, - Text, - TouchableOpacity, - useColorScheme, - View, -} from "react-native"; -import { EmptyScreen } from "@/components/EmptyScreen"; -import { EventTypeListItem } from "@/components/event-type-list-item/EventTypeListItem"; -import { EventTypeListSkeleton } from "@/components/event-type-list-item/EventTypeListItemSkeleton"; -import { - useCreateEventType, - useDeleteEventType, - useDuplicateEventType, - useEventTypes, - useUserProfile, -} from "@/hooks"; -import { useEventTypeFilter } from "@/hooks/useEventTypeFilter"; -import type { EventType } from "@/services/calcom"; -import { showErrorAlert, showSuccessAlert } from "@/utils/alerts"; -import { openInAppBrowser } from "@/utils/browser"; -import { getAvatarUrl } from "@/utils/getAvatarUrl"; -import { getEventDuration } from "@/utils/getEventDuration"; -import { offlineAwareRefresh } from "@/utils/network"; -import { slugify } from "@/utils/slugify"; - -import { getColors } from "@/constants/colors"; - -export default function EventTypesIOS() { - const router = useRouter(); - const [searchQuery, setSearchQuery] = useState(""); - const { data: userProfile } = useUserProfile(); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - - // No modal state needed for iOS - using native Alert.prompt - - // Use React Query hooks - const { - data: eventTypes = [], - isLoading: loading, - isFetching, - error: queryError, - refetch, - } = useEventTypes(); - - // Show refresh indicator when fetching - const refreshing = isFetching && !loading; - - const { mutate: createEventTypeMutation } = useCreateEventType(); - const { mutate: deleteEventTypeMutation } = useDeleteEventType(); - const { mutate: duplicateEventTypeMutation } = useDuplicateEventType(); - - // Convert query error to string - // Don't show error UI for authentication errors (user will be redirected to login) - // Only show error UI in development mode for other errors - const isAuthError = - queryError?.message?.includes("Authentication") || - queryError?.message?.includes("sign in") || - queryError?.message?.includes("401"); - const error = queryError && !isAuthError && __DEV__ ? "Failed to load event types." : null; - - // No modal state needed for iOS - using native Alert for delete confirmation - - // Handle pull-to-refresh (offline-aware) - const onRefresh = () => offlineAwareRefresh(refetch); - - // Event type filter and sort hook - const { - sortBy, - filters, - setSortBy, - toggleFilter, - resetFilters, - filteredAndSortedEventTypes, - activeFilterCount, - } = useEventTypeFilter(); - - // Filter event types based on search query and filter/sort options - const filteredEventTypes = useMemo(() => { - // First apply filter/sort from the hook - let filtered = filteredAndSortedEventTypes(eventTypes); - - // Then apply search query filter - if (searchQuery.trim() !== "") { - const searchLower = searchQuery.toLowerCase(); - filtered = filtered.filter( - (eventType) => - eventType.title.toLowerCase().includes(searchLower) || - eventType.description?.toLowerCase().includes(searchLower) - ); - } - - return filtered; - }, [eventTypes, searchQuery, filteredAndSortedEventTypes]); - - const handleSearch = (query: string) => { - setSearchQuery(query); - }; - - const handleEventTypePress = (eventType: EventType) => { - handleEdit(eventType); - }; - - const handleCopyLink = async (eventType: EventType) => { - if (!eventType.bookingUrl) { - showErrorAlert("Error", "Booking URL not available for this event type."); - return; - } - try { - await Clipboard.setStringAsync(eventType.bookingUrl); - showSuccessAlert("Link Copied", "Event type link copied!"); - } catch { - showErrorAlert("Error", "Failed to copy link. Please try again."); - } - }; - - const _handleShare = async (eventType: EventType) => { - if (!eventType.bookingUrl) { - showErrorAlert("Error", "Booking URL not available for this event type."); - return; - } - try { - await Share.share({ - message: `Book a meeting: ${eventType.title}`, - url: eventType.bookingUrl, - }); - } catch { - showErrorAlert("Error", "Failed to share link. Please try again."); - } - }; - - const handleEdit = (eventType: EventType) => { - const duration = getEventDuration(eventType); - router.push({ - pathname: "/event-type-detail", - params: { - id: eventType.id.toString(), - title: eventType.title, - description: eventType.description || "", - duration: duration.toString(), - price: eventType.price?.toString() || "", - currency: eventType.currency || "", - slug: eventType.slug || "", - }, - }); - }; - - const handleDelete = (eventType: EventType) => { - // Use native iOS Alert for confirmation - Alert.alert( - "Delete Event Type", - `Are you sure you want to delete "${eventType.title}"? This action cannot be undone.`, - [ - { text: "Cancel", style: "cancel" }, - { - text: "Delete", - style: "destructive", - onPress: () => { - deleteEventTypeMutation(eventType.id, { - onSuccess: () => { - showSuccessAlert("Success", "Event type deleted successfully"); - }, - onError: (deleteError) => { - const message = - deleteError instanceof Error ? deleteError.message : String(deleteError); - console.error("Failed to delete event type", message); - if (__DEV__) { - const stack = deleteError instanceof Error ? deleteError.stack : undefined; - console.debug("[EventTypes] deleteEventType failed", { - message, - stack, - }); - } - showErrorAlert("Error", "Failed to delete event type. Please try again."); - }, - }); - }, - }, - ] - ); - }; - - const handleDuplicate = (eventType: EventType) => { - duplicateEventTypeMutation( - { eventType, existingEventTypes: eventTypes }, - { - onSuccess: (duplicatedEventType) => { - showSuccessAlert("Success", "Event type duplicated successfully"); - - const duration = getEventDuration(eventType); - - // Navigate to edit the newly created duplicate - router.push({ - pathname: "/event-type-detail", - params: { - id: duplicatedEventType.id.toString(), - title: duplicatedEventType.title, - description: duplicatedEventType.description || "", - duration: ( - duplicatedEventType.lengthInMinutes || - duplicatedEventType.length || - duration - ).toString(), - slug: duplicatedEventType.slug || "", - }, - }); - }, - onError: (duplicateError) => { - const message = - duplicateError instanceof Error ? duplicateError.message : String(duplicateError); - console.error("Failed to duplicate event type", message); - if (__DEV__) { - const stack = duplicateError instanceof Error ? duplicateError.stack : undefined; - console.debug("[EventTypes] duplicateEventType failed", { - message, - stack, - }); - } - showErrorAlert("Error", "Failed to duplicate event type. Please try again."); - }, - } - ); - }; - - const handlePreview = async (eventType: EventType) => { - if (!eventType.bookingUrl) { - showErrorAlert("Error", "Booking URL not available for this event type."); - return; - } - try { - await openInAppBrowser(eventType.bookingUrl, "event type preview"); - } catch { - console.error("Failed to open preview"); - showErrorAlert("Error", "Failed to open preview. Please try again."); - } - }; - - const handleOpenCreateModal = () => { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); - // Use native iOS Alert.prompt for a native look - Alert.prompt( - "Add a new event type", - "Set up event types to offer different types of meetings.", - [ - { text: "Cancel", style: "cancel" }, - { - text: "Continue", - onPress: (title?: string) => { - if (!title?.trim()) { - showErrorAlert("Error", "Please enter a title for your event type"); - return; - } - - const autoSlug = slugify(title.trim()); - if (!autoSlug) { - showErrorAlert("Error", "Title must contain at least one letter or number"); - return; - } - - createEventTypeMutation( - { - title: title.trim(), - slug: autoSlug, - lengthInMinutes: 15, // Default duration - description: undefined, // Empty description - }, - { - onSuccess: (newEventType) => { - // Navigate to edit the newly created event type - router.push({ - pathname: "/event-type-detail", - params: { - id: newEventType.id.toString(), - title: newEventType.title, - description: newEventType.description || "", - duration: ( - newEventType.lengthInMinutes || - newEventType.length || - 15 - ).toString(), - slug: newEventType.slug || "", - }, - }); - }, - onError: (createError) => { - const message = - createError instanceof Error ? createError.message : String(createError); - console.error("Failed to create event type", message); - if (__DEV__) { - const stack = createError instanceof Error ? createError.stack : undefined; - console.debug("[EventTypes] createEventType failed", { - message, - stack, - }); - } - showErrorAlert("Error", "Failed to create event type. Please try again."); - }, - } - ); - }, - }, - ], - "plain-text", - "", - "default" - ); - }; - - // Sort by menu handler - const handleSortByOption = (option: "alphabetical" | "newest" | "duration") => { - setSortBy(option); - }; - - // Filter menu handler - toggle filters - const handleFilterToggle = ( - filterKey: - | "hiddenOnly" - | "paidOnly" - | "seatedOnly" - | "requiresConfirmationOnly" - | "recurringOnly" - ) => { - toggleFilter(filterKey); - }; - - if (loading) { - return ( - <> - - Event Types - - - - - - ); - } - - if (error) { - return ( - - - - Unable to load event types - - - {error} - - refetch()} - > - - Retry - - - - ); - } - - if (eventTypes.length === 0) { - return ( - <> - - - - - - ); - } - - return ( - <> - {/* iOS Native Header with Glass UI */} - - Event Types - - - {/* Filter/Sort Menu */} - - - - {/* Sort by Submenu - opens as separate submenu */} - - handleSortByOption("alphabetical")} - > - Alphabetical - - handleSortByOption("newest")} - > - Newest First - - handleSortByOption("duration")} - > - By Duration - - - - {/* Filter Submenu - multi-select toggles */} - 0 ? ` (${activeFilterCount})` : ""}`} - > - handleFilterToggle("hiddenOnly")} - > - Hidden Only - - handleFilterToggle("paidOnly")} - > - Paid Events - - handleFilterToggle("seatedOnly")} - > - Seated Events - - handleFilterToggle("requiresConfirmationOnly")} - > - Requires Confirmation - - handleFilterToggle("recurringOnly")} - > - Recurring - - {activeFilterCount > 0 && ( - - Clear All Filters - - )} - - - - {/* Profile Button - Opens bottom sheet */} - {userProfile?.avatarUrl ? ( - - router.push("/profile-sheet")}> - - - - ) : ( - router.push("/profile-sheet")}> - - - )} - - - {/* Search Bar */} - handleSearch(e.nativeEvent.text)} - obscureBackground={false} - barTintColor={isDark ? "#171717" : "#fff"} - /> - - - {/* Event Types List */} - } - showsVerticalScrollIndicator={false} - contentInsetAdjustmentBehavior="automatic" - > - {refreshing ? ( - - ) : filteredEventTypes.length === 0 && searchQuery.trim() !== "" ? ( - - - - ) : filteredEventTypes.length === 0 && activeFilterCount > 0 ? ( - - - - ) : ( - - - {filteredEventTypes.map((item, index) => ( - - ))} - - - )} - - - {/* Floating Action Button for New Event Type with Glass UI Menu */} - - - - - - - ); -} diff --git a/companion/app/(tabs)/(event-types)/index.tsx b/companion/app/(tabs)/(event-types)/index.tsx deleted file mode 100644 index e4cc562682..0000000000 --- a/companion/app/(tabs)/(event-types)/index.tsx +++ /dev/null @@ -1,1016 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import * as Clipboard from "expo-clipboard"; -import { isLiquidGlassAvailable } from "expo-glass-effect"; -import { Stack, useRouter } from "expo-router"; -import { useMemo, useState } from "react"; -import { - Alert, - Platform, - RefreshControl, - ScrollView, - Share, - Text, - TextInput, - TouchableOpacity, - useColorScheme, - View, -} from "react-native"; -import { EmptyScreen } from "@/components/EmptyScreen"; -import { EventTypeListItem } from "@/components/event-type-list-item/EventTypeListItem"; -import { EventTypeListSkeleton } from "@/components/event-type-list-item/EventTypeListItemSkeleton"; -import { FullScreenModal } from "@/components/FullScreenModal"; -import { Header } from "@/components/Header"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; -import { Text as AlertDialogText } from "@/components/ui/text"; -import { - useCreateEventType, - useDeleteEventType, - useDuplicateEventType, - useEventTypes, -} from "@/hooks"; -import { useEventTypeFilter } from "@/hooks/useEventTypeFilter"; -import type { EventType } from "@/services/calcom"; -import { showErrorAlert, showSuccessAlert } from "@/utils/alerts"; -import { openInAppBrowser } from "@/utils/browser"; -import { getEventDuration } from "@/utils/getEventDuration"; -import { offlineAwareRefresh } from "@/utils/network"; -import { normalizeMarkdown } from "@/utils/normalizeMarkdown"; -import { shadows } from "@/utils/shadows"; -import { slugify } from "@/utils/slugify"; - -import { getColors } from "@/constants/colors"; - -export default function EventTypes() { - const router = useRouter(); - const [searchQuery, setSearchQuery] = useState(""); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - - // Modal state for creating new event type - const [showCreateModal, setShowCreateModal] = useState(false); - const [newEventTitle, setNewEventTitle] = useState(""); - const [titleError, setTitleError] = useState(""); - - // Use React Query hooks - const { - data: eventTypes = [], - isLoading: loading, - isFetching, - error: queryError, - refetch, - } = useEventTypes(); - - // Show refresh indicator when fetching - const refreshing = isFetching && !loading; - - const { mutate: createEventTypeMutation, isPending: creating } = useCreateEventType(); - const { mutate: deleteEventTypeMutation, isPending: isDeleting } = useDeleteEventType(); - const { mutate: duplicateEventTypeMutation } = useDuplicateEventType(); - - // Convert query error to string - // Don't show error UI for authentication errors (user will be redirected to login) - // Only show error UI in development mode for other errors - const isAuthError = - queryError?.message?.includes("Authentication") || - queryError?.message?.includes("sign in") || - queryError?.message?.includes("401"); - const error = queryError && !isAuthError && __DEV__ ? "Failed to load event types." : null; - - // Modal state for web platform action sheet - const [showActionModal, setShowActionModal] = useState(false); - const [selectedEventType, setSelectedEventType] = useState(null); - - // Modal state for New button menu - const [showNewModal, setShowNewModal] = useState(false); - - // Modal state for delete confirmation - const [showDeleteModal, setShowDeleteModal] = useState(false); - const [eventTypeToDelete, setEventTypeToDelete] = useState(null); - - // Handle pull-to-refresh - // Handle pull-to-refresh (offline-aware) - const onRefresh = () => offlineAwareRefresh(refetch); - - // Event type filter and sort hook - const { - sortBy, - filters, - setSortBy, - toggleFilter, - resetFilters, - filteredAndSortedEventTypes, - activeFilterCount, - } = useEventTypeFilter(); - - // Filter event types based on search query and filter/sort options - const filteredEventTypes = useMemo(() => { - // First apply filter/sort from the hook - let filtered = filteredAndSortedEventTypes(eventTypes); - - // Then apply search query filter - if (searchQuery.trim() !== "") { - const searchLower = searchQuery.toLowerCase(); - filtered = filtered.filter( - (eventType) => - eventType.title.toLowerCase().includes(searchLower) || - eventType.description?.toLowerCase().includes(searchLower) - ); - } - - return filtered; - }, [eventTypes, searchQuery, filteredAndSortedEventTypes]); - - const handleSearch = (query: string) => { - setSearchQuery(query); - }; - - const handleEventTypePress = (eventType: EventType) => { - handleEdit(eventType); - }; - - const handleCopyLink = async (eventType: EventType) => { - if (!eventType.bookingUrl) { - showErrorAlert("Error", "Booking URL not available for this event type."); - return; - } - try { - await Clipboard.setStringAsync(eventType.bookingUrl); - showSuccessAlert("Link Copied", "Event type link copied!"); - } catch { - showErrorAlert("Error", "Failed to copy link. Please try again."); - } - }; - - const _handleShare = async (eventType: EventType) => { - if (!eventType.bookingUrl) { - showErrorAlert("Error", "Booking URL not available for this event type."); - return; - } - try { - await Share.share({ - message: `Book a meeting: ${eventType.title}`, - url: eventType.bookingUrl, - }); - } catch { - showErrorAlert("Error", "Failed to share link. Please try again."); - } - }; - - const handleEdit = (eventType: EventType) => { - const duration = getEventDuration(eventType); - router.push({ - pathname: "/event-type-detail", - params: { - id: eventType.id.toString(), - title: eventType.title, - description: eventType.description || "", - duration: duration.toString(), - price: eventType.price?.toString() || "", - currency: eventType.currency || "", - slug: eventType.slug || "", - }, - }); - }; - - const handleDelete = (eventType: EventType) => { - // Use native Alert.alert on Android for simple yes/no confirmation - if (Platform.OS === "android") { - Alert.alert( - "Delete Event Type", - `This will permanently delete the "${eventType.title}" event type. This action cannot be undone.`, - [ - { text: "Cancel", style: "cancel" }, - { - text: "Delete", - style: "destructive", - onPress: () => { - deleteEventTypeMutation(eventType.id, { - onSuccess: () => { - showSuccessAlert("Success", "Event type deleted successfully"); - }, - onError: (error) => { - const message = error instanceof Error ? error.message : String(error); - console.error("Failed to delete event type", message); - if (__DEV__) { - const stack = error instanceof Error ? error.stack : undefined; - console.debug("[EventTypes] deleteEventType failed", { - message, - stack, - }); - } - showErrorAlert("Error", "Failed to delete event type. Please try again."); - }, - }); - }, - }, - ] - ); - return; - } - - // Use custom modal for iOS and web - setEventTypeToDelete(eventType); - setShowDeleteModal(true); - }; - - const confirmDelete = () => { - if (!eventTypeToDelete) return; - - deleteEventTypeMutation(eventTypeToDelete.id, { - onSuccess: () => { - // Close modal and reset state - setShowDeleteModal(false); - setEventTypeToDelete(null); - - showSuccessAlert("Success", "Event type deleted successfully"); - }, - onError: (error) => { - const message = error instanceof Error ? error.message : String(error); - console.error("Failed to delete event type", message); - if (__DEV__) { - const stack = error instanceof Error ? error.stack : undefined; - console.debug("[EventTypes] deleteEventType failed", { - message, - stack, - }); - } - showErrorAlert("Error", "Failed to delete event type. Please try again."); - }, - }); - }; - - const handleDuplicate = (eventType: EventType) => { - duplicateEventTypeMutation( - { eventType, existingEventTypes: eventTypes }, - { - onSuccess: (duplicatedEventType) => { - showSuccessAlert("Success", "Event type duplicated successfully"); - - const duration = getEventDuration(eventType); - - // Navigate to edit the newly created duplicate - router.push({ - pathname: "/event-type-detail", - params: { - id: duplicatedEventType.id.toString(), - title: duplicatedEventType.title, - description: duplicatedEventType.description || "", - duration: ( - duplicatedEventType.lengthInMinutes || - duplicatedEventType.length || - duration - ).toString(), - slug: duplicatedEventType.slug || "", - }, - }); - }, - onError: (error) => { - const message = error instanceof Error ? error.message : String(error); - console.error("Failed to duplicate event type", message); - if (__DEV__) { - const stack = error instanceof Error ? error.stack : undefined; - console.debug("[EventTypes] duplicateEventType failed", { - message, - stack, - }); - } - showErrorAlert("Error", "Failed to duplicate event type. Please try again."); - }, - } - ); - }; - - const handlePreview = async (eventType: EventType) => { - if (!eventType.bookingUrl) { - showErrorAlert("Error", "Booking URL not available for this event type."); - return; - } - try { - if (Platform.OS === "web") { - window.open(eventType.bookingUrl, "_blank"); - } else { - await openInAppBrowser(eventType.bookingUrl, "event type preview"); - } - } catch { - console.error("Failed to open preview"); - showErrorAlert("Error", "Failed to open preview. Please try again."); - } - }; - - const handleCreateNew = () => { - handleOpenCreateModal(); - }; - - const handleOpenCreateModal = () => { - setShowCreateModal(true); - }; - - const handleCloseCreateModal = () => { - setShowCreateModal(false); - setNewEventTitle(""); - setTitleError(""); - }; - - const handleCreateEventType = () => { - // Clear previous error - setTitleError(""); - - if (!newEventTitle.trim()) { - // Use inline error for Android AlertDialog, showErrorAlert for others - if (Platform.OS === "android") { - setTitleError("Please enter a title for your event type"); - } else { - showErrorAlert("Error", "Please enter a title for your event type"); - } - return; - } - - const autoSlug = slugify(newEventTitle.trim()); - if (!autoSlug) { - if (Platform.OS === "android") { - setTitleError("Title must contain at least one letter or number"); - } else { - showErrorAlert("Error", "Title must contain at least one letter or number"); - } - return; - } - - createEventTypeMutation( - { - title: newEventTitle.trim(), - slug: autoSlug, - lengthInMinutes: 15, // Default duration - description: undefined, // Empty description - }, - { - onSuccess: (newEventType) => { - // Close modal and reset form - handleCloseCreateModal(); - - // Navigate to edit the newly created event type - router.push({ - pathname: "/event-type-detail", - params: { - id: newEventType.id.toString(), - title: newEventType.title, - description: newEventType.description || "", - duration: (newEventType.lengthInMinutes || newEventType.length || 15).toString(), - slug: newEventType.slug || "", - }, - }); - }, - onError: (error) => { - const message = error instanceof Error ? error.message : String(error); - console.error("Failed to create event type", message); - if (__DEV__) { - const stack = error instanceof Error ? error.stack : undefined; - console.debug("[EventTypes] createEventType failed", { - message, - stack, - }); - } - showErrorAlert("Error", "Failed to create event type. Please try again."); - }, - } - ); - }; - - if (loading) { - return ( - - {Platform.OS === "web" &&
} - - - - - ); - } - - if (error) { - return ( - - {Platform.OS === "web" &&
} - - - - Unable to load event types - - - {error} - - refetch()} - > - - Retry - - - - - ); - } - - if (eventTypes.length === 0) { - return ( - - {Platform.OS === "web" &&
} - - - - - ); - } - - if (filteredEventTypes.length === 0 && searchQuery.trim() !== "") { - return ( - - {Platform.OS === "web" && ( - <> -
- - - - - - New - - - - - )} - - - - - ); - } - - return ( - <> - - {(Platform.OS === "web" || Platform.OS === "android") && ( - <> -
- - - - - - New - - - - - )} - - } - showsVerticalScrollIndicator={false} - contentInsetAdjustmentBehavior="automatic" - > - {refreshing ? ( - - ) : filteredEventTypes.length === 0 && activeFilterCount > 0 ? ( - - - - ) : ( - - - {filteredEventTypes.map((item, index) => ( - - ))} - - - )} - - - {/* Create Event Type Modal - Android uses AlertDialog */} - {Platform.OS === "android" ? ( - { - if (!open) handleCloseCreateModal(); - }} - > - - - - - Add a new event type - - - - - Set up event types to offer different types of meetings. - - - - - {/* Title Input */} - - Title - { - setNewEventTitle(text); - if (titleError) setTitleError(""); - }} - autoFocus - autoCapitalize="words" - returnKeyType="done" - onSubmitEditing={handleCreateEventType} - /> - {titleError ? ( - - {titleError} - - ) : null} - - - - - Cancel - - - Continue - - - - - ) : ( - - - e.stopPropagation()} - style={{ ...shadows.xl(), backgroundColor: theme.backgroundSecondary }} - > - {/* Header */} - - - Add a new event type - - - Set up event types to offer different types of meetings. - - - - {/* Content */} - - {/* Title */} - - - Title - - - - - - {/* Footer */} - - - - - Close - - - - - Continue - - - - - - - - )} - - {/* Action Modal for Web Platform */} - { - setShowActionModal(false); - setSelectedEventType(null); - }} - > - { - setShowActionModal(false); - setSelectedEventType(null); - }} - > - e.stopPropagation()} - > - {selectedEventType ? ( - <> - - - {selectedEventType.title} - - {selectedEventType.description ? ( - - {normalizeMarkdown(selectedEventType.description)} - - ) : null} - - - - { - setShowActionModal(false); - const eventType = selectedEventType; - setSelectedEventType(null); - if (eventType) handleEdit(eventType); - }} - > - - Edit - - - { - setShowActionModal(false); - const eventType = selectedEventType; - setSelectedEventType(null); - if (eventType) handleDuplicate(eventType); - }} - > - - Duplicate - - - {/* Separator before delete button */} - - - { - setShowActionModal(false); - const eventType = selectedEventType; - setSelectedEventType(null); - if (eventType) handleDelete(eventType); - }} - > - - - Delete - - - - - - { - setShowActionModal(false); - setSelectedEventType(null); - }} - > - - Cancel - - - - - ) : null} - - - - - {/* New Menu Modal for Web Platform */} - setShowNewModal(false)} - > - setShowNewModal(false)} - > - e.stopPropagation()} - > - {/* Header */} - - New - - Choose what to create - - - - {/* Options List */} - - {/* New Event Type */} - { - setShowNewModal(false); - // Small delay to ensure the "New" modal closes before opening create modal - setTimeout(() => { - handleOpenCreateModal(); - }, 100); - }} - className="flex-row items-center p-2 hover:bg-gray-50 dark:hover:bg-[#2C2C2E] md:p-4" - > - - New Event Type - - - - {/* Cancel button */} - - setShowNewModal(false)} - > - - Cancel - - - - - - - - {/* Delete Confirmation Modal */} - { - if (!isDeleting) { - setShowDeleteModal(false); - setEventTypeToDelete(null); - } - }} - > - - - {/* Header with icon and title */} - - - {/* Danger icon */} - - - - - {/* Title and description */} - - - Delete Event Type - - - {eventTypeToDelete ? ( - <> - This will permanently delete the "{eventTypeToDelete.title}" event type. - This action cannot be undone. - - ) : null} - - - - - - {/* Footer with buttons */} - - - - Delete - - - - { - setShowDeleteModal(false); - setEventTypeToDelete(null); - }} - disabled={isDeleting} - > - - Cancel - - - - - - - - ); -} diff --git a/companion/app/(tabs)/(more)/_layout.tsx b/companion/app/(tabs)/(more)/_layout.tsx deleted file mode 100644 index 0540f7eb85..0000000000 --- a/companion/app/(tabs)/(more)/_layout.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Stack } from "expo-router"; -import { Platform } from "react-native"; - -export default function MoreLayout() { - return ( - - - - ); -} diff --git a/companion/app/(tabs)/(more)/index.ios.tsx b/companion/app/(tabs)/(more)/index.ios.tsx deleted file mode 100644 index 205ae8d389..0000000000 --- a/companion/app/(tabs)/(more)/index.ios.tsx +++ /dev/null @@ -1,362 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import { isLiquidGlassAvailable } from "expo-glass-effect"; -import { Image } from "expo-image"; -import { Stack, useRouter } from "expo-router"; -import { useState } from "react"; -import { - Alert, - Pressable, - ScrollView, - Text, - TouchableOpacity, - useColorScheme, - View, -} from "react-native"; -import { LandingPagePicker } from "@/components/LandingPagePicker"; -import { LogoutConfirmModal } from "@/components/LogoutConfirmModal"; -import { useAuth } from "@/contexts/AuthContext"; -import { useQueryContext } from "@/contexts/QueryContext"; -import { useUserProfile } from "@/hooks"; -import { type LandingPage, useUserPreferences } from "@/hooks/useUserPreferences"; -import { showErrorAlert, showSuccessAlert, showNotAvailableAlert } from "@/utils/alerts"; -import { openInAppBrowser } from "@/utils/browser"; -import { getAvatarUrl } from "@/utils/getAvatarUrl"; -import { getColors } from "@/constants/colors"; - -interface MoreMenuItem { - name: string; - icon: keyof typeof Ionicons.glyphMap; - isExternal?: boolean; - href?: string; - onPress?: () => void; -} - -export default function More() { - const router = useRouter(); - const { logout } = useAuth(); - const { clearCache } = useQueryContext(); - const { preferences, setLandingPage, landingPageLabel } = useUserPreferences(); - const [showLogoutModal, setShowLogoutModal] = useState(false); - const [showLandingPagePicker, setShowLandingPagePicker] = useState(false); - const { data: userProfile } = useUserProfile(); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - - const colors = { - background: isDark ? "#000000" : "#FFFFFF", - backgroundSecondary: isDark ? "#171717" : "#FFFFFF", - backgroundActive: isDark ? "#2C2C2E" : "#F8F9FA", - text: isDark ? "#FFFFFF" : "#333333", - textSecondary: isDark ? "#A3A3A3" : "#C7C7CC", - border: isDark ? "#4D4D4D" : "#E5E5EA", - }; - - const handleLandingPageSelect = async (value: LandingPage) => { - try { - await setLandingPage(value); - showSuccessAlert("Saved", "First page updated"); - } catch { - showErrorAlert("Error", "Failed to save preference. Please try again."); - } - }; - - const performLogout = async () => { - try { - // Clear in-memory cache before logout - await clearCache(); - await logout(); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - console.error("Logout error", message); - if (__DEV__) { - const stack = error instanceof Error ? error.stack : undefined; - console.debug("[More] logout failed", { message, stack }); - } - showErrorAlert("Error", "Failed to sign out. Please try again."); - } - }; - - const handleSignOut = () => { - Alert.alert("Sign Out", "Are you sure you want to sign out?", [ - { text: "Cancel", style: "cancel" }, - { - text: "Sign Out", - style: "destructive", - onPress: performLogout, - }, - ]); - }; - - // handleNotAvailable replaced by global showNotAvailableAlert - - const menuItems: MoreMenuItem[] = [ - { - name: "Apps", - icon: "grid-outline", - isExternal: false, - onPress: () => showNotAvailableAlert(), - }, - { - name: "Routing", - icon: "git-branch-outline", - isExternal: false, - onPress: () => showNotAvailableAlert(), - }, - { - name: "Workflows", - icon: "flash-outline", - isExternal: false, - onPress: () => showNotAvailableAlert(), - }, - { - name: "Insights", - icon: "bar-chart-outline", - isExternal: false, - onPress: () => showNotAvailableAlert(), - }, - { - name: "Support", - icon: "help-circle-outline", - isExternal: false, - onPress: () => showNotAvailableAlert(), - }, - ]; - - return ( - <> - {/* iOS Native Header with Glass UI */} - - More - - {userProfile?.avatarUrl ? ( - - router.push("/profile-sheet")}> - - - - ) : ( - router.push("/profile-sheet")}> - - - )} - - - - {/* Content */} - - - {menuItems.map((item, index) => ( - - - - - {item.name} - - - {item.isExternal ? ( - - ) : ( - - )} - - ))} - - - {/* App Settings */} - - - - App Settings - - - setShowLandingPagePicker(true)} - style={{ - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between", - backgroundColor: colors.backgroundSecondary, - paddingHorizontal: 20, - paddingVertical: 16, - }} - activeOpacity={0.7} - > - - - - First Page - - - - - - {landingPageLabel} - - - - - - - - {/* Delete Account Link */} - - - openInAppBrowser("https://app.cal.com/settings/my-account/profile", "Delete Account") - } - style={{ - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between", - backgroundColor: colors.backgroundSecondary, - paddingHorizontal: 20, - paddingVertical: 16, - }} - activeOpacity={0.7} - > - - - - Delete Account - - - - - - - {/* Sign Out Button */} - - - - - Sign Out - - - - - {/* Footer Note */} - - The companion app is an extension of the web application.{"\n"} - For advanced features, visit{" "} - app.cal.com - - - - {/* Logout Confirmation Modal - kept for consistency */} - { - setShowLogoutModal(false); - performLogout(); - }} - onCancel={() => setShowLogoutModal(false)} - /> - - {/* Landing Page Picker */} - setShowLandingPagePicker(false)} - /> - - ); -} diff --git a/companion/app/(tabs)/(more)/index.tsx b/companion/app/(tabs)/(more)/index.tsx deleted file mode 100644 index 0479ce894e..0000000000 --- a/companion/app/(tabs)/(more)/index.tsx +++ /dev/null @@ -1,269 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import { useState } from "react"; -import { - Alert, - Platform, - ScrollView, - Text, - TouchableOpacity, - useColorScheme, - View, -} from "react-native"; -import { Header } from "@/components/Header"; -import { LandingPagePicker } from "@/components/LandingPagePicker"; -import { LogoutConfirmModal } from "@/components/LogoutConfirmModal"; -import { useAuth } from "@/contexts/AuthContext"; -import { useQueryContext } from "@/contexts/QueryContext"; -import { type LandingPage, useUserPreferences } from "@/hooks/useUserPreferences"; -import { showErrorAlert, showSuccessAlert } from "@/utils/alerts"; -import { openInAppBrowser } from "@/utils/browser"; -import { getColors } from "@/constants/colors"; - -interface MoreMenuItem { - name: string; - icon: keyof typeof Ionicons.glyphMap; - isExternal?: boolean; - href?: string; - onPress?: () => void; -} - -export default function More() { - const { logout } = useAuth(); - const { clearCache } = useQueryContext(); - const { preferences, setLandingPage, landingPageLabel } = useUserPreferences(); - const [showLogoutModal, setShowLogoutModal] = useState(false); - const [showLandingPagePicker, setShowLandingPagePicker] = useState(false); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - - const handleLandingPageSelect = async (value: LandingPage) => { - try { - await setLandingPage(value); - showSuccessAlert("Saved", "First page updated"); - } catch { - showErrorAlert("Error", "Failed to save preference. Please try again."); - } - }; - - const performLogout = async () => { - try { - // Clear in-memory cache before logout - await clearCache(); - await logout(); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - console.error("Logout error", message); - if (__DEV__) { - const stack = error instanceof Error ? error.stack : undefined; - console.debug("[More] logout failed", { message, stack }); - } - showErrorAlert("Error", "Failed to sign out. Please try again."); - } - }; - - const handleSignOut = () => { - if (Platform.OS === "web") { - // Use modal for web/extension since Alert.alert doesn't work - setShowLogoutModal(true); - } else { - // Use native Alert for Android - Alert.alert("Sign Out", "Are you sure you want to sign out?", [ - { text: "Cancel", style: "cancel" }, - { - text: "Sign Out", - style: "destructive", - onPress: performLogout, - }, - ]); - } - }; - - const menuItems: MoreMenuItem[] = [ - { - name: "Apps", - icon: "grid-outline", - isExternal: true, - onPress: () => openInAppBrowser("https://app.cal.com/apps", "Apps page"), - }, - { - name: "Routing", - icon: "git-branch-outline", - isExternal: true, - onPress: () => openInAppBrowser("https://app.cal.com/routing", "Routing page"), - }, - { - name: "Workflows", - icon: "flash-outline", - isExternal: true, - onPress: () => openInAppBrowser("https://app.cal.com/workflows", "Workflows page"), - }, - { - name: "Insights", - icon: "bar-chart-outline", - isExternal: true, - onPress: () => openInAppBrowser("https://app.cal.com/insights", "Insights page"), - }, - { - name: "Support", - icon: "help-circle-outline", - isExternal: true, - onPress: () => openInAppBrowser("https://go.cal.com/support", "Support"), - }, - ]; - - return ( - -
- - - {menuItems.map((item, index) => ( - - - - - {item.name} - - - {item.isExternal ? ( - - ) : ( - - )} - - ))} - - - {/* App Settings */} - - - - App Settings - - - setShowLandingPagePicker(true)} - className="flex-row items-center justify-between bg-white px-5 py-4 active:bg-[#F8F9FA]" - style={{ backgroundColor: theme.backgroundSecondary }} - > - - - - First Page - - - - - - {landingPageLabel} - - - - - - - {/* Delete Account Link */} - - - openInAppBrowser("https://app.cal.com/settings/my-account/profile", "Delete Account") - } - className="flex-row items-center justify-between bg-white px-5 py-4 active:bg-red-50" - style={{ backgroundColor: theme.backgroundSecondary }} - > - - - - Delete Account - - - - - - - {/* Sign Out Button */} - - - - - Sign Out - - - - - {/* Footer Note */} - - The companion app is an extension of the web application.{"\n"} - For advanced features, visit{" "} - openInAppBrowser("https://app.cal.com", "Cal.com")} - > - app.cal.com - - - - - {/* Logout Confirmation Modal for Web */} - { - setShowLogoutModal(false); - performLogout(); - }} - onCancel={() => setShowLogoutModal(false)} - /> - - {/* Landing Page Picker Modal */} - setShowLandingPagePicker(false)} - /> - - ); -} diff --git a/companion/app/(tabs)/_layout.tsx b/companion/app/(tabs)/_layout.tsx deleted file mode 100644 index dd72a6c896..0000000000 --- a/companion/app/(tabs)/_layout.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import type { ColorValue, ImageSourcePropType } from "react-native"; -import { Tabs, VectorIcon, useRouter } from "expo-router"; -import { NativeTabs } from "expo-router/unstable-native-tabs"; -import { useColorScheme } from "react-native"; -import { Platform } from "react-native"; -import { useEffect, useRef } from "react"; -import { getRouteFromPreference, useUserPreferences } from "@/hooks/useUserPreferences"; - -// Type for vector icon families that support getImageSource -type VectorIconFamily = { - getImageSource: (name: string, size: number, color: ColorValue) => Promise; -}; - -export default function TabLayout() { - const router = useRouter(); - const { preferences, isLoading } = useUserPreferences(); - const hasNavigated = useRef(false); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - - const colors = { - selected: isDark ? "#FFFFFF" : "#000000", - inactive: "#A3A3A3", - background: isDark ? "#000000" : "#FFFFFF", - border: isDark ? "#4D4D4D" : "#C6C6C8", - indicator: isDark ? "#FFFFFF15" : "#00000015", - }; - - // Navigate to preferred landing page on first load - // Since Bookings is the default first tab, we only navigate if preference is different - useEffect(() => { - if (!isLoading && !hasNavigated.current) { - hasNavigated.current = true; - - // Only navigate if preference is not the default (bookings) - if (preferences.landingPage !== "bookings") { - const route = getRouteFromPreference(preferences.landingPage); - console.log("[TabLayout] Navigating to preferred landing page:", route); - - // Use a minimal delay to ensure NativeTabs is initialized - setTimeout(() => { - // biome-ignore lint/suspicious/noExplicitAny: expo-router types don't support dynamic route strings from preferences - router.navigate(route as any); - }, 50); - } - } - }, [isLoading, preferences.landingPage, router]); - - if (Platform.OS === "web") { - return ; - } - - return ( - - - {Platform.select({ - ios: , - android: ( - } - selectedColor={colors.selected} - /> - ), - })} - Bookings - - - - {Platform.select({ - ios: , - android: ( - } - selectedColor={colors.selected} - /> - ), - })} - Event Types - - - - {Platform.select({ - ios: , - android: ( - } - selectedColor={colors.selected} - /> - ), - })} - Availability - - - - {Platform.select({ - ios: , - android: ( - } - selectedColor={colors.selected} - /> - ), - })} - More - - - ); -} - -type TabColors = { - selected: string; - inactive: string; - background: string; - border: string; - indicator: string; -}; - -// TODO: Remove this once native tabs are supported on web -function WebTabs({ colors }: { colors: TabColors }) { - return ( - - - ( - - ), - }} - /> - - ( - - ), - }} - /> - - ( - - ), - }} - /> - - ( - - ), - }} - /> - - ); -} diff --git a/companion/app/(tabs)/index.tsx b/companion/app/(tabs)/index.tsx deleted file mode 100644 index a1ac454952..0000000000 --- a/companion/app/(tabs)/index.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Redirect } from "expo-router"; -import { ActivityIndicator, View } from "react-native"; -import { getRouteFromPreference, useUserPreferences } from "@/hooks/useUserPreferences"; - -export default function TabsIndex() { - const { preferences, isLoading } = useUserPreferences(); - - if (isLoading) { - return ( - - - - ); - } - - const route = getRouteFromPreference(preferences.landingPage); - console.log("[TabsIndex] Redirecting to:", route, "from preference:", preferences.landingPage); - - // biome-ignore lint/suspicious/noExplicitAny: expo-router types don't support dynamic route strings from preferences - return ; -} diff --git a/companion/app/_layout.tsx b/companion/app/_layout.tsx deleted file mode 100644 index 3bbee33423..0000000000 --- a/companion/app/_layout.tsx +++ /dev/null @@ -1,466 +0,0 @@ -import "./_webRuntimeGuards"; -import "../global.css"; - -import { DarkTheme, DefaultTheme, ThemeProvider } from "@react-navigation/native"; -import { PortalHost } from "@rn-primitives/portal"; -import { isLiquidGlassAvailable } from "expo-glass-effect"; -import { Stack } from "expo-router"; -import { Platform, StatusBar, useColorScheme, View } from "react-native"; -import { CalComLogo } from "@/components/CalComLogo"; -import LoginScreenComponent from "@/components/LoginScreen"; -import { NetworkStatusBanner } from "@/components/NetworkStatusBanner"; -import { GlobalToast } from "@/components/ui/GlobalToast"; -import { WidgetSyncProvider } from "@/components/WidgetSyncProvider"; -import { getColors, colors as semanticColors } from "@/constants/colors"; -import { AuthProvider, useAuth } from "@/contexts/AuthContext"; -import { QueryProvider } from "@/contexts/QueryContext"; -import { ToastProvider } from "@/contexts/ToastContext"; - -function RootLayoutContent() { - const { isAuthenticated, loading } = useAuth(); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const colors = getColors(isDark); - - // Show Cal.com logo while checking auth state to prevent login flash - if (loading) { - return ( - - - - - ); - } - - const content = isAuthenticated ? ( - - - - - - - - - - - - - - - ) : ( - - ); - - const containerClass = - Platform.OS === "web" - ? "w-[400px] flex-1 flex-col self-end border-l border-gray-200 dark:border-gray-700 bg-white dark:bg-black" - : "bg-white dark:bg-black"; - - const containerStyle = - Platform.OS === "web" - ? { - width: 400, - flex: 1, - display: "flex" as const, - flexDirection: "column" as const, - } - : { flex: 1 }; - - return ( - - - - {content} - - - - - ); -} - -export default function RootLayout() { - return ( - - - - - - - - - - - ); -} diff --git a/companion/app/_webRuntimeGuards.ts b/companion/app/_webRuntimeGuards.ts deleted file mode 100644 index 1df2781bdb..0000000000 --- a/companion/app/_webRuntimeGuards.ts +++ /dev/null @@ -1,69 +0,0 @@ -// Web-only runtime guards for Companion. -// -// This runs as early as possible on web to prevent a noisy (but harmless) -// react-native-css-interop error when darkMode is set to "media". - -declare const window: unknown; -declare const require: undefined | ((id: string) => unknown); - -if (typeof window !== "undefined") { - const w = window as { - addEventListener?: (type: string, listener: (event: unknown) => void) => void; - }; - - const shouldSuppressCssInteropError = (err: unknown) => { - if (!(err instanceof Error)) return false; - if (!err.message.includes("Cannot manually set color scheme")) return false; - - // Some environments omit stack traces; if we have one, ensure it's the expected source. - if (typeof err.stack === "string") { - return err.stack.includes("react-native-css-interop/dist/runtime/web/color-scheme"); - } - - return true; - }; - - const suppressIfMatch = (event: unknown, err: unknown) => { - if (!event || typeof event !== "object") return; - const e = event as { preventDefault?: () => void }; - if (shouldSuppressCssInteropError(err)) e.preventDefault?.(); - }; - - w.addEventListener?.("error", (event) => { - const e = event as { error?: unknown }; - suppressIfMatch(event, e.error); - }); - - w.addEventListener?.("unhandledrejection", (event) => { - const e = event as { reason?: unknown }; - suppressIfMatch(event, e.reason); - }); - - const patchCssInterop = (mod: unknown) => { - const colorScheme = (mod as { colorScheme?: { set?: (value: unknown) => void } }).colorScheme; - if (!colorScheme?.set) return; - - // Completely no-op the set call when darkMode is 'media'. - // Manual color scheme setting is not supported in this mode, - // so we silently ignore attempts to call it. - colorScheme.set = () => { - // No-op: system preference controls the color scheme - }; - }; - - // Patch react-native-css-interop on web to ignore the known error. - // This can happen very early during init when darkMode is "media", so prefer sync require. - try { - if (typeof require === "function") { - patchCssInterop(require("react-native-css-interop/dist/runtime/web/color-scheme")); - } else { - void import("react-native-css-interop/dist/runtime/web/color-scheme") - .then(patchCssInterop) - .catch(() => { - // noop - }); - } - } catch { - // noop - } -} diff --git a/companion/app/add-guests.ios.tsx b/companion/app/add-guests.ios.tsx deleted file mode 100644 index 1832807f47..0000000000 --- a/companion/app/add-guests.ios.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { osName } from "expo-device"; -import { isLiquidGlassAvailable } from "expo-glass-effect"; -import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, useColorScheme, View } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { getColors } from "@/constants/colors"; -import type { AddGuestsScreenHandle } from "@/components/screens/AddGuestsScreen"; -import AddGuestsScreenComponent from "@/components/screens/AddGuestsScreen"; -import { type Booking, CalComAPIService } from "@/services/calcom"; -import { showErrorAlert } from "@/utils/alerts"; - -// Semi-transparent background to prevent black flash while preserving glass effect - -function getPresentationStyle(): "formSheet" | "modal" { - if (isLiquidGlassAvailable() && osName !== "iPadOS") { - return "formSheet"; - } - return "modal"; -} - -export default function AddGuestsIOS() { - const { uid } = useLocalSearchParams<{ uid: string }>(); - const router = useRouter(); - const insets = useSafeAreaInsets(); - const [booking, setBooking] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [isSaving, setIsSaving] = useState(false); - const [guestCount, setGuestCount] = useState(0); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - - const addGuestsScreenRef = useRef(null); - - useEffect(() => { - if (uid) { - setIsLoading(true); - CalComAPIService.getBookingByUid(uid) - .then(setBooking) - .catch(() => { - showErrorAlert("Error", "Failed to load booking details"); - router.back(); - }) - .finally(() => setIsLoading(false)); - } else { - setIsLoading(false); - showErrorAlert("Error", "Booking ID is missing"); - router.back(); - } - }, [uid, router]); - - const handleSave = useCallback(() => { - addGuestsScreenRef.current?.submit(); - }, []); - - const handleAddGuestsSuccess = useCallback(() => { - router.back(); - }, [router]); - - const presentationStyle = getPresentationStyle(); - const useGlassEffect = isLiquidGlassAvailable(); - - // Semi-transparent background to prevent flashes while preserving glass effect - const glassBackground = isDark ? "rgba(28, 28, 30, 0.01)" : "rgba(248, 248, 250, 0.01)"; - - const showSaveButton = guestCount > 0; - - return ( - <> - - - - - router.back()} - tintColor={theme.backgroundEmphasis} - variant="prominent" - > - - - - - Add Guests - - - {showSaveButton ? ( - - - - ) : null} - - - - - {isLoading ? ( - - - - ) : ( - - )} - - - ); -} diff --git a/companion/app/add-guests.tsx b/companion/app/add-guests.tsx deleted file mode 100644 index 8082812ab4..0000000000 --- a/companion/app/add-guests.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, Platform, useColorScheme, View } from "react-native"; -import { AppPressable } from "@/components/AppPressable"; -import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; -import type { AddGuestsScreenHandle } from "@/components/screens/AddGuestsScreen"; -import AddGuestsScreenComponent from "@/components/screens/AddGuestsScreen"; -import { getColors } from "@/constants/colors"; -import { type Booking, CalComAPIService } from "@/services/calcom"; -import { showErrorAlert } from "@/utils/alerts"; - -export default function AddGuests() { - const { uid } = useLocalSearchParams<{ uid: string }>(); - const router = useRouter(); - const [booking, setBooking] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [isSaving, setIsSaving] = useState(false); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - - const addGuestsScreenRef = useRef(null); - - useEffect(() => { - if (uid) { - setIsLoading(true); - CalComAPIService.getBookingByUid(uid) - .then(setBooking) - .catch(() => { - showErrorAlert("Error", "Failed to load booking details"); - router.back(); - }) - .finally(() => setIsLoading(false)); - } else { - setIsLoading(false); - showErrorAlert("Error", "Booking ID is missing"); - router.back(); - } - }, [uid, router]); - - const handleSave = useCallback(() => { - addGuestsScreenRef.current?.submit(); - }, []); - - const handleAddGuestsSuccess = useCallback(() => { - router.back(); - }, [router]); - - const renderHeaderLeft = useCallback( - () => ( - - router.back()} - className="h-10 w-10 items-center justify-center rounded-full border" - style={{ - borderColor: theme.border, - backgroundColor: theme.background, - marginRight: 8, - }} - > - - - - ), - [router, theme.border, theme.background, theme.text] - ); - - const renderHeaderRight = useCallback( - () => ( - - - - - - ), - [handleSave, isSaving, theme.text, theme.border, theme.background] - ); - - if (isLoading) { - return ( - <> - - - {Platform.OS === "ios" && ( - - Add Guests - - )} - - - - - - ); - } - - return ( - <> - - - {Platform.OS === "ios" && ( - - - router.back()}> - - - - - Add Guests - - - - - - - - )} - - - - ); -} diff --git a/companion/app/edit-availability-day.ios.tsx b/companion/app/edit-availability-day.ios.tsx deleted file mode 100644 index 777d2e9ae7..0000000000 --- a/companion/app/edit-availability-day.ios.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { osName } from "expo-device"; -import { isLiquidGlassAvailable } from "expo-glass-effect"; -import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, useColorScheme, View } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { getColors } from "@/constants/colors"; -import type { EditAvailabilityDayScreenHandle } from "@/components/screens/EditAvailabilityDayScreen.ios"; -import EditAvailabilityDayScreenComponent from "@/components/screens/EditAvailabilityDayScreen.ios"; -import { CalComAPIService, type Schedule } from "@/services/calcom"; -import { showErrorAlert } from "@/utils/alerts"; - -const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; - -// Semi-transparent background to prevent black flash while preserving glass effect - -function getPresentationStyle(): "formSheet" | "modal" { - if (isLiquidGlassAvailable() && osName !== "iPadOS") { - return "formSheet"; - } - return "modal"; -} - -export default function EditAvailabilityDayIOS() { - const { id, day } = useLocalSearchParams<{ id: string; day: string }>(); - const router = useRouter(); - const insets = useSafeAreaInsets(); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - const [schedule, setSchedule] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [isSaving, setIsSaving] = useState(false); - - const screenRef = useRef(null); - - const dayIndex = day ? parseInt(day, 10) : 0; - const dayName = DAYS[dayIndex] || "Day"; - - useEffect(() => { - if (id) { - setIsLoading(true); - CalComAPIService.getScheduleById(Number(id)) - .then(setSchedule) - .catch(() => { - showErrorAlert("Error", "Failed to load schedule details"); - router.back(); - }) - .finally(() => setIsLoading(false)); - } else { - setIsLoading(false); - showErrorAlert("Error", "Schedule ID is missing"); - router.back(); - } - }, [id, router]); - - const handleSave = useCallback(() => { - screenRef.current?.submit(); - }, []); - - const handleSuccess = useCallback(() => { - router.back(); - }, [router]); - - const presentationStyle = getPresentationStyle(); - const useGlassEffect = isLiquidGlassAvailable(); - - // Semi-transparent background to prevent flashes while preserving glass effect - const glassBackground = isDark ? "rgba(28, 28, 30, 0.01)" : "rgba(248, 248, 250, 0.01)"; - - return ( - <> - - - - - router.back()} - tintColor={theme.backgroundEmphasis} - variant="prominent" - > - - - - - {dayName} - - - - - - - - - - {isLoading ? ( - - - - ) : ( - - )} - - - ); -} diff --git a/companion/app/edit-availability-day.tsx b/companion/app/edit-availability-day.tsx deleted file mode 100644 index b88046ed9b..0000000000 --- a/companion/app/edit-availability-day.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, useColorScheme, View } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { AppPressable } from "@/components/AppPressable"; -import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; -import type { EditAvailabilityDayScreenHandle } from "@/components/screens/EditAvailabilityDayScreen"; -import EditAvailabilityDayScreenComponent from "@/components/screens/EditAvailabilityDayScreen"; -import { getColors } from "@/constants/colors"; -import { CalComAPIService, type Schedule } from "@/services/calcom"; -import { showErrorAlert } from "@/utils/alerts"; - -const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; - -export default function EditAvailabilityDay() { - const { id, day } = useLocalSearchParams<{ id: string; day: string }>(); - const router = useRouter(); - const insets = useSafeAreaInsets(); - const [schedule, setSchedule] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [isSaving, setIsSaving] = useState(false); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - - const screenRef = useRef(null); - - const dayIndex = day ? parseInt(day, 10) : 0; - const dayName = DAYS[dayIndex] || "Day"; - - useEffect(() => { - if (id) { - setIsLoading(true); - CalComAPIService.getScheduleById(Number(id)) - .then(setSchedule) - .catch(() => { - showErrorAlert("Error", "Failed to load schedule details"); - router.back(); - }) - .finally(() => setIsLoading(false)); - } else { - setIsLoading(false); - showErrorAlert("Error", "Schedule ID is missing"); - router.back(); - } - }, [id, router]); - - const handleSave = useCallback(() => { - if (isSaving) return; - screenRef.current?.submit(); - }, [isSaving]); - - const handleSuccess = useCallback(() => { - router.back(); - }, [router]); - - const handleClose = useCallback(() => { - router.back(); - }, [router]); - - if (isLoading) { - return ( - - - - - ); - } - - return ( - - ( - - - - - - ), - headerRight: () => ( - - - - - - ), - }} - /> - - - ); -} diff --git a/companion/app/edit-availability-hours.ios.tsx b/companion/app/edit-availability-hours.ios.tsx deleted file mode 100644 index f60642bc51..0000000000 --- a/companion/app/edit-availability-hours.ios.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { osName } from "expo-device"; -import { isLiquidGlassAvailable } from "expo-glass-effect"; -import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import { useCallback, useEffect } from "react"; -import { ActivityIndicator, useColorScheme, View } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { getColors } from "@/constants/colors"; -import EditAvailabilityHoursScreenComponent from "@/components/screens/EditAvailabilityHoursScreen.ios"; -import { useScheduleById } from "@/hooks/useSchedules"; -import { showErrorAlert } from "@/utils/alerts"; - -// Semi-transparent background to prevent black flash while preserving glass effect - -function getPresentationStyle(): "formSheet" | "modal" { - if (isLiquidGlassAvailable() && osName !== "iPadOS") { - return "formSheet"; - } - return "modal"; -} - -export default function EditAvailabilityHoursIOS() { - const { id } = useLocalSearchParams<{ id: string }>(); - const router = useRouter(); - const insets = useSafeAreaInsets(); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - - // Use React Query hook to read from cache (syncs with mutations) - const { data: schedule, isLoading, isError } = useScheduleById(id ? Number(id) : undefined); - - // Handle missing ID or error - useEffect(() => { - if (!id) { - showErrorAlert("Error", "Schedule ID is missing"); - router.back(); - } else if (isError) { - showErrorAlert("Error", "Failed to load schedule details"); - router.back(); - } - }, [id, isError, router]); - - const handleDayPress = useCallback( - (dayIndex: number) => { - router.push(`/edit-availability-day?id=${id}&day=${dayIndex}` as never); - }, - [router, id] - ); - - const presentationStyle = getPresentationStyle(); - const useGlassEffect = isLiquidGlassAvailable(); - - // Semi-transparent background to prevent flashes while preserving glass effect - const glassBackground = isDark ? "rgba(28, 28, 30, 0.01)" : "rgba(248, 248, 250, 0.01)"; - - return ( - <> - - - - - router.back()} - tintColor={theme.backgroundEmphasis} - variant="prominent" - > - - - - - Working Hours - - - - {isLoading ? ( - - - - ) : ( - - )} - - - ); -} diff --git a/companion/app/edit-availability-hours.tsx b/companion/app/edit-availability-hours.tsx deleted file mode 100644 index c16c2d7929..0000000000 --- a/companion/app/edit-availability-hours.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import { useCallback, useEffect } from "react"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { ActivityIndicator, useColorScheme, View } from "react-native"; -import { AppPressable } from "@/components/AppPressable"; -import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; -import EditAvailabilityHoursScreenComponent from "@/components/screens/EditAvailabilityHoursScreen"; -import { getColors } from "@/constants/colors"; -import { useScheduleById } from "@/hooks/useSchedules"; -import { showErrorAlert } from "@/utils/alerts"; - -export default function EditAvailabilityHours() { - const { id } = useLocalSearchParams<{ id: string }>(); - const router = useRouter(); - const insets = useSafeAreaInsets(); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - - // Use React Query hook to read from cache (syncs with mutations) - const { data: schedule, isLoading, isError } = useScheduleById(id ? Number(id) : undefined); - - // Handle missing ID or error - useEffect(() => { - if (!id) { - showErrorAlert("Error", "Schedule ID is missing"); - router.back(); - } else if (isError) { - showErrorAlert("Error", "Failed to load schedule details"); - router.back(); - } - }, [id, isError, router]); - - const handleClose = useCallback(() => { - router.back(); - }, [router]); - - const handleDayPress = useCallback( - (dayIndex: number) => { - router.push(`/edit-availability-day?id=${id}&day=${dayIndex}` as never); - }, - [router, id] - ); - - if (isLoading) { - return ( - - - - - ); - } - - return ( - - ( - - - - - - ), - }} - /> - - - ); -} diff --git a/companion/app/edit-availability-name.ios.tsx b/companion/app/edit-availability-name.ios.tsx deleted file mode 100644 index adcc2d414c..0000000000 --- a/companion/app/edit-availability-name.ios.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { osName } from "expo-device"; -import { isLiquidGlassAvailable } from "expo-glass-effect"; -import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, useColorScheme, View } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { getColors } from "@/constants/colors"; -import type { EditAvailabilityNameScreenHandle } from "@/components/screens/EditAvailabilityNameScreen.ios"; -import EditAvailabilityNameScreenComponent from "@/components/screens/EditAvailabilityNameScreen.ios"; -import { CalComAPIService, type Schedule } from "@/services/calcom"; -import { showErrorAlert } from "@/utils/alerts"; - -// Semi-transparent background to prevent black flash while preserving glass effect - -function getPresentationStyle(): "formSheet" | "modal" { - if (isLiquidGlassAvailable() && osName !== "iPadOS") { - return "formSheet"; - } - return "modal"; -} - -export default function EditAvailabilityNameIOS() { - const { id } = useLocalSearchParams<{ id: string }>(); - const router = useRouter(); - const insets = useSafeAreaInsets(); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - const [schedule, setSchedule] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [isSaving, setIsSaving] = useState(false); - - const screenRef = useRef(null); - - useEffect(() => { - if (id) { - setIsLoading(true); - CalComAPIService.getScheduleById(Number(id)) - .then(setSchedule) - .catch(() => { - showErrorAlert("Error", "Failed to load schedule details"); - router.back(); - }) - .finally(() => setIsLoading(false)); - } else { - setIsLoading(false); - showErrorAlert("Error", "Schedule ID is missing"); - router.back(); - } - }, [id, router]); - - const handleSave = useCallback(() => { - screenRef.current?.submit(); - }, []); - - const handleSuccess = useCallback(() => { - router.back(); - }, [router]); - - const presentationStyle = getPresentationStyle(); - const useGlassEffect = isLiquidGlassAvailable(); - - // Semi-transparent background to prevent flashes while preserving glass effect - const glassBackground = isDark ? "rgba(28, 28, 30, 0.01)" : "rgba(248, 248, 250, 0.01)"; - - return ( - <> - - - - - router.back()} - tintColor={theme.backgroundEmphasis} - variant="prominent" - > - - - - - Edit Name & Timezone - - - - - - - - - - {isLoading ? ( - - - - ) : ( - - )} - - - ); -} diff --git a/companion/app/edit-availability-name.tsx b/companion/app/edit-availability-name.tsx deleted file mode 100644 index 3acc2e7428..0000000000 --- a/companion/app/edit-availability-name.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, useColorScheme, View } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { AppPressable } from "@/components/AppPressable"; -import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; -import type { EditAvailabilityNameScreenHandle } from "@/components/screens/EditAvailabilityNameScreen"; -import EditAvailabilityNameScreenComponent from "@/components/screens/EditAvailabilityNameScreen"; -import { getColors } from "@/constants/colors"; -import { CalComAPIService, type Schedule } from "@/services/calcom"; -import { showErrorAlert } from "@/utils/alerts"; - -export default function EditAvailabilityName() { - const { id } = useLocalSearchParams<{ id: string }>(); - const router = useRouter(); - const insets = useSafeAreaInsets(); - const [schedule, setSchedule] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [isSaving, setIsSaving] = useState(false); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - - const screenRef = useRef(null); - - useEffect(() => { - if (id) { - setIsLoading(true); - CalComAPIService.getScheduleById(Number(id)) - .then(setSchedule) - .catch(() => { - showErrorAlert("Error", "Failed to load schedule details"); - router.back(); - }) - .finally(() => setIsLoading(false)); - } else { - setIsLoading(false); - showErrorAlert("Error", "Schedule ID is missing"); - router.back(); - } - }, [id, router]); - - const handleClose = useCallback(() => { - router.back(); - }, [router]); - - const handleSave = useCallback(() => { - if (isSaving) return; - screenRef.current?.submit(); - }, [isSaving]); - - const handleSuccess = useCallback(() => { - router.back(); - }, [router]); - - if (isLoading) { - return ( - - - - - ); - } - - return ( - - ( - - - - - - ), - headerRight: () => ( - - - - - - ), - }} - /> - - - ); -} diff --git a/companion/app/edit-availability-override.ios.tsx b/companion/app/edit-availability-override.ios.tsx deleted file mode 100644 index 19a75ed1a2..0000000000 --- a/companion/app/edit-availability-override.ios.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { osName } from "expo-device"; -import { isLiquidGlassAvailable } from "expo-glass-effect"; -import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, useColorScheme, View } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { getColors } from "@/constants/colors"; -import type { EditAvailabilityOverrideScreenHandle } from "@/components/screens/EditAvailabilityOverrideScreen.ios"; -import EditAvailabilityOverrideScreenComponent from "@/components/screens/EditAvailabilityOverrideScreen.ios"; -import { CalComAPIService, type Schedule } from "@/services/calcom"; -import { showErrorAlert } from "@/utils/alerts"; - -// Semi-transparent background to prevent black flash while preserving glass effect - -function getPresentationStyle(): "formSheet" | "modal" { - if (isLiquidGlassAvailable() && osName !== "iPadOS") { - return "formSheet"; - } - return "modal"; -} - -export default function EditAvailabilityOverrideIOS() { - const { id, overrideIndex } = useLocalSearchParams<{ - id: string; - overrideIndex?: string; - }>(); - const router = useRouter(); - const insets = useSafeAreaInsets(); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - const [schedule, setSchedule] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [isSaving, setIsSaving] = useState(false); - - const screenRef = useRef(null); - - const editingIndex = overrideIndex ? parseInt(overrideIndex, 10) : undefined; - const isEditing = editingIndex !== undefined; - - useEffect(() => { - if (id) { - setIsLoading(true); - CalComAPIService.getScheduleById(Number(id)) - .then(setSchedule) - .catch(() => { - showErrorAlert("Error", "Failed to load schedule details"); - router.back(); - }) - .finally(() => setIsLoading(false)); - } else { - setIsLoading(false); - showErrorAlert("Error", "Schedule ID is missing"); - router.back(); - } - }, [id, router]); - - const handleSave = useCallback(() => { - screenRef.current?.submit(); - }, []); - - const handleSuccess = useCallback(() => { - router.back(); - }, [router]); - - const handleEditOverride = useCallback( - (index: number) => { - // Push a new edit screen on top of the current one - // This allows user to go back to the override list after editing - router.push(`/edit-availability-override?id=${id}&overrideIndex=${index}` as never); - }, - [id, router] - ); - - const presentationStyle = getPresentationStyle(); - const useGlassEffect = isLiquidGlassAvailable(); - - // Semi-transparent background to prevent flashes while preserving glass effect - const glassBackground = isDark ? "rgba(28, 28, 30, 0.01)" : "rgba(248, 248, 250, 0.01)"; - - return ( - <> - - - - - router.back()} - tintColor={theme.backgroundEmphasis} - variant="prominent" - > - - - - - {isEditing ? "Edit Override" : "Add Override"} - - - - - - - - - - {isLoading ? ( - - - - ) : ( - - )} - - - ); -} diff --git a/companion/app/edit-availability-override.tsx b/companion/app/edit-availability-override.tsx deleted file mode 100644 index b5f1d59179..0000000000 --- a/companion/app/edit-availability-override.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, useColorScheme, View } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { AppPressable } from "@/components/AppPressable"; -import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; -import type { EditAvailabilityOverrideScreenHandle } from "@/components/screens/EditAvailabilityOverrideScreen"; -import EditAvailabilityOverrideScreenComponent from "@/components/screens/EditAvailabilityOverrideScreen"; -import { getColors } from "@/constants/colors"; -import { CalComAPIService, type Schedule } from "@/services/calcom"; -import { showErrorAlert } from "@/utils/alerts"; - -export default function EditAvailabilityOverride() { - const { id, overrideIndex } = useLocalSearchParams<{ - id: string; - overrideIndex?: string; - }>(); - const router = useRouter(); - const insets = useSafeAreaInsets(); - const [schedule, setSchedule] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [isSaving, setIsSaving] = useState(false); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - - const screenRef = useRef(null); - - const editingIndex = overrideIndex ? parseInt(overrideIndex, 10) : undefined; - const isEditing = editingIndex !== undefined; - - useEffect(() => { - if (id) { - setIsLoading(true); - CalComAPIService.getScheduleById(Number(id)) - .then(setSchedule) - .catch(() => { - showErrorAlert("Error", "Failed to load schedule details"); - router.back(); - }) - .finally(() => setIsLoading(false)); - } else { - setIsLoading(false); - showErrorAlert("Error", "Schedule ID is missing"); - router.back(); - } - }, [id, router]); - - const handleClose = useCallback(() => { - router.back(); - }, [router]); - - const handleSave = useCallback(() => { - if (isSaving) return; - screenRef.current?.submit(); - }, [isSaving]); - - const handleSuccess = useCallback(() => { - router.back(); - }, [router]); - - const handleEditOverride = useCallback( - (index: number) => { - // Push a new edit screen on top of the current one - // This allows user to go back to the override list after editing - router.push(`/edit-availability-override?id=${id}&overrideIndex=${index}` as never); - }, - [id, router] - ); - - const title = isEditing ? "Edit Override" : "Add Override"; - - if (isLoading) { - return ( - - - - - ); - } - - return ( - - ( - - - - - - ), - headerRight: () => ( - - - - - - ), - }} - /> - - - ); -} diff --git a/companion/app/edit-location.ios.tsx b/companion/app/edit-location.ios.tsx deleted file mode 100644 index ec0b5ab3f9..0000000000 --- a/companion/app/edit-location.ios.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { osName } from "expo-device"; -import { isLiquidGlassAvailable } from "expo-glass-effect"; -import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, useColorScheme, View } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { getColors } from "@/constants/colors"; -import type { EditLocationScreenHandle } from "@/components/screens/EditLocationScreen"; -import EditLocationScreenComponent from "@/components/screens/EditLocationScreen"; -import { type Booking, CalComAPIService } from "@/services/calcom"; -import { showErrorAlert } from "@/utils/alerts"; - -// Semi-transparent background to prevent black flash while preserving glass effect - -function getPresentationStyle(): "formSheet" | "modal" { - if (isLiquidGlassAvailable() && osName !== "iPadOS") { - return "formSheet"; - } - return "modal"; -} - -export default function EditLocationIOS() { - const { uid } = useLocalSearchParams<{ uid: string }>(); - const router = useRouter(); - const insets = useSafeAreaInsets(); - const [booking, setBooking] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [isSaving, setIsSaving] = useState(false); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - - const editLocationScreenRef = useRef(null); - - useEffect(() => { - if (uid) { - setIsLoading(true); - CalComAPIService.getBookingByUid(uid) - .then(setBooking) - .catch(() => { - showErrorAlert("Error", "Failed to load booking details"); - router.back(); - }) - .finally(() => setIsLoading(false)); - } else { - setIsLoading(false); - showErrorAlert("Error", "Booking ID is missing"); - router.back(); - } - }, [uid, router]); - - const handleSave = useCallback(() => { - editLocationScreenRef.current?.submit(); - }, []); - - const handleUpdateSuccess = useCallback(() => { - router.back(); - }, [router]); - - const presentationStyle = getPresentationStyle(); - const useGlassEffect = isLiquidGlassAvailable(); - - // Semi-transparent background to prevent flashes while preserving glass effect - const glassBackground = isDark ? "rgba(28, 28, 30, 0.01)" : "rgba(248, 248, 250, 0.01)"; - - return ( - <> - - - - - router.back()} - tintColor={theme.backgroundEmphasis} - variant="prominent" - > - - - - - Edit Location - - - - - - - - - - {isLoading ? ( - - - - ) : ( - - )} - - - ); -} diff --git a/companion/app/edit-location.tsx b/companion/app/edit-location.tsx deleted file mode 100644 index c9cebaaa21..0000000000 --- a/companion/app/edit-location.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, Platform, useColorScheme, View } from "react-native"; -import { AppPressable } from "@/components/AppPressable"; -import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; -import type { EditLocationScreenHandle } from "@/components/screens/EditLocationScreen"; -import EditLocationScreenComponent from "@/components/screens/EditLocationScreen"; -import { getColors } from "@/constants/colors"; -import { type Booking, CalComAPIService } from "@/services/calcom"; -import { showErrorAlert } from "@/utils/alerts"; - -export default function EditLocation() { - const { uid } = useLocalSearchParams<{ uid: string }>(); - const router = useRouter(); - const [booking, setBooking] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [isSaving, setIsSaving] = useState(false); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - - const editLocationScreenRef = useRef(null); - - useEffect(() => { - if (uid) { - setIsLoading(true); - CalComAPIService.getBookingByUid(uid) - .then(setBooking) - .catch(() => { - showErrorAlert("Error", "Failed to load booking details"); - router.back(); - }) - .finally(() => setIsLoading(false)); - } else { - setIsLoading(false); - showErrorAlert("Error", "Booking ID is missing"); - router.back(); - } - }, [uid, router]); - - const handleSave = useCallback(() => { - editLocationScreenRef.current?.submit(); - }, []); - - const handleUpdateSuccess = useCallback(() => { - router.back(); - }, [router]); - - const renderHeaderLeft = useCallback( - () => ( - - router.back()} - className="h-10 w-10 items-center justify-center rounded-full border" - style={{ - borderColor: theme.border, - backgroundColor: theme.background, - marginRight: 8, - }} - > - - - - ), - [router, theme.border, theme.background, theme.text] - ); - - const renderHeaderRight = useCallback( - () => ( - - - - - - ), - [handleSave, isSaving, theme.text, theme.border, theme.background] - ); - - if (isLoading) { - return ( - <> - - - {Platform.OS === "ios" && ( - - Edit Location - - )} - - - - - - ); - } - - return ( - <> - - - {Platform.OS === "ios" && ( - - - router.back()}> - - - - - Edit Location - - - - - - - - )} - - - - ); -} diff --git a/companion/app/mark-no-show.ios.tsx b/companion/app/mark-no-show.ios.tsx deleted file mode 100644 index 5818bab994..0000000000 --- a/companion/app/mark-no-show.ios.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { osName } from "expo-device"; -import { isLiquidGlassAvailable } from "expo-glass-effect"; -import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import { useEffect, useState } from "react"; -import { ActivityIndicator, useColorScheme, View } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { getColors } from "@/constants/colors"; -import MarkNoShowScreenComponent from "@/components/screens/MarkNoShowScreen"; -import { type Booking, CalComAPIService } from "@/services/calcom"; -import { showErrorAlert } from "@/utils/alerts"; - -interface Attendee { - id?: number | string; - email: string; - name: string; - noShow?: boolean; -} - -interface BookingAttendee { - id?: number | string; - email: string; - name?: string; - noShow?: boolean; - absent?: boolean; -} - -function getPresentationStyle(): "formSheet" | "modal" { - if (isLiquidGlassAvailable() && osName !== "iPadOS") { - return "formSheet"; - } - return "modal"; -} - -export default function MarkNoShowIOS() { - const { uid } = useLocalSearchParams<{ uid: string }>(); - const router = useRouter(); - const insets = useSafeAreaInsets(); - const [booking, setBooking] = useState(null); - const [attendees, setAttendees] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - - useEffect(() => { - if (uid) { - setIsLoading(true); - CalComAPIService.getBookingByUid(uid) - .then((bookingData) => { - setBooking(bookingData); - const bookingAttendees: Attendee[] = []; - if (bookingData.attendees && Array.isArray(bookingData.attendees)) { - bookingData.attendees.forEach((att: BookingAttendee) => { - bookingAttendees.push({ - id: att.id, - email: att.email, - name: att.name || att.email, - noShow: att.absent === true || att.noShow === true, - }); - }); - } - setAttendees(bookingAttendees); - }) - .catch(() => { - showErrorAlert("Error", "Failed to load booking details"); - router.back(); - }) - .finally(() => setIsLoading(false)); - } else { - setIsLoading(false); - showErrorAlert("Error", "Booking ID is missing"); - router.back(); - } - }, [uid, router]); - - const presentationStyle = getPresentationStyle(); - const useGlassEffect = isLiquidGlassAvailable(); - - return ( - <> - - - - - router.back()}> - - - - - Mark No-Show - - - - {isLoading ? ( - - - - ) : ( - { - setBooking(updatedBooking); - const updatedAttendees: Attendee[] = []; - if (updatedBooking.attendees && Array.isArray(updatedBooking.attendees)) { - updatedBooking.attendees.forEach((att: BookingAttendee) => { - updatedAttendees.push({ - id: att.id, - email: att.email, - name: att.name || att.email, - noShow: att.absent === true || att.noShow === true, - }); - }); - } - setAttendees(updatedAttendees); - }} - transparentBackground={useGlassEffect} - /> - )} - - - ); -} diff --git a/companion/app/mark-no-show.tsx b/companion/app/mark-no-show.tsx deleted file mode 100644 index 1fe2ed5314..0000000000 --- a/companion/app/mark-no-show.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import { useCallback, useEffect, useState } from "react"; -import { ActivityIndicator, Platform, useColorScheme, View } from "react-native"; -import { AppPressable } from "@/components/AppPressable"; -import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; -import { getColors } from "@/constants/colors"; -import MarkNoShowScreenComponent from "@/components/screens/MarkNoShowScreen"; -import { type Booking, CalComAPIService } from "@/services/calcom"; -import { showErrorAlert } from "@/utils/alerts"; - -interface Attendee { - id?: number | string; - email: string; - name: string; - noShow?: boolean; -} - -interface BookingAttendee { - id?: number | string; - email: string; - name?: string; - noShow?: boolean; - absent?: boolean; -} - -export default function MarkNoShow() { - const { uid } = useLocalSearchParams<{ uid: string }>(); - const router = useRouter(); - const [booking, setBooking] = useState(null); - const [attendees, setAttendees] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - - useEffect(() => { - if (uid) { - setIsLoading(true); - CalComAPIService.getBookingByUid(uid) - .then((bookingData) => { - setBooking(bookingData); - // API may return "absent" or "noShow" depending on endpoint version - const bookingAttendees: Attendee[] = []; - if (bookingData.attendees && Array.isArray(bookingData.attendees)) { - bookingData.attendees.forEach((att: BookingAttendee) => { - bookingAttendees.push({ - id: att.id, - email: att.email, - name: att.name || att.email, - noShow: att.absent === true || att.noShow === true, - }); - }); - } - setAttendees(bookingAttendees); - }) - .catch(() => { - showErrorAlert("Error", "Failed to load booking details"); - router.back(); - }) - .finally(() => setIsLoading(false)); - } else { - setIsLoading(false); - showErrorAlert("Error", "Booking ID is missing"); - router.back(); - } - }, [uid, router]); - - const renderHeaderLeft = useCallback( - () => ( - - router.back()} - className="h-10 w-10 items-center justify-center rounded-full border" - style={{ - borderColor: theme.border, - backgroundColor: theme.background, - marginRight: 8, - }} - > - - - - ), - [router, theme.border, theme.background, theme.text] - ); - - if (isLoading) { - return ( - <> - - - {Platform.OS === "ios" && ( - - Mark No-Show - - )} - - - - - - ); - } - - return ( - <> - - - {Platform.OS === "ios" && ( - - - router.back()}> - - - - - Mark No-Show - - )} - - { - setBooking(updatedBooking); - const updatedAttendees: Attendee[] = []; - if (updatedBooking.attendees && Array.isArray(updatedBooking.attendees)) { - updatedBooking.attendees.forEach((att: BookingAttendee) => { - updatedAttendees.push({ - id: att.id, - email: att.email, - name: att.name || att.email, - noShow: att.absent === true || att.noShow === true, - }); - }); - } - setAttendees(updatedAttendees); - }} - /> - - ); -} diff --git a/companion/app/meeting-session-details.ios.tsx b/companion/app/meeting-session-details.ios.tsx deleted file mode 100644 index 6380403098..0000000000 --- a/companion/app/meeting-session-details.ios.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { osName } from "expo-device"; -import { isLiquidGlassAvailable } from "expo-glass-effect"; -import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import { useEffect, useState } from "react"; -import { ActivityIndicator, useColorScheme, View } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { getColors } from "@/constants/colors"; -import MeetingSessionDetailsScreenComponent from "@/components/screens/MeetingSessionDetailsScreen"; -import { CalComAPIService } from "@/services/calcom"; -import type { ConferencingSession } from "@/services/types/bookings.types"; -import { showErrorAlert } from "@/utils/alerts"; - -/** - * Get the presentation style for the meeting session details sheet - * - Uses formSheet on iPhone with liquid glass support - * - Uses modal on iPad or older iOS devices - */ -function getPresentationStyle(): "formSheet" | "modal" { - if (isLiquidGlassAvailable() && osName !== "iPadOS") { - return "formSheet"; - } - return "modal"; -} - -export default function MeetingSessionDetailsIOS() { - const { uid } = useLocalSearchParams<{ uid: string }>(); - const router = useRouter(); - const insets = useSafeAreaInsets(); - const [sessions, setSessions] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - - useEffect(() => { - if (uid) { - setIsLoading(true); - CalComAPIService.getConferencingSessions(uid) - .then((sessionsData) => { - setSessions(sessionsData); - }) - .catch(() => { - showErrorAlert("Error", "Failed to load session details"); - router.back(); - }) - .finally(() => setIsLoading(false)); - } else { - setIsLoading(false); - showErrorAlert("Error", "Booking ID is missing"); - router.back(); - } - }, [uid, router]); - - const presentationStyle = getPresentationStyle(); - const useGlassEffect = isLiquidGlassAvailable(); - - return ( - <> - - - - - router.back()}> - - - - - Session Details - - - - {isLoading ? ( - - - - ) : ( - - )} - - - ); -} diff --git a/companion/app/meeting-session-details.tsx b/companion/app/meeting-session-details.tsx deleted file mode 100644 index 14341cb1f1..0000000000 --- a/companion/app/meeting-session-details.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import { useCallback, useEffect, useState } from "react"; -import { ActivityIndicator, Platform, useColorScheme, View } from "react-native"; -import { AppPressable } from "@/components/AppPressable"; -import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; -import { getColors } from "@/constants/colors"; -import MeetingSessionDetailsScreenComponent from "@/components/screens/MeetingSessionDetailsScreen"; -import { CalComAPIService } from "@/services/calcom"; -import type { ConferencingSession } from "@/services/types/bookings.types"; -import { showErrorAlert } from "@/utils/alerts"; - -export default function MeetingSessionDetails() { - const { uid } = useLocalSearchParams<{ uid: string }>(); - const router = useRouter(); - const [sessions, setSessions] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - - useEffect(() => { - if (uid) { - setIsLoading(true); - CalComAPIService.getConferencingSessions(uid) - .then((sessionsData) => { - setSessions(sessionsData); - }) - .catch(() => { - showErrorAlert("Error", "Failed to load booking details"); - router.back(); - }) - .finally(() => setIsLoading(false)); - } else { - setIsLoading(false); - showErrorAlert("Error", "Booking ID is missing"); - router.back(); - } - }, [uid, router]); - - const renderHeaderLeft = useCallback( - () => ( - - router.back()} - className="h-10 w-10 items-center justify-center rounded-full border" - style={{ - borderColor: theme.border, - backgroundColor: theme.background, - marginRight: 8, - }} - > - - - - ), - [router, theme.border, theme.background, theme.text] - ); - - if (isLoading) { - return ( - <> - - - {Platform.OS === "ios" && ( - - Session Details - - )} - - - - - - ); - } - - return ( - <> - - - {Platform.OS === "ios" && ( - - - router.back()}> - - - - - Session Details - - )} - - - - ); -} diff --git a/companion/app/oauth/callback.tsx b/companion/app/oauth/callback.tsx deleted file mode 100644 index d2115ee8d8..0000000000 --- a/companion/app/oauth/callback.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { useLocalSearchParams, useRouter } from "expo-router"; -import { useEffect } from "react"; -import { ActivityIndicator, Platform, Text, View } from "react-native"; -import { useAuth } from "@/contexts"; -import { safeLogError } from "@/utils/safeLogger"; - -export default function OAuthCallback() { - const router = useRouter(); - const params = useLocalSearchParams(); - const auth = useAuth(); - - useEffect(() => { - if (Platform.OS === "android") { - if (auth.userInfo) { - router.replace("/"); - } - } - - if (Platform.OS !== "web") return; - - // Extract code, state, and error parameters from URL - const code = params.code as string; - const state = params.state as string; - const error = params.error as string; - const errorDescription = params.error_description as string; - - // Handle OAuth error response - if (error) { - const errorMessage = errorDescription || error || "OAuth authorization failed"; - safeLogError("OAuth error occurred", { error, errorDescription }); - - if (typeof window !== "undefined") { - if (window.opener) { - window.opener.postMessage( - { - type: "OAUTH_ERROR", - error: errorMessage, - errorCode: error, - }, - "*" - ); - window.close(); - } else { - // Store error for main app to pick up - if (state) { - window.localStorage.setItem(`oauth_callback_error_${state}`, errorMessage); - window.localStorage.setItem(`oauth_callback_error_code_${state}`, error); - } - router.replace("/"); - } - } - return; - } - - if (code && state) { - // Store the parameters temporarily using state-specific keys to prevent race conditions - // The OAuth service should be able to pick these up - if (typeof window !== "undefined") { - window.localStorage.setItem(`oauth_callback_code_${state}`, code); - window.localStorage.setItem(`oauth_callback_state_${state}`, state); - - // Close the OAuth popup/tab if it exists - if (window.opener) { - window.opener.postMessage( - { - type: "OAUTH_SUCCESS", - code, - state, - }, - "*" - ); - window.close(); - } else { - // Redirect to main app - router.replace("/"); - } - } - } else { - safeLogError("No authorization code or state in OAuth callback", { code, state }); - - // Handle error case - if (typeof window !== "undefined") { - if (window.opener) { - window.opener.postMessage( - { - type: "OAUTH_ERROR", - error: "No authorization code received", - }, - "*" - ); - window.close(); - } else { - router.replace("/"); - } - } - } - }, [params, router, auth.userInfo]); - - return ( - - - - Processing OAuth callback... - - - Please wait while we complete the authentication. - - - ); -} diff --git a/companion/app/profile-sheet.ios.tsx b/companion/app/profile-sheet.ios.tsx deleted file mode 100644 index 0d055d12b5..0000000000 --- a/companion/app/profile-sheet.ios.tsx +++ /dev/null @@ -1,254 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import * as Clipboard from "expo-clipboard"; -import { osName } from "expo-device"; -import { isLiquidGlassAvailable } from "expo-glass-effect"; -import * as Haptics from "expo-haptics"; -import { Stack, useRouter } from "expo-router"; -import { - ActivityIndicator, - Image, - Text, - TouchableOpacity, - useColorScheme, - View, -} from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { getColors } from "@/constants/colors"; -import { useUserProfile } from "@/hooks"; -import { showSuccessAlert } from "@/utils/alerts"; -import { openInAppBrowser } from "@/utils/browser"; -import { getAvatarUrl } from "@/utils/getAvatarUrl"; - -interface ProfileMenuItem { - id: string; - label: string; - icon: keyof typeof Ionicons.glyphMap; - onPress: () => void; - external?: boolean; -} - -/** - * Get the presentation style for the profile sheet - * - Uses formSheet on iPhone with liquid glass support - * - Uses modal on iPad or older iOS devices - */ -function getPresentationStyle(): "formSheet" | "modal" { - if (isLiquidGlassAvailable() && osName !== "iPadOS") { - return "formSheet"; - } - return "modal"; -} - -export default function ProfileSheet() { - const router = useRouter(); - const insets = useSafeAreaInsets(); - const { data: userProfile, isLoading } = useUserProfile(); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - - const colors = { - background: isDark ? "#000000" : "#FFFFFF", - text: isDark ? "#FFFFFF" : "#111827", - textSecondary: isDark ? "#A3A3A3" : "#6B7280", - icon: isDark ? "#FFFFFF" : "#374151", - iconSecondary: isDark ? "#636366" : "#9CA3AF", - avatarBg: isDark ? "#4D4D4D" : "#E5E7EB", - avatarText: isDark ? "#FFFFFF" : "#4B5563", - }; - - const publicPageUrl = userProfile?.username ? `https://cal.com/${userProfile.username}` : null; - - const menuItems: ProfileMenuItem[] = [ - { - id: "profile", - label: "My Profile", - icon: "person-outline", - onPress: () => - openInAppBrowser("https://app.cal.com/settings/my-account/profile", "Profile page"), - external: true, - }, - { - id: "settings", - label: "My Settings", - icon: "settings-outline", - onPress: () => - openInAppBrowser("https://app.cal.com/settings/my-account/general", "Settings page"), - external: true, - }, - { - id: "outOfOffice", - label: "Out of Office", - icon: "moon-outline", - onPress: () => - openInAppBrowser( - "https://app.cal.com/settings/my-account/out-of-office", - "Out of Office page" - ), - external: true, - }, - - { - id: "copyPublicPage", - label: "Copy public page link", - icon: "copy-outline", - onPress: async () => { - if (publicPageUrl) { - await Clipboard.setStringAsync(publicPageUrl); - await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); - showSuccessAlert("Copied!", "Public page link copied to clipboard"); - } - }, - external: false, - }, - { - id: "roadmap", - label: "Roadmap", - icon: "map-outline", - onPress: () => openInAppBrowser("https://cal.com/roadmap", "Roadmap page"), - external: true, - }, - { - id: "help", - label: "Help", - icon: "help-circle-outline", - onPress: () => openInAppBrowser("https://cal.com/help", "Help page"), - external: true, - }, - ]; - - const handleClose = () => { - router.back(); - }; - - const presentationStyle = getPresentationStyle(); - const useGlassEffect = isLiquidGlassAvailable(); - - // Semi-transparent background to prevent flashes while preserving glass effect - const glassBackground = isDark ? "rgba(28, 28, 30, 0.01)" : "rgba(248, 248, 250, 0.01)"; - - return ( - <> - null, - headerRight: () => null, - }} - /> - - - You - - - - - - - - - - {/* Profile Header */} - - {isLoading ? ( - - - - ) : ( - - {/* Avatar */} - {userProfile?.avatarUrl ? ( - - ) : ( - - - {userProfile?.name?.charAt(0).toUpperCase() || - userProfile?.email?.charAt(0).toUpperCase() || - "?"} - - - )} - - {/* Name and Status */} - - - {userProfile?.name || "User"} - - {userProfile?.email ? ( - - {userProfile.email} - - ) : null} - - - )} - - - {/* Menu Items */} - - {menuItems.map((item, index) => ( - - - - - {item.label} - - - {item.external ? ( - - ) : ( - - )} - - ))} - - - - ); -} diff --git a/companion/app/profile-sheet.tsx b/companion/app/profile-sheet.tsx deleted file mode 100644 index e341e1ce32..0000000000 --- a/companion/app/profile-sheet.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import * as Clipboard from "expo-clipboard"; -import * as Haptics from "expo-haptics"; -import { Stack, useRouter } from "expo-router"; -import { - ActivityIndicator, - Image, - ScrollView, - Text, - TouchableOpacity, - useColorScheme, - View, -} from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; -import { useUserProfile } from "@/hooks"; -import { showSuccessAlert } from "@/utils/alerts"; -import { openInAppBrowser } from "@/utils/browser"; -import { getAvatarUrl } from "@/utils/getAvatarUrl"; - -interface ProfileMenuItem { - id: string; - label: string; - icon: keyof typeof Ionicons.glyphMap; - onPress: () => void; - external?: boolean; -} - -export default function ProfileSheet() { - const router = useRouter(); - const insets = useSafeAreaInsets(); - const { data: userProfile, isLoading } = useUserProfile(); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - - const colors = { - background: isDark ? "#000000" : "#FFFFFF", - text: isDark ? "#FFFFFF" : "#111827", - textSecondary: isDark ? "#A3A3A3" : "#6B7280", - icon: isDark ? "#FFFFFF" : "#374151", - iconSecondary: isDark ? "#636366" : "#9CA3AF", - avatarBg: isDark ? "#4D4D4D" : "#E5E7EB", - avatarText: isDark ? "#FFFFFF" : "#4B5563", - activeBackground: isDark ? "#171717" : "#F3F4F6", - }; - - const publicPageUrl = userProfile?.username ? `https://cal.com/${userProfile.username}` : null; - - const menuItems: ProfileMenuItem[] = [ - { - id: "profile", - label: "My Profile", - icon: "person-outline", - onPress: () => - openInAppBrowser("https://app.cal.com/settings/my-account/profile", "Profile page"), - external: true, - }, - { - id: "settings", - label: "My Settings", - icon: "settings-outline", - onPress: () => - openInAppBrowser("https://app.cal.com/settings/my-account/general", "Settings page"), - external: true, - }, - { - id: "outOfOffice", - label: "Out of Office", - icon: "moon-outline", - onPress: () => - openInAppBrowser( - "https://app.cal.com/settings/my-account/out-of-office", - "Out of Office page" - ), - external: true, - }, - { - id: "publicPage", - label: "View public page", - icon: "globe-outline", - onPress: () => { - if (publicPageUrl) openInAppBrowser(publicPageUrl, "Public page"); - }, - external: true, - }, - { - id: "copyPublicPage", - label: "Copy public page link", - icon: "copy-outline", - onPress: async () => { - if (publicPageUrl) { - await Clipboard.setStringAsync(publicPageUrl); - await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); - showSuccessAlert("Copied!", "Public page link copied to clipboard"); - } - }, - external: false, - }, - { - id: "roadmap", - label: "Roadmap", - icon: "map-outline", - onPress: () => openInAppBrowser("https://cal.com/roadmap", "Roadmap page"), - external: true, - }, - { - id: "help", - label: "Help", - icon: "help-circle-outline", - onPress: () => openInAppBrowser("https://cal.com/help", "Help page"), - external: true, - }, - ]; - - const handleClose = () => { - router.back(); - }; - - return ( - <> - null, - headerRight: () => ( - - - - - - ), - }} - /> - - - {/* Profile Header */} - - {isLoading ? ( - - - - ) : ( - - {/* Avatar */} - {userProfile?.avatarUrl ? ( - - ) : ( - - - {userProfile?.name?.charAt(0).toUpperCase() || - userProfile?.email?.charAt(0).toUpperCase() || - "?"} - - - )} - - {/* Name and Email */} - - - {userProfile?.name || "User"} - - {userProfile?.email ? ( - - {userProfile.email} - - ) : null} - - - )} - - - {/* Menu Items */} - - {menuItems.map((item, index) => ( - - - - - {item.label} - - - {item.external ? ( - - ) : ( - - )} - - ))} - - - - ); -} diff --git a/companion/app/reschedule.ios.tsx b/companion/app/reschedule.ios.tsx deleted file mode 100644 index d0d6ff54ea..0000000000 --- a/companion/app/reschedule.ios.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { osName } from "expo-device"; -import { isLiquidGlassAvailable } from "expo-glass-effect"; -import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, useColorScheme, View } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { getColors } from "@/constants/colors"; -import type { RescheduleScreenHandle } from "@/components/screens/RescheduleScreen"; -import RescheduleScreenComponent from "@/components/screens/RescheduleScreen"; -import { type Booking, CalComAPIService } from "@/services/calcom"; -import { showErrorAlert } from "@/utils/alerts"; - -// Semi-transparent background to prevent black flash while preserving glass effect - -function getPresentationStyle(): "formSheet" | "modal" { - if (isLiquidGlassAvailable() && osName !== "iPadOS") { - return "formSheet"; - } - return "modal"; -} - -export default function RescheduleIOS() { - const { uid } = useLocalSearchParams<{ uid: string }>(); - const router = useRouter(); - const insets = useSafeAreaInsets(); - const [booking, setBooking] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [isSaving, setIsSaving] = useState(false); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - - const rescheduleScreenRef = useRef(null); - - useEffect(() => { - if (uid) { - setIsLoading(true); - CalComAPIService.getBookingByUid(uid) - .then(setBooking) - .catch(() => { - showErrorAlert("Error", "Failed to load booking details"); - router.back(); - }) - .finally(() => setIsLoading(false)); - } else { - setIsLoading(false); - showErrorAlert("Error", "Booking ID is missing"); - router.back(); - } - }, [uid, router]); - - const handleSave = useCallback(() => { - rescheduleScreenRef.current?.submit(); - }, []); - - const handleRescheduleSuccess = useCallback(() => { - router.back(); - }, [router]); - - const presentationStyle = getPresentationStyle(); - const useGlassEffect = isLiquidGlassAvailable(); - - // Semi-transparent background to prevent flashes while preserving glass effect - const glassBackground = isDark ? "rgba(28, 28, 30, 0.01)" : "rgba(248, 248, 250, 0.01)"; - - return ( - <> - - - - - router.back()} - tintColor={theme.backgroundEmphasis} - variant="prominent" - > - - - - - Reschedule - - - - - - - - - - {isLoading ? ( - - - - ) : ( - - )} - - - ); -} diff --git a/companion/app/reschedule.tsx b/companion/app/reschedule.tsx deleted file mode 100644 index 4a675501c7..0000000000 --- a/companion/app/reschedule.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, Platform, useColorScheme, View } from "react-native"; -import { AppPressable } from "@/components/AppPressable"; -import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; -import type { RescheduleScreenHandle } from "@/components/screens/RescheduleScreen"; -import RescheduleScreenComponent from "@/components/screens/RescheduleScreen"; -import { getColors } from "@/constants/colors"; -import { type Booking, CalComAPIService } from "@/services/calcom"; -import { showErrorAlert } from "@/utils/alerts"; - -export default function Reschedule() { - const { uid } = useLocalSearchParams<{ uid: string }>(); - const router = useRouter(); - const [booking, setBooking] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [isSaving, setIsSaving] = useState(false); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - - const rescheduleScreenRef = useRef(null); - - useEffect(() => { - if (uid) { - setIsLoading(true); - CalComAPIService.getBookingByUid(uid) - .then(setBooking) - .catch(() => { - showErrorAlert("Error", "Failed to load booking details"); - router.back(); - }) - .finally(() => setIsLoading(false)); - } else { - setIsLoading(false); - showErrorAlert("Error", "Booking ID is missing"); - router.back(); - } - }, [uid, router]); - - const handleSave = useCallback(() => { - rescheduleScreenRef.current?.submit(); - }, []); - - const handleRescheduleSuccess = useCallback(() => { - router.back(); - }, [router]); - - const renderHeaderLeft = useCallback( - () => ( - - router.back()} - className="h-10 w-10 items-center justify-center rounded-full border" - style={{ - borderColor: theme.border, - backgroundColor: theme.background, - marginRight: 8, - }} - > - - - - ), - [router, theme.border, theme.background, theme.text] - ); - - const renderHeaderRight = useCallback( - () => ( - - - - - - ), - [handleSave, isSaving, theme.text, theme.border, theme.background] - ); - - if (isLoading) { - return ( - <> - - - {Platform.OS === "ios" && ( - - Reschedule - - )} - - - - - - ); - } - - return ( - <> - - - {Platform.OS === "ios" && ( - - - router.back()}> - - - - - Reschedule - - - - - - - - )} - - - - ); -} diff --git a/companion/app/view-recordings.ios.tsx b/companion/app/view-recordings.ios.tsx deleted file mode 100644 index 7022ff5814..0000000000 --- a/companion/app/view-recordings.ios.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { osName } from "expo-device"; -import { isLiquidGlassAvailable } from "expo-glass-effect"; -import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import { useEffect, useState } from "react"; -import { ActivityIndicator, useColorScheme, View } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { getColors } from "@/constants/colors"; -import ViewRecordingsScreenComponent from "@/components/screens/ViewRecordingsScreen"; -import { CalComAPIService } from "@/services/calcom"; -import type { BookingRecording } from "@/services/types/bookings.types"; -import { showErrorAlert } from "@/utils/alerts"; -import { safeLogError } from "@/utils/safeLogger"; - -function getPresentationStyle(): "formSheet" | "modal" { - if (isLiquidGlassAvailable() && osName !== "iPadOS") { - return "formSheet"; - } - return "modal"; -} - -export default function ViewRecordingsIOS() { - const { uid } = useLocalSearchParams<{ uid: string }>(); - const router = useRouter(); - const insets = useSafeAreaInsets(); - const [recordings, setRecordings] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - - useEffect(() => { - if (uid) { - setIsLoading(true); - CalComAPIService.getRecordings(uid) - .then(setRecordings) - .catch((error) => { - safeLogError("Failed to load recordings:", error); - showErrorAlert("Error", "Failed to load recordings"); - router.back(); - }) - .finally(() => setIsLoading(false)); - } else { - setIsLoading(false); - showErrorAlert("Error", "Booking ID is missing"); - router.back(); - } - }, [uid, router]); - - const presentationStyle = getPresentationStyle(); - const useGlassEffect = isLiquidGlassAvailable(); - - return ( - <> - - - - - router.back()}> - - - - - Recordings - - - - {isLoading ? ( - - - - ) : ( - - )} - - - ); -} diff --git a/companion/app/view-recordings.tsx b/companion/app/view-recordings.tsx deleted file mode 100644 index 1f97dd62fd..0000000000 --- a/companion/app/view-recordings.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import { useCallback, useEffect, useState } from "react"; -import { ActivityIndicator, Platform, useColorScheme, View } from "react-native"; -import { AppPressable } from "@/components/AppPressable"; -import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; -import { getColors } from "@/constants/colors"; -import ViewRecordingsScreenComponent from "@/components/screens/ViewRecordingsScreen"; -import { CalComAPIService } from "@/services/calcom"; -import type { BookingRecording } from "@/services/types/bookings.types"; -import { showErrorAlert } from "@/utils/alerts"; -import { safeLogError } from "@/utils/safeLogger"; - -export default function ViewRecordings() { - const { uid } = useLocalSearchParams<{ uid: string }>(); - const router = useRouter(); - const [recordings, setRecordings] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - - useEffect(() => { - if (uid) { - setIsLoading(true); - CalComAPIService.getRecordings(uid) - .then(setRecordings) - .catch((error) => { - safeLogError("Failed to load recordings:", error); - showErrorAlert("Error", "Failed to load recordings"); - router.back(); - }) - .finally(() => setIsLoading(false)); - } else { - setIsLoading(false); - showErrorAlert("Error", "Booking ID is missing"); - router.back(); - } - }, [uid, router]); - - const renderHeaderLeft = useCallback( - () => ( - - router.back()} - className="h-10 w-10 items-center justify-center rounded-full border" - style={{ - borderColor: theme.border, - backgroundColor: theme.background, - marginRight: 8, - }} - > - - - - ), - [router, theme.border, theme.background, theme.text] - ); - - if (isLoading) { - return ( - <> - - - {Platform.OS === "ios" && ( - - Recordings - - )} - - - - - - ); - } - - return ( - <> - - - {Platform.OS === "ios" && ( - - - router.back()}> - - - - - Recordings - - )} - - - - ); -} diff --git a/companion/assets/adaptive-icon.png b/companion/assets/adaptive-icon.png deleted file mode 100644 index 8185f1cba3..0000000000 Binary files a/companion/assets/adaptive-icon.png and /dev/null differ diff --git a/companion/assets/cal-logo.icon/Assets/cal.svg b/companion/assets/cal-logo.icon/Assets/cal.svg deleted file mode 100644 index 93c6dc2d10..0000000000 --- a/companion/assets/cal-logo.icon/Assets/cal.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/companion/assets/cal-logo.icon/icon.json b/companion/assets/cal-logo.icon/icon.json deleted file mode 100644 index 324a13c59f..0000000000 --- a/companion/assets/cal-logo.icon/icon.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "fill": "automatic", - "groups": [ - { - "layers": [ - { - "blend-mode-specializations": [ - { - "appearance": "tinted", - "value": "normal" - } - ], - "fill-specializations": [ - { - "appearance": "dark", - "value": { - "solid": "srgb:1.00000,1.00000,1.00000,1.00000" - } - }, - { - "appearance": "tinted", - "value": { - "solid": "srgb:1.00000,1.00000,1.00000,1.00000" - } - } - ], - "glass": false, - "image-name": "cal.svg", - "name": "cal", - "position": { - "scale": 19.57, - "translation-in-points": [-17, 2] - } - } - ], - "shadow": { - "kind": "neutral", - "opacity": 0.5 - }, - "translucency": { - "enabled": true, - "value": 0.5 - } - } - ], - "supported-platforms": { - "circles": ["watchOS"], - "squares": "shared" - } -} diff --git a/companion/assets/favicon-new3.png b/companion/assets/favicon-new3.png deleted file mode 100644 index 379c5a5132..0000000000 Binary files a/companion/assets/favicon-new3.png and /dev/null differ diff --git a/companion/assets/favicon.png b/companion/assets/favicon.png deleted file mode 100644 index e75f697b18..0000000000 Binary files a/companion/assets/favicon.png and /dev/null differ diff --git a/companion/assets/icon.png b/companion/assets/icon.png deleted file mode 100644 index a0b1526fc7..0000000000 Binary files a/companion/assets/icon.png and /dev/null differ diff --git a/companion/assets/logo.svg b/companion/assets/logo.svg deleted file mode 100644 index bccfe813ef..0000000000 --- a/companion/assets/logo.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/companion/assets/splash-icon.png b/companion/assets/splash-icon.png deleted file mode 100644 index dd85a83ed2..0000000000 Binary files a/companion/assets/splash-icon.png and /dev/null differ diff --git a/companion/assets/widget-preview.png b/companion/assets/widget-preview.png deleted file mode 100644 index c5db4c4d9e..0000000000 Binary files a/companion/assets/widget-preview.png and /dev/null differ diff --git a/companion/babel.config.js b/companion/babel.config.js deleted file mode 100644 index 8d6d35baf3..0000000000 --- a/companion/babel.config.js +++ /dev/null @@ -1,17 +0,0 @@ -module.exports = (api) => { - api.cache(true); - return { - presets: [ - [ - "babel-preset-expo", - { - jsxImportSource: "nativewind", - "react-compiler": { - panicThreshold: "all_errors", - }, - }, - ], - "nativewind/babel", - ], - }; -}; diff --git a/companion/biome.json b/companion/biome.json deleted file mode 100644 index bb3eafde64..0000000000 --- a/companion/biome.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "$schema": "https://biomejs.dev/schemas/2.3.10/schema.json", - "root": false, - "vcs": { - "enabled": true, - "clientKind": "git", - "useIgnoreFile": true - }, - "files": { - "ignoreUnknown": true, - "includes": [ - "**/*.js", - "**/*.jsx", - "**/*.ts", - "**/*.tsx", - "**/*.json", - "!**/node_modules", - "!**/.expo", - "!**/dist", - "!**/.output", - "!**/*.d.ts", - "!**/bun.lock", - "!**/android", - "!**/ios", - "!**/safari-extension" - ] - }, - "formatter": { - "enabled": true, - "indentStyle": "space", - "indentWidth": 2, - "lineWidth": 100 - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true, - "suspicious": { - "noExplicitAny": "error", - "noArrayIndexKey": "error", - "useIterableCallbackReturn": "error", - "noImplicitAnyLet": "off" - }, - "correctness": { - "useExhaustiveDependencies": "error", - "useHookAtTopLevel": "error", - "noInvalidUseBeforeDeclaration": "off", - "noUnusedFunctionParameters": "error", - "noUnusedVariables": "error" - }, - "complexity": { - "noStaticOnlyClass": "error" - }, - "style": { - "noNonNullAssertion": "error" - }, - "a11y": { - "noStaticElementInteractions": "error" - } - } - }, - "javascript": { - "formatter": { - "quoteStyle": "double", - "trailingCommas": "es5", - "semicolons": "always", - "arrowParentheses": "always" - } - }, - "assist": { - "enabled": true, - "actions": { - "source": { - "organizeImports": "on" - } - } - }, - "overrides": [ - { - "includes": ["extension/**/*.ts", "extension/**/*.tsx"], - "javascript": { - "globals": ["chrome", "browser"] - } - } - ] -} diff --git a/companion/bun.lock b/companion/bun.lock deleted file mode 100644 index b4bb2fba63..0000000000 --- a/companion/bun.lock +++ /dev/null @@ -1,2897 +0,0 @@ -{ - "lockfileVersion": 1, - "workspaces": { - "": { - "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", - "@react-native-community/datetimepicker": "8.4.4", - "@react-native-community/netinfo": "11.4.1", - "@react-native-segmented-control/segmented-control": "2.5.7", - "@rn-primitives/alert-dialog": "1.2.0", - "@rn-primitives/dropdown-menu": "1.2.0", - "@rn-primitives/slot": "1.2.0", - "@tanstack/react-query": "5.62.0", - "@tanstack/react-query-persist-client": "5.62.0", - "base64-js": "1.5.1", - "class-variance-authority": "0.7.1", - "clsx": "2.1.1", - "expo": "55.0.0-canary-20251230-fc48ddc", - "expo-auth-session": "7.0.11-canary-20251230-fc48ddc", - "expo-clipboard": "9.0.0-canary-20251230-fc48ddc", - "expo-constants": "18.1.0-canary-20251230-fc48ddc", - "expo-crypto": "15.0.9-canary-20251230-fc48ddc", - "expo-dev-client": "6.1.0-canary-20251230-fc48ddc", - "expo-device": "8.0.11-canary-20251230-fc48ddc", - "expo-glass-effect": "0.2.0-canary-20251230-fc48ddc", - "expo-haptics": "15.0.9-canary-20251230-fc48ddc", - "expo-image": "3.1.0-canary-20251230-fc48ddc", - "expo-linking": "8.0.12-canary-20251230-fc48ddc", - "expo-router": "7.0.0-canary-20251230-fc48ddc", - "expo-secure-store": "15.0.9-canary-20251230-fc48ddc", - "expo-splash-screen": "31.0.14-canary-20251230-fc48ddc", - "expo-status-bar": "3.0.10-canary-20251230-fc48ddc", - "expo-store-review": "9.0.10-canary-20251230-fc48ddc", - "expo-web-browser": "15.0.11-canary-20251230-fc48ddc", - "lucide-react-native": "0.562.0", - "nativewind": "4.2.1", - "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", - "react-native-screens": "4.19.0", - "react-native-svg": "15.12.1", - "react-native-web": "0.21.2", - "react-native-worklets": "0.7.1", - "tailwind-merge": "3.4.0", - "tailwindcss-animate": "1.0.7", - }, - "devDependencies": { - "@biomejs/biome": "2.3.10", - "@types/chrome": "0.1.31", - "@types/node": "22.19.6", - "@types/react": "19.2.10", - "@types/react-dom": "19.2.3", - "babel-plugin-react-compiler": "19.0.0-beta-af1b7da-20250417", - "babel-preset-expo": "54.0.7", - "eslint": "9.28.0", - "eslint-plugin-react-compiler": "19.0.0-beta-af1b7da-20250417", - "husky": "9.0.11", - "lint-staged": "15.2.0", - "tailwindcss": "3.4.17", - "typescript": "5.9.3", - "typescript-eslint": "8.50.1", - "wxt": "0.20.11", - }, - }, - }, - "packages": { - "@0no-co/graphql.web": ["@0no-co/graphql.web@1.2.0", "", { "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" }, "optionalPeers": ["graphql"] }, "sha512-/1iHy9TTr63gE1YcR5idjx8UREz1s0kFhydf3bBLCXyqjhkIc6igAzTOx3zPifCwFR87tsh/4Pa9cNts6d2otw=="], - - "@1natsu/wait-element": ["@1natsu/wait-element@4.1.2", "", { "dependencies": { "defu": "^6.1.4", "many-keys-map": "^2.0.1" } }, "sha512-qWxSJD+Q5b8bKOvESFifvfZ92DuMsY+03SBNjTO34ipJLP6mZ9yK4bQz/vlh48aEQXoJfaZBqUwKL5BdI5iiWw=="], - - "@aklinker1/rollup-plugin-visualizer": ["@aklinker1/rollup-plugin-visualizer@5.12.0", "", { "dependencies": { "open": "^8.4.0", "picomatch": "^2.3.1", "source-map": "^0.7.4", "yargs": "^17.5.1" }, "peerDependencies": { "rollup": "2.x || 3.x || 4.x" }, "optionalPeers": ["rollup"], "bin": { "rollup-plugin-visualizer": "dist/bin/cli.js" } }, "sha512-X24LvEGw6UFmy0lpGJDmXsMyBD58XmX1bbwsaMLhNoM+UMQfQ3b2RtC+nz4b/NoRK5r6QJSKJHBNVeUdwqybaQ=="], - - "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], - - "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], - - "@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="], - - "@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="], - - "@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], - - "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], - - "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.5", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "debug": "^4.4.1", "lodash.debounce": "^4.0.8", "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg=="], - - "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], - - "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], - - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], - - "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="], - - "@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-wrap-function": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA=="], - - "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], - - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - - "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - - "@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.28.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2" } }, "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g=="], - - "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], - - "@babel/highlight": ["@babel/highlight@7.25.9", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw=="], - - "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], - - "@babel/plugin-proposal-decorators": ["@babel/plugin-proposal-decorators@7.28.0", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-decorators": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg=="], - - "@babel/plugin-proposal-export-default-from": ["@babel/plugin-proposal-export-default-from@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hjlsMBl1aJc5lp8MoCDEZCiYzlgdRAShOjAfRw6X+GlpLpUPU7c3XNLsKFZbQk/1cRzBlJ7CXg3xJAJMrFa1Uw=="], - - "@babel/plugin-proposal-private-methods": ["@babel/plugin-proposal-private-methods@7.18.6", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA=="], - - "@babel/plugin-syntax-async-generators": ["@babel/plugin-syntax-async-generators@7.8.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw=="], - - "@babel/plugin-syntax-bigint": ["@babel/plugin-syntax-bigint@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg=="], - - "@babel/plugin-syntax-class-properties": ["@babel/plugin-syntax-class-properties@7.12.13", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA=="], - - "@babel/plugin-syntax-class-static-block": ["@babel/plugin-syntax-class-static-block@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw=="], - - "@babel/plugin-syntax-decorators": ["@babel/plugin-syntax-decorators@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A=="], - - "@babel/plugin-syntax-dynamic-import": ["@babel/plugin-syntax-dynamic-import@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ=="], - - "@babel/plugin-syntax-export-default-from": ["@babel/plugin-syntax-export-default-from@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-eBC/3KSekshx19+N40MzjWqJd7KTEdOoLesAfa4IDFI8eRz5a47i5Oszus6zG/cwIXN63YhgLOMSSNJx49sENg=="], - - "@babel/plugin-syntax-flow": ["@babel/plugin-syntax-flow@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA=="], - - "@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww=="], - - "@babel/plugin-syntax-import-meta": ["@babel/plugin-syntax-import-meta@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g=="], - - "@babel/plugin-syntax-json-strings": ["@babel/plugin-syntax-json-strings@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA=="], - - "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w=="], - - "@babel/plugin-syntax-logical-assignment-operators": ["@babel/plugin-syntax-logical-assignment-operators@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig=="], - - "@babel/plugin-syntax-nullish-coalescing-operator": ["@babel/plugin-syntax-nullish-coalescing-operator@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ=="], - - "@babel/plugin-syntax-numeric-separator": ["@babel/plugin-syntax-numeric-separator@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug=="], - - "@babel/plugin-syntax-object-rest-spread": ["@babel/plugin-syntax-object-rest-spread@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA=="], - - "@babel/plugin-syntax-optional-catch-binding": ["@babel/plugin-syntax-optional-catch-binding@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q=="], - - "@babel/plugin-syntax-optional-chaining": ["@babel/plugin-syntax-optional-chaining@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg=="], - - "@babel/plugin-syntax-private-property-in-object": ["@babel/plugin-syntax-private-property-in-object@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg=="], - - "@babel/plugin-syntax-top-level-await": ["@babel/plugin-syntax-top-level-await@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw=="], - - "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ=="], - - "@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA=="], - - "@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1", "@babel/traverse": "^7.28.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q=="], - - "@babel/plugin-transform-async-to-generator": ["@babel/plugin-transform-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA=="], - - "@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g=="], - - "@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA=="], - - "@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.28.3", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg=="], - - "@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.28.4", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/traverse": "^7.28.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA=="], - - "@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/template": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw=="], - - "@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw=="], - - "@babel/plugin-transform-export-namespace-from": ["@babel/plugin-transform-export-namespace-from@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ=="], - - "@babel/plugin-transform-flow-strip-types": ["@babel/plugin-transform-flow-strip-types@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-flow": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg=="], - - "@babel/plugin-transform-for-of": ["@babel/plugin-transform-for-of@7.27.1", "", { "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-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw=="], - - "@babel/plugin-transform-function-name": ["@babel/plugin-transform-function-name@7.27.1", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ=="], - - "@babel/plugin-transform-literals": ["@babel/plugin-transform-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA=="], - - "@babel/plugin-transform-logical-assignment-operators": ["@babel/plugin-transform-logical-assignment-operators@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA=="], - - "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw=="], - - "@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng=="], - - "@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA=="], - - "@babel/plugin-transform-numeric-separator": ["@babel/plugin-transform-numeric-separator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw=="], - - "@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.28.4", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/traverse": "^7.28.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew=="], - - "@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q=="], - - "@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.27.1", "", { "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-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg=="], - - "@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.27.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg=="], - - "@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA=="], - - "@babel/plugin-transform-private-property-in-object": ["@babel/plugin-transform-private-property-in-object@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ=="], - - "@babel/plugin-transform-react-display-name": ["@babel/plugin-transform-react-display-name@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA=="], - - "@babel/plugin-transform-react-jsx": ["@babel/plugin-transform-react-jsx@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/types": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw=="], - - "@babel/plugin-transform-react-jsx-development": ["@babel/plugin-transform-react-jsx-development@7.27.1", "", { "dependencies": { "@babel/plugin-transform-react-jsx": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q=="], - - "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], - - "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], - - "@babel/plugin-transform-react-pure-annotations": ["@babel/plugin-transform-react-pure-annotations@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA=="], - - "@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.28.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA=="], - - "@babel/plugin-transform-runtime": ["@babel/plugin-transform-runtime@7.28.5", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", "babel-plugin-polyfill-regenerator": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-20NUVgOrinudkIBzQ2bNxP08YpKprUkRTiRSd2/Z5GOdPImJGkoN4Z7IQe1T5AdyKI1i5L6RBmluqdSzvaq9/w=="], - - "@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ=="], - - "@babel/plugin-transform-spread": ["@babel/plugin-transform-spread@7.27.1", "", { "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-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q=="], - - "@babel/plugin-transform-sticky-regex": ["@babel/plugin-transform-sticky-regex@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g=="], - - "@babel/plugin-transform-template-literals": ["@babel/plugin-transform-template-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg=="], - - "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA=="], - - "@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw=="], - - "@babel/preset-react": ["@babel/preset-react@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-transform-react-display-name": "^7.28.0", "@babel/plugin-transform-react-jsx": "^7.27.1", "@babel/plugin-transform-react-jsx-development": "^7.27.1", "@babel/plugin-transform-react-pure-annotations": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ=="], - - "@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="], - - "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], - - "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], - - "@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], - - "@babel/traverse--for-generate-function-map": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], - - "@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=="], - - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-Vae7+V6t/Avr8tVbFNjnFSTKZogZHFYl7MMH62P/J1kZtr0tyRQ9Fe0onjqjS2Ek9lmNLmZc/VR5uSekh+p1fg=="], - - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-hhPw2V3/EpHKsileVOFynuWiKRgFEV48cLe0eA+G2wO4SzlwEhLEB9LhlSrVeu2mtSn205W283LkX7Fh48CaxA=="], - - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-B9DszIHkuKtOH2IFeeVkQmSMVUjss9KtHaNXquYYWCjH8IstNgXgx5B0aSBQNr6mn4RcKKRQZXn9Zu1rM3O0/A=="], - - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.10", "", { "os": "linux", "cpu": "x64" }, "sha512-wwAkWD1MR95u+J4LkWP74/vGz+tRrIQvr8kfMMJY8KOQ8+HMVleREOcPYsQX82S7uueco60L58Wc6M1I9WA9Dw=="], - - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.10", "", { "os": "linux", "cpu": "x64" }, "sha512-QTfHZQh62SDFdYc2nfmZFuTm5yYb4eO1zwfB+90YxUumRCR171tS1GoTX5OD0wrv4UsziMPmrePMtkTnNyYG3g=="], - - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-o7lYc9n+CfRbHvkjPhm8s9FgbKdYZu5HCcGVMItLjz93EhgJ8AM44W+QckDqLA9MKDNFrR8nPbO4b73VC5kGGQ=="], - - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.10", "", { "os": "win32", "cpu": "x64" }, "sha512-pHEFgq7dUEsKnqG9mx9bXihxGI49X+ar+UBrEIj3Wqj3UCZp1rNgV+OoyjFgcXsjCWpuEAF4VJdkZr3TrWdCbQ=="], - - "@devicefarmer/adbkit": ["@devicefarmer/adbkit@3.3.8", "", { "dependencies": { "@devicefarmer/adbkit-logcat": "^2.1.2", "@devicefarmer/adbkit-monkey": "~1.2.1", "bluebird": "~3.7", "commander": "^9.1.0", "debug": "~4.3.1", "node-forge": "^1.3.1", "split": "~1.0.1" }, "bin": { "adbkit": "bin/adbkit" } }, "sha512-7rBLLzWQnBwutH2WZ0EWUkQdihqrnLYCUMaB44hSol9e0/cdIhuNFcqZO0xNheAU6qqHVA8sMiLofkYTgb+lmw=="], - - "@devicefarmer/adbkit-logcat": ["@devicefarmer/adbkit-logcat@2.1.3", "", {}, "sha512-yeaGFjNBc/6+svbDeul1tNHtNChw6h8pSHAt5D+JsedUrMTN7tla7B15WLDyekxsuS2XlZHRxpuC6m92wiwCNw=="], - - "@devicefarmer/adbkit-monkey": ["@devicefarmer/adbkit-monkey@1.2.1", "", {}, "sha512-ZzZY/b66W2Jd6NHbAhLyDWOEIBWC11VizGFk7Wx7M61JZRz7HR9Cq5P+65RKWUU7u6wgsE8Lmh9nE4Mz+U2eTg=="], - - "@egjs/hammerjs": ["@egjs/hammerjs@2.0.17", "", { "dependencies": { "@types/hammerjs": "^2.0.36" } }, "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A=="], - - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="], - - "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], - - "@eslint/config-array": ["@eslint/config-array@0.20.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw=="], - - "@eslint/config-helpers": ["@eslint/config-helpers@0.2.3", "", {}, "sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg=="], - - "@eslint/core": ["@eslint/core@0.14.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg=="], - - "@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="], - - "@eslint/js": ["@eslint/js@9.28.0", "", {}, "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg=="], - - "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], - - "@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.5", "", { "dependencies": { "@eslint/core": "^0.15.2", "levn": "^0.4.1" } }, "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w=="], - - "@expo-google-fonts/material-symbols": ["@expo-google-fonts/material-symbols@0.4.15", "", {}, "sha512-EA+HoleZdmUtefeY8a/M8LFK7VapTUyXRIIzP9IkI8Z7DM9r4mVA1omWgoEZp+Tft1jo8dbdnZu38lGOjLohsg=="], - - "@expo/cli": ["@expo/cli@55.0.0-canary-20251230-fc48ddc", "", { "dependencies": { "@0no-co/graphql.web": "^1.0.8", "@expo/code-signing-certificates": "^0.0.5", "@expo/config": "12.0.14-canary-20251230-fc48ddc", "@expo/config-plugins": "54.0.5-canary-20251230-fc48ddc", "@expo/devcert": "^1.2.1", "@expo/env": "2.0.9-canary-20251230-fc48ddc", "@expo/image-utils": "0.8.9-canary-20251230-fc48ddc", "@expo/json-file": "10.0.9-canary-20251230-fc48ddc", "@expo/log-box": "0.0.13-canary-20251230-fc48ddc", "@expo/metro": "~54.2.0", "@expo/metro-config": "54.1.0-canary-20251230-fc48ddc", "@expo/osascript": "2.3.9-canary-20251230-fc48ddc", "@expo/package-manager": "1.9.10-canary-20251230-fc48ddc", "@expo/plist": "0.4.9-canary-20251230-fc48ddc", "@expo/prebuild-config": "54.0.9-canary-20251230-fc48ddc", "@expo/router-server": "0.2.0-canary-20251230-fc48ddc", "@expo/schema-utils": "0.1.9-canary-20251230-fc48ddc", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.3.0", "@react-native/dev-middleware": "0.83.1", "@urql/core": "^5.0.6", "@urql/exchange-retry": "^1.3.0", "accepts": "^1.3.8", "arg": "^5.0.2", "better-opn": "~3.0.2", "bplist-creator": "0.1.0", "bplist-parser": "^0.3.1", "chalk": "^4.0.0", "ci-info": "^3.3.0", "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", "env-editor": "^0.4.1", "expo-server": "1.0.6-canary-20251230-fc48ddc", "freeport-async": "^2.0.0", "getenv": "^2.0.0", "glob": "^13.0.0", "lan-network": "^0.1.6", "minimatch": "^9.0.0", "node-forge": "^1.3.1", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^3.0.1", "pretty-bytes": "^5.6.0", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", "qrcode-terminal": "0.11.0", "require-from-string": "^2.0.2", "requireg": "^0.2.2", "resolve-from": "^5.0.0", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", "source-map-support": "~0.5.21", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", "tar": "^7.5.2", "terminal-link": "^2.1.1", "undici": "^6.18.2", "wrap-ansi": "^7.0.0", "ws": "^8.12.1", "zod": "^3.25.76" }, "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc", "expo-router": "7.0.0-canary-20251230-fc48ddc", "react-native": "*" }, "optionalPeers": ["expo-router", "react-native"], "bin": { "expo-internal": "build/bin/cli" } }, "sha512-AF2OhXEejxOAC4zcsLMFboDFi9SdEUhzRJxdr/BUCBBguE4789Ugs/LswrOgZqg4qpcp8gz70R4kbqRfkkyHwQ=="], - - "@expo/code-signing-certificates": ["@expo/code-signing-certificates@0.0.5", "", { "dependencies": { "node-forge": "^1.2.1", "nullthrows": "^1.1.1" } }, "sha512-BNhXkY1bblxKZpltzAx98G2Egj9g1Q+JRcvR7E99DOj862FTCX+ZPsAUtPTr7aHxwtrL7+fL3r0JSmM9kBm+Bw=="], - - "@expo/config": ["@expo/config@12.0.14-canary-20251230-fc48ddc", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "@expo/config-plugins": "54.0.5-canary-20251230-fc48ddc", "@expo/config-types": "55.0.0-canary-20251230-fc48ddc", "@expo/json-file": "10.0.9-canary-20251230-fc48ddc", "deepmerge": "^4.3.1", "getenv": "^2.0.0", "glob": "^13.0.0", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0", "resolve-workspace-root": "^2.0.0", "semver": "^7.6.0", "slugify": "^1.3.4", "sucrase": "~3.35.1" } }, "sha512-M50hGICoIA974u/Rxdejzc8ZHOWzBUVH/m5DJZFFj2W2LoZ70IJ3pW/XqqtH45+fuERL6vuQbS/9XdgD2F4Djg=="], - - "@expo/config-plugins": ["@expo/config-plugins@54.0.5-canary-20251230-fc48ddc", "", { "dependencies": { "@expo/config-types": "55.0.0-canary-20251230-fc48ddc", "@expo/json-file": "10.0.9-canary-20251230-fc48ddc", "@expo/plist": "0.4.9-canary-20251230-fc48ddc", "@expo/sdk-runtime-versions": "^1.0.0", "chalk": "^4.1.2", "debug": "^4.3.5", "getenv": "^2.0.0", "glob": "^13.0.0", "resolve-from": "^5.0.0", "semver": "^7.5.4", "slash": "^3.0.0", "slugify": "^1.6.6", "xcode": "^3.0.1", "xml2js": "0.6.0" } }, "sha512-xFUuvg+KNufhjiKRH6T1WOMz3XVGulFI1Kr8gDUD/H1FfG+YhlkbL1sOEU+B2eZGSCGmJyGsjVXRYe6H2+UuSQ=="], - - "@expo/config-types": ["@expo/config-types@55.0.0-canary-20251230-fc48ddc", "", {}, "sha512-tFdYULhz0H9FT4+pqHhaQh+jqH1dLrPpU95sF4XIIWkn+KDVEJLVSijc2E8NRHJwahPrtXJjcE+OES2IfCydnQ=="], - - "@expo/devcert": ["@expo/devcert@1.2.1", "", { "dependencies": { "@expo/sudo-prompt": "^9.3.1", "debug": "^3.1.0" } }, "sha512-qC4eaxmKMTmJC2ahwyui6ud8f3W60Ss7pMkpBq40Hu3zyiAaugPXnZ24145U7K36qO9UHdZUVxsCvIpz2RYYCA=="], - - "@expo/devtools": ["@expo/devtools@0.1.9-canary-20251230-fc48ddc", "", { "dependencies": { "chalk": "^4.1.2" }, "peerDependencies": { "react": "*", "react-native": "*" }, "optionalPeers": ["react", "react-native"] }, "sha512-FxazHlZb0Wc67n8aALyaNif8io4/VD3w7YHqPDaYttH849JIL9pZfl7eJw1pyW757I7XCZi1uLBZzM6UQ1FhCQ=="], - - "@expo/dom-webview": ["@expo/dom-webview@0.2.9-canary-20251230-fc48ddc", "", { "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc", "react": "*", "react-native": "*" } }, "sha512-/cVu8YLOVxnLNqQohtumBDeVSLUjky4+szcnwqK5wGbnEaC+xIFryv0EzgEGubC8WGNXDFjC7xGBuvtYrfOlNQ=="], - - "@expo/env": ["@expo/env@2.0.9-canary-20251230-fc48ddc", "", { "dependencies": { "chalk": "^4.0.0", "debug": "^4.3.4", "dotenv": "~16.4.5", "dotenv-expand": "~11.0.6", "getenv": "^2.0.0" } }, "sha512-g4dU6KQbAlwcEiLkdwWSze6hKQjHkdmAMlu6oiQn30p2eqOWbf9a89h2Wh/AVvnq0r1hY87zOroOU1qaDTZJXg=="], - - "@expo/fingerprint": ["@expo/fingerprint@0.15.5-canary-20251230-fc48ddc", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "arg": "^5.0.2", "chalk": "^4.1.2", "debug": "^4.3.4", "getenv": "^2.0.0", "glob": "^13.0.0", "ignore": "^5.3.1", "minimatch": "^9.0.0", "p-limit": "^3.1.0", "resolve-from": "^5.0.0", "semver": "^7.6.0" }, "bin": { "fingerprint": "bin/cli.js" } }, "sha512-TPAdlyIJnqbMK4r8rPSqGgB1IticnBi630HHtjCyEiO25hyuJn2hq8TyNJdfZ3EgcCwELWeIsKWiBMPLPPF8vw=="], - - "@expo/image-utils": ["@expo/image-utils@0.8.9-canary-20251230-fc48ddc", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.0.0", "getenv": "^2.0.0", "jimp-compact": "0.16.1", "parse-png": "^2.1.0", "resolve-from": "^5.0.0", "resolve-global": "^1.0.0", "semver": "^7.6.0", "temp-dir": "~2.0.0", "unique-string": "~2.0.0" } }, "sha512-QNXW9i2a2dW1yEGyPHfvIGefuqXi3eqp8E+IkYIdpP2YVqBfGXVlrm2DPv67aUEx6JuYe/050bsazScDZM7TkQ=="], - - "@expo/json-file": ["@expo/json-file@10.0.9-canary-20251230-fc48ddc", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "json5": "^2.2.3" } }, "sha512-KE1XtrIdxcSL32nV5Oo0x9EEVS5PAbmdHYgn8BiZ1xMVlqtLuMRctXpEo2ubNk9NCVTOSrO1GRIzN0IdJZBxHg=="], - - "@expo/local-build-cache-provider": ["@expo/local-build-cache-provider@0.0.1-canary-20251230-fc48ddc", "", { "dependencies": { "@expo/config": "12.0.14-canary-20251230-fc48ddc", "chalk": "^4.1.2" } }, "sha512-5f3Mry6hUp/Bvxqd9S5LS0bp5a4/W9qg0a6nzztosqwIdkFBg9wfwxcnpKiqpQgKTdeYlmvdAg+3JEmaTBzcGw=="], - - "@expo/log-box": ["@expo/log-box@0.0.13-canary-20251230-fc48ddc", "", { "dependencies": { "@expo/dom-webview": "0.2.9-canary-20251230-fc48ddc", "anser": "^1.4.9", "stacktrace-parser": "^0.1.10" }, "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc", "react": "*", "react-dom": "*", "react-native": "*", "react-native-web": "*" } }, "sha512-7QQmkEohmHFr9lF821+fAlwda9GZjlz3hxrVTlIzICN2nyH1ItaGPEGuDKv9wXfsItRU4Lxs7SyXuz7gQi1RhA=="], - - "@expo/metro": ["@expo/metro@54.2.0", "", { "dependencies": { "metro": "0.83.3", "metro-babel-transformer": "0.83.3", "metro-cache": "0.83.3", "metro-cache-key": "0.83.3", "metro-config": "0.83.3", "metro-core": "0.83.3", "metro-file-map": "0.83.3", "metro-minify-terser": "0.83.3", "metro-resolver": "0.83.3", "metro-runtime": "0.83.3", "metro-source-map": "0.83.3", "metro-symbolicate": "0.83.3", "metro-transform-plugins": "0.83.3", "metro-transform-worker": "0.83.3" } }, "sha512-h68TNZPGsk6swMmLm9nRSnE2UXm48rWwgcbtAHVMikXvbxdS41NDHHeqg1rcQ9AbznDRp6SQVC2MVpDnsRKU1w=="], - - "@expo/metro-config": ["@expo/metro-config@54.1.0-canary-20251230-fc48ddc", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.20.0", "@babel/generator": "^7.20.5", "@expo/config": "12.0.14-canary-20251230-fc48ddc", "@expo/env": "2.0.9-canary-20251230-fc48ddc", "@expo/json-file": "10.0.9-canary-20251230-fc48ddc", "@expo/metro": "~54.2.0", "@expo/spawn-async": "^1.7.2", "browserslist": "^4.25.0", "chalk": "^4.1.0", "debug": "^4.3.2", "dotenv": "~16.4.5", "dotenv-expand": "~11.0.6", "getenv": "^2.0.0", "glob": "^13.0.0", "hermes-parser": "^0.29.1", "jsc-safe-url": "^0.2.4", "lightningcss": "^1.30.1", "minimatch": "^9.0.0", "postcss": "~8.4.32", "resolve-from": "^5.0.0" }, "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc" }, "optionalPeers": ["expo"] }, "sha512-zd+MAFgRUu6SMR4mseAt9nf2xTOB0kIfWXIl02aesJJZcUrbuCniq9QkdQh92mSa/XF9Ztsivq/ZPkmlHYSvBQ=="], - - "@expo/metro-runtime": ["@expo/metro-runtime@6.2.0-canary-20251216-3f01dbf", "", { "dependencies": { "@expo/log-box": "0.0.13-canary-20251216-3f01dbf", "anser": "^1.4.9", "pretty-format": "^29.7.0", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0" }, "peerDependencies": { "expo": "55.0.0-canary-20251216-3f01dbf", "react": "*", "react-dom": "*", "react-native": "*" }, "optionalPeers": ["react-dom"] }, "sha512-H5aG4/fhqdwA/1oypRWrwFlShzhE3xSoXCIOC4enpGlcHAty0dx1shSKh2KhZrISAxmcKnIfGWJs0FLXBmPINA=="], - - "@expo/osascript": ["@expo/osascript@2.3.9-canary-20251230-fc48ddc", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "exec-async": "^2.2.0" } }, "sha512-moInQk/wi7HpVOc5HG5ZNbY7LpVa4IHnaw1j3SiYAhprs33XqphRpe80Y7EtN5HzBxdOcGpC0xurj+0WEbvZmg=="], - - "@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.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=="], - - "@expo/router-server": ["@expo/router-server@0.2.0-canary-20251230-fc48ddc", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "@expo/metro-runtime": "6.2.0-canary-20251230-fc48ddc", "expo": "55.0.0-canary-20251230-fc48ddc", "expo-constants": "18.1.0-canary-20251230-fc48ddc", "expo-font": "14.1.0-canary-20251230-fc48ddc", "expo-router": "7.0.0-canary-20251230-fc48ddc", "expo-server": "1.0.6-canary-20251230-fc48ddc", "react": "*", "react-dom": "*", "react-server-dom-webpack": "~19.0.1 || ~19.1.2 || ~19.2.1" }, "optionalPeers": ["react-dom", "react-server-dom-webpack"] }, "sha512-zquBf5dYjJuSIRPgPchl7z+sSAv5t+EIduHaaVoJZzvB1d4GcYs3KtlN09IPnjXtKVf7Dz3BX+brOKyxxA2wJA=="], - - "@expo/schema-utils": ["@expo/schema-utils@0.1.9-canary-20251230-fc48ddc", "", {}, "sha512-67k1lwEOjpFLMVk5p/utD98UOWqtAU6xJ5L8Wb8+T8HVUz9X6SHL9LE6eOCOnBzB80XTMaKwrhjriI0nadtRRQ=="], - - "@expo/sdk-runtime-versions": ["@expo/sdk-runtime-versions@1.0.0", "", {}, "sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ=="], - - "@expo/spawn-async": ["@expo/spawn-async@1.7.2", "", { "dependencies": { "cross-spawn": "^7.0.3" } }, "sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew=="], - - "@expo/sudo-prompt": ["@expo/sudo-prompt@9.3.2", "", {}, "sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw=="], - - "@expo/ui": ["@expo/ui@0.2.0-canary-20251230-fc48ddc", "", { "dependencies": { "sf-symbols-typescript": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-WcLr0oUc0qpJrrWykfWB8qqWwJm3QvNN3Pvm3MbEJZGtPLjWM0ZfihhFkoi2lIMXr1/7SYDhKPYbTSYmjJ9Qvg=="], - - "@expo/vector-icons": ["@expo/vector-icons@15.0.3", "", { "peerDependencies": { "expo-font": ">=14.0.4", "react": "*", "react-native": "*" } }, "sha512-SBUyYKphmlfUBqxSfDdJ3jAdEVSALS2VUPOUyqn48oZmb2TL/O7t7/PQm5v4NQujYEPLPMTLn9KVw6H7twwbTA=="], - - "@expo/ws-tunnel": ["@expo/ws-tunnel@1.0.6", "", {}, "sha512-nDRbLmSrJar7abvUjp3smDwH8HcbZcoOEa5jVPUv9/9CajgmWw20JNRwTuBRzWIWIkEJDkz20GoNA+tSwUqk0Q=="], - - "@expo/xcpretty": ["@expo/xcpretty@4.3.2", "", { "dependencies": { "@babel/code-frame": "7.10.4", "chalk": "^4.1.0", "find-up": "^5.0.0", "js-yaml": "^4.1.0" }, "bin": { "excpretty": "build/cli.js" } }, "sha512-ReZxZ8pdnoI3tP/dNnJdnmAk7uLT4FjsKDGW7YeDdvdOMz2XCQSmSCM9IWlrXuWtMF9zeSB6WJtEhCQ41gQOfw=="], - - "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="], - - "@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="], - - "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw=="], - - "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], - - "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], - - "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], - - "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], - - "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], - - "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], - - "@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=="], - - "@istanbuljs/load-nyc-config": ["@istanbuljs/load-nyc-config@1.1.0", "", { "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", "get-package-type": "^0.1.0", "js-yaml": "^3.13.1", "resolve-from": "^5.0.0" } }, "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ=="], - - "@istanbuljs/schema": ["@istanbuljs/schema@0.1.3", "", {}, "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA=="], - - "@jest/create-cache-key-function": ["@jest/create-cache-key-function@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3" } }, "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA=="], - - "@jest/environment": ["@jest/environment@29.7.0", "", { "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0" } }, "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw=="], - - "@jest/fake-timers": ["@jest/fake-timers@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ=="], - - "@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "@jest/transform": ["@jest/transform@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", "jest-regex-util": "^29.6.3", "jest-util": "^29.7.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", "write-file-atomic": "^4.0.2" } }, "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw=="], - - "@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - - "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], - - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - - "@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], - - "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], - - "@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=="], - - "@pnpm/npm-conf": ["@pnpm/npm-conf@2.3.1", "", { "dependencies": { "@pnpm/config.env-replace": "^1.1.0", "@pnpm/network.ca-file": "^1.0.1", "config-chain": "^1.1.11" } }, "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw=="], - - "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], - - "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="], - - "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], - - "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], - - "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], - - "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], - - "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], - - "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], - - "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="], - - "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], - - "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], - - "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], - - "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="], - - "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], - - "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], - - "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], - - "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="], - - "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="], - - "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="], - - "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], - - "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], - - "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], - - "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], - - "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], - - "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], - - "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], - - "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], - - "@react-native-async-storage/async-storage": ["@react-native-async-storage/async-storage@2.2.0", "", { "dependencies": { "merge-options": "^3.0.4" }, "peerDependencies": { "react-native": "^0.0.0-0 || >=0.65 <1.0" } }, "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw=="], - - "@react-native-community/datetimepicker": ["@react-native-community/datetimepicker@8.4.4", "", { "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "expo": ">=52.0.0", "react": "*", "react-native": "*", "react-native-windows": "*" }, "optionalPeers": ["expo", "react-native-windows"] }, "sha512-bc4ZixEHxZC9/qf5gbdYvIJiLZ5CLmEsC3j+Yhe1D1KC/3QhaIfGDVdUcid0PdlSoGOSEq4VlB93AWyetEyBSQ=="], - - "@react-native-community/netinfo": ["@react-native-community/netinfo@11.4.1", "", { "peerDependencies": { "react-native": ">=0.59" } }, "sha512-B0BYAkghz3Q2V09BF88RA601XursIEA111tnc2JOaN7axJWmNefmfjZqw/KdSxKZp7CZUuPpjBmz/WCR9uaHYg=="], - - "@react-native-segmented-control/segmented-control": ["@react-native-segmented-control/segmented-control@2.5.7", "", { "peerDependencies": { "react": ">=16.0", "react-native": ">=0.62" } }, "sha512-l84YeVX8xAU3lvOJSvV4nK/NbGhIm2gBfveYolwaoCbRp+/SLXtc6mYrQmM9ScXNwU14mnzjQTpTHWl5YPnkzQ=="], - - "@react-native/assets-registry": ["@react-native/assets-registry@0.83.1", "", {}, "sha512-AT7/T6UwQqO39bt/4UL5EXvidmrddXrt0yJa7ENXndAv+8yBzMsZn6fyiax6+ERMt9GLzAECikv3lj22cn2wJA=="], - - "@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.81.5", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@react-native/codegen": "0.81.5" } }, "sha512-oF71cIH6je3fSLi6VPjjC3Sgyyn57JLHXs+mHWc9MoCiJJcM4nqsS5J38zv1XQ8d3zOW2JtHro+LF0tagj2bfQ=="], - - "@react-native/babel-preset": ["@react-native/babel-preset@0.81.5", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-transform-arrow-functions": "^7.24.7", "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-computed-properties": "^7.24.7", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-for-of": "^7.24.7", "@babel/plugin-transform-function-name": "^7.25.1", "@babel/plugin-transform-literals": "^7.25.2", "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", "@babel/plugin-transform-numeric-separator": "^7.24.7", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-react-display-name": "^7.24.7", "@babel/plugin-transform-react-jsx": "^7.25.2", "@babel/plugin-transform-react-jsx-self": "^7.24.7", "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-shorthand-properties": "^7.24.7", "@babel/plugin-transform-spread": "^7.24.7", "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/template": "^7.25.0", "@react-native/babel-plugin-codegen": "0.81.5", "babel-plugin-syntax-hermes-parser": "0.29.1", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" } }, "sha512-UoI/x/5tCmi+pZ3c1+Ypr1DaRMDLI3y+Q70pVLLVgrnC3DHsHRIbHcCHIeG/IJvoeFqFM2sTdhSOLJrf8lOPrA=="], - - "@react-native/codegen": ["@react-native/codegen@0.83.1", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.32.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "yargs": "^17.6.2" } }, "sha512-FpRxenonwH+c2a5X5DZMKUD7sCudHxB3eSQPgV9R+uxd28QWslyAWrpnJM/Az96AEksHnymDzEmzq2HLX5nb+g=="], - - "@react-native/community-cli-plugin": ["@react-native/community-cli-plugin@0.83.1", "", { "dependencies": { "@react-native/dev-middleware": "0.83.1", "debug": "^4.4.0", "invariant": "^2.2.4", "metro": "^0.83.3", "metro-config": "^0.83.3", "metro-core": "^0.83.3", "semver": "^7.1.3" }, "peerDependencies": { "@react-native-community/cli": "*", "@react-native/metro-config": "*" }, "optionalPeers": ["@react-native-community/cli", "@react-native/metro-config"] }, "sha512-FqR1ftydr08PYlRbrDF06eRiiiGOK/hNmz5husv19sK6iN5nHj1SMaCIVjkH/a5vryxEddyFhU6PzO/uf4kOHg=="], - - "@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.83.1", "", {}, "sha512-01Rn3goubFvPjHXONooLmsW0FLxJDKIUJNOlOS0cPtmmTIx9YIjxhe/DxwHXGk7OnULd7yl3aYy7WlBsEd5Xmg=="], - - "@react-native/debugger-shell": ["@react-native/debugger-shell@0.83.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "fb-dotslash": "0.5.8" } }, "sha512-d+0w446Hxth5OP/cBHSSxOEpbj13p2zToUy6e5e3tTERNJ8ueGlW7iGwGTrSymNDgXXFjErX+dY4P4/3WokPIQ=="], - - "@react-native/dev-middleware": ["@react-native/dev-middleware@0.83.1", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.83.1", "@react-native/debugger-shell": "0.83.1", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", "debug": "^4.4.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "serve-static": "^1.16.2", "ws": "^7.5.10" } }, "sha512-QJaSfNRzj3Lp7MmlCRgSBlt1XZ38xaBNXypXAp/3H3OdFifnTZOeYOpFmcpjcXYnDqkxetuwZg8VL65SQhB8dg=="], - - "@react-native/gradle-plugin": ["@react-native/gradle-plugin@0.83.1", "", {}, "sha512-6ESDnwevp1CdvvxHNgXluil5OkqbjkJAkVy7SlpFsMGmVhrSxNAgD09SSRxMNdKsnLtzIvMsFCzyHLsU/S4PtQ=="], - - "@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.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=="], - - "@react-navigation/bottom-tabs": ["@react-navigation/bottom-tabs@7.8.12", "", { "dependencies": { "@react-navigation/elements": "^2.9.2", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0" }, "peerDependencies": { "@react-navigation/native": "^7.1.25", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-efVt5ydHK+b4ZtjmN81iduaO5dPCmzhLBFwjCR8pV4x4VzUfJmtUJizLqTXpT3WatHdeon2gDPwhhoelsvu/JA=="], - - "@react-navigation/core": ["@react-navigation/core@7.13.6", "", { "dependencies": { "@react-navigation/routers": "^7.5.2", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "query-string": "^7.1.3", "react-is": "^19.1.0", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": ">= 18.2.0" } }, "sha512-7QG29HAWOR8wYuPkfTN8L2Po+kE1xn3nsi2sS35sGngq8HYZRHfXvxrhrAZYfFnFq2hUtOhcXnSS6vEWU/5rmA=="], - - "@react-navigation/elements": ["@react-navigation/elements@2.9.2", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.1.25", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-J1GltOAGowNLznEphV/kr4zs0U7mUBO1wVA2CqpkN8ePBsoxrAmsd+T5sEYUCXN9KgTDFvc6IfcDqrGSQngd/g=="], - - "@react-navigation/native": ["@react-navigation/native@7.1.25", "", { "dependencies": { "@react-navigation/core": "^7.13.6", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*" } }, "sha512-zQeWK9txDePWbYfqTs0C6jeRdJTm/7VhQtW/1IbJNDi9/rFIRzZule8bdQPAnf8QWUsNujRmi1J9OG/hhfbalg=="], - - "@react-navigation/native-stack": ["@react-navigation/native-stack@7.8.6", "", { "dependencies": { "@react-navigation/elements": "^2.9.2", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0", "warn-once": "^0.1.1" }, "peerDependencies": { "@react-navigation/native": "^7.1.25", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-eBY92xb4H53c9jiWriKMOZmQ/Tu9w1qcUrgOA/qjQOvJFbgKF9D6y3e4UuBaDQzjWjLEDZLaiwXe8cwXRb46mg=="], - - "@react-navigation/routers": ["@react-navigation/routers@7.5.2", "", { "dependencies": { "nanoid": "^3.3.11" } }, "sha512-kymreY5aeTz843E+iPAukrsOtc7nabAH6novtAPREmmGu77dQpfxPB2ZWpKb5nRErIRowp1kYRoN2Ckl+S6JYw=="], - - "@rn-primitives/alert-dialog": ["@rn-primitives/alert-dialog@1.2.0", "", { "dependencies": { "@radix-ui/react-alert-dialog": "^1.1.14", "@rn-primitives/hooks": "1.3.0", "@rn-primitives/slot": "1.2.0", "@rn-primitives/types": "1.2.0" }, "peerDependencies": { "@rn-primitives/portal": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native", "react-native-web"] }, "sha512-/XxvQVRMnIyQo29iuQ631CHjghGKY8U4k3gbsS2MCGP0hglZVFoBRiAF3Bgyr16LxWRu1DoPltvzwOrUCsB+YQ=="], - - "@rn-primitives/dropdown-menu": ["@rn-primitives/dropdown-menu@1.2.0", "", { "dependencies": { "@radix-ui/react-dropdown-menu": "^2.1.15", "@rn-primitives/hooks": "1.3.0", "@rn-primitives/slot": "1.2.0", "@rn-primitives/types": "1.2.0", "@rn-primitives/utils": "1.2.0" }, "peerDependencies": { "@rn-primitives/portal": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native", "react-native-web"] }, "sha512-TJDDr8VQfw9CRZ7xZ6kBYLVMqL1xFVC5ZZ4sfRmWP6PCT0lNks4XqGuTFLeVVlNLPSmzt9GKC2DZqzDXui8/NQ=="], - - "@rn-primitives/hooks": ["@rn-primitives/hooks@1.3.0", "", { "dependencies": { "@rn-primitives/types": "1.2.0" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native", "react-native-web"] }, "sha512-BR97reSu7uVDpyMeQdRJHT0w8KdS6jdYnOL6xQtqS2q3H6N7vXBlX4LFERqJZphD+aziJFIAJ3HJF1vtt6XlpQ=="], - - "@rn-primitives/portal": ["@rn-primitives/portal@1.3.0", "", { "dependencies": { "zustand": "^5.0.4" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native", "react-native-web"] }, "sha512-a2DSce7TcSfcs0cCngLadAJOvx/+mdH9NRu+GxkX8NPRsGGhJvDEOqouMgDqLwx7z9mjXoUaZcwaVcemUSW9/A=="], - - "@rn-primitives/slot": ["@rn-primitives/slot@1.2.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native", "react-native-web"] }, "sha512-cpbn+JLjSeq3wcA4uqgFsUimMrWYWx2Ks7r5rkwd1ds1utxynsGkLOKpYVQkATwWrYhtcoF1raxIKEqXuMN+/w=="], - - "@rn-primitives/types": ["@rn-primitives/types@1.2.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native", "react-native-web"] }, "sha512-b+6zKgdKVqAfaFPSfhwlQL0dnPQXPpW890m3eguC0VDI1eOsoEvUfVb6lmgH4bum9MmI0xymq4tOUI/fsKLoCQ=="], - - "@rn-primitives/utils": ["@rn-primitives/utils@1.2.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native", "react-native-web"] }, "sha512-vLXV5NuxIHDeb4Bw57FzdUh89/g8gz6GERm8TsbJaSUPsDXfnC/ffeYiZJb0LxNteKE3Nr8na4Jy2n26tFil7w=="], - - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.3", "", { "os": "android", "cpu": "arm" }, "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w=="], - - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.53.3", "", { "os": "android", "cpu": "arm64" }, "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w=="], - - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.53.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.53.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.53.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.53.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.53.3", "", { "os": "linux", "cpu": "arm" }, "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.53.3", "", { "os": "linux", "cpu": "arm" }, "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.53.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.53.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A=="], - - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g=="], - - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.53.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g=="], - - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.53.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.53.3", "", { "os": "linux", "cpu": "x64" }, "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.53.3", "", { "os": "linux", "cpu": "x64" }, "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q=="], - - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.53.3", "", { "os": "none", "cpu": "arm64" }, "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.53.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.53.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA=="], - - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.53.3", "", { "os": "win32", "cpu": "x64" }, "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.53.3", "", { "os": "win32", "cpu": "x64" }, "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ=="], - - "@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - - "@sinonjs/commons": ["@sinonjs/commons@3.0.1", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ=="], - - "@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="], - - "@tanstack/query-core": ["@tanstack/query-core@5.62.0", "", {}, "sha512-sx38bGrqF9bop92AXOvzDr0L9fWDas5zXdPglxa9cuqeVSWS7lY6OnVyl/oodfXjgOGRk79IfCpgVmxrbHuFHg=="], - - "@tanstack/query-persist-client-core": ["@tanstack/query-persist-client-core@5.62.0", "", { "dependencies": { "@tanstack/query-core": "5.62.0" } }, "sha512-yEDvJjVHqo+IizSjNkdu5fSL4ANOd16o8Tk2NOeaD7kGS1fXjf47j3XOXLIypZ7nmPTjZf9vyZi1WP3VB4saGg=="], - - "@tanstack/react-query": ["@tanstack/react-query@5.62.0", "", { "dependencies": { "@tanstack/query-core": "5.62.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-tj2ltjAn2a3fs+Dqonlvs6GyLQ/LKVJE2DVSYW+8pJ3P6/VCVGrfqv5UEchmlP7tLOvvtZcOuSyI2ooVlR5Yqw=="], - - "@tanstack/react-query-persist-client": ["@tanstack/react-query-persist-client@5.62.0", "", { "dependencies": { "@tanstack/query-persist-client-core": "5.62.0" }, "peerDependencies": { "@tanstack/react-query": "^5.62.0", "react": "^18 || ^19" } }, "sha512-yv7Oe0vNkGDTVFkq9bbdGQO9EoHwXYKIYuj845v1bOmK/Npvcov3yaS0AirOlNklsapDtr0v+x1ev2aJBIbm7A=="], - - "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], - - "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], - - "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], - - "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], - - "@types/chrome": ["@types/chrome@0.1.31", "", { "dependencies": { "@types/filesystem": "*", "@types/har-format": "*" } }, "sha512-8fC9t02LRdqlSQ9bktaiLjP0NKujPNPaCKPfXxfIjZ35A9u831XY0pt0UEf2oup2SjcUEAaIYOB8mzJ51jhjmA=="], - - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - - "@types/filesystem": ["@types/filesystem@0.0.36", "", { "dependencies": { "@types/filewriter": "*" } }, "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA=="], - - "@types/filewriter": ["@types/filewriter@0.0.33", "", {}, "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g=="], - - "@types/graceful-fs": ["@types/graceful-fs@4.1.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ=="], - - "@types/hammerjs": ["@types/hammerjs@2.0.46", "", {}, "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw=="], - - "@types/har-format": ["@types/har-format@1.2.16", "", {}, "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A=="], - - "@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="], - - "@types/istanbul-lib-report": ["@types/istanbul-lib-report@3.0.3", "", { "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA=="], - - "@types/istanbul-reports": ["@types/istanbul-reports@3.0.4", "", { "dependencies": { "@types/istanbul-lib-report": "*" } }, "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ=="], - - "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - - "@types/minimatch": ["@types/minimatch@3.0.5", "", {}, "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ=="], - - "@types/node": ["@types/node@22.19.6", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-qm+G8HuG6hOHQigsi7VGuLjUVu6TtBo/F05zvX04Mw2uCg9Dv0Qxy3Qw7j41SidlTcl5D/5yg0SEZqOB+EqZnQ=="], - - "@types/react": ["@types/react@19.2.10", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw=="], - - "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], - - "@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="], - - "@types/yargs": ["@types/yargs@17.0.35", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg=="], - - "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], - - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.50.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/type-utils": "8.50.1", "@typescript-eslint/utils": "8.50.1", "@typescript-eslint/visitor-keys": "8.50.1", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.50.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw=="], - - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.50.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/types": "8.50.1", "@typescript-eslint/typescript-estree": "8.50.1", "@typescript-eslint/visitor-keys": "8.50.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg=="], - - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.50.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.50.1", "@typescript-eslint/types": "^8.50.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg=="], - - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.50.1", "", { "dependencies": { "@typescript-eslint/types": "8.50.1", "@typescript-eslint/visitor-keys": "8.50.1" } }, "sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw=="], - - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.50.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw=="], - - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.50.1", "", { "dependencies": { "@typescript-eslint/types": "8.50.1", "@typescript-eslint/typescript-estree": "8.50.1", "@typescript-eslint/utils": "8.50.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-7J3bf022QZE42tYMO6SL+6lTPKFk/WphhRPe9Tw/el+cEwzLz1Jjz2PX3GtGQVxooLDKeMVmMt7fWpYRdG5Etg=="], - - "@typescript-eslint/types": ["@typescript-eslint/types@8.50.1", "", {}, "sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA=="], - - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.50.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.50.1", "@typescript-eslint/tsconfig-utils": "8.50.1", "@typescript-eslint/types": "8.50.1", "@typescript-eslint/visitor-keys": "8.50.1", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ=="], - - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.50.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/types": "8.50.1", "@typescript-eslint/typescript-estree": "8.50.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ=="], - - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.50.1", "", { "dependencies": { "@typescript-eslint/types": "8.50.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ=="], - - "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - - "@urql/core": ["@urql/core@5.2.0", "", { "dependencies": { "@0no-co/graphql.web": "^1.0.13", "wonka": "^6.3.2" } }, "sha512-/n0ieD0mvvDnVAXEQgX/7qJiVcvYvNkOHeBvkwtylfjydar123caCXcl58PXFY11oU1oquJocVXHxLAbtv4x1A=="], - - "@urql/exchange-retry": ["@urql/exchange-retry@1.3.2", "", { "dependencies": { "@urql/core": "^5.1.2", "wonka": "^6.3.2" } }, "sha512-TQMCz2pFJMfpNxmSfX1VSfTjwUIFx/mL+p1bnfM1xjjdla7Z+KnGMW/EhFbpckp3LyWAH4PgOsMwOMnIN+MBFg=="], - - "@webext-core/fake-browser": ["@webext-core/fake-browser@1.3.2", "", { "dependencies": { "lodash.merge": "^4.6.2" } }, "sha512-jFyPWWz+VkHAC9DRIiIPOyu6X/KlC8dYqSKweHz6tsDb86QawtVgZSpYcM+GOQBlZc5DHFo92jJ7cIq4uBnU0A=="], - - "@webext-core/isolated-element": ["@webext-core/isolated-element@1.1.2", "", { "dependencies": { "is-potential-custom-element-name": "^1.0.1" } }, "sha512-CNHYhsIR8TPkPb+4yqTIuzaGnVn/Fshev5fyoPW+/8Cyc93tJbCjP9PC1XSK6fDWu+xASdPHLZaoa2nWAYoxeQ=="], - - "@webext-core/match-patterns": ["@webext-core/match-patterns@1.0.3", "", {}, "sha512-NY39ACqCxdKBmHgw361M9pfJma8e4AZo20w9AY+5ZjIj1W2dvXC8J31G5fjfOGbulW9w4WKpT8fPooi0mLkn9A=="], - - "@wxt-dev/browser": ["@wxt-dev/browser@0.1.4", "", { "dependencies": { "@types/filesystem": "*", "@types/har-format": "*" } }, "sha512-9x03I15i79XU8qYwjv4le0K2HdMl/Yga2wUBSoUbcrCnamv8P3nvuYxREQ9C5QY/qPAfeEVdAtaTrS3KWak71g=="], - - "@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.7.13", "", {}, "sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g=="], - - "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], - - "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], - - "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], - - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - - "adm-zip": ["adm-zip@0.5.16", "", {}, "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="], - - "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - - "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], - - "anser": ["anser@1.4.10", "", {}, "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww=="], - - "ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="], - - "ansi-escapes": ["ansi-escapes@7.2.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw=="], - - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], - - "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], - - "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], - - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - - "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], - - "array-differ": ["array-differ@4.0.0", "", {}, "sha512-Q6VPTLMsmXZ47ENG3V+wQyZS1ZxXMxFyYzA+Z/GMrJ6yIutAIEf9wTyroTzmGjNfox9/h3GdGBCVh43GVFx4Uw=="], - - "array-timsort": ["array-timsort@1.0.3", "", {}, "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ=="], - - "array-union": ["array-union@3.0.1", "", {}, "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw=="], - - "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="], - - "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], - - "async-mutex": ["async-mutex@0.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA=="], - - "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], - - "atomically": ["atomically@2.1.0", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q=="], - - "babel-jest": ["babel-jest@29.7.0", "", { "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" }, "peerDependencies": { "@babel/core": "^7.8.0" } }, "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg=="], - - "babel-plugin-istanbul": ["babel-plugin-istanbul@6.1.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-instrument": "^5.0.4", "test-exclude": "^6.0.0" } }, "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA=="], - - "babel-plugin-jest-hoist": ["babel-plugin-jest-hoist@29.6.3", "", { "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", "@types/babel__core": "^7.1.14", "@types/babel__traverse": "^7.0.6" } }, "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg=="], - - "babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.14", "", { "dependencies": { "@babel/compat-data": "^7.27.7", "@babel/helper-define-polyfill-provider": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg=="], - - "babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.13.0", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5", "core-js-compat": "^3.43.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A=="], - - "babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.5", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg=="], - - "babel-plugin-react-compiler": ["babel-plugin-react-compiler@19.0.0-beta-af1b7da-20250417", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-UyTCRmzpxa4H1EqJk8fWeUOzHdEA12NQZ5DrF5hyhCs+Y6f7B4pg1fkul49sRn9GPPGFgkrH4IxOtnQJ7tNXIA=="], - - "babel-plugin-react-native-web": ["babel-plugin-react-native-web@0.21.2", "", {}, "sha512-SPD0J6qjJn8231i0HZhlAGH6NORe+QvRSQM2mwQEzJ2Fb3E4ruWTiiicPlHjmeWShDXLcvoorOCXjeR7k/lyWA=="], - - "babel-plugin-syntax-hermes-parser": ["babel-plugin-syntax-hermes-parser@0.29.1", "", { "dependencies": { "hermes-parser": "0.29.1" } }, "sha512-2WFYnoWGdmih1I1J5eIqxATOeycOqRwYxAQBu3cUu/rhwInwHUg7k60AFNbuGjSDL8tje5GDrAnxzRLcu2pYcA=="], - - "babel-plugin-transform-flow-enums": ["babel-plugin-transform-flow-enums@0.0.2", "", { "dependencies": { "@babel/plugin-syntax-flow": "^7.12.1" } }, "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ=="], - - "babel-preset-current-node-syntax": ["babel-preset-current-node-syntax@1.2.0", "", { "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0 || ^8.0.0-0" } }, "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg=="], - - "babel-preset-expo": ["babel-preset-expo@54.0.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/plugin-proposal-decorators": "^7.12.9", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-transform-class-static-block": "^7.27.1", "@babel/plugin-transform-export-namespace-from": "^7.25.9", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", "@react-native/babel-preset": "0.81.5", "babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-native-web": "~0.21.0", "babel-plugin-syntax-hermes-parser": "^0.29.1", "babel-plugin-transform-flow-enums": "^0.0.2", "debug": "^4.3.4", "resolve-from": "^5.0.0" }, "peerDependencies": { "@babel/runtime": "^7.20.0", "expo": "*", "react-refresh": ">=0.14.0 <1.0.0" }, "optionalPeers": ["@babel/runtime", "expo"] }, "sha512-JENWk0bvxW4I1ftveO8GRtX2t2TH6N4Z0TPvIHxroZ/4SswUfyNsUNbbP7Fm4erj3ar/JHGri5kTZ+s3xdjHZw=="], - - "babel-preset-jest": ["babel-preset-jest@29.6.3", "", { "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA=="], - - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - - "baseline-browser-mapping": ["baseline-browser-mapping@2.9.6", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg=="], - - "better-opn": ["better-opn@3.0.2", "", { "dependencies": { "open": "^8.0.4" } }, "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ=="], - - "big-integer": ["big-integer@1.6.52", "", {}, "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg=="], - - "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], - - "bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="], - - "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], - - "boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="], - - "bplist-creator": ["bplist-creator@0.1.0", "", { "dependencies": { "stream-buffers": "2.2.x" } }, "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg=="], - - "bplist-parser": ["bplist-parser@0.3.2", "", { "dependencies": { "big-integer": "1.6.x" } }, "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ=="], - - "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - - "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], - - "bser": ["bser@2.1.1", "", { "dependencies": { "node-int64": "^0.4.0" } }, "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ=="], - - "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - - "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], - - "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], - - "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - - "c12": ["c12@3.3.2", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-QkikB2X5voO1okL3QsES0N690Sn/K9WokXqUsDQsWy5SnYb+psYQFGA10iy1bZHj3fjISKsI67Q90gruvWWM3A=="], - - "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], - - "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - - "camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="], - - "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], - - "caniuse-lite": ["caniuse-lite@1.0.30001760", "", {}, "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw=="], - - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], - - "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], - - "chrome-launcher": ["chrome-launcher@1.2.0", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^2.0.1" }, "bin": { "print-chrome-path": "bin/print-chrome-path.cjs" } }, "sha512-JbuGuBNss258bvGil7FT4HKdC3SC2K7UAEUqiPy3ACS3Yxo3hAW6bvFpCu2HsIJLgTqxgEX6BkujvzZfLpUD0Q=="], - - "chromium-edge-launcher": ["chromium-edge-launcher@0.2.0", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^1.0.0", "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg=="], - - "ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], - - "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], - - "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], - - "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], - - "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], - - "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], - - "cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="], - - "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], - - "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - - "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], - - "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], - - "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], - - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], - - "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], - - "commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], - - "comment-json": ["comment-json@4.5.0", "", { "dependencies": { "array-timsort": "^1.0.3", "core-util-is": "^1.0.3", "esprima": "^4.0.1" } }, "sha512-aKl8CwoMKxVTfAK4dFN4v54AEvuUh9pzmgVIBeK6gBomLwMgceQUKKWHzJdW1u1VQXQuwnJ7nJGWYYMTl5U4yg=="], - - "compressible": ["compressible@2.0.18", "", { "dependencies": { "mime-db": ">= 1.43.0 < 2" } }, "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=="], - - "compression": ["compression@1.8.1", "", { "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" } }, "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w=="], - - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - - "concat-stream": ["concat-stream@1.6.2", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^2.2.2", "typedarray": "^0.0.6" } }, "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw=="], - - "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="], - - "config-chain": ["config-chain@1.1.13", "", { "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ=="], - - "configstore": ["configstore@7.1.0", "", { "dependencies": { "atomically": "^2.0.3", "dot-prop": "^9.0.0", "graceful-fs": "^4.2.11", "xdg-basedir": "^5.1.0" } }, "sha512-N4oog6YJWbR9kGyXvS7jEykLDXIE2C0ILYqNBZBp9iwiJpoCBWYsuAdW6PPFn6w06jjnC+3JstVvWHO4cZqvRg=="], - - "connect": ["connect@3.7.0", "", { "dependencies": { "debug": "2.6.9", "finalhandler": "1.1.2", "parseurl": "~1.3.3", "utils-merge": "1.0.1" } }, "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ=="], - - "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], - - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - - "core-js-compat": ["core-js-compat@3.47.0", "", { "dependencies": { "browserslist": "^4.28.0" } }, "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ=="], - - "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], - - "cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="], - - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - - "crypto-random-string": ["crypto-random-string@2.0.0", "", {}, "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA=="], - - "css-in-js-utils": ["css-in-js-utils@3.1.0", "", { "dependencies": { "hyphenate-style-name": "^1.0.3" } }, "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A=="], - - "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], - - "css-tree": ["css-tree@1.1.3", "", { "dependencies": { "mdn-data": "2.0.14", "source-map": "^0.6.1" } }, "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q=="], - - "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], - - "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], - - "cssom": ["cssom@0.5.0", "", {}, "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw=="], - - "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - - "debounce": ["debounce@1.2.1", "", {}, "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug=="], - - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "decode-uri-component": ["decode-uri-component@0.2.2", "", {}, "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="], - - "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], - - "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], - - "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], - - "default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="], - - "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], - - "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], - - "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], - - "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], - - "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - - "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], - - "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], - - "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], - - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - - "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], - - "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], - - "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], - - "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], - - "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], - - "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], - - "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], - - "dot-prop": ["dot-prop@9.0.0", "", { "dependencies": { "type-fest": "^4.18.2" } }, "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ=="], - - "dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], - - "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=="], - - "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], - - "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - - "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - - "env-editor": ["env-editor@0.4.2", "", {}, "sha512-ObFo8v4rQJAE59M69QzwloxPZtd33TpYEIjtKD1rrFDcM1Gd7IkDxEBU+HriziN6HSHQnBJi8Dmy+JWkav5HKA=="], - - "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], - - "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], - - "error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="], - - "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], - - "es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="], - - "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - - "escape-goat": ["escape-goat@4.0.0", "", {}, "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg=="], - - "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - - "eslint": ["eslint@9.28.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.28.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ=="], - - "eslint-plugin-react-compiler": ["eslint-plugin-react-compiler@19.0.0-beta-af1b7da-20250417", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "@babel/plugin-proposal-private-methods": "^7.18.6", "hermes-parser": "^0.25.1", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "eslint": ">=7" } }, "sha512-m3eVzHqtXFtu6rViWx/kBhv9Jcpj+Q4pkfRWyF47TIBa+Jo8DAB90OoeDh0JR896rtG1OXogLKKd+b6lzFup0A=="], - - "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], - - "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], - - "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], - - "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], - - "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], - - "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], - - "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], - - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - - "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], - - "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], - - "exec-async": ["exec-async@2.2.0", "", {}, "sha512-87OpwcEiMia/DeiKFzaQNBNFeN3XkkpYIh9FyOqq5mS2oKv3CBE67PXoEKcr6nodWdXNogTiQ0jE2NGuoffXPw=="], - - "execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="], - - "expo": ["expo@55.0.0-canary-20251230-fc48ddc", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "55.0.0-canary-20251230-fc48ddc", "@expo/config": "12.0.14-canary-20251230-fc48ddc", "@expo/config-plugins": "54.0.5-canary-20251230-fc48ddc", "@expo/devtools": "0.1.9-canary-20251230-fc48ddc", "@expo/fingerprint": "0.15.5-canary-20251230-fc48ddc", "@expo/local-build-cache-provider": "0.0.1-canary-20251230-fc48ddc", "@expo/log-box": "0.0.13-canary-20251230-fc48ddc", "@expo/metro": "~54.2.0", "@expo/metro-config": "54.1.0-canary-20251230-fc48ddc", "@expo/vector-icons": "^15.0.2", "@ungap/structured-clone": "^1.3.0", "babel-preset-expo": "54.1.0-canary-20251230-fc48ddc", "expo-asset": "12.0.13-canary-20251230-fc48ddc", "expo-constants": "18.1.0-canary-20251230-fc48ddc", "expo-file-system": "19.0.22-canary-20251230-fc48ddc", "expo-font": "14.1.0-canary-20251230-fc48ddc", "expo-keep-awake": "15.0.9-canary-20251230-fc48ddc", "expo-modules-autolinking": "3.1.0-canary-20251230-fc48ddc", "expo-modules-core": "4.0.0-canary-20251230-fc48ddc", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", "whatwg-url-without-unicode": "8.0.0-3" }, "peerDependencies": { "@expo/dom-webview": "0.2.9-canary-20251230-fc48ddc", "@expo/metro-runtime": "6.2.0-canary-20251230-fc48ddc", "react": "*", "react-native": "*", "react-native-webview": "*" }, "optionalPeers": ["@expo/dom-webview", "@expo/metro-runtime", "react-native-webview"], "bin": { "expo": "bin/cli", "expo-modules-autolinking": "bin/autolinking", "fingerprint": "bin/fingerprint" } }, "sha512-NPj83TmylfKtcb47CetlJuTWB9bsHEYS6z8dFG0HjwsZJo9xg4LYMLGf0HRImK0TwFmohKkXQHdZ0DM6D3Q1+w=="], - - "expo-application": ["expo-application@7.0.9-canary-20251230-fc48ddc", "", { "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc" } }, "sha512-QMv5XMHgTV2uq3oNfXGOVaBDEwRzpVkwGALu7WkE9NmJsFgFAnLS5+EXV0gY52AJDL3IYFX2vi8WdW4uWS7PMA=="], - - "expo-asset": ["expo-asset@12.0.13-canary-20251230-fc48ddc", "", { "dependencies": { "@expo/image-utils": "0.8.9-canary-20251230-fc48ddc", "expo-constants": "18.1.0-canary-20251230-fc48ddc" }, "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc", "react": "*", "react-native": "*" } }, "sha512-FBZzN9f0JSSbW/GbdEtBbr6RjfiOp/FqdqaCIGajYQjikK/pfXjOKJur1NdKBvCRrR55WLxrfj78Ql2p3UviMg=="], - - "expo-auth-session": ["expo-auth-session@7.0.11-canary-20251230-fc48ddc", "", { "dependencies": { "expo-application": "7.0.9-canary-20251230-fc48ddc", "expo-constants": "18.1.0-canary-20251230-fc48ddc", "expo-crypto": "15.0.9-canary-20251230-fc48ddc", "expo-linking": "8.0.12-canary-20251230-fc48ddc", "expo-web-browser": "15.0.11-canary-20251230-fc48ddc", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-wXb6bPFaiTB2QuZiYNoR2TReuWvFozWindIXpLeTtWaRvx7nR7gWAwxUm+xmYNXLlWA7QXhvrxZ9Ufp+5NpJ9g=="], - - "expo-clipboard": ["expo-clipboard@9.0.0-canary-20251230-fc48ddc", "", { "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc", "react": "*", "react-native": "*" } }, "sha512-xCvGGc7d5FMqqVGvdImTDGkxkAJlQHQPdhBqX2gmhPtRGlqIleGgJBrOFQPoCpfrVOW840MVSLI8p0c0updpjQ=="], - - "expo-constants": ["expo-constants@18.1.0-canary-20251230-fc48ddc", "", { "dependencies": { "@expo/config": "12.0.14-canary-20251230-fc48ddc", "@expo/env": "2.0.9-canary-20251230-fc48ddc" }, "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc", "react-native": "*" } }, "sha512-iyMYWluY4vXeRJxwYSyGpJcrjkt+YdtFI9Fj/UNHaylsHfMRVKEVIn/yr0VW7PgIqGSydASQJSQZekP1gifYog=="], - - "expo-crypto": ["expo-crypto@15.0.9-canary-20251230-fc48ddc", "", { "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc" } }, "sha512-CgBkIrPJeEgEVjVIQCFEG0B7TBpagacpa/doTQ2u3roQHqjCzNlb4e3YWMScEimFCn7T3MTiJmMuS+g4vMlbmQ=="], - - "expo-dev-client": ["expo-dev-client@6.1.0-canary-20251230-fc48ddc", "", { "dependencies": { "expo-dev-launcher": "6.1.0-canary-20251230-fc48ddc", "expo-dev-menu": "7.1.0-canary-20251230-fc48ddc", "expo-dev-menu-interface": "2.0.1-canary-20251230-fc48ddc", "expo-manifests": "1.0.11-canary-20251230-fc48ddc", "expo-updates-interface": "2.0.1-canary-20251230-fc48ddc" }, "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc" } }, "sha512-+Vc4hUHak9nzZGAKzYQCIBwmlBj7KFlYSZnb6s+VjhO1ldDGh3B2CGS+sIwMhqQC5XlFQhcu0CSsWxl8WspYxw=="], - - "expo-dev-launcher": ["expo-dev-launcher@6.1.0-canary-20251230-fc48ddc", "", { "dependencies": { "ajv": "^8.11.0", "expo-dev-menu": "7.1.0-canary-20251230-fc48ddc", "expo-manifests": "1.0.11-canary-20251230-fc48ddc" }, "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc" } }, "sha512-wI2AQ9YXn5gFQOxc19v0FwWb/292q8FvaQX9sxkeTu06KUAgTfZWbOFFYaKWIu3oM8ER0hPHwnP5vpssM/BJVw=="], - - "expo-dev-menu": ["expo-dev-menu@7.1.0-canary-20251230-fc48ddc", "", { "dependencies": { "expo-dev-menu-interface": "2.0.1-canary-20251230-fc48ddc" }, "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc" } }, "sha512-IpkBeaRS7xaa1zJqZ0ZoHhHiQ58o/DCTjNo2l6ePai02jO9SA3/RAOI9Q29hYODTBbX6XYShum/oVm1GhncHvg=="], - - "expo-dev-menu-interface": ["expo-dev-menu-interface@2.0.1-canary-20251230-fc48ddc", "", { "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc" } }, "sha512-m3Ciz1xNXMovbawlWatRMWPzb0Eg6HMcnqdbLhK25GNJaSgbIyaMc3LVnmY+ecsrgHbeHLjJh/38otZ+WL63Jg=="], - - "expo-device": ["expo-device@8.0.11-canary-20251230-fc48ddc", "", { "dependencies": { "ua-parser-js": "^0.7.33" }, "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc" } }, "sha512-80Ufafh/YvDzAc5EavplbHJYz67m+EExuR8eVxaBVtOMPPe8O3osxx5qdpCqUmYSNhLPoXX/UcJvYj6pRsgUmw=="], - - "expo-file-system": ["expo-file-system@19.0.22-canary-20251230-fc48ddc", "", { "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc", "react-native": "*" } }, "sha512-AIfJVVi90QrScIZVJS9RTvcMcidMeOf2CPbB9StdUmc+5ob9DXkYcWzNQ366JqPdLwkHpMIO2uPwA+EevFiOhg=="], - - "expo-font": ["expo-font@14.0.10", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-UqyNaaLKRpj4pKAP4HZSLnuDQqueaO5tB1c/NWu5vh1/LF9ulItyyg2kF/IpeOp0DeOLk0GY0HrIXaKUMrwB+Q=="], - - "expo-glass-effect": ["expo-glass-effect@0.2.0-canary-20251230-fc48ddc", "", { "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc", "react": "*", "react-native": "*" } }, "sha512-0HiFsPzY+tEO6wOGWhmst87WaSbILM4dUlH3r0NFv+tM6LJVTnx6xBxzZZvMEOErM1dV2B5oiiGpDzCO3WT21g=="], - - "expo-haptics": ["expo-haptics@15.0.9-canary-20251230-fc48ddc", "", { "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc" } }, "sha512-yzY1/OVDWUUcU33Dj/vumo23Tds250vpaMxaPB0dejGHlBKXM2Q8E72E/iFYl29iVcZ9AHyOsn0o8YqfzYuBDw=="], - - "expo-image": ["expo-image@3.1.0-canary-20251230-fc48ddc", "", { "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-avrbbOzdBgiBGjKsZ8BwGbC/nfLpBtFCZ9E38DAQc8dCMbsgyajJ/SLG0Xc+3XHCkSi2iKsJ/lZ118cKm8nddg=="], - - "expo-json-utils": ["expo-json-utils@0.15.1-canary-20251230-fc48ddc", "", {}, "sha512-hjZo3ARJHR/WjrKUlW61tX347dMNP5yVyvR7EQ4y1CboaNnmj33Ehkhn8lzzTqmz3XD0xIU5lCdLDj4hh4p89w=="], - - "expo-keep-awake": ["expo-keep-awake@15.0.9-canary-20251230-fc48ddc", "", { "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc", "react": "*" } }, "sha512-MDsUnVbE5bpF0pWl2NJUyq1Esjndal/baXVINQz7dyi4P2B4NYDg4M3q/oF0A65bqfU+nMZjvjhoVh8ToJfMig=="], - - "expo-linking": ["expo-linking@8.0.12-canary-20251230-fc48ddc", "", { "dependencies": { "expo-constants": "18.1.0-canary-20251230-fc48ddc", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-Hm6ihRxuEIVg9mRuAcVJG9bYOpC6LIynvsz6Lc0HF7aOcFurYx3Cfidw8FOZa52ftmtLmdLjw/wJpkXjHHHViA=="], - - "expo-manifests": ["expo-manifests@1.0.11-canary-20251230-fc48ddc", "", { "dependencies": { "@expo/config": "12.0.14-canary-20251230-fc48ddc", "expo-json-utils": "0.15.1-canary-20251230-fc48ddc" }, "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc" } }, "sha512-Zpw1hnThSwL5SsXlZbKuNG2x0wIZHiTI7bSu1bsXlvrQ5KxzP095ItoLpjrR6MSGqxkWSQaFjjRnWjALii22BA=="], - - "expo-modules-autolinking": ["expo-modules-autolinking@3.1.0-canary-20251230-fc48ddc", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, "sha512-cMHct3mUNWueHmjS0VLRKfrwoy5D8ooAXESetbu7lQMKY373kVQkYoiND5XRyLyclIop2NNnVE8hU2Myc2aDIg=="], - - "expo-modules-core": ["expo-modules-core@4.0.0-canary-20251230-fc48ddc", "", { "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-diupjWkV0cQGo6A2Lr6ug4WejH+3o2tQZShVgI6A51Wes3754yIRoBvEP9PWr8ZBnUE61aO77MpjzZjX7mu1ig=="], - - "expo-router": ["expo-router@7.0.0-canary-20251230-fc48ddc", "", { "dependencies": { "@expo/metro-runtime": "6.2.0-canary-20251230-fc48ddc", "@expo/schema-utils": "0.1.9-canary-20251230-fc48ddc", "@radix-ui/react-slot": "1.2.0", "@radix-ui/react-tabs": "^1.1.12", "@react-navigation/bottom-tabs": "^7.7.3", "@react-navigation/native": "^7.1.21", "@react-navigation/native-stack": "^7.8.0", "client-only": "^0.0.1", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "expo-server": "1.0.6-canary-20251230-fc48ddc", "expo-symbols": "1.1.0-canary-20251230-fc48ddc", "fast-deep-equal": "^3.1.3", "invariant": "^2.2.4", "nanoid": "^3.3.8", "query-string": "^7.1.3", "react-fast-compare": "^3.2.2", "react-native-is-edge-to-edge": "^1.1.6", "semver": "~7.6.3", "server-only": "^0.0.1", "sf-symbols-typescript": "^2.1.0", "shallowequal": "^1.1.0", "use-latest-callback": "^0.2.1", "vaul": "^1.1.2" }, "peerDependencies": { "@expo/log-box": "0.0.13-canary-20251230-fc48ddc", "@react-navigation/drawer": "^7.7.2", "@testing-library/react-native": ">= 12.0.0", "expo": "55.0.0-canary-20251230-fc48ddc", "expo-constants": "18.1.0-canary-20251230-fc48ddc", "expo-linking": "8.0.12-canary-20251230-fc48ddc", "react": "*", "react-dom": "*", "react-native": "*", "react-native-gesture-handler": "*", "react-native-reanimated": "*", "react-native-safe-area-context": ">= 5.4.0", "react-native-screens": "*", "react-native-web": "*", "react-server-dom-webpack": "~19.0.3 || ~19.1.4 || ~19.2.3" }, "optionalPeers": ["@react-navigation/drawer", "@testing-library/react-native", "react-dom", "react-native-gesture-handler", "react-native-reanimated", "react-native-web", "react-server-dom-webpack"] }, "sha512-KbjVc2+eTBSA2E1cIzb0RD+gpIe0Nok+Qibk9Wp1s1Sa8HVm5jIhKjEG5fvIQDTov+0cA3Vb/nVi3FrhQih4FA=="], - - "expo-secure-store": ["expo-secure-store@15.0.9-canary-20251230-fc48ddc", "", { "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc" } }, "sha512-1TYC2LU5eypfXD9C32IXYiMpwssGXcBVVAyKluDY3iGUSfW7SbZAoEk4bwLgES0Fvwt3t8O6fgfKOsaW+cmD5A=="], - - "expo-server": ["expo-server@1.0.6-canary-20251230-fc48ddc", "", {}, "sha512-YW0GxVbpfupc455TH/lfX9FRgnS+ZpTi9ydENYQ0EaB/2dbmnruD5w/FzixKk/zBNGipZp0ix0OYRo0SPEvaPQ=="], - - "expo-splash-screen": ["expo-splash-screen@31.0.14-canary-20251230-fc48ddc", "", { "dependencies": { "@expo/prebuild-config": "54.0.9-canary-20251230-fc48ddc" }, "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc" } }, "sha512-t6aBi/2u4JjAJ2GlqwO2+OZzwEY3vgV/JVeff0M+cZTM7v7ezaBtbuR/f/43KAvBWduNh+26sPlQC1lMO/pu3w=="], - - "expo-status-bar": ["expo-status-bar@3.0.10-canary-20251230-fc48ddc", "", { "dependencies": { "react-native-is-edge-to-edge": "^1.2.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-Lkbb3e6yyRtkmjpTfwIYbO9XDmhPeSipyJLBa5nADq8eiGf7kXZ1pmaLEelPl05azCPY3109bJzhvbonz86sIg=="], - - "expo-store-review": ["expo-store-review@9.0.10-canary-20251230-fc48ddc", "", { "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc", "react-native": "*" } }, "sha512-n100Y4mTa2EnYbJItbtV8rMYPMimnV9Qgaz44s6g8zk59Jv1Qtv1fsCEXwD1M82QQc6YYmDHkOsN3fbi4ASrEQ=="], - - "expo-symbols": ["expo-symbols@1.1.0-canary-20251230-fc48ddc", "", { "dependencies": { "@expo-google-fonts/material-symbols": "^0.4.1", "sf-symbols-typescript": "^2.0.0" }, "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc", "expo-font": "14.1.0-canary-20251230-fc48ddc", "react": "*", "react-native": "*" } }, "sha512-3cDn97a5NP6bBBHhN/U9/QKZrvqoSUOYtaTPbfH/WBDJC/kM6hzD8/BQBNe7Vt9ygcAh2iFx3w5DcFlxsMyKAw=="], - - "expo-updates-interface": ["expo-updates-interface@2.0.1-canary-20251230-fc48ddc", "", { "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc" } }, "sha512-ERfTb7zR5lAknBGkMs0devAlyFrD3he4SQJtGItIrov99JQRVYoEfCImLYIhzaancLXnHJLpuQtaK0c3mK41FA=="], - - "expo-web-browser": ["expo-web-browser@15.0.11-canary-20251230-fc48ddc", "", { "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc", "react-native": "*" } }, "sha512-MS2Ni67uaZmz+rleU2/NwAQPQd11wVpk8HgRudP16fry15Qc2/v4duwHf5JMomxMOOIA5d6Z8QMAT5fDi4OsdA=="], - - "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], - - "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], - - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - - "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], - - "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], - - "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], - - "fast-redact": ["fast-redact@3.5.0", "", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="], - - "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], - - "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], - - "fb-dotslash": ["fb-dotslash@0.5.8", "", { "bin": { "dotslash": "bin/dotslash" } }, "sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA=="], - - "fb-watchman": ["fb-watchman@2.0.2", "", { "dependencies": { "bser": "2.1.1" } }, "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA=="], - - "fbjs": ["fbjs@3.0.5", "", { "dependencies": { "cross-fetch": "^3.1.5", "fbjs-css-vars": "^1.0.0", "loose-envify": "^1.0.0", "object-assign": "^4.1.0", "promise": "^7.1.1", "setimmediate": "^1.0.5", "ua-parser-js": "^1.0.35" } }, "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg=="], - - "fbjs-css-vars": ["fbjs-css-vars@1.0.2", "", {}, "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ=="], - - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - - "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], - - "filesize": ["filesize@11.0.13", "", {}, "sha512-mYJ/qXKvREuO0uH8LTQJ6v7GsUvVOguqxg2VTwQUkyTPXXRRWPdjuUPVqdBrJQhvci48OHlNGRnux+Slr2Rnvw=="], - - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - - "filter-obj": ["filter-obj@1.1.0", "", {}, "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ=="], - - "finalhandler": ["finalhandler@1.1.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "on-finished": "~2.3.0", "parseurl": "~1.3.3", "statuses": "~1.5.0", "unpipe": "~1.0.0" } }, "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA=="], - - "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - - "firefox-profile": ["firefox-profile@4.7.0", "", { "dependencies": { "adm-zip": "~0.5.x", "fs-extra": "^11.2.0", "ini": "^4.1.3", "minimist": "^1.2.8", "xml2js": "^0.6.2" }, "bin": { "firefox-profile": "lib/cli.js" } }, "sha512-aGApEu5bfCNbA4PGUZiRJAIU6jKmghV2UVdklXAofnNtiDjqYw0czLS46W7IfFqVKgKhFB8Ao2YoNGHY4BoIMQ=="], - - "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], - - "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], - - "flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="], - - "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=="], - - "freeport-async": ["freeport-async@2.0.0", "", {}, "sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ=="], - - "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], - - "fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="], - - "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "fx-runner": ["fx-runner@1.4.0", "", { "dependencies": { "commander": "2.9.0", "shell-quote": "1.7.3", "spawn-sync": "1.0.15", "when": "3.7.7", "which": "1.2.4", "winreg": "0.0.12" }, "bin": { "fx-runner": "bin/fx-runner" } }, "sha512-rci1g6U0rdTg6bAaBboP7XdRu01dzTAaKXxFf+PUqGuCv6Xu7o8NZdY1D5MvKGIjb6EdS1g3VlXOgksir1uGkg=="], - - "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], - - "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - - "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], - - "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], - - "get-package-type": ["get-package-type@0.1.0", "", {}, "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q=="], - - "get-port-please": ["get-port-please@3.2.0", "", {}, "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A=="], - - "get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], - - "getenv": ["getenv@2.0.0", "", {}, "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ=="], - - "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@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=="], - - "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], - - "global-directory": ["global-directory@4.0.1", "", { "dependencies": { "ini": "4.1.1" } }, "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q=="], - - "global-dirs": ["global-dirs@0.1.1", "", { "dependencies": { "ini": "^1.3.4" } }, "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg=="], - - "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], - - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - - "graceful-readlink": ["graceful-readlink@1.0.1", "", {}, "sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w=="], - - "growly": ["growly@1.3.0", "", {}, "sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw=="], - - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - - "hermes-compiler": ["hermes-compiler@0.14.0", "", {}, "sha512-clxa193o+GYYwykWVFfpHduCATz8fR5jvU7ngXpfKHj+E9hr9vjLNtdLSEe8MUbObvVexV3wcyxQ00xTPIrB1Q=="], - - "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], - - "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], - - "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], - - "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], - - "hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="], - - "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], - - "htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="], - - "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], - - "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - - "human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], - - "husky": ["husky@9.0.11", "", { "bin": { "husky": "bin.mjs" } }, "sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw=="], - - "hyphenate-style-name": ["hyphenate-style-name@1.1.0", "", {}, "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw=="], - - "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - - "image-size": ["image-size@1.2.1", "", { "dependencies": { "queue": "6.0.2" }, "bin": { "image-size": "bin/image-size.js" } }, "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw=="], - - "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], - - "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], - - "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], - - "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], - - "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "ini": ["ini@4.1.3", "", {}, "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg=="], - - "inline-style-prefixer": ["inline-style-prefixer@7.0.1", "", { "dependencies": { "css-in-js-utils": "^3.1.0" } }, "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw=="], - - "invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="], - - "is-absolute": ["is-absolute@0.1.7", "", { "dependencies": { "is-relative": "^0.1.0" } }, "sha512-Xi9/ZSn4NFapG8RP98iNPMOeaV3mXPisxKxzKtHVqr3g56j/fBn+yZmnxSVAA8lmZbl2J9b/a4kJvfU3hqQYgA=="], - - "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], - - "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], - - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], - - "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], - - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - - "is-in-ci": ["is-in-ci@1.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg=="], - - "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], - - "is-installed-globally": ["is-installed-globally@1.0.0", "", { "dependencies": { "global-directory": "^4.0.1", "is-path-inside": "^4.0.0" } }, "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ=="], - - "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], - - "is-npm": ["is-npm@6.1.0", "", {}, "sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA=="], - - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - - "is-path-inside": ["is-path-inside@4.0.0", "", {}, "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA=="], - - "is-plain-obj": ["is-plain-obj@2.1.0", "", {}, "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA=="], - - "is-plain-object": ["is-plain-object@2.0.4", "", { "dependencies": { "isobject": "^3.0.1" } }, "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og=="], - - "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], - - "is-primitive": ["is-primitive@3.0.1", "", {}, "sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w=="], - - "is-relative": ["is-relative@0.1.3", "", {}, "sha512-wBOr+rNM4gkAZqoLRJI4myw5WzzIdQosFAAbnvfXP5z1LyzgAI3ivOKehC5KfqlQJZoihVhirgtCBj378Eg8GA=="], - - "is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], - - "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], - - "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], - - "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], - - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - - "isobject": ["isobject@3.0.1", "", {}, "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="], - - "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], - - "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=="], - - "jest-haste-map": ["jest-haste-map@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", "jest-regex-util": "^29.6.3", "jest-util": "^29.7.0", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "walker": "^1.0.8" }, "optionalDependencies": { "fsevents": "^2.3.2" } }, "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA=="], - - "jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="], - - "jest-mock": ["jest-mock@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "jest-util": "^29.7.0" } }, "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw=="], - - "jest-regex-util": ["jest-regex-util@29.6.3", "", {}, "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg=="], - - "jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="], - - "jest-validate": ["jest-validate@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "leven": "^3.1.0", "pretty-format": "^29.7.0" } }, "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw=="], - - "jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="], - - "jimp-compact": ["jimp-compact@0.16.1", "", {}, "sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww=="], - - "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], - - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - - "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - - "jsc-safe-url": ["jsc-safe-url@0.2.4", "", {}, "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q=="], - - "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], - - "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], - - "json-parse-even-better-errors": ["json-parse-even-better-errors@3.0.2", "", {}, "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ=="], - - "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - - "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], - - "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - - "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], - - "jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="], - - "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - - "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], - - "ky": ["ky@1.14.1", "", {}, "sha512-hYje4L9JCmpEQBtudo+v52X5X8tgWXUYyPcxKSuxQNboqufecl9VMWjGiucAFH060AwPXHZuH+WB2rrqfkmafw=="], - - "lan-network": ["lan-network@0.1.7", "", { "bin": { "lan-network": "dist/lan-network-cli.js" } }, "sha512-mnIlAEMu4OyEvUNdzco9xpuB9YVcPkQec+QsgycBCtPZvEqWPCDPfbAE4OJMdBBWpZWtpCn1xw9jJYlwjWI5zQ=="], - - "latest-version": ["latest-version@9.0.0", "", { "dependencies": { "package-json": "^10.0.0" } }, "sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA=="], - - "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], - - "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], - - "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], - - "lighthouse-logger": ["lighthouse-logger@2.0.2", "", { "dependencies": { "debug": "^4.4.1", "marky": "^1.2.2" } }, "sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg=="], - - "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], - - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], - - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], - - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="], - - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="], - - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="], - - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="], - - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="], - - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="], - - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="], - - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="], - - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], - - "lilconfig": ["lilconfig@3.0.0", "", {}, "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g=="], - - "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], - - "linkedom": ["linkedom@0.18.12", "", { "dependencies": { "css-select": "^5.1.0", "cssom": "^0.5.0", "html-escaper": "^3.0.3", "htmlparser2": "^10.0.0", "uhyphen": "^0.2.0" }, "peerDependencies": { "canvas": ">= 2" }, "optionalPeers": ["canvas"] }, "sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q=="], - - "lint-staged": ["lint-staged@15.2.0", "", { "dependencies": { "chalk": "5.3.0", "commander": "11.1.0", "debug": "4.3.4", "execa": "8.0.1", "lilconfig": "3.0.0", "listr2": "8.0.0", "micromatch": "4.0.5", "pidtree": "0.6.0", "string-argv": "0.3.2", "yaml": "2.3.4" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-TFZzUEV00f+2YLaVPWBWGAMq7So6yQx+GG8YRMDeOEIf95Zn5RyiLMsEiX4KTNl9vq/w+NqRJkLA1kPIo15ufQ=="], - - "listr2": ["listr2@8.0.0", "", { "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.0.0", "rfdc": "^1.3.0", "wrap-ansi": "^9.0.0" } }, "sha512-u8cusxAcyqAiQ2RhYvV7kRKNLgUvtObIbhOX2NCXqvp1UU32xIg5CT22ykS2TPKJXZWJwtK3IKLiqAGlGNE+Zg=="], - - "local-pkg": ["local-pkg@1.1.2", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A=="], - - "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - - "lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="], - - "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], - - "lodash.throttle": ["lodash.throttle@4.1.1", "", {}, "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="], - - "log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], - - "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], - - "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], - - "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - - "lucide-react-native": ["lucide-react-native@0.562.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-native": "*", "react-native-svg": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0" } }, "sha512-ZF2ok8SzyUaiCIrLGqYh/6SPs+huVzbZOCv0i411L4+oP3tJgQvvKePiVgWCioa7HsT2xaJZSrdd92cuB2/+ew=="], - - "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - - "magicast": ["magicast@0.3.5", "", { "dependencies": { "@babel/parser": "^7.25.4", "@babel/types": "^7.25.4", "source-map-js": "^1.2.0" } }, "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ=="], - - "make-error": ["make-error@1.3.6", "", {}, "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="], - - "makeerror": ["makeerror@1.0.12", "", { "dependencies": { "tmpl": "1.0.5" } }, "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg=="], - - "many-keys-map": ["many-keys-map@2.0.1", "", {}, "sha512-DHnZAD4phTbZ+qnJdjoNEVU1NecYoSdbOOoVmTDH46AuxDkEVh3MxTVpXq10GtcTC6mndN9dkv1rNfpjRcLnOw=="], - - "marky": ["marky@1.3.0", "", {}, "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ=="], - - "mdn-data": ["mdn-data@2.0.14", "", {}, "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="], - - "memoize-one": ["memoize-one@5.2.1", "", {}, "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="], - - "merge-options": ["merge-options@3.0.4", "", { "dependencies": { "is-plain-obj": "^2.1.0" } }, "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ=="], - - "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], - - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - - "metro": ["metro@0.83.3", "", { "dependencies": { "@babel/code-frame": "^7.24.7", "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "@babel/types": "^7.25.2", "accepts": "^1.3.7", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.32.0", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.83.3", "metro-cache": "0.83.3", "metro-cache-key": "0.83.3", "metro-config": "0.83.3", "metro-core": "0.83.3", "metro-file-map": "0.83.3", "metro-resolver": "0.83.3", "metro-runtime": "0.83.3", "metro-source-map": "0.83.3", "metro-symbolicate": "0.83.3", "metro-transform-plugins": "0.83.3", "metro-transform-worker": "0.83.3", "mime-types": "^2.1.27", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-+rP+/GieOzkt97hSJ0MrPOuAH/jpaS21ZDvL9DJ35QYRDlQcwzcvUlGUf79AnQxq/2NPiS/AULhhM4TKutIt8Q=="], - - "metro-babel-transformer": ["metro-babel-transformer@0.83.3", "", { "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.32.0", "nullthrows": "^1.1.1" } }, "sha512-1vxlvj2yY24ES1O5RsSIvg4a4WeL7PFXgKOHvXTXiW0deLvQr28ExXj6LjwCCDZ4YZLhq6HddLpZnX4dEdSq5g=="], - - "metro-cache": ["metro-cache@0.83.3", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.3" } }, "sha512-3jo65X515mQJvKqK3vWRblxDEcgY55Sk3w4xa6LlfEXgQ9g1WgMh9m4qVZVwgcHoLy0a2HENTPCCX4Pk6s8c8Q=="], - - "metro-cache-key": ["metro-cache-key@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-59ZO049jKzSmvBmG/B5bZ6/dztP0ilp0o988nc6dpaDsU05Cl1c/lRf+yx8m9WW/JVgbmfO5MziBU559XjI5Zw=="], - - "metro-config": ["metro-config@0.83.3", "", { "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.83.3", "metro-cache": "0.83.3", "metro-core": "0.83.3", "metro-runtime": "0.83.3", "yaml": "^2.6.1" } }, "sha512-mTel7ipT0yNjKILIan04bkJkuCzUUkm2SeEaTads8VfEecCh+ltXchdq6DovXJqzQAXuR2P9cxZB47Lg4klriA=="], - - "metro-core": ["metro-core@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.83.3" } }, "sha512-M+X59lm7oBmJZamc96usuF1kusd5YimqG/q97g4Ac7slnJ3YiGglW5CsOlicTR5EWf8MQFxxjDoB6ytTqRe8Hw=="], - - "metro-file-map": ["metro-file-map@0.83.3", "", { "dependencies": { "debug": "^4.4.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "nullthrows": "^1.1.1", "walker": "^1.0.7" } }, "sha512-jg5AcyE0Q9Xbbu/4NAwwZkmQn7doJCKGW0SLeSJmzNB9Z24jBe0AL2PHNMy4eu0JiKtNWHz9IiONGZWq7hjVTA=="], - - "metro-minify-terser": ["metro-minify-terser@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-O2BmfWj6FSfzBLrNCXt/rr2VYZdX5i6444QJU0fFoc7Ljg+Q+iqebwE3K0eTvkI6TRjELsXk1cjU+fXwAR4OjQ=="], - - "metro-resolver": ["metro-resolver@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-0js+zwI5flFxb1ktmR///bxHYg7OLpRpWZlBBruYG8OKYxeMP7SV0xQ/o/hUelrEMdK4LJzqVtHAhBm25LVfAQ=="], - - "metro-runtime": ["metro-runtime@0.83.3", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-JHCJb9ebr9rfJ+LcssFYA2x1qPYuSD/bbePupIGhpMrsla7RCwC/VL3yJ9cSU+nUhU4c9Ixxy8tBta+JbDeZWw=="], - - "metro-source-map": ["metro-source-map@0.83.3", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.3", "nullthrows": "^1.1.1", "ob1": "0.83.3", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-xkC3qwUBh2psVZgVavo8+r2C9Igkk3DibiOXSAht1aYRRcztEZNFtAMtfSB7sdO2iFMx2Mlyu++cBxz/fhdzQg=="], - - "metro-symbolicate": ["metro-symbolicate@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.3", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-F/YChgKd6KbFK3eUR5HdUsfBqVsanf5lNTwFd4Ca7uuxnHgBC3kR/Hba/RGkenR3pZaGNp5Bu9ZqqP52Wyhomw=="], - - "metro-transform-plugins": ["metro-transform-plugins@0.83.3", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" } }, "sha512-eRGoKJU6jmqOakBMH5kUB7VitEWiNrDzBHpYbkBXW7C5fUGeOd2CyqrosEzbMK5VMiZYyOcNFEphvxk3OXey2A=="], - - "metro-transform-worker": ["metro-transform-worker@0.83.3", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "metro": "0.83.3", "metro-babel-transformer": "0.83.3", "metro-cache": "0.83.3", "metro-cache-key": "0.83.3", "metro-minify-terser": "0.83.3", "metro-source-map": "0.83.3", "metro-transform-plugins": "0.83.3", "nullthrows": "^1.1.1" } }, "sha512-Ztekew9t/gOIMZX1tvJOgX7KlSLL5kWykl0Iwu2cL2vKMKVALRl1hysyhUw0vjpAvLFx+Kfq9VLjnHIkW32fPA=="], - - "micromatch": ["micromatch@4.0.5", "", { "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" } }, "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA=="], - - "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - - "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - - "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - - "mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], - - "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], - - "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - - "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - - "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], - - "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], - - "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "multimatch": ["multimatch@6.0.0", "", { "dependencies": { "@types/minimatch": "^3.0.5", "array-differ": "^4.0.0", "array-union": "^3.0.1", "minimatch": "^3.0.4" } }, "sha512-I7tSVxHGPlmPN/enE3mS1aOSo6bWBfls+3HmuEeCUBCE7gWnm3cBXCBkpurzFjVRwC6Kld8lLaZ1Iv5vOcjvcQ=="], - - "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], - - "nano-spawn": ["nano-spawn@1.0.3", "", {}, "sha512-jtpsQDetTnvS2Ts1fiRdci5rx0VYws5jGyC+4IYOTnIQ/wwdf6JdomlHBwqC3bJYOvaKu0C2GSZ1A60anrYpaA=="], - - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "nativewind": ["nativewind@4.2.1", "", { "dependencies": { "comment-json": "^4.2.5", "debug": "^4.3.7", "react-native-css-interop": "0.2.1" }, "peerDependencies": { "tailwindcss": ">3.3.0" } }, "sha512-10uUB2Dlli3MH3NDL5nMHqJHz1A3e/E6mzjTj6cl7hHECClJ7HpE6v+xZL+GXdbwQSnWE+UWMIMsNz7yOQkAJQ=="], - - "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - - "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], - - "nested-error-stacks": ["nested-error-stacks@2.0.1", "", {}, "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A=="], - - "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - - "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], - - "node-forge": ["node-forge@1.3.3", "", {}, "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg=="], - - "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], - - "node-notifier": ["node-notifier@10.0.1", "", { "dependencies": { "growly": "^1.3.0", "is-wsl": "^2.2.0", "semver": "^7.3.5", "shellwords": "^0.1.1", "uuid": "^8.3.2", "which": "^2.0.2" } }, "sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ=="], - - "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], - - "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], - - "npm-package-arg": ["npm-package-arg@11.0.3", "", { "dependencies": { "hosted-git-info": "^7.0.0", "proc-log": "^4.0.0", "semver": "^7.3.5", "validate-npm-package-name": "^5.0.0" } }, "sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw=="], - - "npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="], - - "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], - - "nullthrows": ["nullthrows@1.1.1", "", {}, "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="], - - "nypm": ["nypm@0.6.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g=="], - - "ob1": ["ob1@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-egUxXCDwoWG06NGCS5s5AdcpnumHKJlfd3HH06P3m9TEMwwScfcY35wpQxbm9oHof+dM/lVH9Rfyu1elTVelSA=="], - - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - - "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], - - "ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], - - "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], - - "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], - - "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], - - "on-headers": ["on-headers@1.1.0", "", {}, "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A=="], - - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - - "onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="], - - "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], - - "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], - - "ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="], - - "os-shim": ["os-shim@0.1.3", "", {}, "sha512-jd0cvB8qQ5uVt0lvCIexBaROw1KyKm5sbulg2fWOHjETisuCzWyt+eTZKEMs8v6HwzoGs8xik26jg7eCM6pS+A=="], - - "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], - - "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - - "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], - - "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=="], - - "parse-json": ["parse-json@7.1.1", "", { "dependencies": { "@babel/code-frame": "^7.21.4", "error-ex": "^1.3.2", "json-parse-even-better-errors": "^3.0.0", "lines-and-columns": "^2.0.3", "type-fest": "^3.8.0" } }, "sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw=="], - - "parse-png": ["parse-png@2.1.0", "", { "dependencies": { "pngjs": "^3.3.0" } }, "sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ=="], - - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - - "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], - - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - - "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - - "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=="], - - "perfect-debounce": ["perfect-debounce@2.0.0", "", {}, "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - - "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], - - "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], - - "pino": ["pino@9.7.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg=="], - - "pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="], - - "pino-std-serializers": ["pino-std-serializers@7.0.0", "", {}, "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="], - - "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], - - "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], - - "plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="], - - "pngjs": ["pngjs@3.4.0", "", {}, "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w=="], - - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], - - "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], - - "postcss-js": ["postcss-js@4.1.0", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw=="], - - "postcss-load-config": ["postcss-load-config@4.0.2", "", { "dependencies": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ=="], - - "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], - - "postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], - - "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], - - "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - - "pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="], - - "pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - - "proc-log": ["proc-log@4.2.0", "", {}, "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA=="], - - "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], - - "process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], - - "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], - - "promise": ["promise@8.3.0", "", { "dependencies": { "asap": "~2.0.6" } }, "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg=="], - - "promise-toolbox": ["promise-toolbox@0.21.0", "", { "dependencies": { "make-error": "^1.3.2" } }, "sha512-NV8aTmpwrZv+Iys54sSFOBx3tuVaOBvvrft5PNppnxy9xpU/akHbaWIril22AB22zaPgrgwKdD0KsrM0ptUtpg=="], - - "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], - - "proto-list": ["proto-list@1.2.4", "", {}, "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA=="], - - "publish-browser-extension": ["publish-browser-extension@3.0.3", "", { "dependencies": { "cac": "^6.7.14", "consola": "^3.4.2", "dotenv": "^17.2.3", "form-data-encoder": "^4.1.0", "formdata-node": "^6.0.3", "listr2": "^8.3.3", "ofetch": "^1.4.1", "zod": "^3.25.76 || ^4.0.0" }, "bin": { "publish-extension": "bin/publish-extension.cjs" } }, "sha512-cBINZCkLo7YQaGoUvEHthZ0sDzgJQht28IS+SFMT2omSNhGsPiVNRkWir3qLiTrhGhW9Ci2KVHpA1QAMoBdL2g=="], - - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - - "pupa": ["pupa@3.3.0", "", { "dependencies": { "escape-goat": "^4.0.0" } }, "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA=="], - - "qrcode-terminal": ["qrcode-terminal@0.11.0", "", { "bin": { "qrcode-terminal": "./bin/qrcode-terminal.js" } }, "sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ=="], - - "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], - - "query-string": ["query-string@7.1.3", "", { "dependencies": { "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" } }, "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg=="], - - "queue": ["queue@6.0.2", "", { "dependencies": { "inherits": "~2.0.3" } }, "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA=="], - - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - - "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], - - "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - - "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], - - "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], - - "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], - - "react-devtools-core": ["react-devtools-core@6.1.5", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA=="], - - "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], - - "react-fast-compare": ["react-fast-compare@3.2.2", "", {}, "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="], - - "react-freeze": ["react-freeze@1.0.4", "", { "peerDependencies": { "react": ">=17.0.0" } }, "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA=="], - - "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - - "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=="], - - "react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.2.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q=="], - - "react-native-reanimated": ["react-native-reanimated@4.2.0", "", { "dependencies": { "react-native-is-edge-to-edge": "1.2.1", "semver": "7.7.3" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-worklets": ">=0.7.0" } }, "sha512-frhu5b8/m/VvaMWz48V8RxcsXnE3hrlErQ5chr21MzAeDCpY4X14sQjvm+jvu3aOI+7Cz2atdRpyhhIuqxVaXg=="], - - "react-native-safe-area-context": ["react-native-safe-area-context@5.6.2", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg=="], - - "react-native-screens": ["react-native-screens@4.19.0", "", { "dependencies": { "react-freeze": "^1.0.0", "warn-once": "^0.1.0" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-qSDAO3AL5bti0Ri7KZRSVmWlhDr8MV86N5GruiKVQfEL7Zx2nUi3Dl62lqHUAD/LnDvOPuDDsMHCfIpYSv3hPQ=="], - - "react-native-svg": ["react-native-svg@15.12.1", "", { "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", "warn-once": "0.1.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-vCuZJDf8a5aNC2dlMovEv4Z0jjEUET53lm/iILFnFewa15b4atjVxU6Wirm6O9y6dEsdjDZVD7Q3QM4T1wlI8g=="], - - "react-native-web": ["react-native-web@0.21.2", "", { "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", "fbjs": "^3.0.4", "inline-style-prefixer": "^7.0.1", "memoize-one": "^6.0.0", "nullthrows": "^1.1.1", "postcss-value-parser": "^4.2.0", "styleq": "^0.1.3" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg=="], - - "react-native-worklets": ["react-native-worklets@0.7.1", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "7.27.1", "@babel/plugin-transform-class-properties": "7.27.1", "@babel/plugin-transform-classes": "7.28.4", "@babel/plugin-transform-nullish-coalescing-operator": "7.27.1", "@babel/plugin-transform-optional-chaining": "7.27.1", "@babel/plugin-transform-shorthand-properties": "7.27.1", "@babel/plugin-transform-template-literals": "7.27.1", "@babel/plugin-transform-unicode-regex": "7.27.1", "@babel/preset-typescript": "7.27.1", "convert-source-map": "2.0.0", "semver": "7.7.3" }, "peerDependencies": { "@babel/core": "*", "react": "*", "react-native": "*" } }, "sha512-KNsvR48ULg73QhTlmwPbdJLPsWcyBotrGPsrDRDswb5FYpQaJEThUKc2ncXE4UM5dn/ewLoQHjSjLaKUVPxPhA=="], - - "react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], - - "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], - - "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], - - "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], - - "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], - - "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - - "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], - - "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], - - "regenerate": ["regenerate@1.4.2", "", {}, "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A=="], - - "regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "regenerator-runtime": ["regenerator-runtime@0.13.11", "", {}, "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="], - - "regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "registry-auth-token": ["registry-auth-token@5.1.0", "", { "dependencies": { "@pnpm/npm-conf": "^2.1.0" } }, "sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw=="], - - "registry-url": ["registry-url@6.0.1", "", { "dependencies": { "rc": "1.2.8" } }, "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q=="], - - "regjsgen": ["regjsgen@0.8.0", "", {}, "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q=="], - - "regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - - "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - - "requireg": ["requireg@0.2.2", "", { "dependencies": { "nested-error-stacks": "~2.0.1", "rc": "~1.2.7", "resolve": "~1.7.1" } }, "sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg=="], - - "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - - "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], - - "resolve-global": ["resolve-global@1.0.0", "", { "dependencies": { "global-dirs": "^0.1.1" } }, "sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw=="], - - "resolve-workspace-root": ["resolve-workspace-root@2.0.0", "", {}, "sha512-IsaBUZETJD5WsI11Wt8PKHwaIe45or6pwNc8yflvLJ4DWtImK9kuLoH5kUva/2Mmx/RdIyr4aONNSa2v9LTJsw=="], - - "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], - - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - - "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], - - "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], - - "rollup": ["rollup@4.53.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.3", "@rollup/rollup-android-arm64": "4.53.3", "@rollup/rollup-darwin-arm64": "4.53.3", "@rollup/rollup-darwin-x64": "4.53.3", "@rollup/rollup-freebsd-arm64": "4.53.3", "@rollup/rollup-freebsd-x64": "4.53.3", "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", "@rollup/rollup-linux-arm-musleabihf": "4.53.3", "@rollup/rollup-linux-arm64-gnu": "4.53.3", "@rollup/rollup-linux-arm64-musl": "4.53.3", "@rollup/rollup-linux-loong64-gnu": "4.53.3", "@rollup/rollup-linux-ppc64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-musl": "4.53.3", "@rollup/rollup-linux-s390x-gnu": "4.53.3", "@rollup/rollup-linux-x64-gnu": "4.53.3", "@rollup/rollup-linux-x64-musl": "4.53.3", "@rollup/rollup-openharmony-arm64": "4.53.3", "@rollup/rollup-win32-arm64-msvc": "4.53.3", "@rollup/rollup-win32-ia32-msvc": "4.53.3", "@rollup/rollup-win32-x64-gnu": "4.53.3", "@rollup/rollup-win32-x64-msvc": "4.53.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA=="], - - "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], - - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - - "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - - "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], - - "sax": ["sax@1.4.3", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="], - - "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - - "scule": ["scule@1.3.0", "", {}, "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g=="], - - "semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], - - "send": ["send@0.19.1", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "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-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg=="], - - "serialize-error": ["serialize-error@2.1.0", "", {}, "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw=="], - - "serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="], - - "server-only": ["server-only@0.0.1", "", {}, "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA=="], - - "set-value": ["set-value@4.1.0", "", { "dependencies": { "is-plain-object": "^2.0.4", "is-primitive": "^3.0.1" } }, "sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw=="], - - "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], - - "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - - "sf-symbols-typescript": ["sf-symbols-typescript@2.2.0", "", {}, "sha512-TPbeg0b7ylrswdGCji8FRGFAKuqbpQlLbL8SOle3j1iHSs5Ob5mhvMAxWN2UItOjgALAB5Zp3fmMfj8mbWvXKw=="], - - "shallowequal": ["shallowequal@1.1.0", "", {}, "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="], - - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - - "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], - - "shellwords": ["shellwords@0.1.1", "", {}, "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww=="], - - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - - "simple-plist": ["simple-plist@1.3.1", "", { "dependencies": { "bplist-creator": "0.1.0", "bplist-parser": "0.3.1", "plist": "^3.0.5" } }, "sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw=="], - - "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="], - - "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], - - "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - - "slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], - - "slugify": ["slugify@1.6.6", "", {}, "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw=="], - - "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], - - "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], - - "spawn-sync": ["spawn-sync@1.0.15", "", { "dependencies": { "concat-stream": "^1.4.7", "os-shim": "^0.1.2" } }, "sha512-9DWBgrgYZzNghseho0JOuh+5fg9u6QWhAWa51QC7+U5rCheZ/j1DrEZnyE0RBBRqZ9uEXGPgSSM0nky6burpVw=="], - - "split": ["split@1.0.1", "", { "dependencies": { "through": "2" } }, "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg=="], - - "split-on-first": ["split-on-first@1.1.0", "", {}, "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="], - - "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], - - "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], - - "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], - - "stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="], - - "stacktrace-parser": ["stacktrace-parser@0.1.11", "", { "dependencies": { "type-fest": "^0.7.1" } }, "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg=="], - - "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], - - "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], - - "stream-buffers": ["stream-buffers@2.2.0", "", {}, "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg=="], - - "strict-uri-encode": ["strict-uri-encode@2.0.0", "", {}, "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ=="], - - "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], - - "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=="], - - "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - - "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], - - "structured-headers": ["structured-headers@0.4.1", "", {}, "sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg=="], - - "stubborn-fs": ["stubborn-fs@2.0.0", "", { "dependencies": { "stubborn-utils": "^1.0.1" } }, "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA=="], - - "stubborn-utils": ["stubborn-utils@1.0.2", "", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="], - - "styleq": ["styleq@0.1.3", "", {}, "sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA=="], - - "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], - - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "supports-hyperlinks": ["supports-hyperlinks@2.3.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA=="], - - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - - "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], - - "tailwindcss": ["tailwindcss@3.4.17", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.6", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og=="], - - "tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="], - - "tar": ["tar@7.5.2", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg=="], - - "temp-dir": ["temp-dir@2.0.0", "", {}, "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg=="], - - "terminal-link": ["terminal-link@2.1.1", "", { "dependencies": { "ansi-escapes": "^4.2.1", "supports-hyperlinks": "^2.0.0" } }, "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ=="], - - "terser": ["terser@5.44.1", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw=="], - - "test-exclude": ["test-exclude@6.0.0", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" } }, "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w=="], - - "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], - - "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], - - "thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="], - - "throat": ["throat@5.0.0", "", {}, "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA=="], - - "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], - - "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], - - "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - - "tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="], - - "tmpl": ["tmpl@1.0.5", "", {}, "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw=="], - - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - - "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - - "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], - - "ts-api-utils": ["ts-api-utils@2.3.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-6eg3Y9SF7SsAvGzRHQvvc1skDAhwI4YQ32ui1scxD1Ccr0G5qIIbUBT3pFTKX8kmWIQClHobtUdNuaBgwdfdWg=="], - - "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], - - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - - "type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="], - - "type-fest": ["type-fest@0.7.1", "", {}, "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg=="], - - "typedarray": ["typedarray@0.0.6", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - - "typescript-eslint": ["typescript-eslint@8.50.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.50.1", "@typescript-eslint/parser": "8.50.1", "@typescript-eslint/typescript-estree": "8.50.1", "@typescript-eslint/utils": "8.50.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ=="], - - "ua-parser-js": ["ua-parser-js@0.7.41", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg=="], - - "ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="], - - "uhyphen": ["uhyphen@0.2.0", "", {}, "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA=="], - - "undici": ["undici@6.22.0", "", {}, "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw=="], - - "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "unicode-canonical-property-names-ecmascript": ["unicode-canonical-property-names-ecmascript@2.0.1", "", {}, "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg=="], - - "unicode-match-property-ecmascript": ["unicode-match-property-ecmascript@2.0.0", "", { "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", "unicode-property-aliases-ecmascript": "^2.0.0" } }, "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q=="], - - "unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "unicode-property-aliases-ecmascript": ["unicode-property-aliases-ecmascript@2.2.0", "", {}, "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ=="], - - "unimport": ["unimport@5.5.0", "", { "dependencies": { "acorn": "^8.15.0", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.19", "mlly": "^1.8.0", "pathe": "^2.0.3", "picomatch": "^4.0.3", "pkg-types": "^2.3.0", "scule": "^1.3.0", "strip-literal": "^3.1.0", "tinyglobby": "^0.2.15", "unplugin": "^2.3.10", "unplugin-utils": "^0.3.0" } }, "sha512-/JpWMG9s1nBSlXJAQ8EREFTFy3oy6USFd8T6AoBaw1q2GGcF4R9yp3ofg32UODZlYEO5VD0EWE1RpI9XDWyPYg=="], - - "unique-string": ["unique-string@2.0.0", "", { "dependencies": { "crypto-random-string": "^2.0.0" } }, "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg=="], - - "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - - "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - - "unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], - - "unplugin-utils": ["unplugin-utils@0.3.1", "", { "dependencies": { "pathe": "^2.0.3", "picomatch": "^4.0.3" } }, "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog=="], - - "update-browserslist-db": ["update-browserslist-db@1.2.2", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA=="], - - "update-notifier": ["update-notifier@7.3.1", "", { "dependencies": { "boxen": "^8.0.1", "chalk": "^5.3.0", "configstore": "^7.0.0", "is-in-ci": "^1.0.0", "is-installed-globally": "^1.0.0", "is-npm": "^6.0.0", "latest-version": "^9.0.0", "pupa": "^3.1.0", "semver": "^7.6.3", "xdg-basedir": "^5.1.0" } }, "sha512-+dwUY4L35XFYEzE+OAL3sarJdUioVovq+8f7lcIJ7wnmnYQV5UD1Y/lcwaMSyaQ6Bj3JMj1XSTjZbNLHn/19yA=="], - - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - - "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], - - "use-latest-callback": ["use-latest-callback@0.2.6", "", { "peerDependencies": { "react": ">=16.8" } }, "sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg=="], - - "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], - - "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], - - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - - "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], - - "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], - - "validate-npm-package-name": ["validate-npm-package-name@5.0.1", "", {}, "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ=="], - - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - - "vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="], - - "vite": ["vite@7.2.7", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ=="], - - "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], - - "vlq": ["vlq@1.0.1", "", {}, "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w=="], - - "walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="], - - "warn-once": ["warn-once@0.1.1", "", {}, "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q=="], - - "watchpack": ["watchpack@2.4.4", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA=="], - - "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], - - "web-ext-run": ["web-ext-run@0.2.4", "", { "dependencies": { "@babel/runtime": "7.28.2", "@devicefarmer/adbkit": "3.3.8", "chrome-launcher": "1.2.0", "debounce": "1.2.1", "es6-error": "4.1.1", "firefox-profile": "4.7.0", "fx-runner": "1.4.0", "multimatch": "6.0.0", "node-notifier": "10.0.1", "parse-json": "7.1.1", "pino": "9.7.0", "promise-toolbox": "0.21.0", "set-value": "4.1.0", "source-map-support": "0.5.21", "strip-bom": "5.0.0", "strip-json-comments": "5.0.2", "tmp": "0.2.5", "update-notifier": "7.3.1", "watchpack": "2.4.4", "zip-dir": "2.0.0" } }, "sha512-rQicL7OwuqWdQWI33JkSXKcp7cuv1mJG8u3jRQwx/8aDsmhbTHs9ZRmNYOL+LX0wX8edIEQX8jj4bB60GoXtKA=="], - - "webidl-conversions": ["webidl-conversions@5.0.0", "", {}, "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA=="], - - "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], - - "whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="], - - "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], - - "whatwg-url-without-unicode": ["whatwg-url-without-unicode@8.0.0-3", "", { "dependencies": { "buffer": "^5.4.3", "punycode": "^2.1.1", "webidl-conversions": "^5.0.0" } }, "sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig=="], - - "when": ["when@3.7.7", "", {}, "sha512-9lFZp/KHoqH6bPKjbWqa+3Dg/K/r2v0X/3/G2x4DBGchVS2QX2VXL3cZV994WQVnTM1/PD71Az25nAzryEUugw=="], - - "when-exit": ["when-exit@2.1.5", "", {}, "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg=="], - - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - - "widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="], - - "winreg": ["winreg@0.0.12", "", {}, "sha512-typ/+JRmi7RqP1NanzFULK36vczznSNN8kWVA9vIqXyv8GhghUlwhGp1Xj3Nms1FsPcNnsQrJOR10N58/nQ9hQ=="], - - "wonka": ["wonka@6.3.5", "", {}, "sha512-SSil+ecw6B4/Dm7Pf2sAshKQ5hWFvfyGlfPbEd6A14dOH6VDjrmbY86u6nZvy9omGwwIPFR8V41+of1EezgoUw=="], - - "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - - "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=="], - - "ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], - - "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], - - "wxt": ["wxt@0.20.11", "", { "dependencies": { "@1natsu/wait-element": "^4.1.2", "@aklinker1/rollup-plugin-visualizer": "5.12.0", "@webext-core/fake-browser": "^1.3.2", "@webext-core/isolated-element": "^1.1.2", "@webext-core/match-patterns": "^1.0.3", "@wxt-dev/browser": "^0.1.4", "@wxt-dev/storage": "^1.0.0", "async-mutex": "^0.5.0", "c12": "^3.2.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "ci-info": "^4.3.0", "consola": "^3.4.2", "defu": "^6.1.4", "dotenv": "^17.2.2", "dotenv-expand": "^12.0.3", "esbuild": "^0.25.0", "fast-glob": "^3.3.3", "filesize": "^11.0.2", "fs-extra": "^11.3.1", "get-port-please": "^3.2.0", "giget": "^1.2.3 || ^2.0.0", "hookable": "^5.5.3", "import-meta-resolve": "^4.2.0", "is-wsl": "^3.1.0", "json5": "^2.2.3", "jszip": "^3.10.1", "linkedom": "^0.18.12", "magicast": "^0.3.5", "minimatch": "^10.0.3", "nano-spawn": "^1.0.2", "normalize-path": "^3.0.0", "nypm": "^0.6.1", "ohash": "^2.0.11", "open": "^10.2.0", "ora": "^8.2.0", "perfect-debounce": "^2.0.0", "picocolors": "^1.1.1", "prompts": "^2.4.2", "publish-browser-extension": "^2.3.0 || ^3.0.2", "scule": "^1.3.0", "unimport": "^3.13.1 || ^4.0.0 || ^5.0.0", "vite": "^5.4.19 || ^6.3.4 || ^7.0.0", "vite-node": "^2.1.4 || ^3.1.2", "web-ext-run": "^0.2.4" }, "bin": { "wxt": "bin/wxt.mjs", "wxt-publish-extension": "bin/wxt-publish-extension.cjs" } }, "sha512-DqqHc/5COs8GR21ii99bANXf/mu6zuDpiXFV1YKNsqO5/PvkrCx5arY0aVPL5IATsuacAnNzdj4eMc1qbzS53Q=="], - - "xcode": ["xcode@3.0.1", "", { "dependencies": { "simple-plist": "^1.1.0", "uuid": "^7.0.3" } }, "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA=="], - - "xdg-basedir": ["xdg-basedir@5.1.0", "", {}, "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ=="], - - "xml2js": ["xml2js@0.6.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w=="], - - "xmlbuilder": ["xmlbuilder@14.0.0", "", {}, "sha512-ts+B2rSe4fIckR6iquDjsKbQFK2NlUk6iG5nf14mDEyldgoc2nEKZ3jZWMPTxGQwVgToSjt6VGIho1H8/fNFTg=="], - - "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - - "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], - - "yaml": ["yaml@2.3.4", "", {}, "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA=="], - - "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], - - "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - - "zip-dir": ["zip-dir@2.0.0", "", { "dependencies": { "async": "^3.2.0", "jszip": "^3.2.2" } }, "sha512-uhlsJZWz26FLYXOD6WVuq+fIcZ3aBPGo/cFdiLlv3KNwpa52IF3ISV8fLhQLiqVu5No3VhlqlgthN6gehil1Dg=="], - - "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - - "zod-validation-error": ["zod-validation-error@3.5.4", "", { "peerDependencies": { "zod": "^3.24.4" } }, "sha512-+hEiRIiPobgyuFlEojnqjJnhFvg4r/i3cqgcm67eehZf/WBaK3g6cD02YU9mtdVxZjv8CzCA9n/Rhrs3yAAvAw=="], - - "zustand": ["zustand@5.0.9", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg=="], - - "@aklinker1/rollup-plugin-visualizer/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], - - "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@babel/helper-create-regexp-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@babel/highlight/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=="], - - "@babel/plugin-transform-runtime/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@devicefarmer/adbkit/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], - - "@devicefarmer/adbkit/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], - - "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - - "@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=="], - - "@expo/cli/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "@expo/cli/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/cli/picomatch": ["picomatch@3.0.1", "", {}, "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag=="], - - "@expo/cli/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "@expo/cli/wrap-ansi": ["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=="], - - "@expo/cli/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], - - "@expo/config/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="], - - "@expo/config/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/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=="], - - "@expo/devcert/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - - "@expo/env/dotenv": ["dotenv@16.4.7", "", {}, "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="], - - "@expo/env/dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="], - - "@expo/fingerprint/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/fingerprint/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "@expo/fingerprint/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "@expo/image-utils/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "@expo/json-file/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="], - - "@expo/metro-config/dotenv": ["dotenv@16.4.7", "", {}, "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="], - - "@expo/metro-config/dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="], - - "@expo/metro-config/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/metro-config/hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="], - - "@expo/metro-config/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "@expo/metro-config/postcss": ["postcss@8.4.49", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA=="], - - "@expo/metro-runtime/@expo/log-box": ["@expo/log-box@0.0.13-canary-20251216-3f01dbf", "", { "dependencies": { "@expo/dom-webview": "0.2.9-canary-20251216-3f01dbf", "anser": "^1.4.9", "stacktrace-parser": "^0.1.10" }, "peerDependencies": { "expo": "55.0.0-canary-20251216-3f01dbf", "react": "*", "react-dom": "*", "react-native": "*", "react-native-web": "*" } }, "sha512-7h/9nw26xmhqQyD5Tl4uZvl/iO3ITb58ic8YsL0qZlKCn8oBCRLBOqLpq+9CZMH0Bd3ZU8z0XvbqoAvw0CJHPA=="], - - "@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=="], - - "@istanbuljs/load-nyc-config/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], - - "@jest/environment/@types/node": ["@types/node@25.0.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg=="], - - "@jest/fake-timers/@types/node": ["@types/node@25.0.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg=="], - - "@jest/transform/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - - "@jest/types/@types/node": ["@types/node@25.0.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg=="], - - "@pnpm/network.ca-file/graceful-fs": ["graceful-fs@4.2.10", "", {}, "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="], - - "@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@react-native/babel-plugin-codegen/@react-native/codegen": ["@react-native/codegen@0.81.5", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.29.1", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "yargs": "^17.6.2" } }, "sha512-a2TDA03Up8lpSa9sh5VRGCQDXgCTOyDOFH+aqyinxp1HChG8uk89/G+nkJ9FPd0rqgi25eCTR16TWdS3b+fA6g=="], - - "@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=="], - - "@react-native/dev-middleware/chrome-launcher": ["chrome-launcher@0.15.2", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^1.0.0" }, "bin": { "print-chrome-path": "bin/print-chrome-path.js" } }, "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ=="], - - "@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="], - - "@react-navigation/core/react-is": ["react-is@19.2.3", "", {}, "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA=="], - - "@types/graceful-fs/@types/node": ["@types/node@25.0.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg=="], - - "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "ansi-align/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=="], - - "babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "babel-plugin-syntax-hermes-parser/hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="], - - "babel-preset-expo/babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="], - - "better-opn/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], - - "boxen/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - - "boxen/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], - - "c12/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], - - "c12/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], - - "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "chrome-launcher/@types/node": ["@types/node@25.0.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg=="], - - "chrome-launcher/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - - "chromium-edge-launcher/@types/node": ["@types/node@25.0.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg=="], - - "chromium-edge-launcher/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - - "chromium-edge-launcher/lighthouse-logger": ["lighthouse-logger@1.4.2", "", { "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" } }, "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g=="], - - "cliui/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=="], - - "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "cliui/wrap-ansi": ["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=="], - - "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "compression/negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], - - "compression/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "config-chain/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], - - "connect/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "css-tree/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - - "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - - "dot-prop/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], - - "dotenv-expand/dotenv": ["dotenv@16.4.7", "", {}, "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="], - - "expo/babel-preset-expo": ["babel-preset-expo@54.1.0-canary-20251230-fc48ddc", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/plugin-proposal-decorators": "^7.12.9", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-transform-class-static-block": "^7.27.1", "@babel/plugin-transform-export-namespace-from": "^7.25.9", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", "@react-native/babel-preset": "0.83.1", "babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-native-web": "~0.21.0", "babel-plugin-syntax-hermes-parser": "^0.29.1", "babel-plugin-transform-flow-enums": "^0.0.2", "debug": "^4.3.4", "resolve-from": "^5.0.0" }, "peerDependencies": { "@babel/runtime": "^7.20.0", "expo": "55.0.0-canary-20251230-fc48ddc", "react-refresh": ">=0.14.0 <1.0.0" }, "optionalPeers": ["@babel/runtime", "expo"] }, "sha512-2SMoySepYnhskzwMSjZplBE77FPp0GwjuEgioFgIWLKJ7Lw2P84zoEcQ7uGtiz1W0snVmqfAQtSuLi894BK9Sg=="], - - "expo/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-dev-launcher/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - - "expo-modules-autolinking/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], - - "expo-router/@expo/metro-runtime": ["@expo/metro-runtime@6.2.0-canary-20251230-fc48ddc", "", { "dependencies": { "@expo/log-box": "0.0.13-canary-20251230-fc48ddc", "anser": "^1.4.9", "pretty-format": "^29.7.0", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0" }, "peerDependencies": { "expo": "55.0.0-canary-20251230-fc48ddc", "react": "*", "react-dom": "*", "react-native": "*" }, "optionalPeers": ["react-dom"] }, "sha512-KEMwM5lp1QQw59aS06HLTyPi8SFwkIBRfGQx9U+Vv3U1oP6ylAJLvITSFM+2OBHqEqBHydv3JyZiu4clKiv/Ww=="], - - "expo-symbols/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=="], - - "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "fast-glob/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - - "fbjs/promise": ["promise@7.3.1", "", { "dependencies": { "asap": "~2.0.3" } }, "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg=="], - - "fbjs/ua-parser-js": ["ua-parser-js@1.0.41", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug=="], - - "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "finalhandler/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], - - "finalhandler/on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="], - - "finalhandler/statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="], - - "firefox-profile/xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="], - - "fx-runner/commander": ["commander@2.9.0", "", { "dependencies": { "graceful-readlink": ">= 1.0.0" } }, "sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A=="], - - "fx-runner/shell-quote": ["shell-quote@1.7.3", "", {}, "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw=="], - - "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=="], - - "hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], - - "hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - - "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - - "is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], - - "istanbul-lib-instrument/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "jest-environment-node/@types/node": ["@types/node@25.0.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg=="], - - "jest-haste-map/@types/node": ["@types/node@25.0.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg=="], - - "jest-haste-map/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - - "jest-message-util/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - - "jest-mock/@types/node": ["@types/node@25.0.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg=="], - - "jest-util/@types/node": ["@types/node@25.0.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg=="], - - "jest-util/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], - - "jest-validate/camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], - - "jest-worker/@types/node": ["@types/node@25.0.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg=="], - - "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - - "lint-staged/chalk": ["chalk@5.3.0", "", {}, "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="], - - "lint-staged/debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="], - - "log-symbols/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - - "log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], - - "log-update/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], - - "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - - "metro/ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="], - - "metro/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="], - - "metro/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], - - "metro-babel-transformer/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="], - - "metro-config/yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], - - "metro-file-map/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - - "metro-source-map/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], - - "metro-symbolicate/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], - - "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], - - "node-notifier/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - - "node-notifier/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "npm-package-arg/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], - - "ora/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - - "package-json/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "parse-json/lines-and-columns": ["lines-and-columns@2.0.4", "", {}, "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A=="], - - "parse-json/type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="], - - "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=="], - - "postcss-load-config/yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], - - "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - - "publish-browser-extension/listr2": ["listr2@8.3.3", "", { "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ=="], - - "publish-browser-extension/zod": ["zod@4.1.13", "", {}, "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig=="], - - "rc/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], - - "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=="], - - "react-native-css-interop/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "react-native-reanimated/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "react-native-web/@react-native/normalize-colors": ["@react-native/normalize-colors@0.74.89", "", {}, "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg=="], - - "react-native-web/memoize-one": ["memoize-one@6.0.0", "", {}, "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="], - - "react-native-worklets/@babel/preset-typescript": ["@babel/preset-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ=="], - - "react-native-worklets/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "requireg/resolve": ["resolve@1.7.1", "", { "dependencies": { "path-parse": "^1.0.5" } }, "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw=="], - - "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=="], - - "simple-plist/bplist-parser": ["bplist-parser@0.3.1", "", { "dependencies": { "big-integer": "1.6.x" } }, "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA=="], - - "simple-swizzle/is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="], - - "slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - - "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], - - "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - - "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=="], - - "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], - - "tailwindcss/lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], - - "tailwindcss/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - - "terminal-link/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], - - "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=="], - - "unimport/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - - "unplugin/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - - "unplugin-utils/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - - "update-notifier/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - - "update-notifier/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "vite/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - - "vite/yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], - - "web-ext-run/@babel/runtime": ["@babel/runtime@7.28.2", "", {}, "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA=="], - - "web-ext-run/strip-json-comments": ["strip-json-comments@5.0.2", "", {}, "sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g=="], - - "whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - - "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=="], - - "wxt/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], - - "xcode/uuid": ["uuid@7.0.3", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg=="], - - "xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], - - "yargs/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=="], - - "@aklinker1/rollup-plugin-visualizer/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], - - "@aklinker1/rollup-plugin-visualizer/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - - "@babel/highlight/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], - - "@babel/highlight/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], - - "@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=="], - - "@expo/cli/ora/cli-cursor": ["cli-cursor@2.1.0", "", { "dependencies": { "restore-cursor": "^2.0.0" } }, "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw=="], - - "@expo/cli/ora/log-symbols": ["log-symbols@2.2.0", "", { "dependencies": { "chalk": "^2.0.1" } }, "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg=="], - - "@expo/cli/ora/strip-ansi": ["strip-ansi@5.2.0", "", { "dependencies": { "ansi-regex": "^4.1.0" } }, "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA=="], - - "@expo/cli/wrap-ansi/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=="], - - "@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=="], - - "@expo/metro-runtime/@expo/log-box/@expo/dom-webview": ["@expo/dom-webview@0.2.9-canary-20251216-3f01dbf", "", { "peerDependencies": { "expo": "55.0.0-canary-20251216-3f01dbf", "react": "*", "react-native": "*" } }, "sha512-S7OPcDu+X1XFTaOUVu5lFum8ZUvtySvu46MNnymNuN3+pNQjrpb/5ATnHbS5FLh72OW04WJdZcHOb7ZMlMmmZg=="], - - "@expo/package-manager/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=="], - - "@expo/package-manager/ora/cli-cursor": ["cli-cursor@2.1.0", "", { "dependencies": { "restore-cursor": "^2.0.0" } }, "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw=="], - - "@expo/package-manager/ora/log-symbols": ["log-symbols@2.2.0", "", { "dependencies": { "chalk": "^2.0.1" } }, "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg=="], - - "@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=="], - - "@jest/environment/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "@jest/fake-timers/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "@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=="], - - "@react-native/dev-middleware/chrome-launcher/@types/node": ["@types/node@25.0.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg=="], - - "@react-native/dev-middleware/chrome-launcher/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - - "@react-native/dev-middleware/chrome-launcher/lighthouse-logger": ["lighthouse-logger@1.4.2", "", { "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" } }, "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g=="], - - "@react-native/dev-middleware/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - - "@types/graceful-fs/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - - "ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "babel-plugin-syntax-hermes-parser/hermes-parser/hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="], - - "better-opn/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], - - "better-opn/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - - "c12/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], - - "chrome-launcher/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "chromium-edge-launcher/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "chromium-edge-launcher/lighthouse-logger/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "connect/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "expo-dev-launcher/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - - "expo/babel-preset-expo/@react-native/babel-preset": ["@react-native/babel-preset@0.83.1", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-transform-arrow-functions": "^7.24.7", "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-computed-properties": "^7.24.7", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-for-of": "^7.24.7", "@babel/plugin-transform-function-name": "^7.25.1", "@babel/plugin-transform-literals": "^7.25.2", "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", "@babel/plugin-transform-numeric-separator": "^7.24.7", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-react-display-name": "^7.24.7", "@babel/plugin-transform-react-jsx": "^7.25.2", "@babel/plugin-transform-react-jsx-self": "^7.24.7", "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-shorthand-properties": "^7.24.7", "@babel/plugin-transform-spread": "^7.24.7", "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/template": "^7.25.0", "@react-native/babel-plugin-codegen": "0.83.1", "babel-plugin-syntax-hermes-parser": "0.32.0", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" } }, "sha512-xI+tbsD4fXcI6PVU4sauRCh0a5fuLQC849SINmU2J5wP8kzKu4Ye0YkGjUW3mfGrjaZcjkWmF6s33jpyd3gdTw=="], - - "expo/babel-preset-expo/babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="], - - "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "firefox-profile/xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], - - "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=="], - - "jest-mock/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "jest-util/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "jest-worker/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "lint-staged/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], - - "log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - - "log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], - - "metro-babel-transformer/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="], - - "metro/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="], - - "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], - - "react-native-css-interop/lightningcss/detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], - - "react-native-css-interop/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.27.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gl/lqIXY+d+ySmMbgDf0pgaWSqrWYxVHoc88q+Vhf2YNzZ8DwoRzGt5NZDVqqIW5ScpSnmmjcgXP87Dn2ylSSQ=="], - - "react-native-css-interop/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.27.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-0+mZa54IlcNAoQS9E0+niovhyjjQWEMrwW0p2sSdLRhLDc8LMQ/b67z7+B5q4VmjYCMSfnFi3djAAQFIDuj/Tg=="], - - "react-native-css-interop/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.27.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-n1sEf85fePoU2aDN2PzYjoI8gbBqnmLGEhKq7q0DKLj0UTVmOTwDC7PtLcy/zFxzASTSBlVQYJUhwIStQMIpRA=="], - - "react-native-css-interop/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.27.0", "", { "os": "linux", "cpu": "arm" }, "sha512-MUMRmtdRkOkd5z3h986HOuNBD1c2lq2BSQA1Jg88d9I7bmPGx08bwGcnB75dvr17CwxjxD6XPi3Qh8ArmKFqCA=="], - - "react-native-css-interop/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.27.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-cPsxo1QEWq2sfKkSq2Bq5feQDHdUEwgtA9KaB27J5AX22+l4l0ptgjMZZtYtUnteBofjee+0oW1wQ1guv04a7A=="], - - "react-native-css-interop/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.27.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-rCGBm2ax7kQ9pBSeITfCW9XSVF69VX+fm5DIpvDZQl4NnQoMQyRwhZQm9pd59m8leZ1IesRqWk2v/DntMo26lg=="], - - "react-native-css-interop/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.27.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Dk/jovSI7qqhJDiUibvaikNKI2x6kWPN79AQiD/E/KeQWMjdGe9kw51RAgoWFDi0coP4jinaH14Nrt/J8z3U4A=="], - - "react-native-css-interop/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.27.0", "", { "os": "linux", "cpu": "x64" }, "sha512-QKjTxXm8A9s6v9Tg3Fk0gscCQA1t/HMoF7Woy1u68wCk5kS4fR+q3vXa1p3++REW784cRAtkYKrPy6JKibrEZA=="], - - "react-native-css-interop/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.27.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-/wXegPS1hnhkeG4OXQKEMQeJd48RDC3qdh+OA8pCuOPCyvnm/yEayrJdJVqzBsqpy1aJklRCVxscpFur80o6iQ=="], - - "react-native-css-interop/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.27.0", "", { "os": "win32", "cpu": "x64" }, "sha512-/OJLj94Zm/waZShL8nB5jsNj3CfNATLCTyFxZyouilfTmSoLDX7VlVAmhPHoZWVFp4vdmoiEbPEYC8HID3m6yw=="], - - "react-native/babel-plugin-syntax-hermes-parser/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="], - - "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "serve-static/send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "serve-static/send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], - - "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=="], - - "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@babel/highlight/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - - "@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=="], - - "@expo/cli/ora/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], - - "@expo/cli/ora/cli-cursor/restore-cursor": ["restore-cursor@2.0.0", "", { "dependencies": { "onetime": "^2.0.0", "signal-exit": "^3.0.2" } }, "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q=="], - - "@expo/cli/ora/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="], - - "@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=="], - - "@expo/package-manager/ora/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], - - "@expo/package-manager/ora/cli-cursor/restore-cursor": ["restore-cursor@2.0.0", "", { "dependencies": { "onetime": "^2.0.0", "signal-exit": "^3.0.2" } }, "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q=="], - - "@expo/package-manager/ora/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="], - - "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], - - "@react-native/babel-plugin-codegen/@react-native/codegen/hermes-parser/hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="], - - "@react-native/dev-middleware/chrome-launcher/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "@react-native/dev-middleware/chrome-launcher/lighthouse-logger/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "chromium-edge-launcher/lighthouse-logger/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "expo/babel-preset-expo/@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=="], - - "expo/babel-preset-expo/@react-native/babel-preset/@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.83.1", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@react-native/codegen": "0.83.1" } }, "sha512-VPj8O3pG1ESjZho9WVKxqiuryrotAECPHGF5mx46zLUYNTWR5u9OMUXYk7LeLy+JLWdGEZ2Gn3KoXeFZbuqE+g=="], - - "expo/babel-preset-expo/@react-native/babel-preset/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/babel-plugin-syntax-hermes-parser/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="], - - "serve-static/send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "@babel/highlight/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - - "@expo/cli/ora/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - - "@expo/cli/ora/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], - - "@expo/cli/ora/cli-cursor/restore-cursor/onetime": ["onetime@2.0.1", "", { "dependencies": { "mimic-fn": "^1.0.0" } }, "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ=="], - - "@expo/cli/ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - - "@expo/package-manager/ora/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - - "@expo/package-manager/ora/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], - - "@expo/package-manager/ora/cli-cursor/restore-cursor/onetime": ["onetime@2.0.1", "", { "dependencies": { "mimic-fn": "^1.0.0" } }, "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ=="], - - "@expo/package-manager/ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - - "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], - - "@react-native/dev-middleware/chrome-launcher/lighthouse-logger/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "expo/babel-preset-expo/@react-native/babel-preset/babel-plugin-syntax-hermes-parser/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="], - - "@expo/cli/ora/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - - "@expo/cli/ora/cli-cursor/restore-cursor/onetime/mimic-fn": ["mimic-fn@1.2.0", "", {}, "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="], - - "@expo/package-manager/ora/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - - "@expo/package-manager/ora/cli-cursor/restore-cursor/onetime/mimic-fn": ["mimic-fn@1.2.0", "", {}, "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="], - - "expo/babel-preset-expo/@react-native/babel-preset/babel-plugin-syntax-hermes-parser/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="], - } -} diff --git a/companion/components.json b/companion/components.json deleted file mode 100644 index a45eb48ecf..0000000000 --- a/companion/components.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": false, - "tsx": true, - "tailwind": { - "config": "tailwind.config.js", - "css": "global.css", - "baseColor": "neutral", - "cssVariables": true - }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - } -} diff --git a/companion/components/AppPressable.tsx b/companion/components/AppPressable.tsx deleted file mode 100644 index 8daba79044..0000000000 --- a/companion/components/AppPressable.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import * as Haptics from "expo-haptics"; -import React, { useCallback } from "react"; -import type { GestureResponderEvent, PressableProps, ViewStyle } from "react-native"; -import { Pressable } from "react-native"; -import Animated, { - type SharedValue, - useAnimatedStyle, - useSharedValue, - withTiming, -} from "react-native-reanimated"; - -const AnimatedPressable = Animated.createAnimatedComponent(Pressable); - -type AppPressableProps = PressableProps & { - className?: string; - style?: ViewStyle; - enableHaptics?: boolean; - hapticStyle?: Haptics.ImpactFeedbackStyle; - /** @deprecated Use the built-in animation instead. Kept for backward compatibility. */ - activeOpacity?: number; -}; - -function usePressAnimations(activeOpacity: number) { - const opacity = useSharedValue(1); - const scale = useSharedValue(1); - - const animatedStyle = useAnimatedStyle(() => ({ - opacity: opacity.value, - transform: [{ scale: scale.value }], - })); - - const animatePressIn = useCallback( - (op: SharedValue, sc: SharedValue) => { - op.value = withTiming(activeOpacity, { duration: 100 }); - sc.value = withTiming(0.98, { duration: 100 }); - }, - [activeOpacity] - ); - - const animatePressOut = useCallback((op: SharedValue, sc: SharedValue) => { - op.value = withTiming(1, { duration: 100 }); - sc.value = withTiming(1, { duration: 100 }); - }, []); - - return { animatedStyle, animatePressIn, animatePressOut, opacity, scale }; -} - -export const AppPressable = React.forwardRef, AppPressableProps>( - ( - { - className, - style, - enableHaptics = true, - hapticStyle = Haptics.ImpactFeedbackStyle.Light, - activeOpacity = 0.7, - onPressIn, - onPressOut, - onPress, - ...props - }, - ref - ) => { - const { animatedStyle, animatePressIn, animatePressOut, opacity, scale } = - usePressAnimations(activeOpacity); - - const handlePressIn = useCallback( - (event: GestureResponderEvent) => { - animatePressIn(opacity, scale); - onPressIn?.(event); - }, - [animatePressIn, onPressIn, opacity, scale] - ); - - const handlePressOut = useCallback( - (event: GestureResponderEvent) => { - animatePressOut(opacity, scale); - onPressOut?.(event); - }, - [animatePressOut, onPressOut, opacity, scale] - ); - - const handlePress = useCallback( - (event: GestureResponderEvent) => { - if (enableHaptics) { - Haptics.impactAsync(hapticStyle); - } - onPress?.(event); - }, - [enableHaptics, hapticStyle, onPress] - ); - - return ( - - ); - } -); - -AppPressable.displayName = "AppPressable"; diff --git a/companion/components/AuthTokenProvider.tsx b/companion/components/AuthTokenProvider.tsx deleted file mode 100644 index e5c3fe1fa9..0000000000 --- a/companion/components/AuthTokenProvider.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { useEffect } from "react"; -import { useAuth } from "@/contexts/AuthContext"; -import { CalComAPIService } from "@/services/calcom"; - -export function AuthTokenProvider({ children }: { children: React.ReactNode }) { - const { accessToken, isAuthenticated, isWebSession: _isWebSession } = useAuth(); - - useEffect(() => { - if (isAuthenticated) { - if (accessToken) { - CalComAPIService.setAccessToken(accessToken); - } - } else { - CalComAPIService.clearAuth(); - } - }, [isAuthenticated, accessToken]); - - return <>{children}; -} diff --git a/companion/components/BookingActionsModal.ios.tsx b/companion/components/BookingActionsModal.ios.tsx deleted file mode 100644 index ee775f232f..0000000000 --- a/companion/components/BookingActionsModal.ios.tsx +++ /dev/null @@ -1,341 +0,0 @@ -/** - * BookingActionsModal Component - iOS Implementation - * - * iOS-specific modal for booking actions with Glass UI styling. - * This provides a native iOS action sheet experience with Glass UI design. - */ - -import { Ionicons } from "@expo/vector-icons"; -import { ScrollView, Text, TouchableOpacity, useColorScheme, View } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import type { Booking } from "@/services/calcom"; -import type { BookingActionsResult } from "@/utils/booking-actions"; -import { FullScreenModal } from "./FullScreenModal"; -import { getColors } from "@/constants/colors"; - -export interface BookingActionsModalProps { - visible: boolean; - onClose: () => void; - booking: Booking | null; - actions: BookingActionsResult; - onReschedule: () => void; - onRequestReschedule?: () => void; - onEditLocation: () => void; - onAddGuests: () => void; - onViewRecordings: () => void; - onMeetingSessionDetails: () => void; - onMarkNoShow: () => void; - onReportBooking: () => void; - onCancelBooking: () => void; -} - -// Icon mapping for actions -const ACTION_ICONS: Record = { - reschedule: "calendar", - rescheduleRequest: "send", - changeLocation: "location", - addGuests: "person-add", - viewRecordings: "videocam", - meetingSessionDetails: "information-circle", - markNoShow: "eye-off", - report: "flag", - cancel: "close-circle", -}; - -interface ActionButtonProps { - icon: keyof typeof Ionicons.glyphMap; - label: string; - onPress: () => void; - visible: boolean; - enabled: boolean; - isDanger?: boolean; - isLast?: boolean; -} - -function ActionButton({ - icon, - label, - onPress, - visible, - enabled, - isDanger = false, - isLast = false, -}: ActionButtonProps) { - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - - if (!visible) return null; - - const iconColor = !enabled ? "#C7C7CC" : isDanger ? theme.destructive : "#007AFF"; - const textColor = !enabled ? "#C7C7CC" : isDanger ? theme.destructive : theme.text; - - return ( - { - if (!enabled) return; - onPress(); - }} - disabled={!enabled} - className={`flex-row items-center px-4 py-3.5 active:bg-[#F8F9FA] dark:active:bg-[#262626] ${ - !isLast ? "border-b border-gray-100 dark:border-[#4D4D4D]" : "" - }`} - activeOpacity={0.7} - > - - - - - {label} - - {!enabled && ( - - Unavailable - - )} - - ); -} - -interface SectionHeaderProps { - title: string; -} - -function SectionHeader({ title }: SectionHeaderProps) { - return ( - - - {title} - - - ); -} - -export function BookingActionsModal({ - visible, - onClose, - booking, - actions, - onReschedule, - onRequestReschedule, - onEditLocation, - onAddGuests, - onViewRecordings, - onMeetingSessionDetails, - onMarkNoShow, - onReportBooking, - onCancelBooking, -}: BookingActionsModalProps) { - const insets = useSafeAreaInsets(); - - if (!booking) return null; - - const handleAction = (action: () => void) => { - onClose(); - action(); - }; - - // Check if any edit event actions are visible - const hasEditEventActions = - actions.reschedule.visible || - actions.rescheduleRequest.visible || - actions.changeLocation.visible || - actions.addGuests.visible; - - // Check if any after event actions are visible - const hasAfterEventActions = - actions.viewRecordings.visible || - actions.meetingSessionDetails.visible || - actions.markNoShow.visible; - - // Define edit event actions - const editEventActions = [ - { - key: "reschedule", - icon: ACTION_ICONS.reschedule, - label: "Reschedule Booking", - onPress: () => handleAction(onReschedule), - visible: actions.reschedule.visible, - enabled: actions.reschedule.enabled, - }, - ...(onRequestReschedule - ? [ - { - key: "rescheduleRequest", - icon: ACTION_ICONS.rescheduleRequest, - label: "Request Reschedule", - onPress: () => handleAction(onRequestReschedule), - visible: actions.rescheduleRequest.visible, - enabled: actions.rescheduleRequest.enabled, - }, - ] - : []), - { - key: "changeLocation", - icon: ACTION_ICONS.changeLocation, - label: "Edit Location", - onPress: () => handleAction(onEditLocation), - visible: actions.changeLocation.visible, - enabled: actions.changeLocation.enabled, - }, - { - key: "addGuests", - icon: ACTION_ICONS.addGuests, - label: "Add Guests", - onPress: () => handleAction(onAddGuests), - visible: actions.addGuests.visible, - enabled: actions.addGuests.enabled, - }, - ].filter((action) => action.visible); - - // Define after event actions - const afterEventActions = [ - { - key: "viewRecordings", - icon: ACTION_ICONS.viewRecordings, - label: "View Recordings", - onPress: () => handleAction(onViewRecordings), - visible: actions.viewRecordings.visible, - enabled: actions.viewRecordings.enabled, - }, - { - key: "meetingSessionDetails", - icon: ACTION_ICONS.meetingSessionDetails, - label: "Meeting Session Details", - onPress: () => handleAction(onMeetingSessionDetails), - visible: actions.meetingSessionDetails.visible, - enabled: actions.meetingSessionDetails.enabled, - }, - { - key: "markNoShow", - icon: ACTION_ICONS.markNoShow, - label: "Mark as No-Show", - onPress: () => handleAction(onMarkNoShow), - visible: actions.markNoShow.visible, - enabled: actions.markNoShow.enabled, - }, - ].filter((action) => action.visible); - - // Define danger zone actions - const dangerZoneActions = [ - { - key: "report", - icon: ACTION_ICONS.report, - label: "Report Booking", - onPress: () => handleAction(onReportBooking), - visible: true, - enabled: true, - isDanger: true, - }, - { - key: "cancel", - icon: ACTION_ICONS.cancel, - label: "Cancel Event", - onPress: () => handleAction(onCancelBooking), - visible: actions.cancel.visible, - enabled: actions.cancel.enabled, - isDanger: true, - }, - ].filter((action) => action.visible); - - return ( - - - e.stopPropagation()} - style={{ paddingBottom: insets.bottom || 16 }} - > - {/* Actions Card */} - - {/* Booking Title Header */} - - - {booking.title} - - - - - {/* Edit Event Section */} - {hasEditEventActions && ( - <> - - {editEventActions.map((action, index) => ( - - ))} - - )} - - {/* After Event Section */} - {hasAfterEventActions && ( - <> - - {afterEventActions.map((action, index) => ( - - ))} - - )} - - {/* Danger Zone Section */} - {dangerZoneActions.length > 0 && ( - <> - - {dangerZoneActions.map((action, index) => ( - - ))} - - )} - - - - {/* Cancel Button */} - - - Cancel - - - - - - ); -} diff --git a/companion/components/BookingActionsModal.tsx b/companion/components/BookingActionsModal.tsx deleted file mode 100644 index e759665220..0000000000 --- a/companion/components/BookingActionsModal.tsx +++ /dev/null @@ -1,340 +0,0 @@ -/** - * BookingActionsModal Component - Android/Web Implementation - * - * A reusable modal component for booking actions that can be used in both - * the bookings list screen and the booking detail screen. - * - * This component uses the centralized action gating utility for consistent - * action visibility and enabled state across the app. - * - * Note: iOS uses BookingActionsModal.ios.tsx with native Glass UI styling. - */ - -import { Ionicons } from "@expo/vector-icons"; -import { ScrollView, Text, TouchableOpacity, useColorScheme, View } from "react-native"; -import { getColors } from "@/constants/colors"; -import type { Booking } from "@/services/calcom"; -import type { BookingActionsResult } from "@/utils/booking-actions"; -import { FullScreenModal } from "./FullScreenModal"; - -export interface BookingActionsModalProps { - visible: boolean; - onClose: () => void; - booking: Booking | null; - actions: BookingActionsResult; - onReschedule: () => void; - onRequestReschedule?: () => void; - onEditLocation: () => void; - onAddGuests: () => void; - onViewRecordings: () => void; - onMeetingSessionDetails: () => void; - onMarkNoShow: () => void; - onReportBooking: () => void; - onCancelBooking: () => void; -} - -// Icon mapping for actions -const ACTION_ICONS: Record = { - reschedule: "calendar-outline", - rescheduleRequest: "send-outline", - changeLocation: "location-outline", - addGuests: "person-add-outline", - viewRecordings: "videocam-outline", - meetingSessionDetails: "information-circle-outline", - markNoShow: "eye-off-outline", - report: "flag-outline", - cancel: "close-circle-outline", -}; - -interface ActionButtonProps { - icon: keyof typeof Ionicons.glyphMap; - label: string; - onPress: () => void; - visible: boolean; - enabled: boolean; - isDanger?: boolean; - isLast?: boolean; -} - -function ActionButton({ - icon, - label, - onPress, - visible, - enabled, - isDanger = false, - isLast = false, -}: ActionButtonProps) { - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const theme = getColors(isDark); - - if (!visible) return null; - const iconColor = !enabled ? "#D1D5DB" : isDanger ? theme.destructive : "#6B7280"; - const textColor = !enabled ? "#D1D5DB" : isDanger ? theme.destructive : "#111827"; - - return ( - { - if (!enabled) return; - onPress(); - }} - disabled={!enabled} - className={`flex-row items-center px-4 py-3 active:bg-gray-50 dark:active:bg-[#262626] ${ - !isLast ? "border-b border-gray-100 dark:border-[#4D4D4D]" : "" - }`} - activeOpacity={0.7} - > - - - - - {label} - - {!enabled && ( - - Unavailable - - )} - - ); -} - -interface SectionHeaderProps { - title: string; -} - -function SectionHeader({ title }: SectionHeaderProps) { - return ( - - - {title} - - - ); -} - -export function BookingActionsModal({ - visible, - onClose, - booking, - actions, - onReschedule, - onRequestReschedule, - onEditLocation, - onAddGuests, - onViewRecordings, - onMeetingSessionDetails, - onMarkNoShow, - onReportBooking, - onCancelBooking, -}: BookingActionsModalProps) { - if (!booking) return null; - - const handleAction = (action: () => void) => { - onClose(); - action(); - }; - - // Check if any edit event actions are visible - const hasEditEventActions = - actions.reschedule.visible || - actions.rescheduleRequest.visible || - actions.changeLocation.visible || - actions.addGuests.visible; - - // Check if any after event actions are visible - const hasAfterEventActions = - actions.viewRecordings.visible || - actions.meetingSessionDetails.visible || - actions.markNoShow.visible; - - // Define edit event actions - const editEventActions = [ - { - key: "reschedule", - icon: ACTION_ICONS.reschedule, - label: "Reschedule Booking", - onPress: () => handleAction(onReschedule), - visible: actions.reschedule.visible, - enabled: actions.reschedule.enabled, - }, - ...(onRequestReschedule - ? [ - { - key: "rescheduleRequest", - icon: ACTION_ICONS.rescheduleRequest, - label: "Request Reschedule", - onPress: () => handleAction(onRequestReschedule), - visible: actions.rescheduleRequest.visible, - enabled: actions.rescheduleRequest.enabled, - }, - ] - : []), - { - key: "changeLocation", - icon: ACTION_ICONS.changeLocation, - label: "Edit Location", - onPress: () => handleAction(onEditLocation), - visible: actions.changeLocation.visible, - enabled: actions.changeLocation.enabled, - }, - { - key: "addGuests", - icon: ACTION_ICONS.addGuests, - label: "Add Guests", - onPress: () => handleAction(onAddGuests), - visible: actions.addGuests.visible, - enabled: actions.addGuests.enabled, - }, - ].filter((action) => action.visible); - - // Define after event actions - const afterEventActions = [ - { - key: "viewRecordings", - icon: ACTION_ICONS.viewRecordings, - label: "View Recordings", - onPress: () => handleAction(onViewRecordings), - visible: actions.viewRecordings.visible, - enabled: actions.viewRecordings.enabled, - }, - { - key: "meetingSessionDetails", - icon: ACTION_ICONS.meetingSessionDetails, - label: "Meeting Session Details", - onPress: () => handleAction(onMeetingSessionDetails), - visible: actions.meetingSessionDetails.visible, - enabled: actions.meetingSessionDetails.enabled, - }, - { - key: "markNoShow", - icon: ACTION_ICONS.markNoShow, - label: "Mark as No-Show", - onPress: () => handleAction(onMarkNoShow), - visible: actions.markNoShow.visible, - enabled: actions.markNoShow.enabled, - }, - ].filter((action) => action.visible); - - // Define danger zone actions - const dangerZoneActions = [ - { - key: "report", - icon: ACTION_ICONS.report, - label: "Report Booking", - onPress: () => handleAction(onReportBooking), - visible: true, - enabled: true, - isDanger: true, - }, - { - key: "cancel", - icon: ACTION_ICONS.cancel, - label: "Cancel Event", - onPress: () => handleAction(onCancelBooking), - visible: actions.cancel.visible, - enabled: actions.cancel.enabled, - isDanger: true, - }, - ].filter((action) => action.visible); - - return ( - - - e.stopPropagation()} - > - {/* Actions Card */} - - {/* Booking Title Header */} - - - {booking.title} - - - - - {/* Edit Event Section */} - {hasEditEventActions && ( - <> - - {editEventActions.map((action, index) => ( - - ))} - - )} - - {/* After Event Section */} - {hasAfterEventActions && ( - <> - - {afterEventActions.map((action, index) => ( - - ))} - - )} - - {/* Danger Zone Section */} - {dangerZoneActions.length > 0 && ( - <> - - {dangerZoneActions.map((action, index) => ( - - ))} - - )} - - - - {/* Cancel Button */} - - - - Cancel - - - - - - - ); -} diff --git a/companion/components/CacheStatusIndicator.tsx b/companion/components/CacheStatusIndicator.tsx deleted file mode 100644 index 36e2cb4525..0000000000 --- a/companion/components/CacheStatusIndicator.tsx +++ /dev/null @@ -1,145 +0,0 @@ -/** - * CacheStatusIndicator Component - * - * A subtle, non-intrusive component that displays cache status information: - * - "Last updated: X minutes ago" when online - * - "Offline - showing cached data" when offline - * - * This component is designed to be placed in screen headers or footers - * to provide users with transparency about data freshness. - */ - -import { Ionicons } from "@expo/vector-icons"; -import { StyleSheet, Text, View } from "react-native"; -import { useQueryContext } from "@/contexts/QueryContext"; - -/** - * Props for the CacheStatusIndicator component - */ -interface CacheStatusIndicatorProps { - /** Timestamp when the data was last fetched (from query.dataUpdatedAt) */ - dataUpdatedAt?: number; - /** Whether data is currently being fetched */ - isFetching?: boolean; - /** Optional custom style */ - style?: object; - /** Show compact version (icon only when online) */ - compact?: boolean; -} - -/** - * Format the time difference in a human-readable way - */ -const formatTimeAgo = (timestamp: number): string => { - const now = Date.now(); - const diffMs = now - timestamp; - const diffSeconds = Math.floor(diffMs / 1000); - const diffMinutes = Math.floor(diffSeconds / 60); - const diffHours = Math.floor(diffMinutes / 60); - - if (diffSeconds < 10) { - return "Just now"; - } - if (diffSeconds < 60) { - return `${diffSeconds}s ago`; - } - if (diffMinutes < 60) { - return `${diffMinutes}m ago`; - } - if (diffHours < 24) { - return `${diffHours}h ago`; - } - return "Over a day ago"; -}; - -/** - * CacheStatusIndicator component - * - * @example - * ```tsx - * const { dataUpdatedAt, isFetching } = useBookings(); - * - * - * ``` - */ -export function CacheStatusIndicator({ - dataUpdatedAt, - isFetching = false, - style, - compact = false, -}: CacheStatusIndicatorProps) { - const { isOnline } = useQueryContext(); - - // Don't show anything if no data has been fetched yet - if (!dataUpdatedAt && !isFetching && isOnline) { - return null; - } - - // Offline state - if (!isOnline) { - return ( - - - Offline - showing cached data - - ); - } - - // Fetching state - if (isFetching) { - return ( - - - {!compact && Updating...} - - ); - } - - // Normal state with last updated time - if (dataUpdatedAt) { - if (compact) { - return ( - - - - ); - } - - return ( - - - Updated {formatTimeAgo(dataUpdatedAt)} - - ); - } - - return null; -} - -const styles = StyleSheet.create({ - container: { - flexDirection: "row", - alignItems: "center", - gap: 4, - paddingVertical: 4, - paddingHorizontal: 8, - }, - offlineContainer: { - backgroundColor: "rgba(255, 149, 0, 0.1)", - borderRadius: 4, - }, - text: { - fontSize: 11, - color: "#A3A3A3", - }, - offlineText: { - fontSize: 11, - color: "#FF9500", - fontWeight: "500", - }, -}); - -export default CacheStatusIndicator; diff --git a/companion/components/CalComLogo.tsx b/companion/components/CalComLogo.tsx deleted file mode 100644 index 2856f174e0..0000000000 --- a/companion/components/CalComLogo.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import Svg, { Path } from "react-native-svg"; - -interface CalComLogoProps { - width?: number; - height?: number; - color?: string; -} - -export function CalComLogo({ width = 101, height = 22, color = "#292929" }: CalComLogoProps) { - return ( - - - - - - - - - - ); -} diff --git a/companion/components/EmptyScreen.tsx b/companion/components/EmptyScreen.tsx deleted file mode 100644 index a218477895..0000000000 --- a/companion/components/EmptyScreen.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import { Text, TouchableOpacity, useColorScheme, View } from "react-native"; - -type IoniconName = keyof typeof Ionicons.glyphMap; - -interface EmptyScreenProps { - icon: IoniconName; - headline: string; - description: string; - buttonText?: string; - onButtonPress?: () => void; - className?: string; -} - -export function EmptyScreen({ - icon, - headline, - description, - buttonText, - onButtonPress, - className, -}: EmptyScreenProps) { - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - - const colors = { - text: isDark ? "#FFFFFF" : "#111827", - textSecondary: isDark ? "#A3A3A3" : "#6B7280", - iconBackground: isDark ? "#2C2C2E" : "#F3F4F6", - icon: isDark ? "#E5E5EA" : "#374151", - buttonBg: isDark ? "#FFFFFF" : "#111827", - buttonText: isDark ? "#000000" : "#FFFFFF", - }; - - return ( - - - - - - - - {headline} - - - - {description} - - - {buttonText && onButtonPress ? ( - - - {buttonText} - - - ) : null} - - - ); -} diff --git a/companion/components/FullScreenModal.d.ts b/companion/components/FullScreenModal.d.ts deleted file mode 100644 index a64aae387d..0000000000 --- a/companion/components/FullScreenModal.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type React from "react"; -import type { ModalProps } from "react-native"; - -export interface FullScreenModalProps extends ModalProps { - visible: boolean; - onRequestClose: () => void; - children: React.ReactNode; -} - -export declare function FullScreenModal(props: FullScreenModalProps): JSX.Element; diff --git a/companion/components/FullScreenModal.tsx b/companion/components/FullScreenModal.tsx deleted file mode 100644 index b84d31cbfc..0000000000 --- a/companion/components/FullScreenModal.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import type React from "react"; -import { useEffect } from "react"; -import { Modal, type ModalProps, Platform } from "react-native"; - -interface FullScreenModalProps extends ModalProps { - visible: boolean; - onRequestClose: () => void; - children: React.ReactNode; -} - -/** - * A modal wrapper that expands the browser extension iframe to full screen - * when running in the web platform (browser extension). - * - * For mobile platforms, it behaves like a regular Modal. - */ -export function FullScreenModal({ - visible, - onRequestClose, - children, - ...modalProps -}: FullScreenModalProps) { - useEffect(() => { - // Only send expand/collapse messages on web platform (browser extension) - if (Platform.OS !== "web") { - return; - } - - if (visible) { - // Expand the iframe to full width when modal opens - window.parent.postMessage({ type: "cal-companion-expand" }, "*"); - } else { - // Collapse the iframe back to 400px when modal closes - window.parent.postMessage({ type: "cal-companion-collapse" }, "*"); - } - - // Cleanup: collapse on unmount if still visible - return () => { - if (visible && Platform.OS === "web") { - window.parent.postMessage({ type: "cal-companion-collapse" }, "*"); - } - }; - }, [visible]); - - // For non-web platforms, just use the regular Modal - if (Platform.OS !== "web") { - return ( - - {children} - - ); - } - - // For web platform (browser extension), use the modal with full-screen behavior - return ( - - {children} - - ); -} diff --git a/companion/components/GlassModalHeader.tsx b/companion/components/GlassModalHeader.tsx deleted file mode 100644 index 832b7b5c6d..0000000000 --- a/companion/components/GlassModalHeader.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import { ActivityIndicator, Text, useColorScheme, View } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { AppPressable } from "./AppPressable"; - -interface GlassModalHeaderProps { - title: string; - onClose: () => void; - onAction?: () => void; - actionLabel?: string; - actionDisabled?: boolean; - actionLoading?: boolean; - /** Use icon button style (like three-dot menu) instead of text */ - actionIcon?: keyof typeof Ionicons.glyphMap; -} - -export function GlassModalHeader({ - title, - onClose, - onAction, - actionLabel = "Done", - actionDisabled = false, - actionLoading = false, - actionIcon, -}: GlassModalHeaderProps) { - const insets = useSafeAreaInsets(); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const iconColor = isDark ? "#fff" : "#000"; - - const isDisabled = actionDisabled || actionLoading; - - return ( - - - {/* Back Button - matching native iOS style */} - - - - - {/* Title - centered */} - - {title} - - - {/* Action Button - matching senior's Stack.Header.Menu style */} - {onAction ? ( - - {actionLoading ? ( - - ) : actionIcon ? ( - // Icon button style (like the three-dot menu) - - ) : ( - // Text button style (like "Save", "Done") - - - {actionLabel} - - - )} - - ) : ( - - )} - - - ); -} diff --git a/companion/components/Header.tsx b/companion/components/Header.tsx deleted file mode 100644 index 65a59b0121..0000000000 --- a/companion/components/Header.tsx +++ /dev/null @@ -1,483 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import { useRouter } from "expo-router"; -import { useCallback, useEffect, useState } from "react"; -import { - ActivityIndicator, - Image, - Platform, - Text, - TouchableOpacity, - useColorScheme, - View, -} from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { CalComAPIService, type UserProfile } from "@/services/calcom"; -import { getAvatarUrl } from "@/utils/getAvatarUrl"; -import { CalComLogo } from "./CalComLogo"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { AppPressable } from "@/components/AppPressable"; -import type { EventTypeFilters, EventTypeSortOption } from "@/hooks/useEventTypeFilter"; - -interface FilterOption { - key: string; - label: string; -} - -interface EventTypeFilterConfig { - sortBy: EventTypeSortOption; - filters: EventTypeFilters; - onSortChange: (sort: EventTypeSortOption) => void; - onToggleFilter: (filterKey: keyof EventTypeFilters) => void; - onResetFilters: () => void; - activeFilterCount: number; -} - -interface HeaderProps { - /** Optional: Filter options for dropdown menu (e.g., booking status filter) */ - filterOptions?: FilterOption[]; - /** Optional: Currently active filter key */ - activeFilter?: string; - /** Optional: Callback when filter changes */ - onFilterChange?: (filterKey: string) => void; - /** Optional: Event type filter/sort config for Android */ - eventTypeFilterConfig?: EventTypeFilterConfig; -} - -export function Header({ - filterOptions, - activeFilter, - onFilterChange, - eventTypeFilterConfig, -}: HeaderProps) { - const router = useRouter(); - const insets = useSafeAreaInsets(); - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - const [userProfile, setUserProfile] = useState(null); - const [loading, setLoading] = useState(true); - - // iOS system colors for dark mode - const colors = { - text: isDark ? "#FFFFFF" : "#000000", - textSecondary: isDark ? "#A3A3A3" : "#666666", - background: isDark ? "#000000" : "#FFFFFF", - border: isDark ? "#4D4D4D" : "#E5E5EA", - logoColor: isDark ? "#FFFFFF" : "#333333", - }; - - const fetchUserProfile = useCallback(async () => { - try { - const profile = await CalComAPIService.getUserProfile(); - setUserProfile(profile); - setLoading(false); - } catch (err) { - console.error("Failed to fetch user profile"); - if (__DEV__) { - const message = err instanceof Error ? err.message : String(err); - const stack = err instanceof Error ? err.stack : undefined; - console.debug("[Header] fetchUserProfile failed", { message, stack }); - } - setLoading(false); - } - }, []); - - useEffect(() => { - fetchUserProfile(); - }, [fetchUserProfile]); - - const handleProfile = () => { - // Navigate to profile sheet on all platforms (Android, Web, Extension) - router.push("/profile-sheet"); - }; - - const activeFilterLabel = - filterOptions?.find((opt) => opt.key === activeFilter)?.label ?? "Filter"; - - return ( - - {/* Left: Cal.com Logo */} - - - - - {/* Right: Filter Dropdown + Profile */} - - {/* Booking status filter dropdown (for bookings page) */} - {filterOptions && filterOptions.length > 0 && onFilterChange && ( - - - - - {activeFilterLabel} - - - - - - {filterOptions.map((option) => { - const isSelected = activeFilter === option.key; - // Map filter keys to appropriate icons - const getFilterIcon = (key: string) => { - switch (key) { - case "upcoming": - return "calendar-outline"; - case "unconfirmed": - return "help-circle-outline"; - case "recurring": - return "repeat-outline"; - case "past": - return "checkmark-circle-outline"; - case "cancelled": - return "close-circle-outline"; - default: - return "calendar-outline"; - } - }; - - return ( - onFilterChange(option.key)}> - - - - {option.label} - - - - ); - })} - - - )} - - {/* Event type filter/sort menu (for event types page - Android and Web/Extension) */} - {eventTypeFilterConfig && (Platform.OS === "android" || Platform.OS === "web") && ( - - - - - {/* Badge for active filters */} - {eventTypeFilterConfig.activeFilterCount > 0 && ( - - - {eventTypeFilterConfig.activeFilterCount} - - - )} - - - - - {/* Sort by Submenu */} - - - - - Sort by - - - - eventTypeFilterConfig.onSortChange("alphabetical")} - > - - - - Alphabetical - - - - eventTypeFilterConfig.onSortChange("newest")}> - - - - Newest First - - - - eventTypeFilterConfig.onSortChange("duration")}> - - - - By Duration - - - - - - - - - {/* Filter Label */} - - - - - Filters - - - - - {/* Filter Toggles (multi-select) */} - eventTypeFilterConfig.onToggleFilter("hiddenOnly")}> - - - - Hidden Only - - - - - eventTypeFilterConfig.onToggleFilter("paidOnly")}> - - - - Paid Events - - - - - eventTypeFilterConfig.onToggleFilter("seatedOnly")}> - - - - Seated Events - - - - - eventTypeFilterConfig.onToggleFilter("requiresConfirmationOnly")} - > - - - - Requires Confirmation - - - - - eventTypeFilterConfig.onToggleFilter("recurringOnly")} - > - - - - Recurring - - - - - {/* Clear All - only show when filters are active */} - {eventTypeFilterConfig.activeFilterCount > 0 && ( - <> - - - - - Clear All Filters - - - - )} - - - )} - - {/* Profile Picture */} - - {loading ? ( - - ) : userProfile?.avatarUrl ? ( - - ) : ( - - - - )} - - - - ); -} diff --git a/companion/components/HeaderButtonWrapper.tsx b/companion/components/HeaderButtonWrapper.tsx deleted file mode 100644 index 5033f92629..0000000000 --- a/companion/components/HeaderButtonWrapper.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Platform, View } from "react-native"; - -interface HeaderButtonWrapperProps { - children: React.ReactNode; - side: "left" | "right"; -} - -const WEB_HEADER_INSET = 12; - -export function HeaderButtonWrapper({ children, side }: HeaderButtonWrapperProps) { - if (Platform.OS !== "web") { - return <>{children}; - } - - const style = - side === "left" ? { marginLeft: WEB_HEADER_INSET } : { marginRight: WEB_HEADER_INSET }; - - return {children}; -} diff --git a/companion/components/LandingPagePicker.ios.tsx b/companion/components/LandingPagePicker.ios.tsx deleted file mode 100644 index 8da56239de..0000000000 --- a/companion/components/LandingPagePicker.ios.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useEffect, useRef } from "react"; -import { ActionSheetIOS } from "react-native"; -import { LANDING_PAGE_OPTIONS, type LandingPage } from "@/hooks/useUserPreferences"; - -interface LandingPagePickerProps { - visible: boolean; - currentValue: LandingPage; - onSelect: (value: LandingPage) => void; - onClose: () => void; -} - -export function LandingPagePicker({ - visible, - currentValue: _currentValue, - onSelect, - onClose, -}: LandingPagePickerProps) { - const sheetShownRef = useRef(false); - - useEffect(() => { - if (visible && !sheetShownRef.current) { - sheetShownRef.current = true; - - const options = [...LANDING_PAGE_OPTIONS.map((opt) => opt.label), "Cancel"]; - const cancelButtonIndex = options.length - 1; - - ActionSheetIOS.showActionSheetWithOptions( - { - title: "First Page", - message: "Choose which page opens when you launch the app", - options, - cancelButtonIndex, - destructiveButtonIndex: undefined, - }, - (buttonIndex) => { - if (buttonIndex !== cancelButtonIndex && buttonIndex < LANDING_PAGE_OPTIONS.length) { - const selectedOption = LANDING_PAGE_OPTIONS[buttonIndex]; - if (selectedOption) { - onSelect(selectedOption.value); - } - } - onClose(); - } - ); - } - - if (!visible) { - sheetShownRef.current = false; - } - }, [visible, onSelect, onClose]); - - return null; -} diff --git a/companion/components/LandingPagePicker.tsx b/companion/components/LandingPagePicker.tsx deleted file mode 100644 index 32b2f84878..0000000000 --- a/companion/components/LandingPagePicker.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import { Platform, Text, TouchableOpacity, View } from "react-native"; -import { FullScreenModal } from "./FullScreenModal"; -import { - LANDING_PAGE_OPTIONS, - type LandingPage, - type LandingPageOption, -} from "@/hooks/useUserPreferences"; - -interface LandingPagePickerProps { - visible: boolean; - currentValue: LandingPage; - onSelect: (value: LandingPage) => void; - onClose: () => void; -} - -export function LandingPagePicker({ - visible, - currentValue, - onSelect, - onClose, -}: LandingPagePickerProps) { - const handleSelect = (option: LandingPageOption) => { - onSelect(option.value); - onClose(); - }; - - return ( - - - - - First Page - - Choose which page opens when you launch the app - - - - - {LANDING_PAGE_OPTIONS.map((option) => { - const isSelected = currentValue === option.value; - return ( - handleSelect(option)} - className="flex-row items-center justify-between px-4 py-3 active:bg-gray-100" - style={Platform.OS === "web" ? { cursor: "pointer" } : undefined} - activeOpacity={0.7} - > - - {option.label} - - {isSelected && } - - ); - })} - - - - - Cancel - - - - - - ); -} diff --git a/companion/components/LoadingSpinner.tsx b/companion/components/LoadingSpinner.tsx deleted file mode 100644 index 83feb670fd..0000000000 --- a/companion/components/LoadingSpinner.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect"; -import { ActivityIndicator, Platform, StyleSheet, View } from "react-native"; - -interface LoadingSpinnerProps { - size?: "small" | "large"; - color?: string; - showBackground?: boolean; -} - -export function LoadingSpinner({ - size = "large", - color, - showBackground = true, -}: LoadingSpinnerProps) { - const supportsGlass = isLiquidGlassAvailable(); - - if (supportsGlass && Platform.OS === "ios") { - return ( - - - - ); - } - - if (showBackground) { - return ( - - - - ); - } - - return ; -} - -const styles = StyleSheet.create({ - glassContainer: { - alignItems: "center", - justifyContent: "center", - padding: 24, - borderRadius: 20, - }, - styledContainer: { - alignItems: "center", - justifyContent: "center", - padding: 24, - borderRadius: 20, - backgroundColor: "rgba(255, 255, 255, 0.9)", - ...Platform.select({ - web: { - boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)", - }, - default: { - shadowColor: "#000", - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 8, - elevation: 4, - }, - }), - }, -}); - -export default LoadingSpinner; diff --git a/companion/components/LocationsList.tsx b/companion/components/LocationsList.tsx deleted file mode 100644 index 8fa958b410..0000000000 --- a/companion/components/LocationsList.tsx +++ /dev/null @@ -1,343 +0,0 @@ -/** - * LocationsList Component - * Reusable component for displaying and managing multiple event type locations - */ - -import { Ionicons } from "@expo/vector-icons"; -import type React from "react"; -import { useState } from "react"; -import { - Modal, - Platform, - ScrollView, - Text, - TextInput, - TouchableOpacity, - useColorScheme, - View, -} from "react-native"; -import type { LocationItem, LocationOptionGroup } from "@/types/locations"; -import { - createLocationItemFromOption, - getLocationInputLabel, - getLocationInputPlaceholder, - locationRequiresInput, -} from "@/utils/locationHelpers"; -import { LocationsListIOSPicker } from "./LocationsListIOSPicker"; -import { SvgImage } from "./SvgImage"; - -interface LocationsListProps { - /** Array of current locations */ - locations: LocationItem[]; - /** Callback when a location is added */ - onAdd: (location: LocationItem) => void; - /** Callback when a location is removed */ - onRemove: (locationId: string) => void; - /** Callback when a location is updated (for input fields) */ - onUpdate: (locationId: string, updates: Partial) => void; - /** Available location options grouped by category */ - locationOptions: LocationOptionGroup[]; - /** Whether the component is disabled */ - disabled?: boolean; - /** Whether locations are loading */ - loading?: boolean; - /** Whether to hide the internal add button */ - hideAddButton?: boolean; - /** External control for the add modal */ - showAddModal?: boolean; - /** Callback to change modal visibility */ - onShowAddModalChange?: (visible: boolean) => void; -} - -function isLocationAlreadyAdded(locations: LocationItem[], optionValue: string): boolean { - return locations.some((loc) => { - if (loc.type === "integration" && optionValue.startsWith("integrations:")) { - return loc.integration === optionValue.replace("integrations:", ""); - } - if (["address", "link", "phone"].includes(loc.type)) { - return false; - } - return loc.type === optionValue; - }); -} - -export const AddLocationTrigger = ({ - isHeader = false, - locationOptions, - locations, - onSelectOption, - disabled = false, - loading = false, - onPressFallback, -}: { - isHeader?: boolean; - locationOptions: LocationOptionGroup[]; - locations: LocationItem[]; - onSelectOption: (value: string, label: string) => void; - disabled?: boolean; - loading?: boolean; - onPressFallback?: () => void; -}) => { - const colorScheme = useColorScheme(); - const isDark = colorScheme === "dark"; - - const trigger = isHeader ? ( - - - - ) : ( - - {loading ? ( - Loading options... - ) : ( - <> - - - Add Location - - - )} - - ); - - if (Platform.OS !== "ios") { - return trigger; - } - - const iosPicker = ( - - ); - - return iosPicker ?? trigger; -}; - -export const LocationsList: React.FC = ({ - locations, - onAdd, - onRemove, - onUpdate, - locationOptions, - disabled = false, - loading = false, - hideAddButton = false, - showAddModal: controlledShowAddModal, - onShowAddModalChange, -}) => { - const [internalShowAddModal, setInternalShowAddModal] = useState(false); - const showAddModal = controlledShowAddModal ?? internalShowAddModal; - const setShowAddModal = onShowAddModalChange ?? setInternalShowAddModal; - - const handleAddLocation = () => { - setShowAddModal(true); - }; - - const handleSelectOption = (optionValue: string, optionLabel: string) => { - const newLocation = createLocationItemFromOption(optionValue, optionLabel); - onAdd(newLocation); - setShowAddModal(false); - }; - - const renderLocationIcon = (location: LocationItem) => { - if (location.iconUrl) { - return ( - - - - ); - } - return ( - - - - ); - }; - - const renderLocationInput = (location: LocationItem) => { - if (!locationRequiresInput(location.type)) { - return null; - } - - const placeholder = getLocationInputPlaceholder(location.type); - const label = getLocationInputLabel(location.type); - - let value = ""; - let fieldKey: "address" | "link" | "phone" = "address"; - - switch (location.type) { - case "address": - value = location.address || ""; - fieldKey = "address"; - break; - case "link": - value = location.link || ""; - fieldKey = "link"; - break; - case "phone": - value = location.phone || ""; - fieldKey = "phone"; - break; - } - - return ( - - {label} - onUpdate(location.id, { [fieldKey]: text })} - editable={!disabled} - keyboardType={location.type === "phone" ? "phone-pad" : "default"} - autoCapitalize={location.type === "link" ? "none" : "sentences"} - /> - - ); - }; - - return ( - - {/* Locations List */} - {locations.length > 0 ? ( - - {locations.map((location, index) => ( - - - - - {renderLocationIcon(location)} - - {location.displayName} - - - {!disabled ? ( - onRemove(location.id)} - className="ml-2" - hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} - > - - - ) : null} - - {renderLocationInput(location)} - - - ))} - - ) : null} - - {/* Add Location Button */} - {!hideAddButton && ( - - - - )} - - {/* Add Location Modal (for non-iOS) */} - setShowAddModal(false)} - > - - - {/* Header */} - - - Add Location - - setShowAddModal(false)} className="p-1"> - - - - - {/* Options List */} - - {locationOptions.map((group, groupIndex) => ( - 0 ? "mt-4" : ""}> - - {group.category} - - {group.options.map((option) => { - const alreadyAdded = isLocationAlreadyAdded(locations, option.value); - - return ( - handleSelectOption(option.value, option.label)} - disabled={alreadyAdded} - className={`flex-row items-center rounded-lg px-2 py-3 ${ - alreadyAdded - ? "opacity-40" - : "active:bg-gray-100 dark:active:bg-[#262626]" - }`} - > - {option.iconUrl ? ( - - ) : ( - - - - )} - - {option.label} - - {alreadyAdded && } - - ); - })} - - ))} - {/* Bottom padding for safe area */} - - - - - - - ); -}; diff --git a/companion/components/LocationsListIOSPicker.ios.tsx b/companion/components/LocationsListIOSPicker.ios.tsx deleted file mode 100644 index 42e4ead399..0000000000 --- a/companion/components/LocationsListIOSPicker.ios.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { Button, ContextMenu, Host } from "@expo/ui/swift-ui"; -import { buttonStyle } from "@expo/ui/swift-ui/modifiers"; -import { isLiquidGlassAvailable } from "expo-glass-effect"; -import type React from "react"; -import type { LocationItem, LocationOptionGroup } from "@/types/locations"; - -const getSFSymbolForType = (type: string) => { - switch (type) { - case "address": - return "mappin.and.ellipse"; - case "link": - return "link"; - case "phone": - return "phone"; - default: - return "video"; - } -}; - -function isLocationAlreadyAdded(locations: LocationItem[], optionValue: string): boolean { - return locations.some((loc) => { - if (loc.type === "integration" && optionValue.startsWith("integrations:")) { - return loc.integration === optionValue.replace("integrations:", ""); - } - if (["address", "link", "phone"].includes(loc.type)) { - return false; - } - return loc.type === optionValue; - }); -} - -interface LocationsListIOSPickerProps { - isHeader?: boolean; - locationOptions: LocationOptionGroup[]; - locations: LocationItem[]; - onSelectOption: (value: string, label: string) => void; - trigger: React.ReactNode; -} - -export function LocationsListIOSPicker({ - isHeader = false, - locationOptions, - locations, - onSelectOption, - trigger, -}: LocationsListIOSPickerProps) { - return ( - - - - {locationOptions.flatMap((group) => - group.options - .filter((opt) => !isLocationAlreadyAdded(locations, opt.value)) - .map((option) => ( - - - `; - - const closeBtn = menu.querySelector(".cal-gmail-close-btn") as HTMLButtonElement; - if (closeBtn) { - closeBtn.addEventListener("mouseenter", () => { - closeBtn.style.background = "#E5E7EB"; - }); - closeBtn.addEventListener("mouseleave", () => { - closeBtn.style.background = "#F3F4F6"; - }); - closeBtn.addEventListener("click", () => { - for (const tooltip of tooltipsToCleanup) { - tooltip.remove(); - } - menu.remove(); - }); - } - } - } - } - - function insertEventTypeLink(eventType: { - slug: string; - users?: Array<{ username?: string }>; - bookingUrl?: string; - }): void { - // Construct the Cal.com booking link - const bookingUrl = - eventType.bookingUrl || - `https://cal.com/${eventType.users?.[0]?.username || "user"}/${eventType.slug}`; - - // Try to insert at cursor position in the compose field - const inserted = insertTextAtCursor(bookingUrl); - - if (inserted) { - showNotification("Link inserted", "success"); - } else { - // Fallback: copy to clipboard if insertion fails - navigator.clipboard - .writeText(bookingUrl) - .then(() => { - showNotification("Link copied!", "success"); - }) - .catch(() => { - showNotification("Failed to copy link", "error"); - }); - } - } - - function _copyEventTypeLink(eventType: { - slug: string; - users?: Array<{ username?: string }>; - bookingUrl?: string; - }): void { - // Construct the Cal.com booking link - const bookingUrl = - eventType.bookingUrl || - `https://cal.com/${eventType.users?.[0]?.username || "user"}/${eventType.slug}`; - - // Try to insert at cursor position in the compose field - const inserted = insertTextAtCursor(bookingUrl); - - if (inserted) { - showNotification("Link inserted", "success"); - } else { - // Fallback: copy to clipboard if insertion fails - navigator.clipboard - .writeText(bookingUrl) - .then(() => { - showNotification("Link copied!", "success"); - }) - .catch(() => { - showNotification("Failed to copy link", "error"); - }); - } - } - - function insertTextAtCursor(text: string): boolean { - // Find the active compose field - // Gmail uses contenteditable divs for the compose body - const composeBody = - document.querySelector('[role="textbox"][aria-label*="Message Body"]') || - document.querySelector('[role="textbox"][g_editable="true"]') || - document.querySelector('div[contenteditable="true"][role="textbox"]'); - - if (!composeBody) { - return false; - } - - // Focus the compose field - (composeBody as HTMLElement).focus(); - - // Get the current selection - const selection = window.getSelection(); - if (!selection || selection.rangeCount === 0) { - // If no selection, append to the end - const textNode = document.createTextNode(` ${text} `); - composeBody.appendChild(textNode); - - // Move cursor after inserted text - const range = document.createRange(); - range.setStartAfter(textNode); - range.collapse(true); - selection?.removeAllRanges(); - selection?.addRange(range); - - return true; - } - - // Insert at cursor position - const range = selection.getRangeAt(0); - range.deleteContents(); - - // Create a text node with the link (with spaces around it) - const textNode = document.createTextNode(` ${text} `); - range.insertNode(textNode); - - // Move cursor after inserted text - range.setStartAfter(textNode); - range.collapse(true); - selection.removeAllRanges(); - selection.addRange(range); - - // Trigger input event so Gmail knows content changed - composeBody.dispatchEvent(new Event("input", { bubbles: true })); - composeBody.dispatchEvent(new Event("change", { bubbles: true })); - - return true; - } - - function showNotification(message: string, type: "success" | "error"): void { - const notification = document.createElement("div"); - notification.style.cssText = ` - position: fixed; - bottom: 80px; - right: 80px; - padding: 10px 12px; - background: ${type === "success" ? "#111827" : "#752522"}; - color: white; - border: 1px solid #2b2b2b; - border-radius: 8px; - font-family: "Google Sans",Roboto,RobotoDraft,Helvetica,Arial,sans-serif; - font-size: 14px; - font-weight: 600; - z-index: 10000; - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.3), 0 2px 6px 2px rgba(0, 0, 0, 0.15); - display: flex; - align-items: center; - gap: 8px; - opacity: 0; - transform: translateY(10px); - transition: opacity 0.2s ease, transform 0.2s ease; - `; - - // Add check icon for success - if (type === "success") { - const checkIcon = document.createElement("span"); - checkIcon.innerHTML = ` - - - - `; - checkIcon.style.cssText = ` - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - `; - notification.appendChild(checkIcon); - } - - // Add message text - const messageText = document.createElement("span"); - messageText.textContent = message; - notification.appendChild(messageText); - - document.body.appendChild(notification); - - // Trigger fade-in animation - requestAnimationFrame(() => { - notification.style.opacity = "1"; - notification.style.transform = "translateY(0)"; - }); - - // Fade out and remove after 3 seconds - setTimeout(() => { - notification.style.opacity = "0"; - notification.style.transform = "translateY(10px)"; - setTimeout(() => { - notification.remove(); - }, 200); - }, 3000); - } - - // Add tooltip - calButton.title = "Schedule with Cal.com"; - - // Add button to cell - calButtonCell.appendChild(calButton); - - // Insert the new cell after the send button cell - if (sendButtonCell.nextSibling) { - tableRow.insertBefore(calButtonCell, sendButtonCell.nextSibling); - } else { - tableRow.appendChild(calButtonCell); - } - }); - } - - // Initial injection - setTimeout(injectCalButton, 1000); - - // Watch for DOM changes (Gmail is a SPA) - const observer = new MutationObserver(() => { - injectCalButton(); - }); - - observer.observe(document.body, { - childList: true, - subtree: true, - }); - - // Also inject on URL changes (Gmail navigation) - let currentUrl = window.location.href; - setInterval(() => { - if (window.location.href !== currentUrl) { - currentUrl = window.location.href; - setTimeout(injectCalButton, 500); - } - }, 1000); - - // ========== Google Calendar Chip Integration ========== - // TODO: Implement Google Calendar chip integration - - // Shared functions (reused from Gmail integration) - function createTooltip(text: string, buttonElement: HTMLElement) { - const tooltip = document.createElement("div"); - tooltip.className = "cal-tooltip"; - tooltip.style.cssText = ` - position: fixed; - background-color: #1a1a1a; - color: white; - padding: 4px 8px; - border-radius: 6px; - font-size: 12px; - font-weight: 600; - white-space: nowrap; - pointer-events: none; - opacity: 0; - transition: opacity 0.15s ease; - z-index: 10002; - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); - display: none; - `; - tooltip.textContent = text; - document.body.appendChild(tooltip); - - // Position tooltip on hover - const updatePosition = () => { - const rect = buttonElement.getBoundingClientRect(); - tooltip.style.left = `${rect.left + rect.width / 2}px`; - tooltip.style.top = `${rect.top - 8}px`; - tooltip.style.transform = "translate(-50%, -100%)"; - }; - - buttonElement.addEventListener("mouseenter", () => { - updatePosition(); - tooltip.style.display = "block"; - tooltip.style.opacity = "1"; - }); - - buttonElement.addEventListener("mouseleave", () => { - tooltip.style.opacity = "0"; - setTimeout(() => { - if (tooltip.style.opacity === "0") { - tooltip.style.display = "none"; - } - }, 150); - }); - - return tooltip; - } - - function openCalSidebar() { - // Open Cal.com sidebar or quick schedule flow - if (isClosed) { - // Trigger sidebar open - chrome.runtime.sendMessage({ action: "icon-clicked" }); - } else { - // Toggle sidebar visibility - isVisible = !isVisible; - if (isVisible) { - sidebarContainer.style.transform = "translateX(0)"; - } else { - sidebarContainer.style.transform = "translateX(100%)"; - } - } - } - - async function _fetchEventTypes(menu: HTMLElement, tooltipsToCleanup: HTMLElement[]) { - try { - // Check cache first - const now = Date.now(); - const isCacheValid = - eventTypesCache && cacheTimestamp && now - cacheTimestamp < CACHE_DURATION; - - let eventTypes: Array<{ - id: number; - title: string; - slug: string; - lengthInMinutes?: number; - length?: number; - duration?: number; - description?: string; - users?: Array<{ username?: string }>; - bookingUrl?: string; - }> = []; - - if (isCacheValid && eventTypesCache) { - // Use cached data - eventTypes = eventTypesCache; - } else { - // Check if extension context is valid - if (!chrome.runtime?.id) { - throw new Error("Extension context invalidated. Please reload the page."); - } - - // Fetch fresh data from background script - const response = await new Promise((resolve, reject) => { - try { - chrome.runtime.sendMessage({ action: "fetch-event-types" }, (response) => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)); - } else if (response?.error) { - reject(new Error(response.error)); - } else { - resolve(response); - } - }); - } catch (err) { - reject(err); - } - }); - - if ( - response && - ( - response as { - data?: Array<{ - id: number; - title: string; - slug: string; - lengthInMinutes?: number; - length?: number; - duration?: number; - description?: string; - users?: Array<{ username?: string }>; - bookingUrl?: string; - }>; - } - ).data - ) { - eventTypes = ( - response as { - data: Array<{ - id: number; - title: string; - slug: string; - lengthInMinutes?: number; - length?: number; - duration?: number; - description?: string; - users?: Array<{ username?: string }>; - bookingUrl?: string; - }>; - } - ).data; - } else if (Array.isArray(response)) { - eventTypes = response; - } else { - eventTypes = []; - } - - // Ensure eventTypes is an array - if (!Array.isArray(eventTypes)) { - eventTypes = []; - } - - // Update cache - eventTypesCache = eventTypes; - cacheTimestamp = now; - } - - // Clear loading state - menu.innerHTML = ""; - - if (eventTypes.length === 0) { - menu.innerHTML = ` -
- No event types found -
- `; - return; - } - - // Add event types - with additional safety checks - try { - eventTypes.forEach((eventType, index) => { - // Validate eventType object - if (!eventType || typeof eventType !== "object") { - return; - } - - const title = eventType.title || "Untitled Event"; - const length = - eventType.lengthInMinutes || eventType.length || eventType.duration || 30; - const description = eventType.description || ""; - - const menuItem = document.createElement("div"); - menuItem.style.cssText = ` - padding: 14px 16px; - display: flex; - align-items: center; - justify-content: space-between; - transition: background-color 0.1s ease; - border-bottom: ${index < eventTypes.length - 1 ? "1px solid #E5E5EA" : "none"}; - `; - - // Create content wrapper - const contentWrapper = document.createElement("div"); - contentWrapper.style.cssText = ` - flex: 1; - display: flex; - flex-direction: column; - cursor: pointer; - margin-right: 12px; - position: relative; - min-width: 0; - overflow: hidden; - `; - - contentWrapper.innerHTML = ` -
- ${escapeHtml(title)} -
-
- - ${length}min - - ${ - description - ? `${escapeHtml(description)}` - : "" - } -
- `; - - // Create tooltip for content wrapper - const contentTooltip = document.createElement("div"); - contentTooltip.className = "cal-tooltip"; - contentTooltip.style.cssText = ` - position: fixed; - background-color: #1a1a1a; - color: white; - padding: 4px 8px; - border-radius: 6px; - font-size: 12px; - font-weight: 600; - white-space: nowrap; - pointer-events: none; - opacity: 0; - transition: opacity 0.15s ease; - z-index: 10002; - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); - display: none; - `; - contentTooltip.textContent = "Insert link"; - document.body.appendChild(contentTooltip); - tooltipsToCleanup.push(contentTooltip); - - // Show/hide tooltip - contentWrapper.addEventListener("mouseenter", (_e) => { - const rect = contentWrapper.getBoundingClientRect(); - contentTooltip.style.left = `${rect.left + rect.width / 2 + 80}px`; - contentTooltip.style.top = `${rect.top + 35}px`; - contentTooltip.style.transform = "translate(-50%, -100%)"; - contentTooltip.style.display = "block"; - contentTooltip.style.opacity = "1"; - }); - contentWrapper.addEventListener("mouseleave", () => { - contentTooltip.style.opacity = "0"; - setTimeout(() => { - if (contentTooltip.style.opacity === "0") { - contentTooltip.style.display = "none"; - } - }, 150); - }); - - // Add click handler to content wrapper to insert link - contentWrapper.addEventListener("click", (e) => { - e.stopPropagation(); - // Remove tooltip - contentTooltip.remove(); - menu.remove(); - // Insert link into message text - insertEventTypeLink(eventType); - }); - - // Create buttons container - const buttonsContainer = document.createElement("div"); - buttonsContainer.style.cssText = ` - display: flex; - align-items: center; - gap: 0; - flex-shrink: 0; - `; - - // Preview button - const previewBtn = document.createElement("button"); - previewBtn.innerHTML = ` - - - - - - `; - previewBtn.style.cssText = ` - width: 32px; - height: 32px; - display: flex; - align-items: center; - justify-content: center; - border: 1px solid #e5e5ea; - border-right: none; - border-radius: 6px 0 0 6px; - background: white; - cursor: pointer; - transition: background-color 0.1s ease; - padding: 0; - position: relative; - `; - - // Create tooltip for preview button - const previewTooltip = createTooltip("Preview", previewBtn); - tooltipsToCleanup.push(previewTooltip); - - previewBtn.addEventListener("click", (e) => { - e.stopPropagation(); - const bookingUrl = - eventType.bookingUrl || - `https://cal.com/${eventType.users?.[0]?.username || "user"}/${eventType.slug}`; - window.open(bookingUrl, "_blank"); - }); - previewBtn.addEventListener("mouseenter", () => { - previewBtn.style.backgroundColor = "#f8f9fa"; - }); - previewBtn.addEventListener("mouseleave", () => { - previewBtn.style.backgroundColor = "white"; - }); - - // Copy link button - const copyBtn = document.createElement("button"); - copyBtn.innerHTML = ` - - - - - `; - copyBtn.style.cssText = ` - width: 32px; - height: 32px; - display: flex; - align-items: center; - justify-content: center; - border: 1px solid #e5e5ea; - border-right: none; - background: white; - cursor: pointer; - transition: background-color 0.1s ease; - padding: 0; - position: relative; - `; - - // Create tooltip for copy button - const copyTooltip = createTooltip("Copy link", copyBtn); - tooltipsToCleanup.push(copyTooltip); - - copyBtn.addEventListener("click", (e) => { - e.stopPropagation(); - // Copy to clipboard - const bookingUrl = - eventType.bookingUrl || - `https://cal.com/${eventType.users?.[0]?.username || "user"}/${eventType.slug}`; - navigator.clipboard - .writeText(bookingUrl) - .then(() => { - showNotification("Link copied!", "success"); - // Change icon to checkmark - copyBtn.innerHTML = ` - - - - `; - setTimeout(() => { - copyBtn.innerHTML = ` - - - - - `; - }, 2000); - }) - .catch(() => { - showNotification("Failed to copy link", "error"); - }); - }); - copyBtn.addEventListener("mouseenter", () => { - copyBtn.style.backgroundColor = "#f8f9fa"; - }); - copyBtn.addEventListener("mouseleave", () => { - copyBtn.style.backgroundColor = "white"; - }); - - // Edit button - const editBtn = document.createElement("button"); - editBtn.innerHTML = ` - - - - - `; - editBtn.style.cssText = ` - width: 32px; - height: 32px; - display: flex; - align-items: center; - justify-content: center; - border: 1px solid #e5e5ea; - border-radius: 0 6px 6px 0; - background: white; - cursor: pointer; - transition: background-color 0.1s ease; - padding: 0; - position: relative; - `; - - // Create tooltip for edit button - const editTooltip = createTooltip("Edit", editBtn); - tooltipsToCleanup.push(editTooltip); - - editBtn.addEventListener("click", (e) => { - e.stopPropagation(); - const editUrl = `https://app.cal.com/event-types/${eventType.id}`; - window.open(editUrl, "_blank"); - }); - editBtn.addEventListener("mouseenter", () => { - editBtn.style.backgroundColor = "#f8f9fa"; - }); - editBtn.addEventListener("mouseleave", () => { - editBtn.style.backgroundColor = "white"; - }); - - // Assemble buttons - buttonsContainer.appendChild(previewBtn); - buttonsContainer.appendChild(copyBtn); - buttonsContainer.appendChild(editBtn); - - // Hover effect for whole item - menuItem.addEventListener("mouseenter", () => { - menuItem.style.backgroundColor = "#f8f9fa"; - }); - - menuItem.addEventListener("mouseleave", () => { - menuItem.style.backgroundColor = "transparent"; - }); - - // Assemble menu item - menuItem.appendChild(contentWrapper); - menuItem.appendChild(buttonsContainer); - - menu.appendChild(menuItem); - }); - } catch (_forEachError) { - menu.innerHTML = ` -
- Error displaying event types -
- `; - } - } catch (error) { - const errorMessage = (error as Error).message || ""; - const isAuthError = - errorMessage.includes("OAuth") || - errorMessage.includes("access token") || - errorMessage.includes("sign in") || - errorMessage.includes("authentication"); - - if (isAuthError) { - // Not logged in - open sidebar directly - for (const tooltip of tooltipsToCleanup) { - tooltip.remove(); - } - menu.remove(); - openCalSidebar(); - return; - } else { - // Show generic error for non-auth errors - menu.innerHTML = ` -
-
- - - - - -
-
- Something went wrong -
-
- ${errorMessage || "Failed to load event types"} -
- -
- `; - - const closeBtn = menu.querySelector(".cal-linkedin-close-btn") as HTMLButtonElement; - if (closeBtn) { - closeBtn.addEventListener("mouseenter", () => { - closeBtn.style.background = "#E5E7EB"; - }); - closeBtn.addEventListener("mouseleave", () => { - closeBtn.style.background = "#F3F4F6"; - }); - closeBtn.addEventListener("click", () => { - for (const tooltip of tooltipsToCleanup) { - tooltip.remove(); - } - menu.remove(); - }); - } - } - } - } - - function insertEventTypeLink(eventType: { - slug: string; - users?: Array<{ username?: string }>; - bookingUrl?: string; - }) { - // Construct the Cal.com booking link - const bookingUrl = - eventType.bookingUrl || - `https://cal.com/${eventType.users?.[0]?.username || "user"}/${eventType.slug}`; - - // Try to insert at cursor position in the message field - const inserted = insertTextAtCursor(bookingUrl); - - if (inserted) { - showNotification("Link inserted", "success"); - } else { - // Fallback: copy to clipboard if insertion fails - navigator.clipboard - .writeText(bookingUrl) - .then(() => { - showNotification("Link copied!", "success"); - }) - .catch(() => { - showNotification("Failed to copy link", "error"); - }); - } - } - - function insertTextAtCursor(text: string) { - // Find the active message input field - // LinkedIn uses contenteditable divs for message input - const messageInput = - document.querySelector( - 'div[contenteditable="true"][role="textbox"][aria-label*="message" i]' - ) || - document.querySelector('div[contenteditable="true"][data-testid*="message"]') || - document.querySelector('div[contenteditable="true"].msg-form__contenteditable'); - - if (!messageInput) { - return false; - } - - // Focus the message field - (messageInput as HTMLElement).focus(); - - // Get the current selection - const selection = window.getSelection(); - if (!selection || selection.rangeCount === 0) { - // If no selection, append to the end - const textNode = document.createTextNode(` ${text} `); - messageInput.appendChild(textNode); - - // Move cursor after inserted text - const range = document.createRange(); - range.setStartAfter(textNode); - range.collapse(true); - selection?.removeAllRanges(); - selection?.addRange(range); - - return true; - } - - // Insert at cursor position - const range = selection.getRangeAt(0); - range.deleteContents(); - - // Create a text node with the link (with spaces around it) - const textNode = document.createTextNode(` ${text} `); - range.insertNode(textNode); - - // Move cursor after inserted text - range.setStartAfter(textNode); - range.collapse(true); - selection.removeAllRanges(); - selection.addRange(range); - - // Trigger input event so LinkedIn knows content changed - messageInput.dispatchEvent(new Event("input", { bubbles: true })); - messageInput.dispatchEvent(new Event("change", { bubbles: true })); - - return true; - } - - function showNotification(message: string, type: "success" | "error") { - const notification = document.createElement("div"); - notification.style.cssText = ` - position: fixed; - bottom: 80px; - right: 80px; - padding: 10px 12px; - background: ${type === "success" ? "#111827" : "#752522"}; - color: white; - border: 1px solid #2b2b2b; - border-radius: 8px; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - font-size: 14px; - font-weight: 600; - z-index: 10000; - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.3), 0 2px 6px 2px rgba(0, 0, 0, 0.15); - display: flex; - align-items: center; - gap: 8px; - opacity: 0; - transform: translateY(10px); - transition: opacity 0.2s ease, transform 0.2s ease; - `; - - // Add check icon for success - if (type === "success") { - const checkIcon = document.createElement("span"); - checkIcon.innerHTML = ` - - - - `; - checkIcon.style.cssText = ` - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - `; - notification.appendChild(checkIcon); - } - - // Add message text - const messageText = document.createElement("span"); - messageText.textContent = message; - notification.appendChild(messageText); - - document.body.appendChild(notification); - - // Trigger fade-in animation - requestAnimationFrame(() => { - notification.style.opacity = "1"; - notification.style.transform = "translateY(0)"; - }); - - // Fade out and remove after 3 seconds - setTimeout(() => { - notification.style.opacity = "0"; - notification.style.transform = "translateY(10px)"; - setTimeout(() => { - notification.remove(); - }, 200); - }, 3000); - } - - /** - * Generate HTML email embed for Cal.com booking - * Based on the email embed feature in main Cal.com codebase - */ - function generateEmailEmbedHTML(params: { - eventType: { slug: string; title?: string }; - username: string; - slots: Array<{ - isoDate: string; - date: string; - isoTimestamp: string; - time?: string; - startTime?: string; - endTime?: string; - }>; - duration: number; - timezone: string; - timezoneOffset: string; - }): string { - const { eventType, username, slots, duration, timezone, timezoneOffset } = params; - - // Group slots by date - const slotsByDate: { - [date: string]: Array<{ - isoDate: string; - date: string; - isoTimestamp: string; - time?: string; - startTime?: string; - endTime?: string; - }>; - } = {}; - slots.forEach((slot) => { - if (!slotsByDate[slot.isoDate]) { - slotsByDate[slot.isoDate] = []; - } - slotsByDate[slot.isoDate].push(slot); - }); - - // Generate time slot buttons HTML - const datesHTML = Object.keys(slotsByDate) - .sort() - .map((date) => { - const dateSlots = slotsByDate[date]; - const formattedDate = dateSlots[0].date; // Already formatted like "Thu, 27 November" - - const slotsHTML = dateSlots - .map((slot) => { - // URL-encode the timezone to handle special characters like "/" - const encodedTimezone = encodeURIComponent(timezone); - const bookingURL = `https://cal.com/${username}/${eventType.slug}?duration=${duration}&date=${slot.isoDate}&slot=${slot.isoTimestamp}&cal.tz=${encodedTimezone}`; - - return ` - - - - - - - - -
- - ${slot.startTime} - -
- - `; - }) - .join(""); - - return ` - - - - - - - - - -
- - ${formattedDate} - -
- - - - ${slotsHTML} - - -
-
- `; - }) - .join(""); - - // Complete HTML structure - return ` -
-
- ${eventType.title} -
-
- Duration: ${duration} mins -
-
- - Timezone: ${timezone} (${timezoneOffset}) - -
- ${datesHTML} - -
- Powered by Cal.com -
-
-


- `; - } - - /** - * Helper function to insert HTML into Gmail compose field - * @param html - The HTML content to insert - * @param targetComposeElement - Optional: The chip's compose element to ensure we insert in the correct window - */ - function insertGmailHTML(html: string, targetComposeElement?: HTMLElement): boolean { - try { - // Validate input - if (!html || typeof html !== "string") { - devLog.warn("Invalid HTML to insert"); - return false; - } - - // If a target compose element is provided, find the compose body within its scope - let composeBody: Element | null = null; - - if (targetComposeElement) { - const composeWindow = - targetComposeElement.closest('[role="dialog"]') || - targetComposeElement.closest(".nH") || - targetComposeElement.closest('div[contenteditable="true"]')?.parentElement; - - if (composeWindow) { - composeBody = - composeWindow.querySelector('[role="textbox"][aria-label*="Message Body"]') || - composeWindow.querySelector('[role="textbox"][g_editable="true"]') || - composeWindow.querySelector('div[contenteditable="true"][role="textbox"]'); - } - } - - // Fallback: Try global selectors - if (!composeBody) { - composeBody = - document.querySelector('[role="textbox"][aria-label*="Message Body"]') || - document.querySelector('[role="textbox"][g_editable="true"]') || - document.querySelector('div[contenteditable="true"][role="textbox"]'); - } - - if (!composeBody) { - devLog.warn("Gmail compose field not found"); - return false; - } - - // Focus the compose body - try { - (composeBody as HTMLElement).focus(); - } catch (focusError) { - devLog.warn("Failed to focus compose field:", focusError); - } - - const selection = window.getSelection(); - if (!selection || selection.rangeCount === 0) { - // No selection - append at end - try { - const tempDiv = document.createElement("div"); - tempDiv.innerHTML = html; - composeBody.appendChild(tempDiv); - return true; - } catch (appendError) { - devLog.warn("Failed to append HTML:", appendError); - return false; - } - } - - // Insert at cursor position - try { - const range = selection.getRangeAt(0); - range.deleteContents(); - - const tempDiv = document.createElement("div"); - tempDiv.innerHTML = html; - - // Insert all child nodes - const fragment = document.createDocumentFragment(); - while (tempDiv.firstChild) { - fragment.appendChild(tempDiv.firstChild); - } - - range.insertNode(fragment); - range.collapse(false); - selection.removeAllRanges(); - selection.addRange(range); - - // Trigger input events - (composeBody as HTMLElement).dispatchEvent(new Event("input", { bubbles: true })); - (composeBody as HTMLElement).dispatchEvent(new Event("change", { bubbles: true })); - - return true; - } catch (insertError) { - devLog.warn("Failed to insert HTML at cursor:", insertError); - return false; - } - } catch (_error) { - devLog.error("Critical error inserting HTML"); - return false; - } - } - - /** - * Helper function to insert text into Gmail compose field - * @param text - The text to insert - * @param targetComposeElement - Optional: The chip's compose element to ensure we insert in the correct window - */ - function insertGmailText(text: string, targetComposeElement?: HTMLElement): boolean { - try { - // Validate input - if (!text || typeof text !== "string") { - devLog.warn("Invalid text to insert"); - return false; - } - - // If a target compose element is provided, find the compose body within its scope - // Otherwise, fall back to the first compose field (legacy behavior) - let composeBody: Element | null = null; - - if (targetComposeElement) { - // Find the compose window that contains the chip - const composeWindow = - targetComposeElement.closest('[role="dialog"]') || - targetComposeElement.closest(".nH") || - targetComposeElement.closest('div[contenteditable="true"]')?.parentElement; - - if (composeWindow) { - // Look for compose body within this specific window - composeBody = - composeWindow.querySelector('[role="textbox"][aria-label*="Message Body"]') || - composeWindow.querySelector('[role="textbox"][g_editable="true"]') || - composeWindow.querySelector('div[contenteditable="true"][role="textbox"]'); - } - } - - // Fallback: Try global selectors if no target or if scoped search failed - if (!composeBody) { - composeBody = - document.querySelector('[role="textbox"][aria-label*="Message Body"]') || - document.querySelector('[role="textbox"][g_editable="true"]') || - document.querySelector('div[contenteditable="true"][role="textbox"]'); - } - - if (!composeBody) { - devLog.warn("Gmail compose field not found (structure may have changed)"); - return false; - } - - // Try to focus the compose body - try { - (composeBody as HTMLElement).focus(); - } catch (focusError) { - devLog.warn("Failed to focus compose field:", focusError); - // Continue anyway - might still work - } - - const selection = window.getSelection(); - if (!selection || selection.rangeCount === 0) { - try { - const textNode = document.createTextNode(` ${text} `); - composeBody.appendChild(textNode); - const range = document.createRange(); - range.setStartAfter(textNode); - range.collapse(true); - selection?.removeAllRanges(); - selection?.addRange(range); - return true; - } catch (appendError) { - devLog.warn("Failed to append text (fallback method):", appendError); - return false; - } - } - - try { - const range = selection.getRangeAt(0); - range.deleteContents(); - const textNode = document.createTextNode(` ${text} `); - range.insertNode(textNode); - range.setStartAfter(textNode); - range.collapse(true); - selection.removeAllRanges(); - selection.addRange(range); - composeBody.dispatchEvent(new Event("input", { bubbles: true })); - composeBody.dispatchEvent(new Event("change", { bubbles: true })); - return true; - } catch (insertError) { - devLog.warn("Failed to insert text at cursor:", insertError); - return false; - } - } catch (error) { - devLog.error("Critical error inserting text (Gmail structure may have changed):", error); - return false; - } - } - - /** - * Helper function to show notification - * Fail silently if DOM manipulation fails (prevents breaking Gmail) - */ - function showGmailNotification(message: string, type: "success" | "error"): void { - try { - const notification = document.createElement("div"); - notification.style.cssText = ` - position: fixed; - bottom: 80px; - right: 80px; - padding: 10px 12px; - background: ${type === "success" ? "#111827" : "#752522"}; - color: white; - border: 1px solid #2b2b2b; - border-radius: 8px; - font-family: "Google Sans",Roboto,Arial,sans-serif; - font-size: 14px; - font-weight: 600; - z-index: 10000; - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.3), 0 2px 6px 2px rgba(0, 0, 0, 0.15); - display: flex; - align-items: center; - gap: 8px; - opacity: 0; - transform: translateY(10px); - transition: opacity 0.2s ease, transform 0.2s ease; - `; - - if (type === "success") { - const icon = document.createElement("span"); - icon.innerHTML = ` - - - - `; - icon.style.cssText = - "display: flex; align-items: center; justify-content: center; flex-shrink: 0;"; - notification.appendChild(icon); - } - - const text = document.createElement("span"); - text.textContent = message; - notification.appendChild(text); - - document.body.appendChild(notification); - - requestAnimationFrame(() => { - try { - notification.style.opacity = "1"; - notification.style.transform = "translateY(0)"; - } catch (_e) { - // Ignore animation errors - } - }); - - setTimeout(() => { - try { - notification.style.opacity = "0"; - notification.style.transform = "translateY(10px)"; - setTimeout(() => { - try { - notification.remove(); - } catch (_e) { - // Ignore removal errors - } - }, 200); - } catch (_e) { - // Ignore animation errors - } - }, 3000); - } catch (error) { - // Silently fail - notifications are non-critical - devLog.warn("Failed to show notification:", error); - } - } - - /** - * Watch for Google Calendar scheduling chips and add Cal.com suggestion button - */ - function watchForGoogleChips() { - try { - const observer = new MutationObserver((_mutations) => { - try { - const chips = document.querySelectorAll( - ".gmail_chip.gmail_ad_hoc_v2_content:not([data-calcom-chip-processed])" - ); - - chips.forEach((chip) => { - try { - chip.setAttribute("data-calcom-chip-processed", "true"); - handleGoogleChipDetected(chip as HTMLElement); - } catch (error) { - // Silently fail for individual chips to prevent breaking other chips - devLog.warn("Failed to process chip:", error); - } - }); - } catch (error) { - // Silently fail to prevent Gmail UI from breaking - devLog.warn("Failed to detect chips:", error); - } - }); - - observer.observe(document.body, { - childList: true, - subtree: true, - }); - - // Also check for existing chips on page load - setTimeout(() => { - try { - const existingChips = document.querySelectorAll( - ".gmail_chip.gmail_ad_hoc_v2_content:not([data-calcom-chip-processed])" - ); - existingChips.forEach((chip) => { - try { - chip.setAttribute("data-calcom-chip-processed", "true"); - handleGoogleChipDetected(chip as HTMLElement); - } catch (error) { - devLog.warn("Failed to process existing chip:", error); - } - }); - } catch (error) { - devLog.warn("Failed to find existing chips:", error); - } - }, 1000); - } catch (_error) { - // Critical failure - log but don't break Gmail - devLog.error("Failed to initialize chip watcher"); - } - } - - /** - * Handle when a Google Calendar chip is detected - */ - function handleGoogleChipDetected(chipElement: HTMLElement) { - try { - devLog.log("handleGoogleChipDetected called"); - - // Validate chip element exists and is in DOM - if (!chipElement || !chipElement.isConnected) { - devLog.warn("Invalid or disconnected chip element"); - return; - } - - // Only show action bars in ACTIVE compose windows (not in sent/inbox emails) - // Check if chip is in a contenteditable area (compose window) - const composeBody = chipElement.closest('[contenteditable="true"]'); - - if (!composeBody) { - // Silently skip - chip not in compose area (likely in sent/inbox) - return; - } - - const parsedData = parseGoogleChip(chipElement); - - if (!parsedData || parsedData.slots.length === 0) { - // Silently skip - chip not fully loaded or invalid - return; - } - - devLog.log( - `āœ… Google chip detected - ${parsedData.slots.length} slot${ - parsedData.slots.length > 1 ? "s" : "" - } (${parsedData.detectedDuration}min)` - ); - - // Safely check for parent element - if (!chipElement.parentElement) { - return; - } - - // Check if button already exists - const existingActionBar = chipElement.parentElement.querySelector( - ".cal-companion-action-bar" - ); - const scheduleId = chipElement.getAttribute("data-ad-hoc-schedule-id"); - - if (existingActionBar) { - // Action bar exists - check if schedule ID or duration changed - const existingScheduleId = existingActionBar.getAttribute("data-schedule-id"); - const existingDuration = existingActionBar.getAttribute("data-duration"); - - if ( - existingScheduleId === scheduleId && - existingDuration === String(parsedData.detectedDuration) - ) { - // Nothing changed, no need to recreate - return; - } - // Something changed - remove old action bar and create new one - devLog.log(`šŸ”„ Chip updated - ${parsedData.detectedDuration}min`); - try { - existingActionBar.remove(); - } catch (_e) { - // Silently ignore removal errors - } - } - - // Watch for mutations on this chip to detect ANY changes - const observer = new MutationObserver(() => { - try { - const newScheduleId = chipElement.getAttribute("data-ad-hoc-schedule-id"); - const actionBar = chipElement.parentElement?.querySelector( - ".cal-companion-action-bar" - ); - const currentScheduleId = actionBar?.getAttribute("data-schedule-id"); - - // Check if schedule ID changed (this changes when time range or duration changes) - if (currentScheduleId && newScheduleId && currentScheduleId !== newScheduleId) { - devLog.log(`šŸ”„ Time range/duration changed`); - try { - actionBar?.remove(); - // Recreate action bar with new data - handleGoogleChipDetected(chipElement); - } catch (_e) { - // Silently ignore recreation errors - } - } - } catch (_error) { - // Silently ignore observer errors - expected during DOM updates - } - }); - - try { - observer.observe(chipElement, { - childList: true, - subtree: true, - attributes: true, - attributeFilter: ["data-ad-hoc-v2-params"], - }); - } catch (_error) { - // Silently ignore observer setup errors - } - - // Create our own action bar below the chip - const actionBar = document.createElement("div"); - actionBar.className = "cal-companion-action-bar"; - actionBar.setAttribute("contenteditable", "false"); // Make non-editable - actionBar.setAttribute("data-cal-companion", "true"); // Marker for our elements - actionBar.setAttribute("data-duration", String(parsedData.detectedDuration)); - actionBar.setAttribute("data-slot-count", String(parsedData.slots.length)); - if (scheduleId) { - actionBar.setAttribute("data-schedule-id", scheduleId); - } - actionBar.style.cssText = ` - position: absolute; - z-index: 1000; - padding: 10px 14px; - background: #f8f9fa; - border: 1px solid #e0e0e0; - border-radius: 8px; - display: flex; - align-items: center; - gap: 10px; - font-family: 'Google Sans', Roboto, Arial, sans-serif; - font-size: 14px; - width: 434px; - max-width: 100%; - box-sizing: border-box; - user-select: none; - pointer-events: auto; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - `; - - // Cal.com icon with circular background - const icon = document.createElement("div"); - icon.style.cssText = ` - width: 32px; - height: 32px; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - background: #111827; - border-radius: 50%; - `; - icon.innerHTML = ` - - - - - - `; - - // Text with duration indicator - const text = document.createElement("span"); - text.textContent = `Suggest ${parsedData.detectedDuration}min Cal.com links`; - text.setAttribute("contenteditable", "false"); // Make text non-editable - text.style.cssText = ` - color: #1f1f1f; - font-weight: 500; - flex: 1; - user-select: none; - `; - - // Buttons container - const buttonsContainer = document.createElement("div"); - buttonsContainer.style.cssText = ` - display: flex; - gap: 8px; - flex-shrink: 0; - `; - - // "Suggest Links" Button - const suggestButton = document.createElement("button"); - suggestButton.className = "cal-companion-suggest-button"; - suggestButton.textContent = "Suggest Links"; - suggestButton.style.cssText = ` - padding: 8px 16px; - background: #111827; - color: white; - border: none; - border-radius: 6px; - font-size: 14px; - font-weight: 500; - cursor: pointer; - transition: background 0.2s ease; - font-family: 'Google Sans', Roboto, Arial, sans-serif; - `; - - suggestButton.addEventListener("mouseenter", () => { - suggestButton.style.background = "#1f2937"; - }); - - suggestButton.addEventListener("mouseleave", () => { - suggestButton.style.background = "#111827"; - }); - - suggestButton.addEventListener("click", (e) => { - e.preventDefault(); - e.stopPropagation(); - // Re-parse the chip to get fresh data (in case it changed) - const freshParsedData = parseGoogleChip(chipElement); - devLog.log( - "Suggest Links clicked, fresh parsed data:", - freshParsedData?.detectedDuration, - "min" - ); - if (freshParsedData && freshParsedData.slots.length > 0) { - showCalcomSuggestionMenu(chipElement, freshParsedData); - } else { - devLog.log("Failed to parse fresh data or no slots found"); - } - }); - - // "Insert Embed" Button - const embedButton = document.createElement("button"); - embedButton.className = "cal-companion-embed-button"; - embedButton.textContent = "Insert Embed"; - embedButton.style.cssText = ` - padding: 8px 16px; - background: white; - color: #111827; - border: 1px solid #111827; - border-radius: 6px; - font-size: 14px; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; - font-family: 'Google Sans', Roboto, Arial, sans-serif; - `; - - embedButton.addEventListener("mouseenter", () => { - embedButton.style.background = "#f8f9fa"; - }); - - embedButton.addEventListener("mouseleave", () => { - embedButton.style.background = "white"; - }); - - embedButton.addEventListener("click", async (e) => { - e.preventDefault(); - e.stopPropagation(); - - // Re-parse the chip to get fresh data - const freshParsedData = parseGoogleChip(chipElement); - devLog.log( - "Insert Embed clicked, fresh parsed data:", - freshParsedData?.detectedDuration, - "min" - ); - - if (!freshParsedData || freshParsedData.slots.length === 0) { - showGmailNotification("No time slots found", "error"); - return; - } - - // Fetch event types to get the matching one - try { - // Disable button and show loading state with opacity - embedButton.disabled = true; - embedButton.style.opacity = "0.6"; - embedButton.style.cursor = "not-allowed"; - - const response = await new Promise< - | { - data?: Array<{ - id: number; - title: string; - slug: string; - lengthInMinutes?: number; - users?: Array<{ username?: string }>; - }>; - } - | Array<{ - id: number; - title: string; - slug: string; - lengthInMinutes?: number; - users?: Array<{ username?: string }>; - }> - >((resolve, reject) => { - chrome.runtime.sendMessage({ action: "fetch-event-types" }, (result) => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)); - } else if (result?.error) { - reject(new Error(result.error)); - } else { - resolve(result); - } - }); - }); - - const eventTypes = - (response && "data" in response - ? response.data - : Array.isArray(response) - ? response - : []) || []; - const matchingEventType = eventTypes.find( - (et) => et.lengthInMinutes === freshParsedData.detectedDuration - ); - - if (!matchingEventType) { - showGmailNotification( - `No ${freshParsedData.detectedDuration}min event type found`, - "error" - ); - embedButton.disabled = false; - embedButton.style.opacity = "1"; - embedButton.style.cursor = "pointer"; - return; - } - - const username = matchingEventType.users?.[0]?.username || "user"; - - // Generate HTML embed - const embedHTML = generateEmailEmbedHTML({ - eventType: matchingEventType, - username: username, - slots: freshParsedData.slots, - duration: freshParsedData.detectedDuration, - timezone: freshParsedData.timezone, - timezoneOffset: freshParsedData.timezoneOffset, - }); - - // Insert HTML into Gmail - const inserted = insertGmailHTML(embedHTML, chipElement); - - if (inserted) { - showGmailNotification("Cal.com embed inserted!", "success"); - devLog.log("Email embed inserted successfully"); - - // Immediately remove the Google chip and action bar - try { - chipElement.remove(); - (actionBar as HTMLElement & { __cleanup?: () => void }).__cleanup?.(); - actionBar.remove(); - } catch (removeError) { - devLog.warn("Failed to remove chip/action bar:", removeError); - } - } else { - showGmailNotification("Failed to insert embed", "error"); - } - - embedButton.disabled = false; - embedButton.style.opacity = "1"; - embedButton.style.cursor = "pointer"; - } catch (error) { - devLog.error("Failed to insert embed"); - - // Check if this is the "Extension context invalidated" error - const isContextInvalidated = - error instanceof Error && error.message.includes("Extension context invalidated"); - - if (isContextInvalidated) { - showGmailNotification("Extension reloaded - please reload Gmail", "error"); - } else { - showGmailNotification("Failed to insert embed", "error"); - } - - embedButton.disabled = false; - embedButton.style.opacity = "1"; - embedButton.style.cursor = "pointer"; - } - }); - - buttonsContainer.appendChild(suggestButton); - buttonsContainer.appendChild(embedButton); - - // Close button (Ɨ) to remove the action bar completely - const closeBtn = document.createElement("button"); - closeBtn.className = "cal-companion-close-btn"; - closeBtn.setAttribute("aria-label", "Close"); - closeBtn.style.cssText = ` - padding: 6px; - background: transparent; - border: none; - border-radius: 4px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: background 0.15s; - flex-shrink: 0; - color: #5f6368; - `; - closeBtn.innerHTML = ` - - - - `; - - closeBtn.addEventListener("mouseenter", () => { - closeBtn.style.background = "#e8eaed"; - }); - - closeBtn.addEventListener("mouseleave", () => { - closeBtn.style.background = "transparent"; - }); - - closeBtn.addEventListener("click", (e) => { - e.preventDefault(); - e.stopPropagation(); - - // Remove the action bar completely - try { - // Call cleanup function if it exists - (actionBar as HTMLElement & { __cleanup?: () => void }).__cleanup?.(); - actionBar.remove(); - } catch (_error) { - // Silently ignore removal errors - } - }); - - // Assemble action bar - try { - actionBar.appendChild(icon); - actionBar.appendChild(text); - actionBar.appendChild(buttonsContainer); - actionBar.appendChild(closeBtn); - - // Position the action bar as an overlay (like Grammarly) - // This ensures it never becomes part of the email body - const positionActionBar = () => { - try { - if (!document.body.contains(chipElement)) { - // Chip removed, clean up - actionBar.remove(); - return; - } - - const chipRect = chipElement.getBoundingClientRect(); - - // Position below the chip with 8px gap - const top = chipRect.bottom + window.scrollY + 8; - const left = chipRect.left + window.scrollX; - - actionBar.style.top = `${top}px`; - actionBar.style.left = `${left}px`; - } catch (_error) { - // Silently ignore positioning errors - } - }; - - // Before appending, clean up orphaned or duplicate action bars for THIS chip only - // (happens when Google changes duration - creates new chip, but old action bar still floating) - const existingOverlayBars = document.querySelectorAll(".cal-companion-action-bar"); - existingOverlayBars.forEach((bar) => { - const barScheduleId = bar.getAttribute("data-schedule-id"); - - // Only remove action bars that: - // 1. Belong to THIS chip (same schedule ID) - we're about to create a new one - // 2. OR their chip no longer exists (orphaned) - if (barScheduleId === scheduleId) { - // This bar belongs to the current chip - remove it (we'll create a fresh one) - try { - (bar as Element & { __cleanup?: () => void }).__cleanup?.(); - bar.remove(); - } catch (_e) { - // Ignore removal errors - } - } else { - // This bar belongs to a different chip - check if that chip still exists - try { - const chipForBar = document.querySelector( - `.gmail_chip.gmail_ad_hoc_v2_content[data-ad-hoc-schedule-id="${barScheduleId}"]` - ); - if (!chipForBar) { - // Chip is gone, this is an orphaned action bar - remove it - (bar as Element & { __cleanup?: () => void }).__cleanup?.(); - bar.remove(); - } - // Else: chip exists, keep this action bar (belongs to another chip) - } catch (_e) { - // Ignore errors when checking - } - } - }); - - // Append to document.body (outside email content, like Grammarly) - document.body.appendChild(actionBar); - - // Initial positioning with slight delay to ensure chip is rendered - setTimeout(() => { - positionActionBar(); - }, 100); - - // Update position on scroll and resize - const updatePosition = () => { - if (document.body.contains(chipElement) && document.body.contains(actionBar)) { - positionActionBar(); - } else { - // Chip removed, clean up - try { - actionBar.remove(); - } catch (_e) { - // Ignore - } - } - }; - - window.addEventListener("scroll", updatePosition, true); - window.addEventListener("resize", updatePosition); - - // Store cleanup function - (actionBar as HTMLElement & { __cleanup?: () => void }).__cleanup = () => { - window.removeEventListener("scroll", updatePosition, true); - window.removeEventListener("resize", updatePosition); - }; - } catch (_error) { - devLog.error("Failed to create or insert action bar"); - // Clean up if something failed - try { - actionBar.remove(); - } catch (_e) { - // Ignore cleanup errors - } - } - } catch (_error) { - // Catch-all to prevent breaking Gmail UI - devLog.error("Error handling Google chip"); - return; - } - } - - /** - * Show Cal.com suggestion menu for Google Calendar chip - CENTERED FULL-SCREEN MODAL - */ - async function showCalcomSuggestionMenu( - chipElement: HTMLElement, - parsedData: { - detectedDuration: number; - slots: Array<{ - isoDate: string; - date: string; - isoTimestamp: string; - time?: string; - startTime?: string; - endTime?: string; - }>; - timezone: string; - timezoneOffset: string; - } - ) { - devLog.log( - "Opening menu for", - parsedData.detectedDuration, - "min with", - parsedData.slots.length, - "slots" - ); - - // Remove existing menu if any (to support reopening with new data) - const existingBackdrop = document.querySelector(".cal-companion-google-chip-backdrop"); - if (existingBackdrop) { - devLog.log("Removing existing backdrop"); - existingBackdrop.remove(); - // Wait a tick to ensure DOM is updated before creating new menu - await new Promise((resolve) => setTimeout(resolve, 0)); - } - - // Create full-screen backdrop (like FullScreenModal in companion) - const backdrop = document.createElement("div"); - backdrop.className = "cal-companion-google-chip-backdrop"; - backdrop.style.cssText = ` - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 2147483647; - display: flex; - align-items: center; - justify-content: center; - background-color: rgba(0, 0, 0, 0.5); - backdrop-filter: blur(4px); - `; - - // Create menu (centered modal content) - const menu = document.createElement("div"); - menu.className = "cal-companion-google-chip-menu"; - menu.style.cssText = ` - width: 480px; - max-width: 90vw; - max-height: 80vh; - background: white; - border-radius: 16px; - box-shadow: 0 20px 60px rgba(0,0,0,0.3); - overflow: hidden; - display: flex; - flex-direction: column; - font-family: "Google Sans", Roboto, Arial, sans-serif; - pointer-events: auto; - `; - - // Header - const header = document.createElement("div"); - header.style.cssText = ` - padding: 20px 24px; - border-bottom: 1px solid #e5e5ea; - display: flex; - justify-content: space-between; - align-items: center; - background: white; - flex-shrink: 0; - `; - header.innerHTML = ` -
-
šŸ“… Suggest Cal.com Links
-
${ - parsedData.slots.length - } time slot${parsedData.slots.length > 1 ? "s" : ""} • ${ - parsedData.detectedDuration - }min each
-
- - `; - - menu.appendChild(header); - backdrop.appendChild(menu); - document.body.appendChild(backdrop); - - // Close on backdrop click - backdrop.addEventListener("click", (e) => { - if (e.target === backdrop) { - backdrop.remove(); - } - }); - - // Close button click and hover - const closeBtn = header.querySelector(".close-menu") as HTMLButtonElement; - closeBtn.addEventListener("click", () => { - backdrop.remove(); - }); - closeBtn.addEventListener("mouseenter", () => { - closeBtn.style.background = "#f0f0f0"; - }); - closeBtn.addEventListener("mouseleave", () => { - closeBtn.style.background = "none"; - }); - - // Create scrollable content container - const contentContainer = document.createElement("div"); - contentContainer.className = "cal-companion-menu-content"; - contentContainer.style.cssText = ` - overflow-y: auto; - flex: 1; - `; - menu.appendChild(contentContainer); - - // Show loading - const loadingDiv = document.createElement("div"); - loadingDiv.style.cssText = - "padding: 32px 20px; text-align: center; color: #666; font-size: 14px;"; - loadingDiv.textContent = "Loading event types..."; - contentContainer.appendChild(loadingDiv); - - try { - // Fetch event types (will use cache if available) - let eventTypes: Array<{ - id: number; - title: string; - slug: string; - lengthInMinutes?: number; - description?: string; - users?: Array<{ username?: string }>; - }> = []; - - // Check cache first - const now = Date.now(); - const isCacheValid = - eventTypesCache && cacheTimestamp && now - cacheTimestamp < CACHE_DURATION; - - if (isCacheValid && eventTypesCache) { - eventTypes = eventTypesCache; - } else { - // Fetch from background script - const response = await new Promise< - | { - data?: Array<{ - id: number; - title: string; - slug: string; - lengthInMinutes?: number; - description?: string; - users?: Array<{ username?: string }>; - }>; - } - | Array<{ - id: number; - title: string; - slug: string; - lengthInMinutes?: number; - description?: string; - users?: Array<{ username?: string }>; - }> - >((resolve, reject) => { - chrome.runtime.sendMessage({ action: "fetch-event-types" }, (response) => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)); - } else if (response?.error) { - reject(new Error(response.error)); - } else { - resolve(response); - } - }); - }); - - if (response && "data" in response && response.data) { - eventTypes = response.data; - } else if (Array.isArray(response)) { - eventTypes = response; - } - - // Update cache - eventTypesCache = eventTypes; - cacheTimestamp = now; - } - - if (!eventTypes || eventTypes.length === 0) { - loadingDiv.textContent = "No event types found"; - return; - } - - loadingDiv.remove(); - - // Filter event types by matching duration - const matchingEventTypes = eventTypes.filter( - (et) => et.lengthInMinutes === parsedData.detectedDuration - ); - - // If no matching event types, show create prompt - if (matchingEventTypes.length === 0) { - const noMatchDiv = document.createElement("div"); - noMatchDiv.style.cssText = "padding: 20px 16px; text-align: center;"; - noMatchDiv.innerHTML = ` -
- - - - - -
-
- No ${parsedData.detectedDuration}min Event Type Found -
-
- Google is suggesting ${parsedData.detectedDuration}-minute time slots, but you don't have any ${parsedData.detectedDuration}min event types configured. -
- - `; - contentContainer.appendChild(noMatchDiv); - - // Add click handler for create button - const createBtn = noMatchDiv.querySelector(".create-event-type-btn"); - createBtn?.addEventListener("click", () => { - // Get username from any existing event type, or use a default - const username = - eventTypes.length > 0 ? eventTypes[0].users?.[0]?.username || "user" : "user"; - - const createUrl = `https://app.cal.com/event-types?dialog=new&eventPage=${username}`; - window.open(createUrl, "_blank"); - showGmailNotification( - `Opening Cal.com to create ${parsedData.detectedDuration}min event type`, - "success" - ); - - devLog.log("Opening create URL:", createUrl); - }); - - createBtn?.addEventListener("mouseenter", (e) => { - (e.target as HTMLElement).style.backgroundColor = "#333"; - }); - - createBtn?.addEventListener("mouseleave", (e) => { - (e.target as HTMLElement).style.backgroundColor = "#000"; - }); - - return; // Don't show slots if no matching event types - } - - // Event type selector (only show matching event types) - const selectorDiv = document.createElement("div"); - selectorDiv.style.cssText = - "padding: 12px 16px; border-bottom: 1px solid #e5e5ea; background: #f8f9fa; position: relative; z-index: 5; pointer-events: auto;"; - - // Create label - const label = document.createElement("label"); - label.style.cssText = - "font-size: 12px; color: #666; font-weight: 500; display: block; margin-bottom: 6px;"; - label.textContent = - matchingEventTypes.length === 1 - ? `Event Type (${parsedData.detectedDuration}min)` - : `Select ${parsedData.detectedDuration}min Event Type`; - selectorDiv.appendChild(label); - - // Track selected event type - let selectedEventTypeIndex = 0; - - if (matchingEventTypes.length === 1) { - // Single event type - just show it - const displayDiv = document.createElement("div"); - displayDiv.style.cssText = ` - padding: 10px 12px; - border: 1px solid #e5e5ea; - border-radius: 8px; - font-size: 14px; - background: white; - font-family: inherit; - color: #000; - font-weight: 500; - box-sizing: border-box; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - `; - displayDiv.textContent = matchingEventTypes[0].title; - selectorDiv.appendChild(displayDiv); - } else { - // Multiple event types - custom dropdown - const customDropdown = document.createElement("div"); - customDropdown.className = "custom-event-type-dropdown"; - customDropdown.style.cssText = "position: relative;"; - - // Selected display button - const selectedDisplay = document.createElement("div"); - selectedDisplay.style.cssText = ` - padding: 10px 12px; - border: 1px solid #e5e5ea; - border-radius: 8px; - font-size: 14px; - background: white; - font-family: inherit; - color: #000; - cursor: pointer; - box-sizing: border-box; - display: flex; - justify-content: space-between; - align-items: center; - transition: border-color 0.15s; - pointer-events: auto; - `; - selectedDisplay.innerHTML = ` - ${matchingEventTypes[0].title} (${matchingEventTypes[0].lengthInMinutes}min) - - - - `; - - // Dropdown options container - const optionsContainer = document.createElement("div"); - optionsContainer.style.cssText = ` - position: absolute; - top: 100%; - left: 0; - right: 0; - margin-top: 4px; - background: white; - border: 1px solid #e5e5ea; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); - max-height: 200px; - overflow-y: auto; - display: none; - z-index: 1000; - pointer-events: auto; - `; - - // Create options - matchingEventTypes.forEach((et, index: number) => { - const option = document.createElement("div"); - option.className = "dropdown-option"; - option.style.cssText = ` - padding: 10px 12px; - font-size: 14px; - color: #000; - cursor: pointer; - transition: background-color 0.1s; - border-bottom: ${ - index < matchingEventTypes.length - 1 ? "1px solid #f0f0f0" : "none" - }; - pointer-events: auto; - `; - option.textContent = `${et.title} (${et.lengthInMinutes}min)`; - option.setAttribute("data-index", index.toString()); - - // Hover effect - option.addEventListener("mouseenter", () => { - option.style.backgroundColor = "#f8f9fa"; - }); - option.addEventListener("mouseleave", () => { - option.style.backgroundColor = "white"; - }); - - // Click to select - option.addEventListener("click", (e) => { - e.stopPropagation(); - selectedEventTypeIndex = index; - const selectedText = selectedDisplay.querySelector(".selected-text"); - if (selectedText) { - selectedText.textContent = `${et.title} (${et.lengthInMinutes}min)`; - } - optionsContainer.style.display = "none"; - devLog.log("Event type changed to:", et.slug); - }); - - optionsContainer.appendChild(option); - }); - - // Toggle dropdown on click - selectedDisplay.addEventListener("click", (e) => { - e.stopPropagation(); - const isOpen = optionsContainer.style.display === "block"; - optionsContainer.style.display = isOpen ? "none" : "block"; - selectedDisplay.style.borderColor = isOpen ? "#e5e5ea" : "#000"; - }); - - // Hover effect on selected display - selectedDisplay.addEventListener("mouseenter", () => { - selectedDisplay.style.borderColor = "#000"; - }); - selectedDisplay.addEventListener("mouseleave", () => { - if (optionsContainer.style.display !== "block") { - selectedDisplay.style.borderColor = "#e5e5ea"; - } - }); - - customDropdown.appendChild(selectedDisplay); - customDropdown.appendChild(optionsContainer); - selectorDiv.appendChild(customDropdown); - - // Close dropdown when clicking outside (with cleanup) - const closeDropdown = (e: MouseEvent) => { - if (!customDropdown.contains(e.target as Node)) { - optionsContainer.style.display = "none"; - selectedDisplay.style.borderColor = "#e5e5ea"; - } - }; - document.addEventListener("click", closeDropdown); - - // Clean up listener when modal closes - backdrop.addEventListener( - "remove", - () => { - document.removeEventListener("click", closeDropdown); - }, - { once: true } - ); - } - - contentContainer.appendChild(selectorDiv); - - // Time slots list - const slotsContainer = document.createElement("div"); - slotsContainer.style.cssText = "padding: 12px;"; - - parsedData.slots.forEach((slot, index: number) => { - const slotItem = document.createElement("div"); - slotItem.style.cssText = ` - padding: 14px; - margin: 6px 0; - border: 1px solid #e5e5ea; - border-radius: 10px; - display: flex; - justify-content: space-between; - align-items: center; - transition: all 0.15s; - cursor: default; - background: white; - `; - - slotItem.innerHTML = ` -
-
${slot.date}
-
${slot.startTime} – ${slot.endTime}
-
- - `; - - // Add pointer events to slot item - slotItem.style.pointerEvents = "auto"; - - // Hover effect on entire slot item - slotItem.addEventListener("mouseenter", () => { - devLog.log("Mouse entered slot", index); - slotItem.style.borderColor = "#000"; - slotItem.style.backgroundColor = "#f8f9fa"; - const btn = slotItem.querySelector(".insert-slot-btn") as HTMLElement; - if (btn) btn.style.backgroundColor = "#333"; - }); - - slotItem.addEventListener("mouseleave", () => { - slotItem.style.borderColor = "#e5e5ea"; - slotItem.style.backgroundColor = "white"; - const btn = slotItem.querySelector(".insert-slot-btn") as HTMLElement; - if (btn) btn.style.backgroundColor = "#000"; - }); - - slotsContainer.appendChild(slotItem); - }); - - contentContainer.appendChild(slotsContainer); - - // Add event listeners for insert buttons - const insertButtons = menu.querySelectorAll(".insert-slot-btn"); - - devLog.log("Found", insertButtons.length, "insert buttons"); - - insertButtons.forEach((btn) => { - btn.addEventListener("click", (e) => { - devLog.log("Insert button clicked!"); - e.preventDefault(); - e.stopPropagation(); - const slotIndexAttr = btn.getAttribute("data-slot-index"); - if (!slotIndexAttr) return; - const slotIndex = parseInt(slotIndexAttr, 10); - const slot = parsedData.slots[slotIndex]; - devLog.log("Inserting link for slot", slotIndex, slot); - - // Get selected event type from the tracked index - const selectedEventType = matchingEventTypes[selectedEventTypeIndex]; - const selectedSlug = selectedEventType.slug; - const selectedUsername = selectedEventType.users?.[0]?.username || "user"; - - // Generate Cal.com URL with slot parameters - const baseUrl = `https://cal.com/${selectedUsername}/${selectedSlug}`; - const params = new URLSearchParams({ - overlayCalendar: "true", - date: slot.isoDate, - slot: slot.isoTimestamp, - }); - const calcomUrl = `${baseUrl}?${params.toString()}`; - - devLog.log("Generated URL:", calcomUrl); - - // Insert link into Gmail compose (pass chipElement to target the correct compose window) - const inserted = insertGmailText(calcomUrl, chipElement); - - if (inserted) { - showGmailNotification("Cal.com link inserted!", "success"); - backdrop.remove(); - - // Immediately remove the Google chip and its action bar - try { - const scheduleId = chipElement.getAttribute("data-ad-hoc-schedule-id"); - const actionBar = scheduleId - ? document.querySelector( - `.cal-companion-action-bar[data-schedule-id="${scheduleId}"]` - ) - : chipElement.parentElement?.querySelector(".cal-companion-action-bar"); - - chipElement.remove(); - - if (actionBar) { - (actionBar as Element & { __cleanup?: () => void }).__cleanup?.(); - actionBar.remove(); - } - } catch (removeError) { - devLog.warn("Failed to remove chip/action bar:", removeError); - } - } else { - showGmailNotification("Failed to insert link", "error"); - } - }); - }); - } catch (error) { - devLog.error("Error showing suggestion menu"); - loadingDiv.remove(); - - const errorMessage = error instanceof Error ? error.message : ""; - const isContextInvalidated = errorMessage.includes("Extension context invalidated"); - const isAuthError = - errorMessage.includes("OAuth") || - errorMessage.includes("access token") || - errorMessage.includes("sign in") || - errorMessage.includes("authentication"); - - // Show user-friendly error message - const errorDiv = document.createElement("div"); - errorDiv.style.cssText = "padding: 24px 20px; text-align: center;"; - - if (isAuthError) { - // Not logged in - close backdrop and open sidebar directly - backdrop.remove(); - openSidebar(); - return; - } else if (isContextInvalidated) { - // Extension reloaded error - errorDiv.innerHTML = ` -
- - - - - -
-
- Extension Reloaded -
-
- The extension was updated. Please reload this page to continue. -
- - `; - - contentContainer.appendChild(errorDiv); - - const reloadBtn = errorDiv.querySelector(".reload-page-btn"); - reloadBtn?.addEventListener("click", () => { - window.location.reload(); - }); - reloadBtn?.addEventListener("mouseenter", (e) => { - (e.target as HTMLElement).style.backgroundColor = "#ff5252"; - }); - reloadBtn?.addEventListener("mouseleave", (e) => { - (e.target as HTMLElement).style.backgroundColor = "#ff6b6b"; - }); - } else { - // Generic error - errorDiv.innerHTML = ` -
- - - - - -
-
- Something went wrong -
-
- Failed to load event types. Please try again. -
- `; - - contentContainer.appendChild(errorDiv); - } - } - } - - /** - * Parse Google Calendar scheduling chip - * Returns null if structure has changed or parsing fails (fail gracefully) - */ - function parseGoogleChip(chipElement: HTMLElement): { - scheduleId: string; - timezone: string; - timezoneOffset: string; - slots: Array<{ - date: string; - startTime: string; - endTime: string; - durationMinutes: number; - isoDate: string; - isoTimestamp: string; - googleUrl: string; - }>; - detectedDuration: number; - } | null { - try { - // Validate input - if (!chipElement || typeof chipElement.getAttribute !== "function") { - devLog.warn("Invalid chip element passed to parser"); - return null; - } - - // Get schedule ID - if this is missing, this might not be a valid chip - const scheduleId = chipElement.getAttribute("data-ad-hoc-schedule-id"); - - if (!scheduleId) { - // Silently return - this is likely not a fully-loaded chip - // (happens frequently during Gmail DOM updates) - return null; - } - - devLog.log("Valid chip detected - Schedule ID:", `${scheduleId?.slice(0, 20)}...`); - - // Parse timezone (non-critical - fallback to UTC if structure changed) - let timezone = "UTC"; - let timezoneOffset = "GMT+00:00"; - - try { - // First, try to get the IANA timezone from data-ad-hoc-v2-params - const paramsAttr = chipElement.getAttribute("data-ad-hoc-v2-params"); - devLog.log("data-ad-hoc-v2-params:", paramsAttr); - - if (paramsAttr) { - // The timezone is at the end of the params string, like: "Asia/Kolkata" - // Try multiple patterns as Gmail might format it differently - let tzMatch = paramsAttr.match(/"([^&]+)"\]/); - - // Also try without HTML entities - if (!tzMatch) { - tzMatch = paramsAttr.match(/"([^"]+)"\]/); - } - - // Also try with escaped quotes - if (!tzMatch) { - tzMatch = paramsAttr.match(/\\"([^\\]+)\\"\]/); - } - - if (tzMatch?.[1]) { - timezone = tzMatch[1]; // e.g., "Asia/Kolkata" - devLog.log("Parsed IANA timezone from data attribute:", timezone); - } else { - devLog.warn("Failed to extract timezone from params attribute"); - } - } else { - devLog.warn("No data-ad-hoc-v2-params attribute found"); - } - - // Also get the display timezone and offset from the UI text - const timezoneText = chipElement.querySelector("td")?.textContent?.trim() || ""; - const timezoneMatch = timezoneText.match(/^(.+?)\s*-\s*(.+?)\s*\((.+?)\)$/); - - if (timezoneMatch) { - timezoneOffset = timezoneMatch[3]?.trim() || "GMT+00:00"; - devLog.log("Parsed timezone offset from UI:", timezoneOffset); - } - } catch (tzError) { - // Non-critical error - continue with default timezone - devLog.warn("Failed to parse timezone, using UTC:", tzError); - } - - // Find all time slot links - critical for functionality - let slotLinks: Element[] = []; - try { - slotLinks = Array.from(chipElement.querySelectorAll("a[href*='slotStartTime']")); - } catch (error) { - devLog.warn("Failed to find slot links:", error); - return null; - } - - if (slotLinks.length === 0) { - // Gmail structure might have changed or chip not fully loaded - return null; - } - - const slots: Array<{ - date: string; - startTime: string; - endTime: string; - durationMinutes: number; - isoDate: string; - isoTimestamp: string; - googleUrl: string; - }> = []; - let detectedDuration = 60; - - slotLinks.forEach((link) => { - try { - const href = (link as HTMLAnchorElement).href; - if (!href) return; - - const url = new URL(href); - - const slotStartTime = url.searchParams.get("slotStartTime"); - const slotDurationMinutes = url.searchParams.get("slotDurationMinutes"); - - if (!slotStartTime || !slotDurationMinutes) return; - - const durationMinutes = parseInt(slotDurationMinutes, 10); - if (Number.isNaN(durationMinutes) || durationMinutes <= 0) return; - - detectedDuration = durationMinutes; - - const startTimestamp = parseInt(slotStartTime, 10); - if (Number.isNaN(startTimestamp)) return; - - const startDate = new Date(startTimestamp); - const endDate = new Date(startDate.getTime() + durationMinutes * 60 * 1000); - - // Validate dates - if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) return; - - const dateOptions: Intl.DateTimeFormatOptions = { - weekday: "short", - day: "numeric", - month: "long", - }; - const timeOptions: Intl.DateTimeFormatOptions = { - hour: "numeric", - minute: "2-digit", - hour12: true, - }; - - const date = startDate.toLocaleDateString("en-US", dateOptions); - const startTime = startDate.toLocaleTimeString("en-US", timeOptions).toLowerCase(); - const endTime = endDate.toLocaleTimeString("en-US", timeOptions).toLowerCase(); - - const isoDate = startDate.toISOString().split("T")[0]; - const isoTimestamp = startDate.toISOString(); - - slots.push({ - date, - startTime, - endTime, - durationMinutes, - isoDate, - isoTimestamp, - googleUrl: href, - }); - } catch (slotError) { - // Skip individual slot if it fails - don't break entire parsing - devLog.warn("Failed to parse individual slot:", slotError); - } - }); - - if (slots.length === 0) { - // No valid slots found - Gmail structure might have changed - return null; - } - - devLog.log( - `Parsed chip - ${slots.length} slots, ${detectedDuration}min, timezone: ${timezone}` - ); - - return { - scheduleId, - timezone, - timezoneOffset, - slots, - detectedDuration, - }; - } catch (error) { - // Critical error in parsing - fail gracefully without breaking Gmail - devLog.warn("Failed to parse Google chip (Gmail structure may have changed):", error); - return null; - } - } - - // Start watching for Google Calendar chips - watchForGoogleChips(); - } - - // ========== Initialize Integrations ========== - // These calls are at the end of main() to ensure all functions are defined before being called - - // Initialize Gmail integration if on Gmail - if (window.location.hostname === "mail.google.com") { - try { - initGmailIntegration(); - devLog.log("Gmail integration initialized successfully"); - } catch (error) { - devLog.error("Failed to initialize Gmail integration", error); - } - } - - // Initialize LinkedIn integration if on LinkedIn - if (window.location.hostname === "www.linkedin.com") { - try { - initLinkedInIntegration(); - devLog.log("LinkedIn integration initialized successfully"); - } catch (error) { - devLog.error("Failed to initialize LinkedIn integration", error); - } - } - - // Initialize Google Calendar integration if on Google Calendar - if (window.location.hostname === "calendar.google.com") { - try { - initGoogleCalendarIntegration(); - devLog.log("Google Calendar integration initialized successfully"); - } catch (error) { - devLog.error("Failed to initialize Google Calendar integration", error); - } - } - }, -}); - -function defineContentScript(config: { matches: string[]; main: () => void }) { - return config; -} diff --git a/companion/extension/env.d.ts b/companion/extension/env.d.ts deleted file mode 100644 index 7918435a14..0000000000 --- a/companion/extension/env.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -/// - -interface ImportMetaEnv { - readonly EXPO_PUBLIC_CAL_API_KEY?: string; - readonly EXPO_PUBLIC_COMPANION_DEV_URL?: string; - - // Default OAuth config (Chrome/Brave) - readonly EXPO_PUBLIC_CALCOM_OAUTH_CLIENT_ID?: string; - readonly EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI?: string; - - // Firefox-specific OAuth config - readonly EXPO_PUBLIC_CALCOM_OAUTH_CLIENT_ID_FIREFOX?: string; - readonly EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI_FIREFOX?: string; - - // Safari-specific OAuth config - readonly EXPO_PUBLIC_CALCOM_OAUTH_CLIENT_ID_SAFARI?: string; - readonly EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI_SAFARI?: string; - - // Edge-specific OAuth config - readonly EXPO_PUBLIC_CALCOM_OAUTH_CLIENT_ID_EDGE?: string; - readonly EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI_EDGE?: string; - - // Browser target set during build - readonly BROWSER_TARGET?: "chrome" | "firefox" | "safari" | "edge"; -} - -interface ImportMeta { - readonly env: ImportMetaEnv; -} diff --git a/companion/extension/lib/google-calendar.ts b/companion/extension/lib/google-calendar.ts deleted file mode 100644 index c528cb1027..0000000000 --- a/companion/extension/lib/google-calendar.ts +++ /dev/null @@ -1,1391 +0,0 @@ -/// - -// Google Calendar integration: inject a no-show toggle next to attendees in event popups. -const bookingStatusCache = new Map; timestamp: number }>(); -const CACHE_TTL = 30000; // 30 seconds cache -let authStatusCache: { isAuthenticated: boolean; timestamp: number } | null = null; -const AUTH_CACHE_TTL = 5000; // 5 seconds -const NO_SHOW_BUTTON_SELECTOR = "[data-cal-noshow-btn]"; - -async function getIsAuthenticatedCached(): Promise { - const now = Date.now(); - if (authStatusCache && now - authStatusCache.timestamp < AUTH_CACHE_TTL) { - return authStatusCache.isAuthenticated; - } - - const authResponse = await new Promise<{ isAuthenticated: boolean }>((resolve) => { - chrome.runtime.sendMessage({ action: "check-auth-status" }, (response) => { - if (chrome.runtime.lastError) { - resolve({ isAuthenticated: false }); - } else { - resolve(response || { isAuthenticated: false }); - } - }); - }); - - authStatusCache = { isAuthenticated: authResponse.isAuthenticated, timestamp: now }; - return authResponse.isAuthenticated; -} - -// Styles for injected elements -const STYLES = ` - .cal-noshow-btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 6px; - padding: 4px 10px; - height: 24px; - font-size: 12px; - font-weight: 500; - border-radius: 4px; - border: 1px solid #dadce0; - background-color: #ffffff; - color: #3c4043; - cursor: pointer; - transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease; - font-family: 'Google Sans', Roboto, Arial, sans-serif; - vertical-align: middle; - white-space: nowrap; - } - - .cal-noshow-btn:hover { - background-color: #f8f9fa; - border-color: #bdc1c6; - color: #202124; - } - - .cal-noshow-btn:active { - background-color: #f1f3f4; - border-color: #bdc1c6; - } - - .cal-noshow-btn:disabled { - opacity: 0.5; - cursor: not-allowed; - background-color: #f8f9fa; - } - - .cal-noshow-btn.loading { - opacity: 0.7; - pointer-events: none; - } - - .cal-noshow-btn.marked { - background-color: #f8f9fa; - border-color: #5f6368; - color: #202124; - } - - .cal-noshow-btn.marked:hover { - background-color: #e8eaed; - border-color: #3c4043; - } - - .cal-noshow-btn-icon { - width: 16px; - height: 16px; - display: block; - flex-shrink: 0; - } - - .cal-noshow-btn-container { - display: inline-flex; - align-items: center; - margin-left: 12px; - vertical-align: middle; - } - - [data-cal-noshow-marked="true"] .SDqFWd span, - [data-cal-noshow-marked="true"] .SDqFWd { - text-decoration: line-through; - color: #5f6368; - } - - - .cal-noshow-spinner { - width: 10px; - height: 10px; - border: 2px solid #dadce0; - border-top-color: #5f6368; - border-radius: 50%; - animation: cal-spin 0.6s linear infinite; - } - - @keyframes cal-spin { - to { transform: rotate(360deg); } - } - - .cal-noshow-dialog-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 2147483647; - } - - .cal-noshow-dialog { - background-color: white; - border-radius: 8px; - padding: 24px; - max-width: 400px; - width: 90%; - box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); - font-family: 'Google Sans', Roboto, Arial, sans-serif; - } - - .cal-noshow-dialog h3 { - margin: 0 0 12px 0; - font-size: 18px; - font-weight: 500; - color: #111827; - } - - .cal-noshow-dialog p { - margin: 0 0 20px 0; - font-size: 14px; - color: #6b7280; - line-height: 1.5; - } - - .cal-noshow-dialog-buttons { - display: flex; - justify-content: flex-end; - gap: 12px; - } - - .cal-noshow-dialog-btn { - padding: 8px 16px; - font-size: 14px; - font-weight: 500; - border-radius: 6px; - cursor: pointer; - transition: all 0.15s ease; - font-family: 'Google Sans', Roboto, Arial, sans-serif; - } - - .cal-noshow-dialog-btn.cancel { - background-color: white; - border: 1px solid #d1d5db; - color: #374151; - } - - .cal-noshow-dialog-btn.cancel:hover { - background-color: #f9fafb; - } - - .cal-noshow-dialog-btn.confirm { - background-color: #202124; - border: 1px solid #202124; - color: white; - } - - .cal-noshow-dialog-btn.confirm:hover { - background-color: #000000; - } - - .cal-noshow-dialog-btn.undo { - background-color: #202124; - border: 1px solid #202124; - color: white; - } - - .cal-noshow-dialog-btn.undo:hover { - background-color: #000000; - } - - .cal-noshow-alert { - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - padding: 16px 24px; - border-radius: 8px; - font-size: 14px; - font-weight: 500; - box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); - z-index: 2147483647; - font-family: 'Google Sans', Roboto, Arial, sans-serif; - animation: cal-fade-in 0.3s ease; - min-width: 200px; - text-align: center; - } - - .cal-noshow-alert.error { - background-color: #ffffff; - border: 1px solid #dadce0; - color: #202124; - } - - .cal-noshow-alert.success { - background-color: #ffffff; - border: 1px solid #dadce0; - color: #202124; - } - - .cal-noshow-alert.info { - background-color: #ffffff; - border: 1px solid #dadce0; - color: #202124; - } - - @keyframes cal-fade-in { - from { - opacity: 0; - transform: translate(-50%, -50%) scale(0.95); - } - to { - opacity: 1; - transform: translate(-50%, -50%) scale(1); - } - } -`; - -function setAttendeeMarkedVisual(attendeeElement: Element | null, marked: boolean): void { - if (!attendeeElement) return; - if (marked) { - attendeeElement.setAttribute("data-cal-noshow-marked", "true"); - } else { - attendeeElement.removeAttribute("data-cal-noshow-marked"); - } -} - -// Inject styles into the page -function injectStyles(): void { - if (document.getElementById("cal-noshow-styles")) return; - - const styleSheet = document.createElement("style"); - styleSheet.id = "cal-noshow-styles"; - styleSheet.textContent = STYLES; - document.head.appendChild(styleSheet); -} - -/** - * Extract booking UID from event description - * Looks for patterns like: https://cal.com/booking/{uid} or https://app.cal.com/booking/{uid} - */ -function extractBookingUid(text: string): string | null { - const regex = /https:\/\/(?:app\.)?cal\.com\/booking\/([a-zA-Z0-9]+)/; - const match = text.match(regex); - return match ? match[1] : null; -} - -/** - * Check if an event is in the past - */ -function isEventInPast(eventElement: Element): boolean { - const now = new Date(); - const dateTimeText = eventElement.textContent || ""; - - // Try to infer the end time from the popup text (best effort). - const datePatterns = [ - // Full date format: "Saturday, 20 December 2024ā‹…11:00 – 11:30am" - /(\d{1,2})\s+(January|February|March|April|May|June|July|August|September|October|November|December)(?:\s+(\d{4}))?/i, - // Short date format: "Dec 20, 2024" - /(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2}),?\s+(\d{4})/i, - // Numeric format: "12/20/2024" or "20/12/2024" - /(\d{1,2})[/-](\d{1,2})[/-](\d{4})/, - ]; - - const months: Record = { - january: 0, - february: 1, - march: 2, - april: 3, - may: 4, - june: 5, - july: 6, - august: 7, - september: 8, - october: 9, - november: 10, - december: 11, - }; - - for (const pattern of datePatterns) { - const match = dateTimeText.match(pattern); - if (match) { - let day: number, month: number, year: number; - - if (pattern === datePatterns[0]) { - // Format: "20 December 2024" - day = parseInt(match[1], 10); - month = months[match[2].toLowerCase()]; - year = match[3] ? parseInt(match[3], 10) : now.getFullYear(); - } else if (pattern === datePatterns[1]) { - // Format: "December 20, 2024" - month = months[match[1].toLowerCase()]; - day = parseInt(match[2], 10); - year = parseInt(match[3], 10); - } else { - // Format: "12/20/2024" or "20/12/2024" - // Assume US format (MM/DD/YYYY) first, fallback to DD/MM/YYYY - const first = parseInt(match[1], 10); - const second = parseInt(match[2], 10); - year = parseInt(match[3], 10); - - if (first > 12) { - // DD/MM/YYYY format - day = first; - month = second - 1; - } else { - // MM/DD/YYYY format - month = first - 1; - day = second; - } - } - - // Extract end time if available to check if event has ended - const timeMatch = dateTimeText.match(/(\d{1,2}):(\d{2})\s*(?:AM|PM|am|pm)?/g); - if (timeMatch && timeMatch.length > 0) { - // Use the last time (end time) - const lastTime = timeMatch[timeMatch.length - 1]; - const timeParts = lastTime.match(/(\d{1,2}):(\d{2})\s*(AM|PM|am|pm)?/i); - if (timeParts) { - let hours = parseInt(timeParts[1], 10); - const minutes = parseInt(timeParts[2], 10); - const isPM = timeParts[3]?.toUpperCase() === "PM"; - - if (isPM && hours !== 12) { - hours += 12; - } else if (!isPM && hours === 12) { - hours = 0; - } - - const eventDate = new Date(year, month, day, hours, minutes); - return eventDate < now; - } - } - - // If no time found, check if the date itself is in the past - const eventDate = new Date(year, month, day, 23, 59, 59); - return eventDate < now; - } - } - - // Fallback: data attributes (if present). - const eventStartAttr = - eventElement.getAttribute("data-start") || eventElement.getAttribute("data-event-start"); - if (eventStartAttr) { - try { - const eventDate = new Date(eventStartAttr); - if (!Number.isNaN(eventDate.getTime())) { - return eventDate < now; - } - } catch { - // Invalid date, continue to next strategy - } - } - - // Fallback: CSS/aria hints. - const hasPastIndicator = - eventElement.classList.contains("past") || - eventElement.classList.contains("completed") || - eventElement.getAttribute("aria-label")?.toLowerCase().includes("past") || - eventElement.querySelector(".past, .completed") !== null; - - if (hasPastIndicator) { - return true; - } - - // Fallback: keyword hints. - const pastKeywords = ["ended", "was", "past meeting", "completed", "finished"]; - const textLower = dateTimeText.toLowerCase(); - if (pastKeywords.some((keyword) => textLower.includes(keyword))) { - // Additional check: make sure it's not a future event description - const futureKeywords = ["will", "upcoming", "scheduled for", "starts"]; - if (!futureKeywords.some((keyword) => textLower.includes(keyword))) { - return true; - } - } - - // Default: assume not past if we can't determine - return false; -} - -/** - * Parse attendee information from the event popup - */ -interface AttendeeInfo { - name: string; - email: string; - isOrganizer: boolean; - element: Element; -} - -function parseAttendees(eventPopup: Element): AttendeeInfo[] { - const attendees: AttendeeInfo[] = []; - const seenEmails = new Set(); - - // Prefer Google Calendar attendee rows. - const attendeeItems = eventPopup.querySelectorAll('[role="treeitem"][data-email]'); - - attendeeItems.forEach((el) => { - const email = el.getAttribute("data-email"); - if (!email || seenEmails.has(email.toLowerCase())) { - return; - } - seenEmails.add(email.toLowerCase()); - - // Check if this is an organizer using aria-label - const ariaLabel = el.getAttribute("aria-label") || ""; - const isOrganizer = - ariaLabel.toLowerCase().includes("organiser") || - ariaLabel.toLowerCase().includes("organizer") || - el.textContent?.toLowerCase().includes("organiser") || - el.textContent?.toLowerCase().includes("organizer"); - - // Extract name from the SDqFWd span (Google Calendar's name display) - let name = email.split("@")[0]; - const nameSpan = el.querySelector(".SDqFWd span"); - if (nameSpan?.textContent) { - const nameText = nameSpan.textContent.trim(); - // Only use if it's not just the email - if (nameText !== email && nameText.includes("@") === false) { - name = nameText; - } - } else { - // Fallback: try to extract from aria-label - const labelMatch = ariaLabel.match(/^([^,]+)/); - if (labelMatch && labelMatch[1] !== email) { - name = labelMatch[1].trim(); - } - } - - attendees.push({ - name: name || email.split("@")[0], - email: email, - isOrganizer, - element: el, - }); - }); - - // Fallbacks (DOM variants, then text parsing). - const allText = eventPopup.innerHTML || eventPopup.textContent || ""; - const emailRegex = /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g; - - if (attendees.length === 0) { - const guestSelectors = [ - "[data-email]", - "[data-guest-email]", - "[data-attendee-email]", - '[role="listitem"][data-email]', - "[data-email-id]", - '[jsname="xR2lFb"]', - '[jsname="xR2lFb"] [data-email]', - "[data-attendee]", - "[data-participant]", - ]; - - for (const selector of guestSelectors) { - const guestElements = eventPopup.querySelectorAll(selector); - guestElements.forEach((el) => { - const email = - el.getAttribute("data-email") || - el.getAttribute("data-guest-email") || - el.getAttribute("data-attendee-email") || - el.getAttribute("data-email-id"); - - if (email && !seenEmails.has(email.toLowerCase())) { - seenEmails.add(email.toLowerCase()); - - // Check if this is an organizer - const text = (el.textContent || "").toLowerCase(); - const ariaLabel = el.getAttribute("aria-label") || ""; - const isOrganizer = - text.includes("organiser") || - text.includes("organizer") || - ariaLabel.toLowerCase().includes("organiser") || - ariaLabel.toLowerCase().includes("organizer") || - el.getAttribute("data-organizer") === "true" || - el.closest('[data-organizer="true"]') !== null; - - // Extract name - let name = email.split("@")[0]; - const nameElement = el.querySelector("[data-name], [data-display-name], .SDqFWd span"); - if (nameElement) { - const nameText = - nameElement.textContent || - nameElement.getAttribute("data-name") || - nameElement.getAttribute("data-display-name"); - if (nameText && nameText !== email && !nameText.includes("@")) { - name = nameText.trim(); - } - } else { - // Try to extract name from text content - const textContent = el.textContent || ""; - const nameMatch = textContent.match( - /^([^@\n]+?)(?:\s*[-–]\s*)?(?:organizer|organiser)?\s*$/i - ); - if (nameMatch && nameMatch[1].trim() !== email) { - name = nameMatch[1].trim(); - } - } - - attendees.push({ - name: name || email.split("@")[0], - email: email, - isOrganizer, - element: el, - }); - } - }); - } - } - - // Fallback: parse from "Who:" section. - if (attendees.length === 0) { - const whoSection = allText.match(/Who:([\s\S]*?)(?:Where:|When:|$)/i); - if (whoSection) { - const whoText = whoSection[1]; - const whoEmails: string[] = whoText.match(emailRegex) ?? []; - - whoEmails.forEach((email) => { - const emailLower = email.toLowerCase(); - if (seenEmails.has(emailLower)) return; - seenEmails.add(emailLower); - - const isOrganizer = - whoText.toLowerCase().includes(`${emailLower}`) && - (whoText.toLowerCase().includes("organizer") || - whoText.toLowerCase().includes("organiser")); - - // Try to find the name associated with this email - const nameMatch = whoText.match( - new RegExp( - `([^\\n]+?)\\s*[-–]?\\s*(?:Organizer|Organiser)?\\s*${email.replace( - /[.*+?^${}()|[\]\\]/g, - "\\$&" - )}`, - "i" - ) - ); - - attendees.push({ - name: nameMatch ? nameMatch[1].trim() : email.split("@")[0], - email: email, - isOrganizer, - element: eventPopup, - }); - }); - } - } - - // Fallback: scan guest list containers. - if (attendees.length === 0) { - const guestListContainers = eventPopup.querySelectorAll( - '[data-guests], [aria-label*="guest" i], [aria-label*="attendee" i], [jsname="xR2lFb"]' - ); - - guestListContainers.forEach((container) => { - const containerText = container.textContent || ""; - const emails: string[] = containerText.match(emailRegex) ?? []; - - emails.forEach((email) => { - const emailLower = email.toLowerCase(); - if (seenEmails.has(emailLower)) return; - seenEmails.add(emailLower); - - const text = containerText.toLowerCase(); - const isOrganizer = - text.includes("organiser") || - text.includes("organizer") || - container.querySelector('[aria-label*="organizer" i]') !== null; - - // Find the specific element containing this email - const emailElement = Array.from(container.querySelectorAll("*")).find((el) => - el.textContent?.includes(email) - ); - - const nameMatch = containerText.match( - new RegExp( - `([^\\n]+?)\\s*[-–]?\\s*(?:Organizer|Organiser)?\\s*${email.replace( - /[.*+?^${}()|[\]\\]/g, - "\\$&" - )}`, - "i" - ) - ); - - attendees.push({ - name: nameMatch ? nameMatch[1].trim() : email.split("@")[0], - email: email, - isOrganizer, - element: emailElement || container, - }); - }); - }); - } - - return attendees; -} - -/** - * Show confirmation dialog - */ -function showConfirmDialog( - attendeeEmail: string, - isUndo: boolean, - onConfirm: () => void, - onCancel: () => void -): void { - const overlay = document.createElement("div"); - overlay.className = "cal-noshow-dialog-overlay"; - - const dialog = document.createElement("div"); - dialog.className = "cal-noshow-dialog"; - - const title = isUndo ? "Undo No Show?" : "Mark as No Show?"; - const message = isUndo - ? `Remove the no-show status from "${attendeeEmail}"?` - : `Mark "${attendeeEmail}" as no-show for this meeting?`; - const confirmClass = isUndo ? "undo" : "confirm"; - const confirmText = "Confirm"; - - dialog.innerHTML = ` -

${title}

-

${message}

-
- - -
- `; - - overlay.appendChild(dialog); - document.body.appendChild(overlay); - - const cancelBtn = dialog.querySelector(".cancel"); - const confirmBtn = dialog.querySelector(`.${confirmClass}`); - - const cleanup = () => { - overlay.remove(); - }; - - cancelBtn?.addEventListener("click", () => { - cleanup(); - onCancel(); - }); - - confirmBtn?.addEventListener("click", () => { - cleanup(); - onConfirm(); - }); - - overlay.addEventListener("click", (e) => { - if (e.target === overlay) { - cleanup(); - onCancel(); - } - }); -} - -/** - * Show alert message - */ -function showAlert(message: string, type: "error" | "success" | "info" = "info"): void { - // Remove existing alerts - for (const el of document.querySelectorAll(".cal-noshow-alert")) { - el.remove(); - } - - const alert = document.createElement("div"); - alert.className = `cal-noshow-alert ${type}`; - alert.textContent = message; - document.body.appendChild(alert); - - setTimeout(() => { - alert.remove(); - }, 1200); -} - -/** - * Open the companion sidebar for login - */ -function openSidebarForLogin(): void { - // Dispatch custom event that content script listens for - window.dispatchEvent(new CustomEvent("cal-companion-open-sidebar")); -} - -/** - * Handle mark no-show button click - */ -async function handleMarkNoShow( - container: HTMLDivElement, - bookingUid: string, - attendeeEmail: string, - isCurrentlyMarked: boolean -): Promise { - // Check authentication first - if (!(await getIsAuthenticatedCached())) { - // Not logged in - open sidebar for login (no alert, just open sidebar) - openSidebarForLogin(); - return; - } - - // If no booking UID, show error - if (!bookingUid) { - showAlert("Not a Cal.com booking", "info"); - return; - } - - // Show confirmation dialog - showConfirmDialog( - attendeeEmail, - isCurrentlyMarked, - async () => { - // Set loading state - const button = container.querySelector(".cal-noshow-btn") as HTMLButtonElement; - - if (button) { - button.classList.add("loading"); - button.disabled = true; - } - - const originalButtonContent = button?.innerHTML || ""; - if (button) { - button.innerHTML = '
'; - } - - try { - const response = await new Promise<{ success: boolean; error?: string }>((resolve) => { - chrome.runtime.sendMessage( - { - action: "mark-no-show", - payload: { - bookingUid, - attendeeEmail, - absent: !isCurrentlyMarked, - }, - }, - (res) => { - if (chrome.runtime.lastError) { - // Handle network errors - const errorMsg = - chrome.runtime.lastError.message || - "Network error. Please check your connection and try again."; - resolve({ success: false, error: errorMsg }); - } else { - resolve(res || { success: false, error: "No response" }); - } - } - ); - }); - - if (response.success) { - // Invalidate cache so next time we fetch fresh data - if (bookingUid) { - bookingStatusCache.delete(bookingUid); - } - - // Update button state - re-find button in case DOM changed - const updatedButton = container.querySelector(".cal-noshow-btn") as HTMLButtonElement; - const buttonToUpdate = updatedButton || button; - - if (!isCurrentlyMarked) { - // Mark as no-show - if (buttonToUpdate) { - buttonToUpdate.classList.add("marked"); - buttonToUpdate.innerHTML = `${createEyeIcon()}Unmark no-show`; - buttonToUpdate.setAttribute("aria-label", "Unmark no-show"); - } - container.dataset.marked = "true"; - setAttendeeMarkedVisual( - container.closest('[role="treeitem"][data-email]') ?? - container.closest("[data-email]"), - true - ); - showAlert("Marked as no-show", "success"); - } else { - // Unmark no-show - if (buttonToUpdate) { - buttonToUpdate.classList.remove("marked"); - buttonToUpdate.innerHTML = `${createNoShowIcon()}Mark as no-show`; - buttonToUpdate.setAttribute("aria-label", "Mark as no-show"); - } - container.dataset.marked = "false"; - setAttendeeMarkedVisual( - container.closest('[role="treeitem"][data-email]') ?? - container.closest("[data-email]"), - false - ); - showAlert("No-show status removed", "success"); - } - } else { - // Handle specific error messages - let errorMessage = response.error || "Failed to mark no-show. Please try again."; - - // Map error messages to user-friendly ones - if (response.error?.includes("401") || response.error?.includes("Session expired")) { - errorMessage = "Session expired. Please login again."; - } else if (response.error?.includes("403") || response.error?.includes("permission")) { - errorMessage = "You don't have permission to modify this booking."; - } else if (response.error?.includes("404") || response.error?.includes("not found")) { - errorMessage = "Booking not found in Cal.com."; - } else if (response.error?.includes("Network") || response.error?.includes("network")) { - errorMessage = "Network error. Please check your connection and try again."; - } - - if (button) { - button.innerHTML = originalButtonContent; - } - showAlert(errorMessage, "error"); - } - } catch (error) { - if (button) { - button.innerHTML = originalButtonContent; - } - const errorMessage = - error instanceof Error ? error.message : "Failed to mark no-show. Please try again."; - showAlert(errorMessage, "error"); - } finally { - if (button) { - button.classList.remove("loading"); - button.disabled = false; - } - } - }, - () => { - // Cancel - do nothing - } - ); -} - -/** - * Create SVG icon for "Mark as no-show" (eye with slash) - */ -function createNoShowIcon(): string { - return ` - - -`; -} - -/** - * Create SVG icon for "Unmark no-show" (regular eye) - */ -function createEyeIcon(): string { - return ` - -`; -} - -/** - * Create a "Mark No Show" button container for an attendee - * Returns a container div with icon + text button - */ -function createNoShowButton( - bookingUid: string, - attendee: AttendeeInfo, - isMarked: boolean = false -): HTMLDivElement { - const container = document.createElement("div"); - container.className = "cal-noshow-btn-container"; - container.dataset.attendeeEmail = attendee.email; - container.dataset.marked = isMarked ? "true" : "false"; - container.dataset.calNoshowBtn = "true"; - container.dataset.bookingUid = bookingUid || ""; - - // Create button with icon and text - const button = document.createElement("button"); - button.className = `cal-noshow-btn ${isMarked ? "marked" : ""}`; - const iconHtml = isMarked ? createEyeIcon() : createNoShowIcon(); - const text = isMarked ? "Unmark no-show" : "Mark as no-show"; - button.innerHTML = `${iconHtml}${text}`; - button.dataset.attendeeEmail = attendee.email; - button.setAttribute("aria-label", text); - - const handleClick = (e: Event) => { - e.preventDefault(); - e.stopPropagation(); - - const currentlyMarked = container.dataset.marked === "true"; - // Read the latest booking UID from the DOM at click-time (popup content can render in phases) - const latestBookingUid = container.dataset.bookingUid || bookingUid; - handleMarkNoShow(container, latestBookingUid, attendee.email, currentlyMarked); - }; - - button.addEventListener("click", handleClick); - container.appendChild(button); - - return container; -} - -/** - * Inject no-show buttons into an event popup - */ -async function injectNoShowButtons(eventPopup: Element): Promise { - // Get the event description text - const popupText = eventPopup.textContent || ""; - - // Extract booking UID - const bookingUid = extractBookingUid(popupText); - - // Check if this is a past event - const isPast = isEventInPast(eventPopup); - - if (!isPast) { - // Not a past event, don't show buttons - return; - } - - // Parse attendees - const attendees = parseAttendees(eventPopup); - - // Filter out organizers and hosts (we only mark attendees, not the host/organizer) - // Also filter out the current user if they're the organizer - const nonOrganizerAttendees = attendees.filter((a) => { - // Exclude if marked as organizer - if (a.isOrganizer) { - return false; - } - - // Additional check: exclude if the element or parent has organizer indicators - const element = a.element; - const hasOrganizerIndicator = - element.getAttribute("data-organizer") === "true" || - element.closest('[data-organizer="true"]') !== null || - element.querySelector('[aria-label*="organizer" i]') !== null || - element.querySelector('[aria-label*="organiser" i]') !== null || - (element.textContent || "").toLowerCase().includes("organizer") || - (element.textContent || "").toLowerCase().includes("organiser"); - - return !hasOrganizerIndicator; - }); - - if (nonOrganizerAttendees.length === 0) { - return; - } - - // Per requirements: Show button even for non-Cal.com events, but it will show error on click - // This allows users to see the feature is available - - const existingButtonContainers = Array.from( - eventPopup.querySelectorAll(".cal-noshow-btn-container[data-attendee-email]") - ) as HTMLDivElement[]; - - const existingButtonByEmail = new Map(); - existingButtonContainers.forEach((el) => { - const email = el.getAttribute("data-attendee-email")?.toLowerCase(); - if (email) existingButtonByEmail.set(email, el); - }); - - const attendeesToInject = nonOrganizerAttendees.filter( - (a) => !existingButtonByEmail.has(a.email.toLowerCase()) - ); - - type BookingStatusAttendee = { email: string; noShow?: boolean; absent?: boolean }; - type BookingStatusPayload = { attendees?: BookingStatusAttendee[] }; - type BookingStatusMessageResponse = - | { success: true; data: BookingStatusPayload } - | { success: false; error?: string }; - - // Fetch booking status to compute initial mark/unmark state. - const attendeeNoShowStatus = new Map(); - - if (bookingUid && (await getIsAuthenticatedCached())) { - // Check cache first - const cached = bookingStatusCache.get(bookingUid); - const now = Date.now(); - - if (cached && now - cached.timestamp < CACHE_TTL) { - // Use cached data - cached.data.forEach((value, key) => { - attendeeNoShowStatus.set(key, value); - }); - } else { - const statusResponse = await new Promise((resolve) => { - chrome.runtime.sendMessage( - { action: "get-booking-status", payload: { bookingUid } }, - (response) => { - if (chrome.runtime.lastError) { - resolve({ success: false, error: chrome.runtime.lastError.message }); - return; - } - if (response && typeof response === "object" && "success" in response) { - resolve(response as BookingStatusMessageResponse); - return; - } - resolve({ success: false }); - } - ); - }); - - if (statusResponse.success && statusResponse.data.attendees) { - statusResponse.data.attendees.forEach((att) => { - attendeeNoShowStatus.set( - att.email.toLowerCase(), - att.noShow === true || att.absent === true - ); - }); - - bookingStatusCache.set(bookingUid, { - data: new Map(attendeeNoShowStatus), - timestamp: now, - }); - } - } - } - - // Sync existing buttons (e.g. when bookingUid appears after initial injection) - if (bookingUid && existingButtonByEmail.size > 0) { - nonOrganizerAttendees.forEach((attendee) => { - const emailLower = attendee.email.toLowerCase(); - const existingContainer = existingButtonByEmail.get(emailLower); - if (!existingContainer) return; - - // Ensure click-time booking UID is correct even if we injected before the link rendered - if (!existingContainer.dataset.bookingUid) existingContainer.dataset.bookingUid = bookingUid; - - if (attendeeNoShowStatus.size === 0) return; - - const shouldBeMarked = attendeeNoShowStatus.get(emailLower) || false; - const currentlyMarked = existingContainer.dataset.marked === "true"; - if (shouldBeMarked === currentlyMarked) return; - - const btn = existingContainer.querySelector(".cal-noshow-btn") as HTMLButtonElement | null; - if (btn) { - if (shouldBeMarked) { - btn.classList.add("marked"); - btn.innerHTML = `${createEyeIcon()}Unmark no-show`; - btn.setAttribute("aria-label", "Unmark no-show"); - } else { - btn.classList.remove("marked"); - btn.innerHTML = `${createNoShowIcon()}Mark as no-show`; - btn.setAttribute("aria-label", "Mark as no-show"); - } - } - setAttendeeMarkedVisual( - existingContainer.closest('[role="treeitem"][data-email]') ?? - existingContainer.closest("[data-email]") ?? - attendee.element, - shouldBeMarked - ); - existingContainer.dataset.marked = shouldBeMarked ? "true" : "false"; - }); - } - - if (attendeesToInject.length === 0) { - // Nothing new to inject (but we may have synced existing buttons above) - return; - } - - attendeesToInject.forEach((attendee) => { - // Check if this attendee is already marked as no-show - const isMarked = attendeeNoShowStatus.get(attendee.email.toLowerCase()) || false; - - // Create and inject button container - const buttonContainer = createNoShowButton(bookingUid || "", attendee, isMarked); - // Button will always show, even without booking UID - // The click handler will check auth and booking UID when clicked - - // Strategy: Place button IN FRONT OF attendee name - // Insert before the name span (.SDqFWd span) - let inserted = false; - - if (attendee.element && attendee.element !== eventPopup) { - // Find the name container and name span - const nameContainer = attendee.element.querySelector(".toUqff.DbpAnb, .CVKLNd"); - const nameSpan = nameContainer - ? nameContainer.querySelector(".SDqFWd span, .SDqFWd") - : attendee.element.querySelector(".SDqFWd span, .SDqFWd"); - - if (nameSpan?.parentElement) { - // Insert button AFTER the name span (on the right side) - // Check if button already exists at this location - const parent = nameSpan.parentElement; - const existingBtn = parent.querySelector(`[data-attendee-email="${attendee.email}"]`); - if (existingBtn) { - return; // Skip if already exists - } - - // Insert after the name span - parent.insertBefore(buttonContainer, nameSpan.nextSibling); - inserted = true; - } else if (nameContainer) { - // Fallback: Insert at the beginning of the name container - const existingBtn = nameContainer.querySelector( - `[data-attendee-email="${attendee.email}"]` - ); - if (existingBtn) { - return; // Skip if already exists - } - - const firstChild = nameContainer.firstChild; - if (firstChild) { - nameContainer.insertBefore(buttonContainer, firstChild); - } else { - nameContainer.appendChild(buttonContainer); - } - inserted = true; - } else { - // Fallback: Find the content div and insert at the beginning - const contentDiv = attendee.element.querySelector(".toUqff, .CVKLNd, .UfeRlc"); - if (contentDiv) { - const existingBtn = contentDiv.querySelector(`[data-attendee-email="${attendee.email}"]`); - if (existingBtn) { - return; // Skip if already exists - } - - const firstChild = contentDiv.firstChild; - if (firstChild) { - contentDiv.insertBefore(buttonContainer, firstChild); - } else { - contentDiv.appendChild(buttonContainer); - } - inserted = true; - } else { - // Last resort: prepend to the attendee element itself - const existingBtn = attendee.element.querySelector( - `[data-attendee-email="${attendee.email}"]` - ); - if (existingBtn) { - return; // Skip if already exists - } - - const firstChild = attendee.element.firstChild; - if (firstChild) { - attendee.element.insertBefore(buttonContainer, firstChild); - } else { - attendee.element.appendChild(buttonContainer); - } - inserted = true; - } - } - } - - // Fallback: If we couldn't insert using the structure, try finding by email - if (!inserted) { - // Look for the element with this email - const emailElement = eventPopup.querySelector(`[data-email="${attendee.email}"]`); - if (emailElement && emailElement !== eventPopup) { - const nameSpan = emailElement.querySelector(".SDqFWd span, .SDqFWd"); - if (nameSpan?.parentElement) { - nameSpan.parentElement.insertBefore(buttonContainer, nameSpan.nextSibling); - inserted = true; - } else if (emailElement.parentElement) { - emailElement.parentElement.insertBefore(buttonContainer, emailElement.nextSibling); - inserted = true; - } - } - } - - setAttendeeMarkedVisual(attendee.element, isMarked); - }); -} - -/** - * Observe DOM for event popups and inject buttons - */ -function observeEventPopups(): void { - // Track button injection attempts to avoid infinite loops - const injectionAttempts = new WeakMap(); - const MAX_INJECTION_ATTEMPTS = 10; - // Throttle sync attempts per popup (for phased rendering / login while open) - const lastSyncAttempt = new WeakMap(); - const SYNC_ATTEMPT_TTL = 2000; // 2 seconds - - // Function to check and inject buttons - const checkAndInject = (element: Element, force: boolean = false) => { - const attempts = injectionAttempts.get(element) || 0; - - // Prevent infinite injection loops - if (!force && attempts >= MAX_INJECTION_ATTEMPTS) { - return; - } - - const text = element.textContent || ""; - // Check if this looks like a Cal.com event - const isCalComEvent = - text.includes("cal.com") || - text.includes("Cal.com") || - text.includes("booking") || - extractBookingUid(text) !== null; - - if (isCalComEvent) { - // Check if buttons already exist - const existingButtons = element.querySelectorAll(NO_SHOW_BUTTON_SELECTOR); - const hasValidButtons = Array.from(existingButtons).some((btn) => { - const style = window.getComputedStyle(btn); - return btn.isConnected && style.display !== "none" && style.visibility !== "hidden"; - }); - - // Only inject if buttons don't exist or were removed - if (!hasValidButtons || force) { - // Delay to let the popup fully render - setTimeout(() => { - void injectNoShowButtons(element); - injectionAttempts.set(element, attempts + 1); - }, 200); - return; - } - - // Buttons exist: still attempt a throttled "sync pass" so state can update once bookingUid/auth becomes available. - const now = Date.now(); - const last = lastSyncAttempt.get(element) || 0; - if (now - last < SYNC_ATTEMPT_TTL) return; - - if (extractBookingUid(text)) { - lastSyncAttempt.set(element, now); - setTimeout(() => { - void injectNoShowButtons(element); - }, 200); - } - } - }; - - // MutationObserver to watch for event popup appearance and changes - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - // Handle removed nodes - check if our buttons were removed - if (mutation.type === "childList" && mutation.removedNodes.length > 0) { - mutation.removedNodes.forEach((removedNode) => { - if (removedNode instanceof Element) { - // Check if one of our buttons was removed - const removedButton = - removedNode.querySelector?.(NO_SHOW_BUTTON_SELECTOR) || - (removedNode.hasAttribute?.("data-cal-noshow-btn") ? removedNode : null); - - if (removedButton) { - // Find the popup that contains this button's attendee - const popup = - mutation.target instanceof Element - ? mutation.target.closest('[role="dialog"], [aria-modal="true"], [data-eventid]') - : null; - - if (popup) { - // Re-inject buttons if they were removed - setTimeout(() => checkAndInject(popup, true), 100); - } - } - } - }); - } - - // Handle added nodes - mutation.addedNodes.forEach((node) => { - if (node instanceof Element) { - // Look for event detail popups - const eventPopups = node.querySelectorAll( - '[data-eventid], [data-eventchip], [role="dialog"], [aria-modal="true"]' - ); - - eventPopups.forEach((popup) => { - checkAndInject(popup); - }); - - // Also check if the added node itself is a popup - if ( - node.matches('[data-eventid], [data-eventchip], [role="dialog"], [aria-modal="true"]') - ) { - checkAndInject(node); - } - - // Check if attendee elements were added (Google Calendar might add them dynamically) - if (node.querySelector?.('[role="treeitem"][data-email]')) { - const popup = node.closest('[role="dialog"], [aria-modal="true"], [data-eventid]'); - if (popup) { - checkAndInject(popup, true); - } - } - } - }); - - // Handle attribute changes (Google Calendar might update content dynamically) - if (mutation.type === "attributes" && mutation.target instanceof Element) { - const target = mutation.target; - // Check if this is a popup or inside a popup - const popup = target.closest('[role="dialog"], [aria-modal="true"], [data-eventid]'); - if (popup) { - // Only re-inject if buttons are missing - const hasButtons = popup.querySelector(NO_SHOW_BUTTON_SELECTOR); - if (!hasButtons) { - checkAndInject(popup); - } - } - } - - // Handle character data changes (text content updates) - if (mutation.type === "characterData" && mutation.target.parentElement) { - const popup = mutation.target.parentElement.closest( - '[role="dialog"], [aria-modal="true"], [data-eventid]' - ); - if (popup) { - const hasButtons = popup.querySelector(NO_SHOW_BUTTON_SELECTOR); - if (!hasButtons) { - checkAndInject(popup); - } - } - } - }); - }); - - observer.observe(document.body, { - childList: true, - subtree: true, - attributes: true, - attributeFilter: ["class", "style", "aria-label"], - characterData: true, - }); - - // Also check for existing popups on page load - setTimeout(() => { - const existingPopups = document.querySelectorAll( - '[data-eventid], [data-eventchip], [role="dialog"][aria-modal="true"]' - ); - existingPopups.forEach((popup) => { - checkAndInject(popup); - }); - }, 1000); - - // Periodically check for popups and verify buttons still exist (fallback) - setInterval(() => { - const allPopups = document.querySelectorAll( - '[role="dialog"], [aria-modal="true"], [data-eventid]' - ); - allPopups.forEach((popup) => { - // Only check if popup is visible - const style = window.getComputedStyle(popup); - if (style.display === "none" || style.visibility === "hidden") { - return; - } - - // Check if this is a Cal.com event - const text = popup.textContent || ""; - const isCalComEvent = - text.includes("cal.com") || - text.includes("Cal.com") || - text.includes("booking") || - extractBookingUid(text) !== null; - - if (isCalComEvent) { - // Check if buttons exist and are still attached - const existingButtons = popup.querySelectorAll(NO_SHOW_BUTTON_SELECTOR); - const validButtons = Array.from(existingButtons).filter((btn) => { - const style = window.getComputedStyle(btn); - return btn.isConnected && style.display !== "none" && style.visibility !== "hidden"; - }); - - // If buttons are missing or removed, re-inject - if (validButtons.length === 0) { - checkAndInject(popup, true); - } - } - }); - }, 1000); -} - -/** - * Initialize Google Calendar integration - */ -export function initGoogleCalendarIntegration(): void { - // Inject styles - injectStyles(); - - // Start observing for event popups - observeEventPopups(); -} diff --git a/companion/extension/lib/linkedin.ts b/companion/extension/lib/linkedin.ts deleted file mode 100644 index 6f5b103aea..0000000000 --- a/companion/extension/lib/linkedin.ts +++ /dev/null @@ -1,977 +0,0 @@ -/// -import { escapeHtml } from "./utils"; - -// LinkedIn integration: inject a Cal.com scheduling button in LinkedIn messaging - -// ============================================================================ -// Constants -// ============================================================================ - -const CONSTANTS = { - CACHE_DURATION: 5 * 60 * 1000, // 5 minutes - BUTTON_SIZES: { - SMALL: { button: 24, icon: 14 }, - LARGE: { button: 32, icon: 18 }, - }, - MENU: { - WIDTH: 400, - MAX_HEIGHT: 350, - MARGIN: 20, - GAP: 8, - }, - ANIMATION: { - TOOLTIP_FADE_DELAY: 150, - NOTIFICATION_DURATION: 3000, - NOTIFICATION_FADE_OUT: 200, - }, - COLORS: { - BUTTON_BG: "#000000", - BUTTON_HOVER: "#333333", - TOOLTIP_BG: "#1a1a1a", - NOTIFICATION_SUCCESS: "#111827", - NOTIFICATION_ERROR: "#752522", - HOVER_BG: "#f8f9fa", - }, - SELECTORS: { - LEFT_ACTIONS: ".msg-form__left-actions", - EMOJI_BUTTON: "div:has(> span.artdeco-hoverable-trigger button.emoji-hoverable-trigger)", - EMOJI_BUTTON_FALLBACK: 'div:has(button[aria-label="Open Emoji Keyboard"])', - LARGE_BUTTON: ".artdeco-button--2", - CAL_BUTTON: ".cal-companion-linkedin-button", - CAL_MENU: ".cal-companion-linkedin-menu", - MESSAGE_INPUT: [ - 'div[contenteditable="true"][role="textbox"][aria-label*="message" i]', - 'div[contenteditable="true"][data-testid*="message"]', - 'div[contenteditable="true"].msg-form__contenteditable', - ], - }, - CLASSES: { - BASE_BUTTON: - "cal-companion-linkedin-button msg-form__footer-action artdeco-button artdeco-button--tertiary artdeco-button--circle artdeco-button--muted", - SMALL_BUTTON: "artdeco-button--1", - LARGE_BUTTON: "artdeco-button--2", - WRAPPER: "cal-companion-linkedin-wrapper inline-block", - MENU: "cal-companion-linkedin-menu", - TOOLTIP: "cal-tooltip", - }, -} as const; - -// SVG Icons -const SVG_ICONS = { - CAL_LOGO: (size: number) => ` - - `, - PREVIEW: ` - - - - - - `, - COPY: ` - - - - - `, - COPY_SUCCESS: ` - - - - `, - EDIT: ` - - - - - `, - ERROR: ` - - - - - - `, - CHECK: ` - - - - `, -} as const; - -// ============================================================================ -// Types -// ============================================================================ - -interface EventType { - id: string; - title?: string; - slug: string; - lengthInMinutes?: number; - length?: number; - duration?: number; - description?: string; - users?: Array<{ username?: string }>; - bookingUrl?: string; -} - -interface ButtonSize { - button: number; - icon: number; -} - -// ============================================================================ -// Main Integration Function -// ============================================================================ - -export function initLinkedInIntegration() { - // Cache for event types (refreshed on page reload) - let eventTypesCache: EventType[] | null = null; - let cacheTimestamp: number | null = null; - - console.log("[Cal.com] LinkedIn integration starting..."); - - injectStyles(); - - // ============================================================================ - // Button Injection - // ============================================================================ - - function injectCalButton() { - const leftActionsContainers = document.querySelectorAll( - CONSTANTS.SELECTORS.LEFT_ACTIONS - ); - - console.log("[Cal.com] Found left-actions containers:", leftActionsContainers.length); - - leftActionsContainers.forEach((leftActions) => { - if (leftActions.querySelector(CONSTANTS.SELECTORS.CAL_BUTTON)) { - return; - } - - const emojiButtonContainer = findEmojiButtonContainer(leftActions); - const isLargeContext = detectButtonSizeContext(leftActions); - const sizes = isLargeContext ? CONSTANTS.BUTTON_SIZES.LARGE : CONSTANTS.BUTTON_SIZES.SMALL; - - console.log( - "[Cal.com] Injecting Cal.com button into LinkedIn messaging", - isLargeContext ? "(large context)" : "(small context)" - ); - - const calWrapper = createButtonWrapper(); - const calButton = createCalButton(isLargeContext, sizes); - calWrapper.appendChild(calButton); - - insertButtonIntoDOM(leftActions, calWrapper, emojiButtonContainer); - - console.log("[Cal.com] Successfully injected Cal.com button"); - }); - } - - function findEmojiButtonContainer(container: HTMLElement): Element | null { - return ( - container.querySelector(CONSTANTS.SELECTORS.EMOJI_BUTTON) || - container.querySelector(CONSTANTS.SELECTORS.EMOJI_BUTTON_FALLBACK) - ); - } - - function detectButtonSizeContext(container: HTMLElement): boolean { - return !!container.querySelector(CONSTANTS.SELECTORS.LARGE_BUTTON); - } - - function createButtonWrapper(): HTMLDivElement { - const wrapper = document.createElement("div"); - wrapper.className = CONSTANTS.CLASSES.WRAPPER; - wrapper.style.cssText = - "display: flex; align-items: center; align-self: center; margin-left: 8px;"; - return wrapper; - } - - function createCalButton(isLargeContext: boolean, sizes: ButtonSize): HTMLButtonElement { - const button = document.createElement("button"); - const buttonClass = `${CONSTANTS.CLASSES.BASE_BUTTON} ${ - isLargeContext ? CONSTANTS.CLASSES.LARGE_BUTTON : CONSTANTS.CLASSES.SMALL_BUTTON - }`; - button.className = buttonClass; - button.type = "button"; - button.title = "Schedule with Cal.com"; - button.setAttribute("aria-label", "Schedule with Cal.com"); - - button.style.cssText = ` - display: inline-flex !important; - align-items: center !important; - justify-content: center !important; - width: ${sizes.button}px !important; - height: ${sizes.button}px !important; - min-width: ${sizes.button}px !important; - min-height: ${sizes.button}px !important; - border-radius: 50% !important; - background-color: ${CONSTANTS.COLORS.BUTTON_BG} !important; - border: none !important; - cursor: pointer !important; - transition: all 0.2s ease !important; - box-shadow: 0 1px 3px rgba(0,0,0,0.1) !important; - padding: 0 !important; - margin: 0 !important; - `; - - button.innerHTML = SVG_ICONS.CAL_LOGO(sizes.icon); - - // Add hover effects - button.addEventListener("mouseenter", () => { - button.style.backgroundColor = CONSTANTS.COLORS.BUTTON_HOVER; - button.style.transform = "scale(1.05)"; - }); - - button.addEventListener("mouseleave", () => { - button.style.backgroundColor = CONSTANTS.COLORS.BUTTON_BG; - button.style.transform = "scale(1)"; - }); - - // Add click handler - button.addEventListener("click", (e) => { - e.preventDefault(); - e.stopPropagation(); - - const existingMenu = document.querySelector(CONSTANTS.SELECTORS.CAL_MENU); - if (existingMenu) { - existingMenu.remove(); - return; - } - - createEventTypesMenu(button); - }); - - return button; - } - - function insertButtonIntoDOM( - leftActions: HTMLElement, - calWrapper: HTMLDivElement, - emojiButtonContainer: Element | null - ) { - if (emojiButtonContainer?.nextSibling) { - const nextElement = emojiButtonContainer.nextSibling; - if ( - nextElement instanceof Element && - (nextElement.classList.contains("calendly-button") || nextElement.tagName === "TD") - ) { - leftActions.insertBefore(calWrapper, nextElement); - } else { - leftActions.insertBefore(calWrapper, nextElement); - } - } else if (emojiButtonContainer) { - if (emojiButtonContainer.nextSibling) { - leftActions.insertBefore(calWrapper, emojiButtonContainer.nextSibling); - } else { - leftActions.appendChild(calWrapper); - } - } else { - leftActions.appendChild(calWrapper); - } - } - - // ============================================================================ - // Menu Creation - // ============================================================================ - - function createEventTypesMenu(button: HTMLButtonElement) { - const menu = document.createElement("div"); - menu.className = CONSTANTS.CLASSES.MENU; - positionMenu(menu, button); - menu.innerHTML = ` -
- Loading event types... -
- `; - - const tooltipsToCleanup: HTMLElement[] = []; - fetchEventTypes(menu, tooltipsToCleanup); - - // Prevent event bubbling - menu.addEventListener("wheel", (e) => e.stopPropagation(), { passive: true }); - menu.addEventListener("scroll", (e) => e.stopPropagation(), { passive: true }); - - document.body.appendChild(menu); - - // Close menu when clicking outside - setTimeout(() => { - const closeMenu = (evt: MouseEvent) => { - if (!menu.contains(evt.target as Node) && evt.target !== button) { - for (const tooltip of tooltipsToCleanup) { - tooltip.remove(); - } - menu.remove(); - document.removeEventListener("click", closeMenu); - } - }; - document.addEventListener("click", closeMenu); - }, 0); - } - - function positionMenu(menu: HTMLDivElement, button: HTMLButtonElement) { - const buttonRect = button.getBoundingClientRect(); - const { WIDTH, MAX_HEIGHT, MARGIN, GAP } = CONSTANTS.MENU; - - let menuLeft = buttonRect.left; - const menuBottom = window.innerHeight - buttonRect.top + GAP; - - // Keep menu within screen bounds - if (menuLeft + WIDTH > window.innerWidth - MARGIN) { - menuLeft = window.innerWidth - WIDTH - MARGIN; - } - if (menuLeft < MARGIN) { - menuLeft = MARGIN; - } - - // Determine vertical position - const spaceAbove = buttonRect.top; - const spaceBelow = window.innerHeight - buttonRect.bottom; - const positionStyle = - spaceAbove < MAX_HEIGHT && spaceBelow > spaceAbove - ? `top: ${buttonRect.bottom + GAP}px;` - : `bottom: ${menuBottom}px;`; - - menu.style.cssText = ` - position: fixed; - ${positionStyle} - left: ${menuLeft}px; - width: ${WIDTH}px; - max-height: ${Math.min(MAX_HEIGHT, Math.max(spaceAbove, spaceBelow) - MARGIN)}px; - background: white; - border-radius: 12px; - box-shadow: 0 4px 20px rgba(0,0,0,0.15); - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - font-size: 14px; - z-index: 2147483647; - overflow-y: auto; - overflow-x: hidden; - -webkit-overflow-scrolling: touch; - scrollbar-width: thin; - scrollbar-color: #c1c1c1 transparent; - `; - } - - // ============================================================================ - // Event Types Fetching & Display - // ============================================================================ - - async function fetchEventTypes(menu: HTMLElement, tooltipsToCleanup: HTMLElement[]) { - try { - const eventTypes = await getEventTypes(); - renderEventTypes(menu, eventTypes, tooltipsToCleanup); - } catch (error) { - handleFetchError(error, menu, tooltipsToCleanup); - } - } - - async function getEventTypes(): Promise { - const now = Date.now(); - const isCacheValid = - eventTypesCache && cacheTimestamp && now - cacheTimestamp < CONSTANTS.CACHE_DURATION; - - if (isCacheValid && eventTypesCache) { - return eventTypesCache; - } - - if (!chrome.runtime?.id) { - throw new Error("Extension context invalidated. Please reload the page."); - } - - const response = await new Promise((resolve, reject) => { - try { - chrome.runtime.sendMessage({ action: "fetch-event-types" }, (response) => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)); - } else if (response && (response as { error?: string }).error) { - reject(new Error((response as { error: string }).error)); - } else { - resolve(response); - } - }); - } catch (err) { - reject(err); - } - }); - - let eventTypes: EventType[] = []; - if (response && typeof response === "object" && "data" in response) { - eventTypes = Array.isArray((response as { data: unknown }).data) - ? ((response as { data: unknown }).data as EventType[]) - : []; - } else if (Array.isArray(response)) { - eventTypes = response; - } - - eventTypesCache = eventTypes; - cacheTimestamp = now; - return eventTypes; - } - - function renderEventTypes( - menu: HTMLElement, - eventTypes: EventType[], - tooltipsToCleanup: HTMLElement[] - ) { - menu.innerHTML = ""; - - if (eventTypes.length === 0) { - menu.innerHTML = ` -
- No event types found -
- `; - return; - } - - try { - eventTypes.forEach((eventType, index) => { - if (!eventType || typeof eventType !== "object") { - return; - } - - const menuItem = createEventTypeMenuItem( - eventType, - index, - eventTypes.length, - tooltipsToCleanup - ); - menu.appendChild(menuItem); - }); - } catch (_error) { - menu.innerHTML = ` -
- Error displaying event types -
- `; - } - } - - function createEventTypeMenuItem( - eventType: EventType, - index: number, - totalItems: number, - tooltipsToCleanup: HTMLElement[] - ): HTMLElement { - const title = eventType.title || "Untitled Event"; - const length = eventType.lengthInMinutes || eventType.length || eventType.duration || 30; - const description = eventType.description || ""; - - const menuItem = document.createElement("div"); - menuItem.style.cssText = ` - padding: 14px 16px; - display: flex; - align-items: center; - justify-content: space-between; - transition: background-color 0.1s ease; - border-bottom: ${index < totalItems - 1 ? "1px solid #E5E5EA" : "none"}; - `; - - const contentWrapper = createEventTypeContent( - eventType, - title, - length, - description, - tooltipsToCleanup - ); - const buttonsContainer = createEventTypeButtons(eventType, tooltipsToCleanup); - - menuItem.appendChild(contentWrapper); - menuItem.appendChild(buttonsContainer); - - // Hover effect - menuItem.addEventListener("mouseenter", () => { - menuItem.style.backgroundColor = CONSTANTS.COLORS.HOVER_BG; - }); - menuItem.addEventListener("mouseleave", () => { - menuItem.style.backgroundColor = "transparent"; - }); - - return menuItem; - } - - function createEventTypeContent( - eventType: EventType, - title: string, - length: number, - description: string, - tooltipsToCleanup: HTMLElement[] - ): HTMLElement { - const contentWrapper = document.createElement("div"); - contentWrapper.style.cssText = ` - flex: 1; - display: flex; - flex-direction: column; - cursor: pointer; - margin-right: 12px; - position: relative; - min-width: 0; - overflow: hidden; - `; - - contentWrapper.innerHTML = ` -
- ${escapeHtml(title)} -
-
- - ${length}min - - ${ - description - ? `${escapeHtml(description)}` - : "" - } -
- `; - - const tooltip = createTooltip("Insert link", contentWrapper); - tooltipsToCleanup.push(tooltip); - - contentWrapper.addEventListener("mouseenter", () => { - const rect = contentWrapper.getBoundingClientRect(); - tooltip.style.left = `${rect.left + rect.width / 2 + 80}px`; - tooltip.style.top = `${rect.top + 35}px`; - tooltip.style.transform = "translate(-50%, -100%)"; - tooltip.style.display = "block"; - tooltip.style.opacity = "1"; - }); - - contentWrapper.addEventListener("mouseleave", () => { - tooltip.style.opacity = "0"; - setTimeout(() => { - if (tooltip.style.opacity === "0") { - tooltip.style.display = "none"; - } - }, CONSTANTS.ANIMATION.TOOLTIP_FADE_DELAY); - }); - - contentWrapper.addEventListener("click", (e) => { - e.stopPropagation(); - tooltip.remove(); - const menu = document.querySelector(CONSTANTS.SELECTORS.CAL_MENU); - if (menu) menu.remove(); - insertEventTypeLink(eventType); - }); - - return contentWrapper; - } - - function createEventTypeButtons( - eventType: EventType, - tooltipsToCleanup: HTMLElement[] - ): HTMLElement { - const buttonsContainer = document.createElement("div"); - buttonsContainer.style.cssText = ` - display: flex; - align-items: center; - gap: 0; - flex-shrink: 0; - `; - - const previewBtn = createActionButton( - SVG_ICONS.PREVIEW, - "Preview", - "6px 0 0 6px", - tooltipsToCleanup - ); - previewBtn.addEventListener("click", (e) => { - e.stopPropagation(); - const bookingUrl = buildBookingUrl(eventType); - window.open(bookingUrl, "_blank"); - }); - - const copyBtn = createActionButton(SVG_ICONS.COPY, "Copy link", "none", tooltipsToCleanup); - copyBtn.addEventListener("click", (e) => { - e.stopPropagation(); - const bookingUrl = buildBookingUrl(eventType); - navigator.clipboard - .writeText(bookingUrl) - .then(() => { - showNotification("Link copied!", "success"); - copyBtn.innerHTML = SVG_ICONS.COPY_SUCCESS; - setTimeout(() => { - copyBtn.innerHTML = SVG_ICONS.COPY; - }, 2000); - }) - .catch(() => { - showNotification("Failed to copy link", "error"); - }); - }); - - const editBtn = createActionButton(SVG_ICONS.EDIT, "Edit", "0 6px 6px 0", tooltipsToCleanup); - editBtn.addEventListener("click", (e) => { - e.stopPropagation(); - const editUrl = `https://app.cal.com/event-types/${eventType.id}`; - window.open(editUrl, "_blank"); - }); - - buttonsContainer.appendChild(previewBtn); - buttonsContainer.appendChild(copyBtn); - buttonsContainer.appendChild(editBtn); - - return buttonsContainer; - } - - function createActionButton( - icon: string, - tooltipText: string, - borderRadius: string, - tooltipsToCleanup: HTMLElement[] - ): HTMLButtonElement { - const button = document.createElement("button"); - button.innerHTML = icon; - button.style.cssText = ` - width: 32px; - height: 32px; - display: flex; - align-items: center; - justify-content: center; - border: 1px solid #e5e5ea; - border-radius: ${borderRadius}; - ${borderRadius === "none" ? "border-right: none;" : ""} - background: white; - cursor: pointer; - transition: background-color 0.1s ease; - padding: 0; - position: relative; - `; - - const tooltip = createTooltip(tooltipText, button); - tooltipsToCleanup.push(tooltip); - - button.addEventListener("mouseenter", () => { - button.style.backgroundColor = CONSTANTS.COLORS.HOVER_BG; - }); - button.addEventListener("mouseleave", () => { - button.style.backgroundColor = "white"; - }); - - return button; - } - - function buildBookingUrl(eventType: EventType): string { - return ( - eventType.bookingUrl || - `https://cal.com/${eventType.users?.[0]?.username || "user"}/${eventType.slug}` - ); - } - - function handleFetchError(error: unknown, menu: HTMLElement, tooltipsToCleanup: HTMLElement[]) { - const errorMessage = error instanceof Error ? error.message : String(error); - const isAuthError = - errorMessage.includes("OAuth") || - errorMessage.includes("access token") || - errorMessage.includes("sign in") || - errorMessage.includes("authentication"); - - if (isAuthError) { - for (const tooltip of tooltipsToCleanup) { - tooltip.remove(); - } - menu.remove(); - openCalSidebar(); - return; - } - - menu.innerHTML = ` -
-
- ${SVG_ICONS.ERROR} -
-
- Something went wrong -
-
- ${escapeHtml(errorMessage || "Failed to load event types")} -
- -
- `; - - const closeBtn = menu.querySelector(".cal-linkedin-close-btn"); - if (closeBtn) { - closeBtn.addEventListener("mouseenter", () => { - closeBtn.style.background = "#E5E7EB"; - }); - closeBtn.addEventListener("mouseleave", () => { - closeBtn.style.background = "#F3F4F6"; - }); - closeBtn.addEventListener("click", () => { - for (const tooltip of tooltipsToCleanup) { - tooltip.remove(); - } - menu.remove(); - }); - } - } - - // ============================================================================ - // Tooltip Helper - // ============================================================================ - - function createTooltip(text: string, element: HTMLElement): HTMLElement { - const tooltip = document.createElement("div"); - tooltip.className = CONSTANTS.CLASSES.TOOLTIP; - tooltip.style.cssText = ` - position: fixed; - background-color: ${CONSTANTS.COLORS.TOOLTIP_BG}; - color: white; - padding: 4px 8px; - border-radius: 6px; - font-size: 12px; - font-weight: 600; - white-space: nowrap; - pointer-events: none; - opacity: 0; - transition: opacity ${CONSTANTS.ANIMATION.TOOLTIP_FADE_DELAY}ms ease; - z-index: 10002; - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); - display: none; - `; - tooltip.textContent = text; - document.body.appendChild(tooltip); - - const updatePosition = () => { - const rect = element.getBoundingClientRect(); - tooltip.style.left = `${rect.left + rect.width / 2}px`; - tooltip.style.top = `${rect.top - 8}px`; - tooltip.style.transform = "translate(-50%, -100%)"; - }; - - element.addEventListener("mouseenter", () => { - updatePosition(); - tooltip.style.display = "block"; - tooltip.style.opacity = "1"; - }); - - element.addEventListener("mouseleave", () => { - tooltip.style.opacity = "0"; - setTimeout(() => { - if (tooltip.style.opacity === "0") { - tooltip.style.display = "none"; - } - }, CONSTANTS.ANIMATION.TOOLTIP_FADE_DELAY); - }); - - return tooltip; - } - - // ============================================================================ - // Link Insertion - // ============================================================================ - - function insertEventTypeLink(eventType: EventType) { - const bookingUrl = buildBookingUrl(eventType); - const inserted = insertTextAtCursor(bookingUrl); - - if (inserted) { - showNotification("Link inserted", "success"); - } else { - navigator.clipboard - .writeText(bookingUrl) - .then(() => { - showNotification("Link copied!", "success"); - }) - .catch(() => { - showNotification("Failed to copy link", "error"); - }); - } - } - - function insertTextAtCursor(text: string): boolean { - const messageInput = findMessageInput(); - if (!messageInput) { - return false; - } - - messageInput.focus(); - const selection = window.getSelection(); - - if (!selection || selection.rangeCount === 0) { - appendTextToEnd(messageInput, text, selection); - return true; - } - - insertTextAtSelection(text, selection, messageInput); - return true; - } - - function findMessageInput(): HTMLElement | null { - for (const selector of CONSTANTS.SELECTORS.MESSAGE_INPUT) { - const element = document.querySelector(selector); - if (element) return element; - } - return null; - } - - function appendTextToEnd(messageInput: HTMLElement, text: string, selection: Selection | null) { - const textNode = document.createTextNode(` ${text} `); - messageInput.appendChild(textNode); - - const range = document.createRange(); - range.setStartAfter(textNode); - range.collapse(true); - selection?.removeAllRanges(); - selection?.addRange(range); - } - - function insertTextAtSelection(text: string, selection: Selection, messageInput: HTMLElement) { - const range = selection.getRangeAt(0); - range.deleteContents(); - - const textNode = document.createTextNode(` ${text} `); - range.insertNode(textNode); - - range.setStartAfter(textNode); - range.collapse(true); - selection.removeAllRanges(); - selection.addRange(range); - - messageInput.dispatchEvent(new Event("input", { bubbles: true })); - messageInput.dispatchEvent(new Event("change", { bubbles: true })); - } - - // ============================================================================ - // Notification & Sidebar - // ============================================================================ - - function showNotification(message: string, type: "success" | "error") { - const notification = document.createElement("div"); - notification.style.cssText = ` - position: fixed; - bottom: 80px; - right: 80px; - padding: 10px 12px; - background: ${type === "success" ? CONSTANTS.COLORS.NOTIFICATION_SUCCESS : CONSTANTS.COLORS.NOTIFICATION_ERROR}; - color: white; - border: 1px solid #2b2b2b; - border-radius: 8px; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - font-size: 14px; - font-weight: 600; - z-index: 10000; - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.3), 0 2px 6px 2px rgba(0, 0, 0, 0.15); - display: flex; - align-items: center; - gap: 8px; - opacity: 0; - transform: translateY(10px); - transition: opacity 0.2s ease, transform 0.2s ease; - `; - - if (type === "success") { - const checkIcon = document.createElement("span"); - checkIcon.innerHTML = SVG_ICONS.CHECK; - checkIcon.style.cssText = ` - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - `; - notification.appendChild(checkIcon); - } - - const messageText = document.createElement("span"); - messageText.textContent = message; - notification.appendChild(messageText); - - document.body.appendChild(notification); - - requestAnimationFrame(() => { - notification.style.opacity = "1"; - notification.style.transform = "translateY(0)"; - }); - - setTimeout(() => { - notification.style.opacity = "0"; - notification.style.transform = "translateY(10px)"; - setTimeout(() => { - notification.remove(); - }, CONSTANTS.ANIMATION.NOTIFICATION_FADE_OUT); - }, CONSTANTS.ANIMATION.NOTIFICATION_DURATION); - } - - function openCalSidebar() { - window.dispatchEvent(new CustomEvent("cal-companion-open-sidebar")); - } - - // ============================================================================ - // Styles - // ============================================================================ - - function injectStyles() { - if (document.getElementById("cal-linkedin-styles")) { - return; - } - - const styleSheet = document.createElement("style"); - styleSheet.id = "cal-linkedin-styles"; - styleSheet.textContent = ` - .cal-companion-linkedin-menu::-webkit-scrollbar { - width: 6px; - } - .cal-companion-linkedin-menu::-webkit-scrollbar-track { - background: transparent; - border-radius: 3px; - } - .cal-companion-linkedin-menu::-webkit-scrollbar-thumb { - background-color: #c1c1c1; - border-radius: 3px; - } - .cal-companion-linkedin-menu::-webkit-scrollbar-thumb:hover { - background-color: #a1a1a1; - } - `; - document.head.appendChild(styleSheet); - } - - // ============================================================================ - // ============================================================================ - // Initialization - // ============================================================================ - - setTimeout(injectCalButton, 1000); - - const observer = new MutationObserver(() => { - injectCalButton(); - }); - - observer.observe(document.body, { - childList: true, - subtree: true, - }); - - let currentUrl = window.location.href; - setInterval(() => { - if (window.location.href !== currentUrl) { - currentUrl = window.location.href; - setTimeout(injectCalButton, 500); - } - }, 1000); -} diff --git a/companion/extension/lib/utils.ts b/companion/extension/lib/utils.ts deleted file mode 100644 index 783a2b0eee..0000000000 --- a/companion/extension/lib/utils.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Escapes HTML special characters to prevent XSS attacks. - * Uses the browser's built-in text encoding via textContent. - */ -export function escapeHtml(text: string): string { - if (typeof text !== "string") return ""; - const div = document.createElement("div"); - div.textContent = text; - return div.innerHTML; -} diff --git a/companion/extension/public/icon-128.png b/companion/extension/public/icon-128.png deleted file mode 100644 index 6ad39d9bfb..0000000000 Binary files a/companion/extension/public/icon-128.png and /dev/null differ diff --git a/companion/extension/public/icon-16.png b/companion/extension/public/icon-16.png deleted file mode 100644 index 811fcdf3c4..0000000000 Binary files a/companion/extension/public/icon-16.png and /dev/null differ diff --git a/companion/extension/public/icon-48.png b/companion/extension/public/icon-48.png deleted file mode 100644 index f570ab73d5..0000000000 Binary files a/companion/extension/public/icon-48.png and /dev/null differ diff --git a/companion/extension/public/icon.png b/companion/extension/public/icon.png deleted file mode 100644 index 2d352eca86..0000000000 Binary files a/companion/extension/public/icon.png and /dev/null differ diff --git a/companion/extension/public/manifest.json b/companion/extension/public/manifest.json deleted file mode 100644 index 97a39d5eac..0000000000 --- a/companion/extension/public/manifest.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "manifest_version": 3, - "name": "Cal.com Companion", - "version": "1.7.0", - "description": "Your calendar companion for quick booking and scheduling", - "permissions": ["activeTab", "https://companion.cal.com/*", "identity"], - "content_security_policy": { - "extension_pages": "script-src 'self'; object-src 'self'; frame-src 'self' https://companion.cal.com;" - }, - "data_collection_permissions": { - "libraries": [ - { - "name": "cal.com", - "url": "https://cal.com/privacy", - "purpose": "Booking and scheduling" - } - ] - }, - "action": { - "default_title": "Cal.com Companion" - }, - "background": { - "service_worker": "background.js" - }, - "content_scripts": [ - { - "matches": [""], - "js": ["content-scripts/content.js"] - } - ] -} diff --git a/companion/extension/public/oauth/callback.html b/companion/extension/public/oauth/callback.html deleted file mode 100644 index 89188bb930..0000000000 --- a/companion/extension/public/oauth/callback.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - Cal.com OAuth Callback - - -

Processing authentication...

- - diff --git a/companion/extension/types/google-calendar.ts b/companion/extension/types/google-calendar.ts deleted file mode 100644 index cecd03071a..0000000000 --- a/companion/extension/types/google-calendar.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Types for Google Calendar integration in the extension - * These types are used for parsing and handling Google Calendar data - */ - -/** - * Represents a time slot parsed from Google Calendar - */ -export interface TimeSlot { - date: string; - startTime: string; - endTime: string; - duration: number; -} - -/** - * Parsed data from Google Calendar chip elements - */ -export interface ParsedGoogleCalendarData { - title?: string; - startTime?: string; - endTime?: string; - detectedDuration?: number; - attendees?: string[]; - location?: string; - slots: TimeSlot[]; -} - -/** - * Basic event type information used in the extension - */ -export interface EventTypeBasic { - id: number; - title: string; - slug: string; - lengthInMinutes: number; -} - -/** - * Response from fetching event types - */ -export interface EventTypesResponse { - data?: EventTypeBasic[]; - error?: string; -} - -/** - * Token request for OAuth exchange - */ -export interface TokenExchangeRequest { - grant_type: string; - client_id: string; - code?: string; - redirect_uri?: string; - code_verifier?: string; - refresh_token?: string; -} - -/** - * Slots grouped by date for display - */ -export interface SlotsByDate { - [date: string]: TimeSlot[]; -} diff --git a/companion/global.css b/companion/global.css deleted file mode 100644 index 85d98a39f4..0000000000 --- a/companion/global.css +++ /dev/null @@ -1,58 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 0 0% 0%; - --card: 0 0% 100%; - --card-foreground: 0 0% 3.9%; - --popover: 0 0% 100%; - --popover-foreground: 0 0% 3.9%; - --primary: 0 0% 9%; - --primary-foreground: 0 0% 98%; - --secondary: 0 0% 96.1%; - --secondary-foreground: 0 0% 9%; - --muted: 0 0% 96.1%; - --muted-foreground: 0 0% 45.1%; - --accent: 0 0% 96.1%; - --accent-foreground: 0 0% 9%; - --destructive: 350 100% 25%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 89.8%; - --input: 0 0% 89.8%; - --ring: 0 0% 3.9%; - --radius: 0.5rem; - } - - @media (prefers-color-scheme: dark) { - :root { - --background: 0 0% 3.9%; - --foreground: 0 0% 98%; - --card: 0 0% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 0 0% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; - --primary-foreground: 0 0% 9%; - --secondary: 0 0% 14.9%; - --secondary-foreground: 0 0% 98%; - --muted: 0 0% 14.9%; - --muted-foreground: 0 0% 63.9%; - --accent: 0 0% 14.9%; - --accent-foreground: 0 0% 98%; - --destructive: 350 100% 15%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 14.9%; - --input: 0 0% 14.9%; - --ring: 0 0% 83.1%; - } - } - - input:focus { - --tw-ring-color: rgb(0 0 0); - --tw-border-opacity: 1; - border-color: rgb(0 0 0); - } -} diff --git a/companion/hooks/index.ts b/companion/hooks/index.ts deleted file mode 100644 index 98d341fef9..0000000000 --- a/companion/hooks/index.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Query Hooks Index - * - * Central export point for all React Query hooks. - * Import hooks from this file for clean imports: - * - * @example - * ```tsx - * import { useBookings, useEventTypes, useSchedules } from '../hooks'; - * ``` - */ - -// Re-export query keys for advanced use cases -export { queryKeys } from "@/config/cache.config"; -// Re-export query context utilities -export { useQueryContext } from "@/contexts/QueryContext"; -// UI State Management hooks -export { - type BookingFilter, - type BookingFilterOption, - useActiveBookingFilter, -} from "./useActiveBookingFilter"; -export { useBookingActionModals } from "./useBookingActionModals"; -export { useBookingActions } from "./useBookingActions"; -// Bookings hooks -export { - type Booking, - type BookingFilters, - useBookingByUid, - useBookings, - useCancelBooking, - useConfirmBooking, - useDeclineBooking, - useInvalidateBookings, - usePrefetchBookings, - useRescheduleBooking, -} from "./useBookings"; -// Event Types hooks -export { - type CreateEventTypeInput, - type EventType, - useCreateEventType, - useDeleteEventType, - useDuplicateEventType, - useEventTypeById, - useEventTypes, - useInvalidateEventTypes, - usePrefetchEventTypes, - useUpdateEventType, -} from "./useEventTypes"; -// Schedules (Availability) hooks -export { - type CreateScheduleInput, - type Schedule, - type UpdateScheduleInput, - useCreateSchedule, - useDeleteSchedule, - useDuplicateSchedule, - useInvalidateSchedules, - usePrefetchSchedules, - useScheduleById, - useSchedules, - useSetScheduleAsDefault, - useUpdateSchedule, -} from "./useSchedules"; -// Toast hook -export { type ToastState, type ToastType, useToast } from "./useToast"; -// User Preferences hooks -export { - getRouteFromPreference, - LANDING_PAGE_OPTIONS, - type LandingPage, - type LandingPageOption, - type UserPreferences, - useUserPreferences, -} from "./useUserPreferences"; -// User Profile hooks -export { - type UpdateUserProfileInput, - type UserProfile, - useInvalidateUserProfile, - usePrefetchUserProfile, - useUpdateUserProfile, - useUsername, - useUserProfile, -} from "./useUserProfile"; -// Widget sync hook -export { useWidgetSync } from "./useWidgetSync"; diff --git a/companion/hooks/useActiveBookingFilter.tsx b/companion/hooks/useActiveBookingFilter.tsx deleted file mode 100644 index 612cff0d85..0000000000 --- a/companion/hooks/useActiveBookingFilter.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { useCallback, useMemo, useState } from "react"; -import type { NativeSyntheticEvent } from "react-native"; -import type { BookingFilters } from "./useBookings"; - -export type BookingFilter = "upcoming" | "unconfirmed" | "recurring" | "past" | "cancelled"; - -export interface BookingFilterOption { - key: BookingFilter; - label: string; -} - -const FILTER_OPTIONS: BookingFilterOption[] = [ - { key: "upcoming", label: "Upcoming" }, - { key: "unconfirmed", label: "Unconfirmed" }, - { key: "recurring", label: "Recurring" }, - { key: "past", label: "Past" }, - { key: "cancelled", label: "Cancelled" }, -]; - -/** - * Hook to manage active booking filter state and provide filter-related utilities. - * - * Features: - * - Manages active filter state - * - Provides filter options and labels for UI components - * - Converts filter selection to API-compatible parameters - * - Handles segmented control integration - * - Supports change callbacks for dependent filter cleanup - * - * @param initialFilter - Initial filter value (defaults to "upcoming") - * @param onFilterChange - Optional callback invoked when filter changes - * @returns Object containing filter state, handlers, and computed values - * - * @example - * ```tsx - * const { - * activeFilter, - * filterLabels, - * activeIndex, - * filterParams, - * handleFilterChange, - * handleSegmentChange, - * } = useActiveBookingFilter("upcoming", () => { - * // Clear dependent filters like search query, event type filter, etc. - * setSearchQuery(""); - * setSelectedEventTypeId(null); - * }); - * - * // Use with React Query - * const { data: bookings } = useBookings(filterParams); - * - * // Use with SegmentedControl - * - * ``` - */ -export function useActiveBookingFilter( - initialFilter: BookingFilter = "upcoming", - onFilterChange?: (filter: BookingFilter) => void -) { - const [activeFilter, setActiveFilter] = useState(initialFilter); - - /** - * Converts the active filter to API-compatible query parameters - */ - const filterParams = useMemo(() => { - switch (activeFilter) { - case "upcoming": - return { status: ["upcoming"], limit: 50 }; - case "unconfirmed": - return { status: ["unconfirmed"], limit: 50 }; - case "recurring": - return { status: ["recurring"], limit: 100 }; - case "past": - return { status: ["past"], limit: 100 }; - case "cancelled": - return { status: ["cancelled"], limit: 100 }; - default: - return { status: ["upcoming"], limit: 50 }; - } - }, [activeFilter]); - - /** - * Array of filter labels for UI components (e.g., SegmentedControl) - */ - const filterLabels = useMemo(() => FILTER_OPTIONS.map((option) => option.label), []); - - /** - * Current active filter index for UI components - */ - const activeIndex = useMemo( - () => FILTER_OPTIONS.findIndex((option) => option.key === activeFilter), - [activeFilter] - ); - - /** - * Changes the active filter and invokes the optional callback - */ - const handleFilterChange = useCallback( - (filter: BookingFilter) => { - setActiveFilter(filter); - onFilterChange?.(filter); - }, - [onFilterChange] - ); - - /** - * Handler for React Native SegmentedControl onChange event - */ - const handleSegmentChange = useCallback( - (event: NativeSyntheticEvent<{ selectedSegmentIndex: number }>) => { - const { selectedSegmentIndex } = event.nativeEvent; - const selectedFilter = FILTER_OPTIONS[selectedSegmentIndex]; - if (selectedFilter) { - handleFilterChange(selectedFilter.key); - } - }, - [handleFilterChange] - ); - - return { - // State - activeFilter, - - // Computed values - filterOptions: FILTER_OPTIONS, - filterLabels, - activeIndex, - filterParams, - - // Handlers - handleFilterChange, - handleSegmentChange, - }; -} diff --git a/companion/hooks/useAppStoreRating.ts b/companion/hooks/useAppStoreRating.ts deleted file mode 100644 index 5f3742e0e0..0000000000 --- a/companion/hooks/useAppStoreRating.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { useCallback } from "react"; -import AsyncStorage from "@react-native-async-storage/async-storage"; -import { Platform } from "react-native"; - -/** - * Rating triggers - add new triggers here as needed. - * Each trigger will only prompt the user once (tracked via AsyncStorage). - */ -export const RatingTrigger = { - EVENT_TYPE_SAVED: "event_type_saved", - BOOKING_CONFIRMED: "booking_confirmed", - BOOKING_REJECTED: "booking_rejected", -} as const; - -export type RatingTriggerType = (typeof RatingTrigger)[keyof typeof RatingTrigger]; - -const STORAGE_KEY_PREFIX = "app_rating_triggered_"; - -function getStorageKey(trigger: RatingTriggerType): string { - return `${STORAGE_KEY_PREFIX}${trigger}`; -} - -async function requestReviewIfAvailable(): Promise { - // Only available on native platforms (iOS/Android) - if (Platform.OS === "web") { - return false; - } - - try { - // Dynamic import to avoid bundling issues on web - const StoreReview = await import("expo-store-review"); - const isAvailable = await StoreReview.isAvailableAsync(); - if (isAvailable) { - await StoreReview.requestReview(); - return true; - } - return false; - } catch (error) { - console.warn("Failed to request app store review:", error); - return false; - } -} - -async function hasAlreadyTriggered(trigger: RatingTriggerType): Promise { - try { - const value = await AsyncStorage.getItem(getStorageKey(trigger)); - return value === "true"; - } catch (error) { - console.warn("Failed to check rating trigger status:", error); - return false; - } -} - -async function markAsTriggered(trigger: RatingTriggerType): Promise { - try { - await AsyncStorage.setItem(getStorageKey(trigger), "true"); - } catch (error) { - console.warn("Failed to mark rating as triggered:", error); - } -} - -/** - * Request an app store rating for a specific trigger. - * Only prompts the user once per trigger type. - * - * @param trigger - The trigger type from RatingTrigger - * @returns true if the rating dialog was shown, false otherwise - * - * @example - * // Request rating after first event type save - * await requestRating(RatingTrigger.EVENT_TYPE_SAVED); - * - * // Request rating after first booking confirmation - * await requestRating(RatingTrigger.BOOKING_CONFIRMED); - */ -export async function requestRating(trigger: RatingTriggerType): Promise { - const alreadyTriggered = await hasAlreadyTriggered(trigger); - if (alreadyTriggered) { - return false; - } - - await markAsTriggered(trigger); - return requestReviewIfAvailable(); -} - -/** - * Hook for requesting app store ratings. - * Provides a memoized callback for use in React components. - */ -export function useAppStoreRating() { - const requestRatingCallback = useCallback( - async (trigger: RatingTriggerType): Promise => { - return requestRating(trigger); - }, - [] - ); - - return { - requestRating: requestRatingCallback, - RatingTrigger, - }; -} diff --git a/companion/hooks/useBookingActionModals.ts b/companion/hooks/useBookingActionModals.ts deleted file mode 100644 index 75d2ff60f0..0000000000 --- a/companion/hooks/useBookingActionModals.ts +++ /dev/null @@ -1,345 +0,0 @@ -/** - * useBookingActionModals Hook - * - * Centralized hook for managing all booking action modals and their API calls. - * This hook is designed to be used across iOS, Android, and extension platforms. - */ - -import { useQueryClient } from "@tanstack/react-query"; -import { useCallback, useState } from "react"; -import { Alert, Linking } from "react-native"; -import { queryKeys } from "@/config/cache.config"; -import { type Booking, CalComAPIService } from "@/services/calcom"; -import type { - AddGuestInput, - BookingRecording, - ConferencingSession, -} from "@/services/types/bookings.types"; -import { showErrorAlert, showSuccessAlert } from "@/utils/alerts"; - -interface UseBookingActionModalsReturn { - // Selected booking for actions - selectedBooking: Booking | null; - setSelectedBooking: (booking: Booking | null) => void; - - // Add Guests Modal - showAddGuestsModal: boolean; - isAddingGuests: boolean; - openAddGuestsModal: (booking: Booking) => void; - closeAddGuestsModal: () => void; - handleAddGuests: (guests: AddGuestInput[]) => Promise; - - // Edit Location Modal - showEditLocationModal: boolean; - isUpdatingLocation: boolean; - openEditLocationModal: (booking: Booking) => void; - closeEditLocationModal: () => void; - handleUpdateLocation: (location: string) => Promise; - - // View Recordings Modal - showViewRecordingsModal: boolean; - isLoadingRecordings: boolean; - recordings: BookingRecording[]; - openViewRecordingsModal: (booking: Booking) => void; - closeViewRecordingsModal: () => void; - - // Meeting Session Details Modal - showMeetingSessionDetailsModal: boolean; - isLoadingSessions: boolean; - sessions: ConferencingSession[]; - openMeetingSessionDetailsModal: (booking: Booking) => void; - closeMeetingSessionDetailsModal: () => void; - - // Mark No-Show Modal - showMarkNoShowModal: boolean; - isMarkingNoShow: boolean; - openMarkNoShowModal: (booking: Booking) => void; - closeMarkNoShowModal: () => void; - handleMarkNoShow: (attendeeEmail: string, absent: boolean) => Promise; - - // Request Reschedule (deep link to web) - handleRequestReschedule: (booking: Booking) => void; -} - -export function useBookingActionModals(): UseBookingActionModalsReturn { - "use no memo"; - const queryClient = useQueryClient(); - - // Selected booking state - const [selectedBooking, setSelectedBooking] = useState(null); - - // Add Guests Modal state - const [showAddGuestsModal, setShowAddGuestsModal] = useState(false); - const [isAddingGuests, setIsAddingGuests] = useState(false); - - // Edit Location Modal state - const [showEditLocationModal, setShowEditLocationModal] = useState(false); - const [isUpdatingLocation, setIsUpdatingLocation] = useState(false); - - // View Recordings Modal state - const [showViewRecordingsModal, setShowViewRecordingsModal] = useState(false); - const [isLoadingRecordings, setIsLoadingRecordings] = useState(false); - const [recordings, setRecordings] = useState([]); - - // Meeting Session Details Modal state - const [showMeetingSessionDetailsModal, setShowMeetingSessionDetailsModal] = useState(false); - const [isLoadingSessions, setIsLoadingSessions] = useState(false); - const [sessions, setSessions] = useState([]); - - // Mark No-Show Modal state - const [showMarkNoShowModal, setShowMarkNoShowModal] = useState(false); - const [isMarkingNoShow, setIsMarkingNoShow] = useState(false); - - // Invalidate booking queries after mutations - const invalidateBookingQueries = useCallback(() => { - queryClient.invalidateQueries({ queryKey: queryKeys.bookings.all }); - if (selectedBooking?.uid) { - queryClient.invalidateQueries({ queryKey: queryKeys.bookings.detail(selectedBooking.uid) }); - } - }, [queryClient, selectedBooking?.uid]); - - // ============================================================================ - // Add Guests - // ============================================================================ - - const openAddGuestsModal = useCallback((booking: Booking) => { - setSelectedBooking(booking); - setShowAddGuestsModal(true); - }, []); - - const closeAddGuestsModal = useCallback(() => { - setShowAddGuestsModal(false); - }, []); - - const handleAddGuests = useCallback( - async (guests: AddGuestInput[]) => { - if (!selectedBooking) { - showErrorAlert("Error", "No booking selected"); - return; - } - - setIsAddingGuests(true); - try { - await CalComAPIService.addGuests(selectedBooking.uid, guests); - showSuccessAlert( - "Success", - "Guests added successfully. They will receive an email notification." - ); - invalidateBookingQueries(); - closeAddGuestsModal(); - setIsAddingGuests(false); - } catch (error) { - const message = error instanceof Error ? error.message : "Failed to add guests"; - showErrorAlert("Error", message); - setIsAddingGuests(false); - } - }, - [selectedBooking, invalidateBookingQueries, closeAddGuestsModal] - ); - - // ============================================================================ - // Edit Location - // ============================================================================ - - const openEditLocationModal = useCallback((booking: Booking) => { - setSelectedBooking(booking); - setShowEditLocationModal(true); - }, []); - - const closeEditLocationModal = useCallback(() => { - setShowEditLocationModal(false); - }, []); - - const handleUpdateLocation = useCallback( - async (location: string) => { - if (!selectedBooking) { - showErrorAlert("Error", "No booking selected"); - return; - } - - setIsUpdatingLocation(true); - try { - await CalComAPIService.updateLocation(selectedBooking.uid, location); - showSuccessAlert( - "Success", - "Location updated successfully. Note: The calendar event may not be automatically updated." - ); - invalidateBookingQueries(); - closeEditLocationModal(); - setIsUpdatingLocation(false); - } catch (error) { - const message = error instanceof Error ? error.message : "Failed to update location"; - showErrorAlert("Error", message); - setIsUpdatingLocation(false); - } - }, - [selectedBooking, invalidateBookingQueries, closeEditLocationModal] - ); - - // ============================================================================ - // View Recordings - // ============================================================================ - - const openViewRecordingsModal = useCallback(async (booking: Booking) => { - setSelectedBooking(booking); - setShowViewRecordingsModal(true); - setIsLoadingRecordings(true); - setRecordings([]); - - try { - const recordingsData = await CalComAPIService.getRecordings(booking.uid); - setRecordings(recordingsData); - setIsLoadingRecordings(false); - } catch (error) { - if (__DEV__) { - console.debug("[useBookingActionModals] Failed to load recordings:", error); - } - showErrorAlert("Error", "Failed to load recordings. Please try again."); - setShowViewRecordingsModal(false); - setIsLoadingRecordings(false); - } - }, []); - - const closeViewRecordingsModal = useCallback(() => { - setShowViewRecordingsModal(false); - setRecordings([]); - }, []); - - // ============================================================================ - // Meeting Session Details - // ============================================================================ - - const openMeetingSessionDetailsModal = useCallback(async (booking: Booking) => { - setSelectedBooking(booking); - setShowMeetingSessionDetailsModal(true); - setIsLoadingSessions(true); - setSessions([]); - - try { - const sessionsData = await CalComAPIService.getConferencingSessions(booking.uid); - setSessions(sessionsData); - setIsLoadingSessions(false); - } catch (error) { - if (__DEV__) { - console.debug("[useBookingActionModals] Failed to load sessions:", error); - } - showErrorAlert("Error", "Failed to load meeting session details. Please try again."); - setShowMeetingSessionDetailsModal(false); - setIsLoadingSessions(false); - } - }, []); - - const closeMeetingSessionDetailsModal = useCallback(() => { - setShowMeetingSessionDetailsModal(false); - setSessions([]); - }, []); - - // ============================================================================ - // Mark No-Show - // ============================================================================ - - const openMarkNoShowModal = useCallback((booking: Booking) => { - setSelectedBooking(booking); - setShowMarkNoShowModal(true); - }, []); - - const closeMarkNoShowModal = useCallback(() => { - setShowMarkNoShowModal(false); - }, []); - - const handleMarkNoShow = useCallback( - async (attendeeEmail: string, absent: boolean) => { - if (!selectedBooking) { - showErrorAlert("Error", "No booking selected"); - return; - } - - setIsMarkingNoShow(true); - try { - await CalComAPIService.markAbsent(selectedBooking.uid, attendeeEmail, absent); - showSuccessAlert( - "Success", - absent ? "Attendee marked as no-show." : "No-show status removed." - ); - invalidateBookingQueries(); - closeMarkNoShowModal(); - setIsMarkingNoShow(false); - } catch (error) { - const message = error instanceof Error ? error.message : "Failed to update no-show status"; - showErrorAlert("Error", message); - setIsMarkingNoShow(false); - } - }, - [selectedBooking, invalidateBookingQueries, closeMarkNoShowModal] - ); - - // ============================================================================ - // Request Reschedule (deep link to web) - // ============================================================================ - - const handleRequestReschedule = useCallback((booking: Booking) => { - // Request reschedule is a server-driven operation that requires the web app - // We deep link to the booking detail page where the user can trigger it - const webUrl = `https://app.cal.com/booking/${booking.uid}`; - - Alert.alert( - "Request Reschedule", - "This action will open the Cal.com web app where you can request a reschedule from the attendee.", - [ - { text: "Cancel", style: "cancel" }, - { - text: "Open in Browser", - onPress: () => { - Linking.openURL(webUrl).catch(() => { - showErrorAlert("Error", "Failed to open the web app. Please try again."); - }); - }, - }, - ] - ); - }, []); - - return { - // Selected booking - selectedBooking, - setSelectedBooking, - - // Add Guests - showAddGuestsModal, - isAddingGuests, - openAddGuestsModal, - closeAddGuestsModal, - handleAddGuests, - - // Edit Location - showEditLocationModal, - isUpdatingLocation, - openEditLocationModal, - closeEditLocationModal, - handleUpdateLocation, - - // View Recordings - showViewRecordingsModal, - isLoadingRecordings, - recordings, - openViewRecordingsModal, - closeViewRecordingsModal, - - // Meeting Session Details - showMeetingSessionDetailsModal, - isLoadingSessions, - sessions, - openMeetingSessionDetailsModal, - closeMeetingSessionDetailsModal, - - // Mark No-Show - showMarkNoShowModal, - isMarkingNoShow, - openMarkNoShowModal, - closeMarkNoShowModal, - handleMarkNoShow, - - // Request Reschedule - handleRequestReschedule, - }; -} diff --git a/companion/hooks/useBookingActions.ts b/companion/hooks/useBookingActions.ts deleted file mode 100644 index 2822bec514..0000000000 --- a/companion/hooks/useBookingActions.ts +++ /dev/null @@ -1,506 +0,0 @@ -import type { useRouter } from "expo-router"; -import { useState } from "react"; -import { Alert, Platform } from "react-native"; -import type { Booking } from "@/services/calcom"; -import { showErrorAlert, showSuccessAlert } from "@/utils/alerts"; - -interface UseBookingActionsParams { - router: ReturnType; - cancelMutation: ( - params: { uid: string; reason: string }, - options?: { - onSuccess?: () => void; - onError?: (error: Error) => void; - } - ) => void; - confirmMutation: ( - params: { uid: string }, - options?: { - onSuccess?: () => void; - onError?: (error: Error) => void; - } - ) => void; - declineMutation: ( - params: { uid: string; reason?: string }, - options?: { - onSuccess?: () => void; - onError?: (error: Error) => void; - } - ) => void; - rescheduleMutation: ( - params: { uid: string; start: string; reschedulingReason?: string }, - options?: { - onSuccess?: () => void; - onError?: (error: Error) => void; - } - ) => void; - isConfirming: boolean; - isDeclining: boolean; - isRescheduling: boolean; -} - -export const useBookingActions = ({ - router, - cancelMutation, - confirmMutation, - declineMutation, - rescheduleMutation, - isConfirming, - isDeclining, - isRescheduling, -}: UseBookingActionsParams) => { - // Reschedule modal state - const [showRescheduleModal, setShowRescheduleModal] = useState(false); - const [rescheduleBooking, setRescheduleBooking] = useState(null); - const [rescheduleDate, setRescheduleDate] = useState(""); - const [rescheduleTime, setRescheduleTime] = useState(""); - const [rescheduleReason, setRescheduleReason] = useState(""); - - // Reject modal state - const [showRejectModal, setShowRejectModal] = useState(false); - const [rejectBooking, setRejectBooking] = useState(null); - const [rejectReason, setRejectReason] = useState(""); - - // Cancel modal state (for Android AlertDialog) - const [showCancelModal, setShowCancelModal] = useState(false); - const [cancelBooking, setCancelBooking] = useState(null); - const [cancelReason, setCancelReason] = useState(""); - - // Selected booking for actions modal - const [selectedBooking, setSelectedBooking] = useState(null); - - /** - * Navigate to booking detail page - */ - const handleBookingPress = (booking: Booking) => { - router.push({ - pathname: "/(tabs)/(bookings)/booking-detail", - params: { uid: booking.uid }, - }); - }; - - /** - * Open reschedule modal with pre-filled data - */ - const handleRescheduleBooking = (booking: Booking) => { - // Get the start time from booking (prefer startTime, fallback to start) - const startTimeValue = booking.startTime || booking.start; - - // Validate we have a valid date - if (!startTimeValue) { - showErrorAlert("Error", "Unable to reschedule: booking has no start time"); - return; - } - - const currentDate = new Date(startTimeValue); - - // Check if the date is valid - if (Number.isNaN(currentDate.getTime())) { - showErrorAlert("Error", "Unable to reschedule: invalid booking date"); - return; - } - - // Use local timezone consistently (not UTC) to avoid date/time mismatch - const year = currentDate.getFullYear(); - const month = String(currentDate.getMonth() + 1).padStart(2, "0"); - const day = String(currentDate.getDate()).padStart(2, "0"); - const hours = String(currentDate.getHours()).padStart(2, "0"); - const minutes = String(currentDate.getMinutes()).padStart(2, "0"); - - const dateStr = `${year}-${month}-${day}`; - const timeStr = `${hours}:${minutes}`; - - setRescheduleBooking(booking); - setRescheduleDate(dateStr); - setRescheduleTime(timeStr); - setRescheduleReason(""); - setShowRescheduleModal(true); - }; - - /** - * Validate and submit reschedule request (uses internal state) - */ - const handleSubmitReschedule = () => { - if (!rescheduleBooking || !rescheduleDate || !rescheduleTime) { - showErrorAlert("Error", "Please enter both date and time"); - return; - } - - // Parse the date and time - const dateTimeStr = `${rescheduleDate}T${rescheduleTime}:00`; - const newDateTime = new Date(dateTimeStr); - - // Validate the date - if (Number.isNaN(newDateTime.getTime())) { - showErrorAlert( - "Error", - "Invalid date or time format. Please use YYYY-MM-DD for date and HH:MM for time." - ); - return; - } - - // Check if the new time is in the future - if (newDateTime <= new Date()) { - showErrorAlert("Error", "Please select a future date and time"); - return; - } - - // Convert to UTC ISO string - const startUtc = newDateTime.toISOString(); - - rescheduleMutation( - { - uid: rescheduleBooking.uid, - start: startUtc, - reschedulingReason: rescheduleReason || undefined, - }, - { - onSuccess: () => { - setShowRescheduleModal(false); - setRescheduleBooking(null); - showSuccessAlert("Success", "Booking rescheduled successfully"); - }, - onError: (error) => { - showErrorAlert("Error", error.message || "Failed to reschedule booking"); - }, - } - ); - }; - - /** - * Submit reschedule with provided values (used by RescheduleModal component) - */ - const handleRescheduleWithValues = async ( - date: string, - time: string, - reason?: string - ): Promise => { - if (!rescheduleBooking) { - throw new Error("No booking selected"); - } - - // Parse the date and time - const dateTimeStr = `${date}T${time}:00`; - const newDateTime = new Date(dateTimeStr); - - // Validate the date - if (Number.isNaN(newDateTime.getTime())) { - throw new Error( - "Invalid date or time format. Please use YYYY-MM-DD for date and HH:MM for time." - ); - } - - // Check if the new time is in the future - if (newDateTime <= new Date()) { - throw new Error("Please select a future date and time"); - } - - // Convert to UTC ISO string - const startUtc = newDateTime.toISOString(); - - return new Promise((resolve, reject) => { - rescheduleMutation( - { - uid: rescheduleBooking.uid, - start: startUtc, - reschedulingReason: reason || undefined, - }, - { - onSuccess: () => { - setShowRescheduleModal(false); - setRescheduleBooking(null); - showSuccessAlert("Success", "Booking rescheduled successfully"); - resolve(); - }, - onError: (error) => { - reject(new Error(error.message || "Failed to reschedule booking")); - }, - } - ); - }); - }; - - /** - * Close reschedule modal and reset state - */ - const handleCloseRescheduleModal = () => { - setShowRescheduleModal(false); - setRescheduleBooking(null); - }; - - /** - * Open cancel modal for Android, use Alert.prompt for iOS - */ - const handleOpenCancelModal = (booking: Booking) => { - setCancelBooking(booking); - setCancelReason(""); - setShowCancelModal(true); - }; - - /** - * Submit cancel with reason (used by Android AlertDialog) - */ - const handleSubmitCancel = () => { - if (!cancelBooking) return; - - const reason = cancelReason.trim() || "Event cancelled by host"; - - cancelMutation( - { uid: cancelBooking.uid, reason }, - { - onSuccess: () => { - setShowCancelModal(false); - setCancelBooking(null); - setCancelReason(""); - showSuccessAlert("Success", "Event cancelled successfully"); - }, - onError: (_error) => { - console.error("Failed to cancel booking"); - showErrorAlert("Error", "Failed to cancel event. Please try again."); - }, - } - ); - }; - - /** - * Close cancel modal and reset state - */ - const handleCloseCancelModal = () => { - setShowCancelModal(false); - setCancelBooking(null); - setCancelReason(""); - }; - - /** - * Show alert and cancel booking (iOS Alert.prompt pattern, Android opens modal) - */ - const handleCancelBooking = (booking: Booking) => { - // For Android and Web, open the cancel modal (AlertDialog on Android, content-modal on Web) - if (Platform.OS === "android" || Platform.OS === "web") { - handleOpenCancelModal(booking); - return; - } - - // For iOS, use native Alert.prompt - Alert.alert("Cancel Event", `Are you sure you want to cancel "${booking.title}"?`, [ - { text: "Cancel", style: "cancel" }, - { - text: "Cancel Event", - style: "destructive", - onPress: () => { - // Prompt for cancellation reason - Alert.prompt( - "Cancellation Reason", - "Please provide a reason for cancelling this booking:", - [ - { text: "Cancel", style: "cancel" }, - { - text: "Cancel Event", - style: "destructive", - onPress: (reason?: string) => { - const cancellationReason = reason?.trim() || "Event cancelled by host"; - cancelMutation( - { uid: booking.uid, reason: cancellationReason }, - { - onSuccess: () => { - showSuccessAlert("Success", "Event cancelled successfully"); - }, - onError: (_error) => { - console.error("Failed to cancel booking"); - showErrorAlert("Error", "Failed to cancel event. Please try again."); - }, - } - ); - }, - }, - ], - "plain-text", - "", - "default" - ); - }, - }, - ]); - }; - - /** - * Show alert and confirm booking - */ - const handleConfirmBooking = (booking: Booking) => { - Alert.alert("Confirm Booking", `Are you sure you want to confirm "${booking.title}"?`, [ - { text: "Cancel", style: "cancel" }, - { - text: "Confirm", - onPress: () => { - confirmMutation( - { uid: booking.uid }, - { - onSuccess: () => { - showSuccessAlert("Success", "Booking confirmed successfully"); - }, - onError: (error) => { - showErrorAlert("Error", error.message || "Failed to confirm booking"); - }, - } - ); - }, - }, - ]); - }; - - /** - * Open reject modal (used by iOS for custom modal UI) - */ - const handleOpenRejectModal = (booking: Booking) => { - setRejectBooking(booking); - setRejectReason(""); - setShowRejectModal(true); - }; - - /** - * Show alert and reject booking (Android Alert.prompt pattern) - */ - const handleRejectBooking = (booking: Booking) => { - Alert.alert("Decline Booking", `Are you sure you want to decline "${booking.title}"?`, [ - { text: "Cancel", style: "cancel" }, - { - text: "Decline", - style: "destructive", - onPress: () => { - // Show optional reason input - Alert.prompt( - "Decline Reason", - "Optionally provide a reason for declining (press OK to skip)", - [ - { text: "Cancel", style: "cancel" }, - { - text: "OK", - onPress: (reason?: string) => { - declineMutation( - { uid: booking.uid, reason: reason || undefined }, - { - onSuccess: () => { - showSuccessAlert("Success", "Booking declined successfully"); - }, - onError: (error) => { - showErrorAlert("Error", error.message || "Failed to decline booking"); - }, - } - ); - }, - }, - ], - "plain-text", - "", - "default" - ); - }, - }, - ]); - }; - - /** - * Submit rejection with reason (used by iOS custom modal) - * @param reasonOverride - Optional reason passed directly to avoid race condition with state updates - */ - const handleSubmitReject = (reasonOverride?: string) => { - if (!rejectBooking) return; - - // Use passed reason if provided, otherwise fall back to state - const reason = reasonOverride !== undefined ? reasonOverride : rejectReason; - - declineMutation( - { uid: rejectBooking.uid, reason: reason || undefined }, - { - onSuccess: () => { - setShowRejectModal(false); - setRejectBooking(null); - setRejectReason(""); - showSuccessAlert("Success", "Booking rejected successfully"); - }, - onError: (_error) => { - showErrorAlert("Error", "Failed to reject booking. Please try again."); - }, - } - ); - }; - - /** - * Close reject modal and reset state - */ - const handleCloseRejectModal = () => { - setShowRejectModal(false); - setRejectBooking(null); - setRejectReason(""); - }; - - /** - * Inline confirm handler (for use directly in JSX) - */ - const handleInlineConfirm = (booking: Booking) => { - confirmMutation( - { uid: booking.uid }, - { - onSuccess: () => { - showSuccessAlert("Success", "Booking confirmed successfully"); - }, - onError: (_error) => { - showErrorAlert("Error", "Failed to confirm booking. Please try again."); - }, - } - ); - }; - - return { - // Reschedule state - showRescheduleModal, - setShowRescheduleModal, - rescheduleBooking, - rescheduleDate, - setRescheduleDate, - rescheduleTime, - setRescheduleTime, - rescheduleReason, - setRescheduleReason, - - // Reject state - showRejectModal, - setShowRejectModal, - rejectBooking, - rejectReason, - setRejectReason, - - // Cancel state (for Android AlertDialog) - showCancelModal, - setShowCancelModal, - cancelBooking, - cancelReason, - setCancelReason, - - // Selected booking state - selectedBooking, - setSelectedBooking, - - // Handlers - handleBookingPress, - handleRescheduleBooking, - handleSubmitReschedule, - handleRescheduleWithValues, - handleCloseRescheduleModal, - handleCancelBooking, - handleOpenCancelModal, - handleSubmitCancel, - handleCloseCancelModal, - handleConfirmBooking, - handleOpenRejectModal, - handleRejectBooking, - handleSubmitReject, - handleCloseRejectModal, - handleInlineConfirm, - - // Loading states (pass through) - isConfirming, - isDeclining, - isRescheduling, - }; -}; diff --git a/companion/hooks/useBookingActionsGating.ts b/companion/hooks/useBookingActionsGating.ts deleted file mode 100644 index 894ef253dc..0000000000 --- a/companion/hooks/useBookingActionsGating.ts +++ /dev/null @@ -1,120 +0,0 @@ -/** - * useBookingActionsGating Hook - * - * This hook provides the booking actions state using the centralized gating utility. - * It computes which actions are visible and enabled for a given booking. - */ - -import { useMemo } from "react"; -import type { Booking } from "@/services/calcom"; -import type { EventType } from "@/services/types/event-types.types"; -import { - type BookingActionsResult, - getBookingActions, - type NormalizedBooking, - normalizeBooking, -} from "@/utils/booking-actions"; - -interface UseBookingActionsGatingParams { - booking: Booking | null; - eventType?: EventType | null; - currentUserId?: number; - currentUserEmail?: string; - isOnline?: boolean; -} - -interface UseBookingActionsGatingResult { - actions: BookingActionsResult; - normalizedBooking: NormalizedBooking | null; -} - -/** - * Default actions result when no booking is provided - */ -const defaultActions: BookingActionsResult = { - reschedule: { visible: false, enabled: false }, - rescheduleRequest: { visible: false, enabled: false }, - cancel: { visible: false, enabled: false }, - changeLocation: { visible: false, enabled: false }, - addGuests: { visible: false, enabled: false }, - viewRecordings: { visible: false, enabled: false }, - meetingSessionDetails: { visible: false, enabled: false }, - markNoShow: { visible: false, enabled: false }, -}; - -/** - * Hook to compute booking actions visibility and enabled state. - * - * @param params - Parameters including booking, eventType, and user info - * @returns Actions result and normalized booking - */ -export function useBookingActionsGating({ - booking, - eventType, - currentUserId, - currentUserEmail, - isOnline = true, -}: UseBookingActionsGatingParams): UseBookingActionsGatingResult { - // Normalize the booking and compute actions - const result = useMemo(() => { - if (!booking) { - return { - actions: defaultActions, - normalizedBooking: null, - }; - } - - const normalizedBooking = normalizeBooking(booking); - const actions = getBookingActions({ - booking: normalizedBooking, - eventType, - currentUserId, - currentUserEmail, - isOnline, - }); - - return { - actions, - normalizedBooking, - }; - }, [booking, eventType, currentUserId, currentUserEmail, isOnline]); - - return result; -} - -/** - * Helper hook to get simple boolean flags for backward compatibility. - * This can be used by components that still need the old-style flags. - */ -export function useBookingStatusFlags(booking: Booking | null) { - return useMemo(() => { - if (!booking) { - return { - isUpcoming: false, - isPast: false, - isOngoing: false, - isCancelled: false, - isRejected: false, - isPending: false, - isConfirmed: false, - }; - } - - const normalized = normalizeBooking(booking); - const now = new Date(); - - const isPast = normalized.endTime < now; - const isUpcoming = normalized.endTime >= now; - const isOngoing = isUpcoming && now >= normalized.startTime; - - return { - isUpcoming, - isPast, - isOngoing, - isCancelled: normalized.status === "cancelled", - isRejected: normalized.status === "rejected", - isPending: normalized.status === "pending", - isConfirmed: normalized.status === "accepted", - }; - }, [booking]); -} diff --git a/companion/hooks/useBookings.ts b/companion/hooks/useBookings.ts deleted file mode 100644 index 05a3925924..0000000000 --- a/companion/hooks/useBookings.ts +++ /dev/null @@ -1,394 +0,0 @@ -/** - * Bookings Query Hooks - * - * This module provides React Query hooks for fetching and mutating bookings data. - * It integrates with the existing CalComAPIService and provides: - * - Automatic caching with configurable stale times - * - Pull-to-refresh support via refetch - * - Optimistic updates for mutations - * - Cache invalidation on mutations - */ - -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { CACHE_CONFIG, queryKeys } from "@/config/cache.config"; -import { type Booking, CalComAPIService } from "@/services/calcom"; -import { requestRating, RatingTrigger } from "@/hooks/useAppStoreRating"; - -/** - * Filter options for fetching bookings - */ -export interface BookingFilters { - status?: string[]; - fromDate?: string; - toDate?: string; - eventTypeId?: number; - limit?: number; - offset?: number; - [key: string]: unknown; // Index signature for Record compatibility -} - -/** - * Hook to fetch bookings with optional filters - * - * @param filters - Optional filters for the bookings query - * @returns Query result with bookings data, loading state, error, and refetch function - * - * @example - * ```tsx - * const { data: bookings, isLoading, refetch } = useBookings({ status: ['upcoming'] }); - * - * // Pull-to-refresh - * - * ``` - */ -export function useBookings(filters?: BookingFilters) { - return useQuery({ - queryKey: queryKeys.bookings.list(filters || {}), - queryFn: () => CalComAPIService.getBookings(filters), - staleTime: CACHE_CONFIG.bookings.staleTime, - // Keep previous data while fetching new data (smoother UX) - placeholderData: (previousData) => previousData, - // Don't retry on network errors (keeps cache intact) - retry: (failureCount, error) => { - // Don't retry network errors - keeps cached data visible - if (error?.message?.includes("Network") || error?.message?.includes("fetch")) { - return false; - } - return failureCount < 2; - }, - // Keep showing cached data even if refetch fails - refetchOnReconnect: true, - }); -} - -/** - * Hook to fetch a single booking by UID - * - * @param uid - The unique identifier of the booking - * @returns Query result with booking data - * - * @example - * ```tsx - * const { data: booking, isLoading } = useBookingByUid('abc-123'); - * ``` - */ -export function useBookingByUid(uid: string | undefined) { - return useQuery({ - queryKey: queryKeys.bookings.detail(uid || ""), - queryFn: () => { - if (!uid) throw new Error("uid is required"); - return CalComAPIService.getBookingByUid(uid); - }, - enabled: !!uid, // Only fetch when uid is provided - staleTime: CACHE_CONFIG.bookings.staleTime, - }); -} - -/** - * Hook to cancel a booking - * - * @returns Mutation function and state - * - * @example - * ```tsx - * const { mutate: cancelBooking, isPending } = useCancelBooking(); - * - * cancelBooking({ uid: 'abc-123', reason: 'No longer needed' }); - * ``` - */ -export function useCancelBooking() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ uid, reason }: { uid: string; reason?: string }) => - CalComAPIService.cancelBooking(uid, reason), - onSuccess: (_, variables) => { - // Invalidate all booking queries to refetch fresh data - queryClient.invalidateQueries({ queryKey: queryKeys.bookings.all }); - - // Also invalidate the specific booking detail - queryClient.invalidateQueries({ - queryKey: queryKeys.bookings.detail(variables.uid), - }); - }, - onError: (_error) => { - console.error("Failed to cancel booking"); - }, - }); -} - -/** - * Hook to mark an attendee as no-show (absent) - * - * @returns Mutation function and state - * - * @example - * ```tsx - * const { mutate: markNoShow, isPending } = useMarkNoShow(); - * - * markNoShow({ uid: 'abc-123', attendeeEmail: 'attendee@example.com', absent: true }); - * ``` - */ -export function useMarkNoShow() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ - uid, - attendeeEmail, - absent, - }: { - uid: string; - attendeeEmail: string; - absent: boolean; - }) => CalComAPIService.markAbsent(uid, attendeeEmail, absent), - onSuccess: (_, variables) => { - // Invalidate all booking queries to refetch fresh data - queryClient.invalidateQueries({ queryKey: queryKeys.bookings.all }); - - // Also invalidate the specific booking detail - queryClient.invalidateQueries({ - queryKey: queryKeys.bookings.detail(variables.uid), - }); - }, - onError: (_error) => { - console.error("Failed to mark attendee as no-show"); - }, - }); -} - -/** - * Hook to reschedule a booking - * - * @returns Mutation function and state - * - * @example - * ```tsx - * const { mutate: rescheduleBooking, isPending } = useRescheduleBooking(); - * - * rescheduleBooking({ - * uid: 'abc-123', - * start: '2024-01-15T10:00:00Z', - * reschedulingReason: 'Conflict with another meeting' - * }); - * ``` - */ -export function useRescheduleBooking() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ - uid, - start, - reschedulingReason, - }: { - uid: string; - start: string; - reschedulingReason?: string; - }) => CalComAPIService.rescheduleBooking(uid, { start, reschedulingReason }), - onSuccess: (_, variables) => { - // Invalidate all booking queries - queryClient.invalidateQueries({ queryKey: queryKeys.bookings.all }); - - // Also invalidate the specific booking detail - queryClient.invalidateQueries({ - queryKey: queryKeys.bookings.detail(variables.uid), - }); - }, - onError: (_error) => { - console.error("Failed to reschedule booking"); - }, - }); -} - -/** - * Hook to confirm a pending booking - * - * @returns Mutation function and state - * - * @example - * ```tsx - * const { mutate: confirmBooking, isPending } = useConfirmBooking(); - * - * confirmBooking({ uid: 'abc-123' }); - * ``` - */ -export function useConfirmBooking() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ uid }: { uid: string }) => CalComAPIService.confirmBooking(uid), - onSuccess: (_, variables) => { - // Invalidate all booking queries to refetch fresh data - queryClient.invalidateQueries({ queryKey: queryKeys.bookings.all }); - - // Also invalidate the specific booking detail - queryClient.invalidateQueries({ - queryKey: queryKeys.bookings.detail(variables.uid), - }); - - // Request app store rating on first booking confirmation - requestRating(RatingTrigger.BOOKING_CONFIRMED); - }, - onError: (_error) => { - console.error("Failed to confirm booking"); - }, - }); -} - -/** - * Hook to decline a pending booking - * - * @returns Mutation function and state - * - * @example - * ```tsx - * const { mutate: declineBooking, isPending } = useDeclineBooking(); - * - * declineBooking({ uid: 'abc-123', reason: 'Schedule conflict' }); - * ``` - */ -export function useDeclineBooking() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ uid, reason }: { uid: string; reason?: string }) => - CalComAPIService.declineBooking(uid, reason), - onSuccess: (_, variables) => { - // Invalidate all booking queries to refetch fresh data - queryClient.invalidateQueries({ queryKey: queryKeys.bookings.all }); - - // Also invalidate the specific booking detail - queryClient.invalidateQueries({ - queryKey: queryKeys.bookings.detail(variables.uid), - }); - - // Request app store rating on first booking rejection - requestRating(RatingTrigger.BOOKING_REJECTED); - }, - onError: (_error) => { - console.error("Failed to decline booking"); - }, - }); -} - -/** - * Hook to update the location of a booking - * - * @returns Mutation function and state - * - * @example - * ```tsx - * const { mutate: updateLocation, isPending } = useUpdateLocation(); - * - * updateLocation({ - * uid: 'abc-123', - * location: { type: 'link', link: 'https://meet.example.com' } - * }); - * ``` - */ -export function useUpdateLocation() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ - uid, - location, - }: { - uid: string; - location: { type: string; [key: string]: string }; - }) => CalComAPIService.updateLocationV2(uid, location), - onSuccess: (_, variables) => { - // Invalidate all booking queries - queryClient.invalidateQueries({ queryKey: queryKeys.bookings.all }); - - // Also invalidate the specific booking detail - queryClient.invalidateQueries({ - queryKey: queryKeys.bookings.detail(variables.uid), - }); - }, - onError: (_error) => { - console.error("Failed to update location"); - }, - }); -} - -/** - * Hook to add guests to a booking - * - * @returns Mutation function and state - * - * @example - * ```tsx - * const { mutate: addGuests, isPending } = useAddGuests(); - * - * addGuests({ - * uid: 'abc-123', - * guests: [{ email: 'guest@example.com', name: 'Guest Name' }] - * }); - * ``` - */ -export function useAddGuests() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ uid, guests }: { uid: string; guests: { email: string; name?: string }[] }) => - CalComAPIService.addGuests(uid, guests), - onSuccess: (_, variables) => { - // Invalidate all booking queries - queryClient.invalidateQueries({ queryKey: queryKeys.bookings.all }); - - // Also invalidate the specific booking detail - queryClient.invalidateQueries({ - queryKey: queryKeys.bookings.detail(variables.uid), - }); - }, - onError: (_error) => { - console.error("Failed to add guests"); - }, - }); -} - -/** - * Hook to prefetch bookings (useful for navigation) - * - * @returns Function to prefetch bookings - * - * @example - * ```tsx - * const prefetchBookings = usePrefetchBookings(); - * - * // Prefetch when user hovers over bookings tab - * onHover={() => prefetchBookings({ status: ['upcoming'] })} - * ``` - */ -export function usePrefetchBookings() { - const queryClient = useQueryClient(); - - return (filters?: BookingFilters) => { - queryClient.prefetchQuery({ - queryKey: queryKeys.bookings.list(filters || {}), - queryFn: () => CalComAPIService.getBookings(filters), - staleTime: CACHE_CONFIG.bookings.staleTime, - }); - }; -} - -/** - * Hook to invalidate all bookings cache - * Useful when you know data has changed externally - * - * @returns Function to invalidate bookings cache - */ -export function useInvalidateBookings() { - const queryClient = useQueryClient(); - - return () => { - queryClient.invalidateQueries({ queryKey: queryKeys.bookings.all }); - }; -} - -/** - * Type exports for consumers - */ -export type { Booking }; diff --git a/companion/hooks/useEventTypeFilter.tsx b/companion/hooks/useEventTypeFilter.tsx deleted file mode 100644 index cffa71112e..0000000000 --- a/companion/hooks/useEventTypeFilter.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import { useCallback, useMemo, useState } from "react"; -import type { EventType } from "@/services/calcom"; - -// Sort options for event types -export type EventTypeSortOption = "alphabetical" | "newest" | "duration"; - -// Filter options (multi-select toggles) -// When ALL filters are OFF: show ALL event types (no filtering) -// When a filter is ON: only show events matching that filter -export interface EventTypeFilters { - hiddenOnly: boolean; // When ON, show ONLY hidden events - paidOnly: boolean; // When ON, show ONLY paid events - seatedOnly: boolean; // When ON, show ONLY seated events - requiresConfirmationOnly: boolean; // When ON, show ONLY events requiring confirmation - recurringOnly: boolean; // When ON, show ONLY recurring events -} - -const DEFAULT_FILTERS: EventTypeFilters = { - hiddenOnly: false, - paidOnly: false, - seatedOnly: false, - requiresConfirmationOnly: false, - recurringOnly: false, -}; - -interface UseEventTypeFilterResult { - // State - sortBy: EventTypeSortOption; - filters: EventTypeFilters; - - // Actions - setSortBy: (sort: EventTypeSortOption) => void; - toggleFilter: (filterKey: keyof EventTypeFilters) => void; - resetFilters: () => void; - - // Computed - filteredAndSortedEventTypes: (eventTypes: EventType[]) => EventType[]; - activeFilterCount: number; -} - -/** - * Helper function to sort event types by the given sort option. - */ -function sortEventTypes(eventTypes: EventType[], sortBy: EventTypeSortOption): EventType[] { - return [...eventTypes].sort((a, b) => { - switch (sortBy) { - case "alphabetical": - return a.title.localeCompare(b.title); - case "newest": - return b.id - a.id; - case "duration": { - const durationA = a.lengthInMinutes || a.length || 0; - const durationB = b.lengthInMinutes || b.length || 0; - return durationA - durationB; - } - default: - return 0; - } - }); -} - -/** - * Hook to manage event type filtering and sorting. - * - * Supports: - * - Sorting: Alphabetical, Newest First, By Duration - * - Filters (multi-select toggles): - * - Include Hidden: Show hidden event types - * - Paid Only: Show only paid events (price > 0) - * - Seated Only: Show only seated events - * - Requires Confirmation: Show only events requiring confirmation - * - Recurring Only: Show only recurring events - */ -export function useEventTypeFilter(): UseEventTypeFilterResult { - const [sortBy, setSortBy] = useState("alphabetical"); - const [filters, setFilters] = useState(DEFAULT_FILTERS); - - const toggleFilter = useCallback((filterKey: keyof EventTypeFilters) => { - setFilters((prev) => ({ - ...prev, - [filterKey]: !prev[filterKey], - })); - }, []); - - const resetFilters = useCallback(() => { - setFilters(DEFAULT_FILTERS); - }, []); - - // Count active filters - const activeFilterCount = useMemo(() => { - let count = 0; - if (filters.hiddenOnly) count++; - if (filters.paidOnly) count++; - if (filters.seatedOnly) count++; - if (filters.requiresConfirmationOnly) count++; - if (filters.recurringOnly) count++; - return count; - }, [filters]); - - const filteredAndSortedEventTypes = useCallback( - (eventTypes: EventType[]): EventType[] => { - if (!eventTypes || eventTypes.length === 0) { - return []; - } - - // Check if ANY filter is ON - const hasAnyFilter = - filters.hiddenOnly || - filters.paidOnly || - filters.seatedOnly || - filters.requiresConfirmationOnly || - filters.recurringOnly; - - // If NO filters are enabled, show ALL event types (no filtering) - if (!hasAnyFilter) { - return sortEventTypes(eventTypes, sortBy); - } - - // Apply filters - each filter narrows down results (AND logic) - const filtered = eventTypes.filter((eventType) => { - // Hidden filter: when ON, show ONLY hidden events - if (filters.hiddenOnly && eventType.hidden !== true) { - return false; - } - - // Paid filter: if enabled, only show paid events (price > 0) - if (filters.paidOnly) { - const isPaid = - eventType.price !== undefined && eventType.price !== null && eventType.price > 0; - if (!isPaid) { - return false; - } - } - - // Seated filter: check if seats is defined and not disabled - if (filters.seatedOnly) { - const hasSeats = - eventType.seats && - typeof eventType.seats === "object" && - !("disabled" in eventType.seats && eventType.seats.disabled === true); - if (!hasSeats) { - return false; - } - } - - // Requires confirmation filter - check both requiresConfirmation and confirmationPolicy - if (filters.requiresConfirmationOnly) { - const hasRequiresConfirmation = eventType.requiresConfirmation === true; - const hasConfirmationPolicy = - eventType.confirmationPolicy && - typeof eventType.confirmationPolicy === "object" && - !( - "disabled" in eventType.confirmationPolicy && - eventType.confirmationPolicy.disabled === true - ); - - if (!hasRequiresConfirmation && !hasConfirmationPolicy) { - return false; - } - } - - // Recurring filter: check if recurrence is defined and not disabled - if (filters.recurringOnly) { - const isRecurring = - eventType.recurrence && - typeof eventType.recurrence === "object" && - !("disabled" in eventType.recurrence && eventType.recurrence.disabled === true); - if (!isRecurring) { - return false; - } - } - - return true; - }); - - // Apply sorting using helper function - return sortEventTypes(filtered, sortBy); - }, - [sortBy, filters] - ); - - return { - sortBy, - filters, - setSortBy, - toggleFilter, - resetFilters, - filteredAndSortedEventTypes, - activeFilterCount, - }; -} diff --git a/companion/hooks/useEventTypes.ts b/companion/hooks/useEventTypes.ts deleted file mode 100644 index 97f2482714..0000000000 --- a/companion/hooks/useEventTypes.ts +++ /dev/null @@ -1,361 +0,0 @@ -/** - * Event Types Query Hooks - * - * This module provides React Query hooks for fetching and mutating event types. - * It integrates with the existing CalComAPIService and provides: - * - Automatic caching with configurable stale times - * - Pull-to-refresh support via refetch - * - Optimistic updates for mutations - * - Cache invalidation on create/update/delete - */ - -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { CACHE_CONFIG, queryKeys } from "@/config/cache.config"; -import { CalComAPIService, type CreateEventTypeInput, type EventType } from "@/services/calcom"; -import { requestRating, RatingTrigger } from "@/hooks/useAppStoreRating"; - -/** - * Hook to fetch all event types - * - * @returns Query result with event types data, loading state, error, and refetch function - * - * @example - * ```tsx - * const { data: eventTypes, isLoading, refetch, isRefetching } = useEventTypes(); - * - * // Pull-to-refresh - * - * ``` - */ -export function useEventTypes() { - return useQuery({ - queryKey: queryKeys.eventTypes.lists(), - queryFn: () => CalComAPIService.getEventTypes(), - staleTime: CACHE_CONFIG.eventTypes.staleTime, - // Keep previous data while fetching new data (smoother UX) - placeholderData: (previousData) => previousData, - // Don't retry on network errors (keeps cache intact) - retry: (failureCount, error) => { - if (error?.message?.includes("Network") || error?.message?.includes("fetch")) { - return false; - } - return failureCount < 2; - }, - refetchOnReconnect: true, - }); -} - -/** - * Hook to fetch a single event type by ID - * - * @param id - The ID of the event type - * @returns Query result with event type data - * - * @example - * ```tsx - * const { data: eventType, isLoading } = useEventTypeById(123); - * ``` - */ -export function useEventTypeById(id: number | undefined) { - return useQuery({ - queryKey: queryKeys.eventTypes.detail(id || 0), - queryFn: () => { - if (!id) throw new Error("id is required"); - return CalComAPIService.getEventTypeById(id); - }, - enabled: !!id, // Only fetch when id is provided - staleTime: CACHE_CONFIG.eventTypes.staleTime, - }); -} - -/** - * Hook to create a new event type - * - * @returns Mutation function and state - * - * @example - * ```tsx - * const { mutate: createEventType, isPending } = useCreateEventType(); - * - * createEventType({ - * title: 'Quick Chat', - * slug: 'quick-chat', - * lengthInMinutes: 15, - * }); - * ``` - */ -export function useCreateEventType() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: (input: CreateEventTypeInput) => CalComAPIService.createEventType(input), - onSuccess: (newEventType) => { - // Invalidate the list to include the new event type - queryClient.invalidateQueries({ queryKey: queryKeys.eventTypes.lists() }); - - // Optionally, add the new event type to cache immediately - queryClient.setQueryData(queryKeys.eventTypes.detail(newEventType.id), newEventType); - - // Request app store rating on first event type save - requestRating(RatingTrigger.EVENT_TYPE_SAVED); - }, - onError: (error) => { - console.error("Failed to create event type"); - if (__DEV__) { - const message = error instanceof Error ? error.message : String(error); - const stack = error instanceof Error ? error.stack : undefined; - console.debug("[useCreateEventType] failed", { message, stack }); - } - }, - }); -} - -/** - * Hook to update an event type with optimistic updates - * - * @returns Mutation function and state - * - * @example - * ```tsx - * const { mutate: updateEventType, isPending } = useUpdateEventType(); - * - * updateEventType({ - * id: 123, - * updates: { title: 'Updated Title' } - * }); - * ``` - */ -export function useUpdateEventType() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ id, updates }: { id: number; updates: Record }) => - CalComAPIService.updateEventType(id, updates), - onMutate: async ({ id, updates }) => { - // Cancel any outgoing refetches to prevent overwriting optimistic update - await queryClient.cancelQueries({ queryKey: queryKeys.eventTypes.detail(id) }); - await queryClient.cancelQueries({ queryKey: queryKeys.eventTypes.lists() }); - - // Snapshot the previous values for rollback - const previousEventType = queryClient.getQueryData( - queryKeys.eventTypes.detail(id) - ); - const previousEventTypes = queryClient.getQueryData( - queryKeys.eventTypes.lists() - ); - - // Optimistically update the detail cache if it exists - if (previousEventType) { - const optimisticEventType: EventType = { - ...previousEventType, - ...updates, - }; - queryClient.setQueryData(queryKeys.eventTypes.detail(id), optimisticEventType); - } - - // Update the list cache optimistically (even if detail cache doesn't exist) - if (previousEventTypes) { - const updatedList = previousEventTypes.map((et) => { - if (et.id === id) { - return { - ...et, - ...updates, - }; - } - return et; - }); - queryClient.setQueryData(queryKeys.eventTypes.lists(), updatedList); - } - - return { previousEventType, previousEventTypes }; - }, - onSuccess: (updatedEventType, variables) => { - // Update the specific event type in cache with server response - queryClient.setQueryData(queryKeys.eventTypes.detail(variables.id), updatedEventType); - - // Update the list cache with the server response - const currentEventTypes = queryClient.getQueryData(queryKeys.eventTypes.lists()); - if (currentEventTypes) { - const updatedList = currentEventTypes.map((et) => - et.id === variables.id ? updatedEventType : et - ); - queryClient.setQueryData(queryKeys.eventTypes.lists(), updatedList); - } else { - // If list cache doesn't exist, invalidate to trigger refetch when user navigates to list - queryClient.invalidateQueries({ queryKey: queryKeys.eventTypes.lists() }); - } - - // Request app store rating on first event type save - requestRating(RatingTrigger.EVENT_TYPE_SAVED); - }, - onError: (error, variables, context) => { - // Rollback to previous values on error - if (context?.previousEventType) { - queryClient.setQueryData( - queryKeys.eventTypes.detail(variables.id), - context.previousEventType - ); - } - if (context?.previousEventTypes) { - queryClient.setQueryData(queryKeys.eventTypes.lists(), context.previousEventTypes); - } - console.error("Failed to update event type"); - if (__DEV__) { - const message = error instanceof Error ? error.message : String(error); - const stack = error instanceof Error ? error.stack : undefined; - console.debug("[useUpdateEventType] failed", { message, stack }); - } - }, - }); -} - -/** - * Hook to delete an event type - * - * @returns Mutation function and state - * - * @example - * ```tsx - * const { mutate: deleteEventType, isPending } = useDeleteEventType(); - * - * deleteEventType(123); - * ``` - */ -export function useDeleteEventType() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: (id: number) => CalComAPIService.deleteEventType(id), - onMutate: async (deletedId) => { - // Cancel any outgoing refetches - await queryClient.cancelQueries({ queryKey: queryKeys.eventTypes.lists() }); - - // Snapshot the previous value - const previousEventTypes = queryClient.getQueryData( - queryKeys.eventTypes.lists() - ); - - // Optimistically remove from the list - if (previousEventTypes) { - queryClient.setQueryData( - queryKeys.eventTypes.lists(), - previousEventTypes.filter((et) => et.id !== deletedId) - ); - } - - return { previousEventTypes }; - }, - onError: (error, _deletedId, context) => { - // Rollback on error - if (context?.previousEventTypes) { - queryClient.setQueryData(queryKeys.eventTypes.lists(), context.previousEventTypes); - } - console.error("Failed to delete event type"); - if (__DEV__) { - const message = error instanceof Error ? error.message : String(error); - const stack = error instanceof Error ? error.stack : undefined; - console.debug("[useDeleteEventType] failed", { message, stack }); - } - }, - onSettled: () => { - // Always refetch after error or success - queryClient.invalidateQueries({ queryKey: queryKeys.eventTypes.lists() }); - }, - }); -} - -/** - * Hook to duplicate an event type - * - * @returns Mutation function and state - * - * @example - * ```tsx - * const { mutate: duplicateEventType, isPending } = useDuplicateEventType(); - * - * duplicateEventType({ - * eventType: existingEventType, - * existingEventTypes: allEventTypes - * }); - * ``` - */ -export function useDuplicateEventType() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: async ({ - eventType, - existingEventTypes, - }: { - eventType: EventType; - existingEventTypes: EventType[]; - }) => { - // Generate a new title and slug for the duplicate - const newTitle = `${eventType.title} (copy)`; - let newSlug = `${eventType.slug}-copy`; - - // Check if slug already exists and append a number if needed - let counter = 1; - while (existingEventTypes.some((et) => et.slug === newSlug)) { - newSlug = `${eventType.slug}-copy-${counter}`; - counter++; - } - - const duration = eventType.lengthInMinutes ?? eventType.length ?? 15; - - return CalComAPIService.createEventType({ - title: newTitle, - slug: newSlug, - lengthInMinutes: duration, - description: eventType.description || undefined, - }); - }, - onSuccess: () => { - // Invalidate the list to include the duplicated event type - queryClient.invalidateQueries({ queryKey: queryKeys.eventTypes.lists() }); - }, - onError: (error) => { - console.error("Failed to duplicate event type"); - if (__DEV__) { - const message = error instanceof Error ? error.message : String(error); - const stack = error instanceof Error ? error.stack : undefined; - console.debug("[useDuplicateEventType] failed", { message, stack }); - } - }, - }); -} - -/** - * Hook to prefetch event types (useful for navigation) - * - * @returns Function to prefetch event types - */ -export function usePrefetchEventTypes() { - const queryClient = useQueryClient(); - - return () => { - queryClient.prefetchQuery({ - queryKey: queryKeys.eventTypes.lists(), - queryFn: () => CalComAPIService.getEventTypes(), - staleTime: CACHE_CONFIG.eventTypes.staleTime, - }); - }; -} - -/** - * Hook to invalidate all event types cache - * - * @returns Function to invalidate event types cache - */ -export function useInvalidateEventTypes() { - const queryClient = useQueryClient(); - - return () => { - queryClient.invalidateQueries({ queryKey: queryKeys.eventTypes.all }); - }; -} - -/** - * Type exports for consumers - */ -export type { EventType, CreateEventTypeInput }; diff --git a/companion/hooks/useNetworkStatus.ts b/companion/hooks/useNetworkStatus.ts deleted file mode 100644 index af2f3c3ba2..0000000000 --- a/companion/hooks/useNetworkStatus.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * useNetworkStatus Hook - * - * This hook provides real-time network status for components. - * Used to disable mutations when offline. - */ -import NetInfo, { type NetInfoState } from "@react-native-community/netinfo"; -import { useEffect, useState } from "react"; - -interface NetworkStatus { - isOnline: boolean; - isLoading: boolean; -} - -/** - * Hook to monitor network connectivity status. - * Returns whether the device is online and if the status is still loading. - */ -export function useNetworkStatus(): NetworkStatus { - const [isOnline, setIsOnline] = useState(true); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - // Get initial state - NetInfo.fetch().then((state: NetInfoState) => { - setIsOnline(state.isConnected === true && state.isInternetReachable !== false); - setIsLoading(false); - }); - - // Subscribe to network state changes - const unsubscribe = NetInfo.addEventListener((state: NetInfoState) => { - setIsOnline(state.isConnected === true && state.isInternetReachable !== false); - }); - - return () => { - unsubscribe(); - }; - }, []); - - return { isOnline, isLoading }; -} diff --git a/companion/hooks/useSchedules.ts b/companion/hooks/useSchedules.ts deleted file mode 100644 index eb82687047..0000000000 --- a/companion/hooks/useSchedules.ts +++ /dev/null @@ -1,390 +0,0 @@ -/** - * Schedules (Availability) Query Hooks - * - * This module provides React Query hooks for fetching and mutating schedules. - * It integrates with the existing CalComAPIService and provides: - * - Automatic caching with configurable stale times - * - Pull-to-refresh support via refetch - * - Optimistic updates for mutations - * - Cache invalidation on create/update/delete - */ - -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { CACHE_CONFIG, queryKeys } from "@/config/cache.config"; -import { CalComAPIService, type Schedule } from "@/services/calcom"; - -/** - * Sort schedules: default first, then alphabetically by name - */ -function sortSchedules(schedules: Schedule[]): Schedule[] { - return schedules.sort((a, b) => { - if (a.isDefault && !b.isDefault) return -1; - if (!a.isDefault && b.isDefault) return 1; - return a.name.localeCompare(b.name); - }); -} - -/** - * Schedule creation input type - */ -export interface CreateScheduleInput { - name: string; - timeZone: string; - isDefault?: boolean; - availability?: Array<{ - days: string[]; - startTime: string; - endTime: string; - }>; - overrides?: Array<{ - date: string; - startTime: string; - endTime: string; - }>; -} - -/** - * Schedule update input type - */ -export interface UpdateScheduleInput { - isDefault?: boolean; - name?: string; - timeZone?: string; - availability?: Array<{ - days: string[]; - startTime: string; - endTime: string; - }>; - overrides?: Array<{ - date: string; - startTime: string; - endTime: string; - }>; -} - -/** - * Hook to fetch all schedules - * - * @returns Query result with schedules data, loading state, error, and refetch function - * - * @example - * ```tsx - * const { data: schedules, isLoading, refetch, isRefetching } = useSchedules(); - * - * // Pull-to-refresh - * - * ``` - */ -export function useSchedules() { - return useQuery({ - queryKey: queryKeys.schedules.lists(), - queryFn: async () => { - const schedules = await CalComAPIService.getSchedules(); - return sortSchedules(schedules); - }, - staleTime: CACHE_CONFIG.schedules.staleTime, - // Keep previous data while fetching new data (smoother UX) - placeholderData: (previousData) => previousData, - // Don't retry on network errors (keeps cache intact) - retry: (failureCount, error) => { - if (error?.message?.includes("Network") || error?.message?.includes("fetch")) { - return false; - } - return failureCount < 2; - }, - refetchOnReconnect: true, - }); -} - -/** - * Hook to fetch a single schedule by ID - * - * @param id - The ID of the schedule - * @returns Query result with schedule data - * - * @example - * ```tsx - * const { data: schedule, isLoading } = useScheduleById(123); - * ``` - */ -export function useScheduleById(id: number | undefined) { - return useQuery({ - queryKey: queryKeys.schedules.detail(id || 0), - queryFn: () => { - if (!id) throw new Error("id is required"); - return CalComAPIService.getScheduleById(id); - }, - enabled: !!id, // Only fetch when id is provided - staleTime: CACHE_CONFIG.schedules.staleTime, - }); -} - -/** - * Hook to create a new schedule - * - * @returns Mutation function and state - * - * @example - * ```tsx - * const { mutate: createSchedule, isPending } = useCreateSchedule(); - * - * createSchedule({ - * name: 'Working Hours', - * timeZone: 'America/New_York', - * availability: [ - * { days: ['Monday', 'Tuesday'], startTime: '09:00', endTime: '17:00' } - * ] - * }); - * ``` - */ -export function useCreateSchedule() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: (input: CreateScheduleInput) => CalComAPIService.createSchedule(input), - onSuccess: (newSchedule) => { - // Invalidate the list to include the new schedule - queryClient.invalidateQueries({ queryKey: queryKeys.schedules.lists() }); - - // Optionally, add the new schedule to cache immediately - queryClient.setQueryData(queryKeys.schedules.detail(newSchedule.id), newSchedule); - }, - onError: (_error) => { - console.error("Failed to create schedule"); - }, - }); -} - -/** - * Hook to update a schedule with optimistic updates - * - * @returns Mutation function and state - * - * @example - * ```tsx - * const { mutate: updateSchedule, isPending } = useUpdateSchedule(); - * - * updateSchedule({ - * id: 123, - * updates: { name: 'Updated Schedule Name' } - * }); - * ``` - */ -export function useUpdateSchedule() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ id, updates }: { id: number; updates: UpdateScheduleInput }) => - CalComAPIService.updateSchedule(id, updates), - onMutate: async ({ id, updates }) => { - // Cancel any outgoing refetches to prevent overwriting optimistic update - await queryClient.cancelQueries({ queryKey: queryKeys.schedules.detail(id) }); - await queryClient.cancelQueries({ queryKey: queryKeys.schedules.lists() }); - - // Snapshot the previous values for rollback - const previousSchedule = queryClient.getQueryData( - queryKeys.schedules.detail(id) - ); - const previousSchedules = queryClient.getQueryData(queryKeys.schedules.lists()); - - // Optimistically update the detail cache if it exists - if (previousSchedule) { - const optimisticSchedule: Schedule = { - ...previousSchedule, - ...updates, - name: updates.name ?? previousSchedule.name, - timeZone: updates.timeZone ?? previousSchedule.timeZone, - }; - queryClient.setQueryData(queryKeys.schedules.detail(id), optimisticSchedule); - } - - // Update the list cache optimistically (even if detail cache doesn't exist) - if (previousSchedules) { - const updatedList = previousSchedules.map((s) => { - if (s.id === id) { - // Merge updates into the existing schedule from the list - return { - ...s, - ...updates, - name: updates.name ?? s.name, - timeZone: updates.timeZone ?? s.timeZone, - }; - } - return s; - }); - queryClient.setQueryData(queryKeys.schedules.lists(), sortSchedules(updatedList)); - } - - return { previousSchedule, previousSchedules }; - }, - onSuccess: (updatedSchedule, variables) => { - // Update the specific schedule in cache with server response - queryClient.setQueryData(queryKeys.schedules.detail(variables.id), updatedSchedule); - - // Update the list cache with the server response - const currentSchedules = queryClient.getQueryData(queryKeys.schedules.lists()); - if (currentSchedules) { - const updatedList = currentSchedules.map((s) => - s.id === variables.id ? updatedSchedule : s - ); - queryClient.setQueryData(queryKeys.schedules.lists(), sortSchedules(updatedList)); - } else { - // If list cache doesn't exist, invalidate to trigger refetch when user navigates to list - queryClient.invalidateQueries({ queryKey: queryKeys.schedules.lists() }); - } - }, - onError: (_error, variables, context) => { - // Rollback to previous values on error - if (context?.previousSchedule) { - queryClient.setQueryData( - queryKeys.schedules.detail(variables.id), - context.previousSchedule - ); - } - if (context?.previousSchedules) { - queryClient.setQueryData(queryKeys.schedules.lists(), context.previousSchedules); - } - console.error("Failed to update schedule"); - }, - }); -} - -/** - * Hook to set a schedule as default - * - * @returns Mutation function and state - * - * @example - * ```tsx - * const { mutate: setAsDefault, isPending } = useSetScheduleAsDefault(); - * - * setAsDefault(123); - * ``` - */ -export function useSetScheduleAsDefault() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: (id: number) => CalComAPIService.updateSchedule(id, { isDefault: true }), - onSuccess: () => { - // Invalidate all schedules to update the default flag - queryClient.invalidateQueries({ queryKey: queryKeys.schedules.all }); - }, - onError: (_error) => { - console.error("Failed to set schedule as default"); - }, - }); -} - -/** - * Hook to delete a schedule - * - * @returns Mutation function and state - * - * @example - * ```tsx - * const { mutate: deleteSchedule, isPending } = useDeleteSchedule(); - * - * deleteSchedule(123); - * ``` - */ -export function useDeleteSchedule() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: (id: number) => CalComAPIService.deleteSchedule(id), - onMutate: async (deletedId) => { - // Cancel any outgoing refetches - await queryClient.cancelQueries({ queryKey: queryKeys.schedules.lists() }); - - // Snapshot the previous value - const previousSchedules = queryClient.getQueryData(queryKeys.schedules.lists()); - - // Optimistically remove from the list - if (previousSchedules) { - queryClient.setQueryData( - queryKeys.schedules.lists(), - previousSchedules.filter((s) => s.id !== deletedId) - ); - } - - return { previousSchedules }; - }, - onError: (_error, _deletedId, context) => { - // Rollback on error - if (context?.previousSchedules) { - queryClient.setQueryData(queryKeys.schedules.lists(), context.previousSchedules); - } - console.error("Failed to delete schedule"); - }, - onSettled: () => { - // Always refetch after error or success - queryClient.invalidateQueries({ queryKey: queryKeys.schedules.lists() }); - }, - }); -} - -/** - * Hook to duplicate a schedule - * - * @returns Mutation function and state - * - * @example - * ```tsx - * const { mutate: duplicateSchedule, isPending } = useDuplicateSchedule(); - * - * duplicateSchedule(123); - * ``` - */ -export function useDuplicateSchedule() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: (id: number) => CalComAPIService.duplicateSchedule(id), - onSuccess: () => { - // Invalidate the list to include the duplicated schedule - queryClient.invalidateQueries({ queryKey: queryKeys.schedules.lists() }); - }, - onError: (_error) => { - console.error("Failed to duplicate schedule"); - }, - }); -} - -/** - * Hook to prefetch schedules (useful for navigation) - * - * @returns Function to prefetch schedules - */ -export function usePrefetchSchedules() { - const queryClient = useQueryClient(); - - return () => { - queryClient.prefetchQuery({ - queryKey: queryKeys.schedules.lists(), - queryFn: async () => { - const schedules = await CalComAPIService.getSchedules(); - return sortSchedules(schedules); - }, - staleTime: CACHE_CONFIG.schedules.staleTime, - }); - }; -} - -/** - * Hook to invalidate all schedules cache - * - * @returns Function to invalidate schedules cache - */ -export function useInvalidateSchedules() { - const queryClient = useQueryClient(); - - return () => { - queryClient.invalidateQueries({ queryKey: queryKeys.schedules.all }); - }; -} - -/** - * Type exports for consumers - */ -export type { Schedule }; diff --git a/companion/hooks/useToast.ts b/companion/hooks/useToast.ts deleted file mode 100644 index 6307ccf076..0000000000 --- a/companion/hooks/useToast.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { useCallback, useEffect, useState } from "react"; - -type ToastType = "success" | "error"; - -interface ToastState { - visible: boolean; - message: string; - type: ToastType; -} - -/** - * Hook for managing toast notifications with auto-dismiss functionality. - * - * @param autoDismissMs - Time in milliseconds before the toast auto-dismisses (default: 2500) - * @returns Object containing toast state, showToast function, and hideToast function - * - * @example - * ```tsx - * const { toast, showToast } = useToast(); - * - * // Show success toast - * showToast("Operation completed successfully"); - * - * // Show error toast - * showToast("Something went wrong", "error"); - * - * // Render toast - * - * ``` - */ -export function useToast(autoDismissMs = 2500) { - const [toast, setToast] = useState({ - visible: false, - message: "", - type: "success", - }); - - const showToast = useCallback((message: string, type: ToastType = "success") => { - setToast({ visible: true, message, type }); - }, []); - - const hideToast = useCallback(() => { - setToast({ visible: false, message: "", type: "success" }); - }, []); - - useEffect(() => { - if (toast.visible) { - const timer = setTimeout(hideToast, autoDismissMs); - return () => clearTimeout(timer); - } - }, [toast, autoDismissMs, hideToast]); - - return { toast, showToast, hideToast }; -} - -export type { ToastState, ToastType }; diff --git a/companion/hooks/useUserPreferences.ts b/companion/hooks/useUserPreferences.ts deleted file mode 100644 index 9b9ba89ee0..0000000000 --- a/companion/hooks/useUserPreferences.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { useCallback, useEffect, useState } from "react"; -import { generalStorage } from "@/utils/storage"; - -export type LandingPage = "event-types" | "bookings" | "bookings:unconfirmed"; - -export interface UserPreferences { - landingPage: LandingPage; -} - -export const USER_PREFERENCES_KEY = "cal_user_preferences"; - -const DEFAULT_PREFERENCES: UserPreferences = { - landingPage: "event-types", -}; - -export interface LandingPageOption { - value: LandingPage; - label: string; -} - -export const LANDING_PAGE_OPTIONS: LandingPageOption[] = [ - { value: "event-types", label: "Event Types" }, - { value: "bookings", label: "Bookings" }, - { value: "bookings:unconfirmed", label: "Bookings (Unconfirmed)" }, -]; - -export function getLandingPageLabel(landingPage: LandingPage): string { - const option = LANDING_PAGE_OPTIONS.find((opt) => opt.value === landingPage); - return option?.label ?? "Event Types"; -} - -export function getRouteFromPreference(landingPage: LandingPage): string { - switch (landingPage) { - case "event-types": - return "/(tabs)/(event-types)"; - case "bookings": - return "/(tabs)/(bookings)"; - case "bookings:unconfirmed": - return "/(tabs)/(bookings)?filter=unconfirmed"; - default: - return "/(tabs)/(event-types)"; - } -} - -export function useUserPreferences() { - const [preferences, setPreferences] = useState(DEFAULT_PREFERENCES); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - const loadPreferences = async () => { - try { - const stored = await generalStorage.getItem(USER_PREFERENCES_KEY); - if (stored) { - const parsed = JSON.parse(stored) as UserPreferences; - setPreferences(parsed); - } - setIsLoading(false); - } catch { - console.warn("Failed to load user preferences"); - setIsLoading(false); - } - }; - - loadPreferences(); - }, []); - - const setLandingPage = useCallback( - async (landingPage: LandingPage): Promise => { - const newPreferences: UserPreferences = { - ...preferences, - landingPage, - }; - - setPreferences(newPreferences); - - try { - await generalStorage.setItem(USER_PREFERENCES_KEY, JSON.stringify(newPreferences)); - } catch (error) { - console.error("Failed to save user preferences:", error); - throw error; - } - }, - [preferences] - ); - - return { - preferences, - isLoading, - setLandingPage, - landingPageLabel: getLandingPageLabel(preferences.landingPage), - }; -} diff --git a/companion/hooks/useUserProfile.ts b/companion/hooks/useUserProfile.ts deleted file mode 100644 index 8ebd76b8ae..0000000000 --- a/companion/hooks/useUserProfile.ts +++ /dev/null @@ -1,164 +0,0 @@ -/** - * User Profile Query Hooks - * - * This module provides React Query hooks for fetching and updating user profile. - * It integrates with the existing CalComAPIService and provides: - * - Automatic caching with configurable stale times - * - Profile update mutations with cache invalidation - */ - -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { CACHE_CONFIG, queryKeys } from "@/config/cache.config"; -import { CalComAPIService, type UserProfile } from "@/services/calcom"; - -/** - * User profile update input type - */ -export interface UpdateUserProfileInput { - email?: string; - name?: string; - timeFormat?: number; - defaultScheduleId?: number; - weekStart?: string; - timeZone?: string; - locale?: string; - avatarUrl?: string; - bio?: string; - metadata?: Record; -} - -/** - * Hook to fetch the current user profile - * - * @returns Query result with user profile data, loading state, error, and refetch function - * - * @example - * ```tsx - * const { data: profile, isLoading } = useUserProfile(); - * - * if (profile) { - * console.log(profile.username, profile.email); - * } - * ``` - */ -export function useUserProfile() { - return useQuery({ - queryKey: queryKeys.userProfile.current(), - queryFn: () => CalComAPIService.getUserProfile(), - staleTime: CACHE_CONFIG.userProfile.staleTime, - // Keep previous data while fetching new data (smoother UX) - placeholderData: (previousData) => previousData, - }); -} - -/** - * Hook to get the current username - * - * @returns Query result with username - * - * @example - * ```tsx - * const { data: username } = useUsername(); - * ``` - */ -export function useUsername() { - const { data: profile, ...rest } = useUserProfile(); - - return { - ...rest, - data: profile?.username, - }; -} - -/** - * Hook to update the user profile - * - * @returns Mutation function and state - * - * @example - * ```tsx - * const { mutate: updateProfile, isPending } = useUpdateUserProfile(); - * - * updateProfile({ - * name: 'New Name', - * timeZone: 'America/New_York' - * }); - * ``` - */ -export function useUpdateUserProfile() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: (updates: UpdateUserProfileInput) => CalComAPIService.updateUserProfile(updates), - onMutate: async (newData) => { - // Cancel any outgoing refetches - await queryClient.cancelQueries({ queryKey: queryKeys.userProfile.current() }); - - // Snapshot the previous value - const previousProfile = queryClient.getQueryData( - queryKeys.userProfile.current() - ); - - // Optimistically update to the new value - if (previousProfile) { - queryClient.setQueryData(queryKeys.userProfile.current(), { - ...previousProfile, - ...newData, - }); - } - - return { previousProfile }; - }, - onError: (error, _newData, context) => { - // Rollback on error - if (context?.previousProfile) { - queryClient.setQueryData(queryKeys.userProfile.current(), context.previousProfile); - } - console.error("Failed to update user profile"); - if (__DEV__) { - const message = error instanceof Error ? error.message : String(error); - const stack = error instanceof Error ? error.stack : undefined; - console.debug("[useUpdateUserProfile] failed", { message, stack }); - } - }, - onSettled: () => { - // Always refetch after error or success - queryClient.invalidateQueries({ queryKey: queryKeys.userProfile.current() }); - }, - }); -} - -/** - * Hook to prefetch user profile (useful for app initialization) - * - * @returns Function to prefetch user profile - */ -export function usePrefetchUserProfile() { - const queryClient = useQueryClient(); - - return () => { - queryClient.prefetchQuery({ - queryKey: queryKeys.userProfile.current(), - queryFn: () => CalComAPIService.getUserProfile(), - staleTime: CACHE_CONFIG.userProfile.staleTime, - }); - }; -} - -/** - * Hook to invalidate user profile cache - * - * @returns Function to invalidate user profile cache - */ -export function useInvalidateUserProfile() { - const queryClient = useQueryClient(); - - return () => { - queryClient.invalidateQueries({ queryKey: queryKeys.userProfile.all }); - }; -} - -/** - * Type exports for consumers - */ -export type { UserProfile }; diff --git a/companion/hooks/useWidgetSync.ts b/companion/hooks/useWidgetSync.ts deleted file mode 100644 index c7747dc393..0000000000 --- a/companion/hooks/useWidgetSync.ts +++ /dev/null @@ -1,155 +0,0 @@ -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 deleted file mode 100644 index a84621eb20..0000000000 --- a/companion/index.js +++ /dev/null @@ -1,6 +0,0 @@ -import { registerWidgetTaskHandler } from "react-native-android-widget"; -import { widgetTaskHandler } from "./widgets/widgetTaskHandler"; - -registerWidgetTaskHandler(widgetTaskHandler); - -import "expo-router/entry"; diff --git a/companion/lib/queryPersister.ts b/companion/lib/queryPersister.ts deleted file mode 100644 index fe170ca44d..0000000000 --- a/companion/lib/queryPersister.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @deprecated Import from '../utils/queryPersister' instead. - * This file is kept for backward compatibility. - */ - -export { - clearQueryCache, - createQueryPersister, - getCacheMetadata, - storage, -} from "@/utils/queryPersister"; diff --git a/companion/lib/storage.ts b/companion/lib/storage.ts deleted file mode 100644 index 2f726b5b3a..0000000000 --- a/companion/lib/storage.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @deprecated Import from '../utils/storage' instead. - * This file is kept for backward compatibility. - */ - -export { - generalStorage, - isChromeStorageAvailable, - type StorageAdapter, - secureStorage, -} from "@/utils/storage"; diff --git a/companion/lib/utils.ts b/companion/lib/utils.ts deleted file mode 100644 index 365058cebd..0000000000 --- a/companion/lib/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { type ClassValue, clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} diff --git a/companion/metro.config.js b/companion/metro.config.js deleted file mode 100644 index c8a0d7667d..0000000000 --- a/companion/metro.config.js +++ /dev/null @@ -1,23 +0,0 @@ -const path = require("node:path"); -const { getDefaultConfig } = require("expo/metro-config"); -const { withNativeWind } = require("nativewind/metro"); - -const config = getDefaultConfig(__dirname); - -// Resolve react/compiler-runtime to the actual file path -// Required for React Compiler support in Expo/Metro -const originalResolveRequest = config.resolver.resolveRequest; -config.resolver.resolveRequest = (context, moduleName, platform) => { - if (moduleName === "react/compiler-runtime") { - return { - filePath: path.resolve(__dirname, "node_modules/react/compiler-runtime.js"), - type: "sourceFile", - }; - } - if (originalResolveRequest) { - return originalResolveRequest(context, moduleName, platform); - } - return context.resolveRequest(context, moduleName, platform); -}; - -module.exports = withNativeWind(config, { input: "./global.css", inlineRem: 16 }); diff --git a/companion/nativewind-env.d.ts b/companion/nativewind-env.d.ts deleted file mode 100644 index a13e3136bb..0000000000 --- a/companion/nativewind-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/companion/package.json b/companion/package.json deleted file mode 100644 index 0b4f84fb96..0000000000 --- a/companion/package.json +++ /dev/null @@ -1,124 +0,0 @@ -{ - "name": "cal-companion", - "displayName": "Cal.com Companion", - "version": "1.7.6", - "main": "index.js", - "scripts": { - "start": "expo start", - "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", - "export": "expo export", - "export:ios": "expo export --platform ios", - "export:android": "expo export --platform android", - "export:optimized": "EXPO_UNSTABLE_METRO_OPTIMIZE_GRAPH=1 EXPO_UNSTABLE_TREE_SHAKING=1 expo export", - "export:optimized:ios": "EXPO_UNSTABLE_METRO_OPTIMIZE_GRAPH=1 EXPO_UNSTABLE_TREE_SHAKING=1 expo export --platform ios", - "export:optimized:android": "EXPO_UNSTABLE_METRO_OPTIMIZE_GRAPH=1 EXPO_UNSTABLE_TREE_SHAKING=1 expo export --platform android", - "ext": "wxt", - "ext:build": "wxt build", - "ext:build-prod": "BUILD_FOR_STORE=true wxt build", - "ext:zip": "wxt zip", - "ext:zip-prod": "BUILD_FOR_STORE=true wxt zip", - "ext:build-chrome": "BROWSER_TARGET=chrome wxt build -b chrome", - "ext:build-chrome-prod": "BUILD_FOR_STORE=true BROWSER_TARGET=chrome wxt build -b chrome", - "ext:zip-chrome": "BROWSER_TARGET=chrome wxt zip -b chrome", - "ext:zip-chrome-prod": "BUILD_FOR_STORE=true BROWSER_TARGET=chrome wxt zip -b chrome", - "ext:build-firefox": "BROWSER_TARGET=firefox wxt build -b firefox", - "ext:build-firefox-prod": "BUILD_FOR_STORE=true BROWSER_TARGET=firefox wxt build -b firefox", - "ext:zip-firefox": "BROWSER_TARGET=firefox wxt zip -b firefox", - "ext:zip-firefox-prod": "BUILD_FOR_STORE=true BROWSER_TARGET=firefox wxt zip -b firefox", - "ext:build-safari": "BROWSER_TARGET=safari wxt build -b safari", - "ext:build-safari-prod": "BUILD_FOR_STORE=true BROWSER_TARGET=safari wxt build -b safari", - "ext:zip-safari": "BROWSER_TARGET=safari wxt zip -b safari", - "ext:zip-safari-prod": "BUILD_FOR_STORE=true BROWSER_TARGET=safari wxt zip -b safari", - "ext:build-edge": "BROWSER_TARGET=edge wxt build -b edge", - "ext:build-edge-prod": "BUILD_FOR_STORE=true BROWSER_TARGET=edge wxt build -b edge", - "ext:zip-edge": "BROWSER_TARGET=edge wxt zip -b edge", - "ext:zip-edge-prod": "BUILD_FOR_STORE=true BROWSER_TARGET=edge wxt zip -b edge", - "ext:build-all": "npm run ext:build && npm run ext:build-firefox && npm run ext:build-safari && npm run ext:build-edge", - "ext:build-all-prod": "npm run ext:build-prod && npm run ext:build-firefox-prod && npm run ext:build-safari-prod && npm run ext:build-edge-prod", - "lint": "biome lint .", - "lint:react-compiler": "eslint .", - "lint:all": "bun run lint && bun run lint:react-compiler", - "format": "biome format --write .", - "format:check": "biome format .", - "check": "biome check --write .", - "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", - "@react-native-community/datetimepicker": "8.4.4", - "@react-native-community/netinfo": "11.4.1", - "@react-native-segmented-control/segmented-control": "2.5.7", - "@rn-primitives/alert-dialog": "1.2.0", - "@rn-primitives/dropdown-menu": "1.2.0", - "@rn-primitives/slot": "1.2.0", - "@tanstack/react-query": "5.62.0", - "@tanstack/react-query-persist-client": "5.62.0", - "base64-js": "1.5.1", - "class-variance-authority": "0.7.1", - "clsx": "2.1.1", - "expo": "55.0.0-canary-20251230-fc48ddc", - "expo-auth-session": "7.0.11-canary-20251230-fc48ddc", - "expo-clipboard": "9.0.0-canary-20251230-fc48ddc", - "expo-constants": "18.1.0-canary-20251230-fc48ddc", - "expo-crypto": "15.0.9-canary-20251230-fc48ddc", - "expo-dev-client": "6.1.0-canary-20251230-fc48ddc", - "expo-device": "8.0.11-canary-20251230-fc48ddc", - "expo-glass-effect": "0.2.0-canary-20251230-fc48ddc", - "expo-haptics": "15.0.9-canary-20251230-fc48ddc", - "expo-image": "3.1.0-canary-20251230-fc48ddc", - "expo-linking": "8.0.12-canary-20251230-fc48ddc", - "expo-router": "7.0.0-canary-20251230-fc48ddc", - "expo-secure-store": "15.0.9-canary-20251230-fc48ddc", - "expo-splash-screen": "31.0.14-canary-20251230-fc48ddc", - "expo-status-bar": "3.0.10-canary-20251230-fc48ddc", - "expo-store-review": "9.0.10-canary-20251230-fc48ddc", - "expo-web-browser": "15.0.11-canary-20251230-fc48ddc", - "lucide-react-native": "0.562.0", - "nativewind": "4.2.1", - "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", - "react-native-screens": "4.19.0", - "react-native-svg": "15.12.1", - "react-native-web": "0.21.2", - "react-native-worklets": "0.7.1", - "tailwind-merge": "3.4.0", - "tailwindcss-animate": "1.0.7" - }, - "private": true, - "devDependencies": { - "@biomejs/biome": "2.3.10", - "@types/chrome": "0.1.31", - "@types/react": "19.2.10", - "@types/node": "22.19.6", - "@types/react-dom": "19.2.3", - "babel-plugin-react-compiler": "19.0.0-beta-af1b7da-20250417", - "babel-preset-expo": "54.0.7", - "eslint": "9.28.0", - "eslint-plugin-react-compiler": "19.0.0-beta-af1b7da-20250417", - "husky": "9.0.11", - "lint-staged": "15.2.0", - "tailwindcss": "3.4.17", - "typescript": "5.9.3", - "typescript-eslint": "8.50.1", - "wxt": "0.20.11" - }, - "lint-staged": { - "**/*.{js,jsx,ts,tsx,json,css,md}": [ - "biome format --write" - ] - } -} diff --git a/companion/services/calcom/auth.ts b/companion/services/calcom/auth.ts deleted file mode 100644 index 013247effb..0000000000 --- a/companion/services/calcom/auth.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Authentication configuration and functions for Cal.com API - */ - -// Authentication configuration -interface AuthConfig { - accessToken?: string; - refreshToken?: string; -} - -// Global auth state -const authConfig: AuthConfig = {}; - -// Token refresh callback - will be set by AuthContext -let tokenRefreshCallback: ((accessToken: string, refreshToken?: string) => Promise) | null = - null; - -// Refresh token function - will be set by AuthContext -let refreshTokenFunction: - | ((refreshToken: string) => Promise<{ accessToken: string; refreshToken?: string }>) - | null = null; - -/** - * Set OAuth access token for authentication - */ -export function setAccessToken(accessToken: string, refreshToken?: string): void { - authConfig.accessToken = accessToken; - if (refreshToken) { - authConfig.refreshToken = refreshToken; - } -} - -/** - * Set refresh token function for automatic token refresh - */ -export function setRefreshTokenFunction( - refreshFn: (refreshToken: string) => Promise<{ accessToken: string; refreshToken?: string }> -): void { - refreshTokenFunction = refreshFn; -} - -/** - * Clear all authentication - */ -export function clearAuth(): void { - authConfig.accessToken = undefined; - authConfig.refreshToken = undefined; - tokenRefreshCallback = null; - refreshTokenFunction = null; -} - -/** - * Set token refresh callback for OAuth token refresh - */ -export function setTokenRefreshCallback( - callback: (accessToken: string, refreshToken?: string) => Promise -): void { - tokenRefreshCallback = callback; -} - -/** - * Get current authentication header - */ -export function getAuthHeader(): string { - if (authConfig.accessToken) { - return `Bearer ${authConfig.accessToken}`; - } else { - throw new Error("No authentication configured. Please sign in with OAuth."); - } -} - -/** - * Get the current auth config (for internal use by request module) - */ -export function getAuthConfig(): AuthConfig { - return authConfig; -} - -/** - * Get the token refresh callback (for internal use by request module) - */ -export function getTokenRefreshCallback(): typeof tokenRefreshCallback { - return tokenRefreshCallback; -} - -/** - * Get the refresh token function (for internal use by request module) - */ -export function getRefreshTokenFunction(): typeof refreshTokenFunction { - return refreshTokenFunction; -} diff --git a/companion/services/calcom/bookings.ts b/companion/services/calcom/bookings.ts deleted file mode 100644 index ef9253b98c..0000000000 --- a/companion/services/calcom/bookings.ts +++ /dev/null @@ -1,588 +0,0 @@ -/** - * Booking-related functions for Cal.com API - */ - -import { safeLogError, safeLogInfo } from "@/utils/safeLogger"; - -import type { - AddGuestInput, - AddGuestsResponse, - Booking, - BookingParticipationResult, - BookingRecording, - BookingTranscript, - ConferencingSession, - GetConferencingSessionsResponse, - GetRecordingsResponse, - GetTranscriptsResponse, - MarkAbsentRequest, - MarkAbsentResponse, - UpdateLocationResponse, - UserProfile, -} from "../types"; - -import { makeRequest } from "./request"; -import { getUserProfile } from "./user"; - -/** - * Determine user's participation role in a booking - */ -export const getBookingParticipation = ( - booking: Booking, - userId?: number, - userEmail?: string -): BookingParticipationResult => { - const normalizedUserEmail = userEmail?.toLowerCase(); - const emailEq = (a?: string, b?: string) => a?.toLowerCase() === b?.toLowerCase(); - const idEq = (a?: string | number, b?: string | number) => - a !== undefined && b !== undefined && String(a) === String(b); - - const isOrganizer = - !!booking.user && - (idEq(booking.user.id, userId) || emailEq(booking.user.email, normalizedUserEmail)); - - const isHost = - Array.isArray(booking.hosts) && - booking.hosts.some((host) => idEq(host.id, userId) || emailEq(host.email, normalizedUserEmail)); - - const isAttendee = - Array.isArray(booking.attendees) && - booking.attendees.some( - (attendee) => idEq(attendee.id, userId) || emailEq(attendee.email, normalizedUserEmail) - ); - - const isParticipating = isOrganizer || isHost || isAttendee; - - return { isOrganizer, isHost, isAttendee, isParticipating }; -}; - -/** - * Get a single booking by UID - */ -export async function getBookingByUid(bookingUid: string): Promise { - try { - const response = await makeRequest<{ status: string; data: Booking }>( - `/bookings/${bookingUid}`, - { - headers: { - "cal-api-version": "2024-08-13", - }, - }, - "2024-08-13" - ); - if (response?.data) { - return response.data; - } - throw new Error("Invalid response from get booking API"); - } catch (error) { - console.error("getBookingByUid error:", error); - throw error; - } -} - -/** - * Get bookings with optional filters - */ -export async function getBookings(filters?: { - status?: string[]; - fromDate?: string; - toDate?: string; - eventTypeId?: number; - limit?: number; - offset?: number; -}): Promise { - // Build query parameters - const params = new URLSearchParams(); - - if (filters?.status?.length) { - for (const status of filters.status) { - params.append("status", status); - } - } - if (filters?.fromDate) { - params.append("fromDate", filters.fromDate); - } - if (filters?.toDate) { - params.append("toDate", filters.toDate); - } - if (filters?.eventTypeId) { - params.append("eventTypeId", filters.eventTypeId.toString()); - } - if (filters?.limit) { - params.append("limit", filters.limit.toString()); - } - if (filters?.offset) { - params.append("offset", filters.offset.toString()); - } - - const queryString = params.toString(); - const endpoint = `/bookings${queryString ? `?${queryString}` : ""}`; - - const response = await makeRequest(endpoint); - - // Handle different possible response structures (same logic as event types) - let bookingsArray: Booking[] = []; - - if (Array.isArray(response)) { - bookingsArray = response as Booking[]; - } else if (response && typeof response === "object") { - const resp = response as Record; - if (resp.data && Array.isArray(resp.data)) { - bookingsArray = resp.data as Booking[]; - } else if (resp.bookings && Array.isArray(resp.bookings)) { - bookingsArray = resp.bookings as Booking[]; - } else if (resp.items && Array.isArray(resp.items)) { - bookingsArray = resp.items as Booking[]; - } else if (resp.data && typeof resp.data === "object") { - const dataObj = resp.data as Record; - if (dataObj.bookings && Array.isArray(dataObj.bookings)) { - bookingsArray = dataObj.bookings as Booking[]; - } else if (dataObj.items && Array.isArray(dataObj.items)) { - bookingsArray = dataObj.items as Booking[]; - } else { - // Convert object values to array as last resort - const keys = Object.keys(dataObj); - if (keys.length > 0) { - bookingsArray = Object.values(dataObj).filter((item): item is Booking => - Boolean(item && typeof item === "object" && ("id" in item || "uid" in item)) - ); - } - } - } else { - // Try to extract any arrays from the response - const possibleArrays = Object.values(resp).filter((val) => Array.isArray(val)); - if (possibleArrays.length > 0) { - bookingsArray = possibleArrays[0] as Booking[]; - } - } - } - - // Get cached user profile to filter bookings (uses in-flight deduplication) - let userProfile: UserProfile | undefined; - try { - userProfile = await getUserProfile(); - } catch (_error) { - return bookingsArray; - } - - // Extract user info from response - let userId: number | undefined; - let userEmail: string | undefined; - - if (userProfile) { - userId = userProfile.id; - userEmail = userProfile.email; - } - - // Filter bookings to only show ones where the current user is participating - const userBookings = bookingsArray.filter((booking) => { - const { - isOrganizer: _isOrganizer, - isHost: _isHost, - isAttendee: _isAttendee, - isParticipating, - } = getBookingParticipation(booking, userId, userEmail); - - return isParticipating; - }); - - return userBookings; -} - -/** - * Cancel a booking - */ -export async function cancelBooking( - bookingUid: string, - cancellationReason?: string -): Promise { - const body: { cancellationReason?: string } = {}; - if (cancellationReason) { - body.cancellationReason = cancellationReason; - } - - await makeRequest( - `/bookings/${bookingUid}/cancel`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - "cal-api-version": "2024-08-13", - }, - body: JSON.stringify(body), - }, - "2024-08-13" - ); -} - -/** - * Reschedule a booking - */ -export async function rescheduleBooking( - bookingUid: string, - input: { - start: string; - reschedulingReason?: string; - } -): Promise { - try { - safeLogInfo("[CalComAPIService] rescheduleBooking request:", { - bookingUid, - input, - }); - - const response = await makeRequest<{ status: string; data: Booking }>( - `/bookings/${bookingUid}/reschedule`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - "cal-api-version": "2024-08-13", - }, - body: JSON.stringify(input), - }, - "2024-08-13" - ); - - if (response?.data) { - return response.data; - } - - throw new Error("Invalid response from reschedule booking API"); - } catch (error) { - safeLogError("[CalComAPIService] rescheduleBooking error:", error); - if (error instanceof Error) { - safeLogError("[CalComAPIService] Error message:", error.message); - safeLogError("[CalComAPIService] Error stack:", error.stack); - } - throw error; - } -} - -/** - * Confirm a pending booking - */ -export async function confirmBooking(bookingUid: string): Promise { - try { - const response = await makeRequest<{ status: string; data: Booking }>( - `/bookings/${bookingUid}/confirm`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - "cal-api-version": "2024-08-13", - }, - }, - "2024-08-13" - ); - - if (response?.data) { - return response.data; - } - - throw new Error("Invalid response from confirm booking API"); - } catch (error) { - safeLogError("confirmBooking error", { error, bookingUid }); - throw error; - } -} - -/** - * Decline a pending booking - */ -export async function declineBooking(bookingUid: string, reason?: string): Promise { - try { - const body: { reason?: string } = {}; - if (reason) { - body.reason = reason; - } - - const response = await makeRequest<{ status: string; data: Booking }>( - `/bookings/${bookingUid}/decline`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - "cal-api-version": "2024-08-13", - }, - body: JSON.stringify(body), - }, - "2024-08-13" - ); - - if (response?.data) { - return response.data; - } - - throw new Error("Invalid response from decline booking API"); - } catch (error) { - console.error("declineBooking error"); - throw error; - } -} - -/** - * Mark an attendee as absent (no-show) for a booking - * @param bookingUid - The unique identifier of the booking - * @param attendeeEmail - The email of the attendee to mark as absent - * @param absent - Whether to mark as absent (true) or undo (false) - */ -export async function markAbsent( - bookingUid: string, - attendeeEmail: string, - absent: boolean = true -): Promise { - try { - const body: MarkAbsentRequest = { - attendees: [{ email: attendeeEmail, absent }], - }; - - const response = await makeRequest( - `/bookings/${bookingUid}/mark-absent`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - "cal-api-version": "2024-08-13", - }, - body: JSON.stringify(body), - }, - "2024-08-13" - ); - - if (response?.data) { - return response.data; - } - - throw new Error("Invalid response from mark absent API"); - } catch (error) { - console.error("markAbsent error"); - throw error; - } -} - -/** - * Add guests to a booking - * @param bookingUid - The unique identifier of the booking - * @param guests - Array of guests to add (email required, name optional) - * @returns Updated booking with new guests - */ -export async function addGuests(bookingUid: string, guests: AddGuestInput[]): Promise { - try { - const response = await makeRequest( - `/bookings/${bookingUid}/guests`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - "cal-api-version": "2024-08-13", - }, - body: JSON.stringify({ guests }), - }, - "2024-08-13" - ); - - if (response?.data) { - return response.data; - } - - throw new Error("Invalid response from add guests API"); - } catch (error) { - console.error("addGuests error"); - throw error; - } -} - -/** - * Update the location of a booking (legacy - sends location as string) - * Note: This updates the booking location but may not update the calendar event - * @param bookingUid - The unique identifier of the booking - * @param location - The new location string - * @returns Updated booking with new location - * @deprecated Use updateLocationV2 instead for proper typed location updates - */ -export async function updateLocation(bookingUid: string, location: string): Promise { - try { - const response = await makeRequest( - `/bookings/${bookingUid}/location`, - { - method: "PATCH", - headers: { - "Content-Type": "application/json", - "cal-api-version": "2024-08-13", - }, - body: JSON.stringify({ location }), - }, - "2024-08-13" - ); - - if (response?.data) { - return response.data; - } - - throw new Error("Invalid response from update location API"); - } catch (error) { - console.error("updateLocation error"); - throw error; - } -} - -/** - * Update the location of a booking with typed location object - * Supports: address, link, phone, attendeePhone, attendeeAddress, attendeeDefined - * Note: Integration-based locations (Cal Video, Google Meet, Zoom) are NOT supported - * @param bookingUid - The unique identifier of the booking - * @param location - The location object with type and value - * @returns Updated booking with new location - */ -export async function updateLocationV2( - bookingUid: string, - location: { type: string; [key: string]: string } -): Promise { - try { - const response = await makeRequest( - `/bookings/${bookingUid}/location`, - { - method: "PATCH", - headers: { - "Content-Type": "application/json", - "cal-api-version": "2024-08-13", - }, - body: JSON.stringify({ location }), - }, - "2024-08-13" - ); - - if (response?.data) { - return response.data; - } - - throw new Error("Invalid response from update location API"); - } catch (error) { - safeLogError("[CalComAPIService] updateLocationV2 error:", error); - throw error; - } -} - -/** - * Get recordings for a booking (Cal Video only) - * Note: Only available for past Cal Video bookings - * @param bookingUid - The unique identifier of the booking - * @returns Array of recording objects with download URLs - */ -export async function getRecordings(bookingUid: string): Promise { - try { - const response = await makeRequest( - `/bookings/${bookingUid}/recordings`, - { - headers: { - "cal-api-version": "2024-08-13", - }, - }, - "2024-08-13" - ); - - if (response?.data) { - return response.data; - } - - return []; - } catch (error) { - // Handle 401/403 gracefully - endpoint may require auth in future - if (error instanceof Error) { - const statusMatch = error.message.match(/API Error: (\d+)/); - if (statusMatch && (statusMatch[1] === "401" || statusMatch[1] === "403")) { - console.warn(`Recordings access denied for booking ${bookingUid}`); - return []; - } - if (statusMatch && statusMatch[1] === "404") { - console.warn(`No recordings found for booking ${bookingUid}`); - return []; - } - } - console.error("getRecordings error"); - throw error; - } -} - -/** - * Get conferencing session details for a booking (Cal Video only) - * Note: This endpoint is PBAC-guarded and requires booking.readRecordings permission - * @param bookingUid - The unique identifier of the booking - * @returns Array of conferencing session objects with participant info - */ -export async function getConferencingSessions(bookingUid: string): Promise { - try { - const response = await makeRequest( - `/bookings/${bookingUid}/conferencing-sessions`, - { - headers: { - "cal-api-version": "2024-08-13", - }, - }, - "2024-08-13" - ); - - if (response?.data) { - return response.data; - } - - return []; - } catch (error) { - // Handle 401/403 gracefully - endpoint is PBAC-guarded - if (error instanceof Error) { - const statusMatch = error.message.match(/API Error: (\d+)/); - if (statusMatch && (statusMatch[1] === "401" || statusMatch[1] === "403")) { - console.warn(`Conferencing sessions access denied for booking ${bookingUid}`); - return []; - } - if (statusMatch && statusMatch[1] === "404") { - console.warn(`No conferencing sessions found for booking ${bookingUid}`); - return []; - } - } - console.error("getConferencingSessions error"); - throw error; - } -} - -/** - * Get transcripts for a booking (Cal Video only) - * Note: This endpoint may require authentication in future releases - * @param bookingUid - The unique identifier of the booking - * @returns Array of transcript objects with download URLs - */ -export async function getTranscripts(bookingUid: string): Promise { - try { - const response = await makeRequest( - `/bookings/${bookingUid}/transcripts`, - { - headers: { - "cal-api-version": "2024-08-13", - }, - }, - "2024-08-13" - ); - - if (response?.data) { - return response.data; - } - - return []; - } catch (error) { - // Handle 401/403 gracefully - endpoint may require auth in future - if (error instanceof Error) { - const statusMatch = error.message.match(/API Error: (\d+)/); - if (statusMatch && (statusMatch[1] === "401" || statusMatch[1] === "403")) { - console.warn(`Transcripts access denied for booking ${bookingUid}`); - return []; - } - if (statusMatch && statusMatch[1] === "404") { - console.warn(`No transcripts found for booking ${bookingUid}`); - return []; - } - } - console.error("getTranscripts error"); - throw error; - } -} diff --git a/companion/services/calcom/conferencing.ts b/companion/services/calcom/conferencing.ts deleted file mode 100644 index 94c00e4f8a..0000000000 --- a/companion/services/calcom/conferencing.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Conferencing functions for Cal.com API - */ - -import type { ConferencingOption } from "../types"; - -import { makeRequest } from "./request"; - -/** - * Get conferencing options for the authenticated user - */ -export async function getConferencingOptions(): Promise { - try { - const response = await makeRequest<{ - status: string; - data: ConferencingOption[]; - }>("/conferencing"); - - if (response?.data && Array.isArray(response.data)) { - return response.data; - } - - return []; - } catch (error) { - console.error("getConferencingOptions error"); - throw error; - } -} diff --git a/companion/services/calcom/event-types.ts b/companion/services/calcom/event-types.ts deleted file mode 100644 index 28aa60911a..0000000000 --- a/companion/services/calcom/event-types.ts +++ /dev/null @@ -1,173 +0,0 @@ -/** - * Event type functions for Cal.com API - */ - -import { safeLogError } from "@/utils/safeLogger"; - -import type { CreateEventTypeInput, EventType } from "../types"; - -import { makeRequest } from "./request"; -import { sanitizePayload } from "./utils"; - -/** - * Get all event types for the authenticated user - */ -export async function getEventTypes(): Promise { - // For authenticated users, no username/orgSlug params needed - API uses auth token - // This also ensures hidden event types are returned (they're filtered out when username is provided) - // Sort by creation date descending (newer first) to match main codebase behavior - const endpoint = `/event-types?sortCreatedAt=desc`; - - const response = await makeRequest(endpoint, {}, "2024-06-14"); - - // Handle different possible response structures - let eventTypesArray: EventType[] = []; - - if (Array.isArray(response)) { - eventTypesArray = response as EventType[]; - } else if (response && typeof response === "object") { - const resp = response as Record; - if (resp.data && Array.isArray(resp.data)) { - eventTypesArray = resp.data as EventType[]; - } else if (resp.eventTypes && Array.isArray(resp.eventTypes)) { - eventTypesArray = resp.eventTypes as EventType[]; - } else if (resp.items && Array.isArray(resp.items)) { - eventTypesArray = resp.items as EventType[]; - } else if (resp.data && typeof resp.data === "object") { - const dataObj = resp.data as Record; - if (dataObj.eventTypes && Array.isArray(dataObj.eventTypes)) { - eventTypesArray = dataObj.eventTypes as EventType[]; - } else if (dataObj.items && Array.isArray(dataObj.items)) { - eventTypesArray = dataObj.items as EventType[]; - } else { - const keys = Object.keys(dataObj); - if (keys.length > 0) { - eventTypesArray = Object.values(dataObj).filter((item): item is EventType => - Boolean(item && typeof item === "object" && "id" in item) - ); - } - } - } else { - const possibleArrays = Object.values(resp).filter((val) => Array.isArray(val)); - if (possibleArrays.length > 0) { - eventTypesArray = possibleArrays[0] as EventType[]; - } - } - } - - return eventTypesArray; -} - -/** - * Get a single event type by ID - */ -export async function getEventTypeById(eventTypeId: number): Promise { - try { - const response = await makeRequest<{ status: string; data: EventType }>( - `/event-types/${eventTypeId}`, - {}, - "2024-06-14" - ); - - if (response?.data) { - return response.data; - } - - return null; - } catch (error) { - // Handle 404 errors gracefully - resource doesn't exist or user doesn't have access - if (error instanceof Error) { - // Check if error message contains 404 status code - const statusMatch = error.message.match(/API Error: (\d+)/); - if (statusMatch && statusMatch[1] === "404") { - console.warn(`Event type ${eventTypeId} not found`); - return null; - } - } - console.error("getEventTypeById error"); - throw error; - } -} - -/** - * Create a new event type - */ -export async function createEventType(input: CreateEventTypeInput): Promise { - try { - const sanitizedInput = sanitizePayload(input as unknown as Record); - - const response = await makeRequest<{ status: string; data: EventType }>( - "/event-types", - { - method: "POST", - headers: { - "Content-Type": "application/json", - "cal-api-version": "2024-06-14", - }, - body: JSON.stringify(sanitizedInput), - }, - "2024-06-14" - ); - - if (response?.data) { - return response.data; - } - - throw new Error("Invalid response from create event type API"); - } catch (error) { - console.error("createEventType error"); - throw error; - } -} - -/** - * Update an event type - */ -export async function updateEventType( - eventTypeId: number, - updates: Partial -): Promise { - try { - const sanitizedUpdates = sanitizePayload(updates as Record); - - const response = await makeRequest<{ status: string; data: EventType }>( - `/event-types/${eventTypeId}`, - { - method: "PATCH", - headers: { - "Content-Type": "application/json", - "cal-api-version": "2024-06-14", - }, - body: JSON.stringify(sanitizedUpdates), - }, - "2024-06-14" - ); - - if (response?.data) { - return response.data; - } - - throw new Error("Invalid response from update event type API"); - } catch (error) { - console.error("updateEventType error"); - throw error; - } -} - -/** - * Delete an event type - */ -export async function deleteEventType(eventTypeId: number): Promise { - try { - await makeRequest( - `/event-types/${eventTypeId}`, - { - method: "DELETE", - }, - "2024-06-14" - ); - } catch (error) { - safeLogError("Delete API error", { error, eventTypeId }); - throw error; - } -} diff --git a/companion/services/calcom/index.ts b/companion/services/calcom/index.ts deleted file mode 100644 index 4d4abe89ec..0000000000 --- a/companion/services/calcom/index.ts +++ /dev/null @@ -1,172 +0,0 @@ -/** - * Cal.com API Service - * - * This module provides a unified interface for interacting with the Cal.com API v2. - * It is organized into logical submodules for better maintainability: - * - * - auth: Authentication configuration and token management - * - bookings: Booking CRUD operations and actions - * - conferencing: Conferencing options - * - event-types: Event type CRUD operations - * - private-links: Private link management for event types - * - request: Core HTTP request functionality - * - schedules: Schedule CRUD operations - * - user: User profile management - * - utils: JSON parsing and payload sanitization utilities - * - webhooks: Webhook management (global and event type specific) - */ - -// Re-export types for backward compatibility -export type { - EventType, - CreateEventTypeInput, - Booking, - BookingParticipationResult, - Schedule, - UserProfile, - ConferencingOption, - Webhook, - CreateWebhookInput, - UpdateWebhookInput, - PrivateLink, - CreatePrivateLinkInput, - UpdatePrivateLinkInput, - BookingLimitsCount, - BookingLimitsDuration, - ConfirmationPolicy, -} from "../types"; - -// Import all functions from submodules -import { - clearAuth, - setAccessToken, - setRefreshTokenFunction, - setTokenRefreshCallback, -} from "./auth"; -import { - addGuests, - cancelBooking, - confirmBooking, - declineBooking, - getBookingByUid, - getBookingParticipation, - getBookings, - getConferencingSessions, - getRecordings, - getTranscripts, - markAbsent, - rescheduleBooking, - updateLocation, - updateLocationV2, -} from "./bookings"; -import { getConferencingOptions } from "./conferencing"; -import { - createEventType, - deleteEventType, - getEventTypeById, - getEventTypes, - updateEventType, -} from "./event-types"; -import { - createEventTypePrivateLink, - deleteEventTypePrivateLink, - getEventTypePrivateLinks, - updateEventTypePrivateLink, -} from "./private-links"; -import { testRawBookingsAPI } from "./request"; -import { - createSchedule, - deleteSchedule, - duplicateSchedule, - getScheduleById, - getSchedules, - updateSchedule, -} from "./schedules"; -import { - clearUserProfile, - getCurrentUser, - getUsername, - getUserProfile, - updateUserProfile, -} from "./user"; -import { - createEventTypeWebhook, - createWebhook, - deleteEventTypeWebhook, - deleteWebhook, - getEventTypeWebhooks, - getWebhooks, - updateEventTypeWebhook, - updateWebhook, -} from "./webhooks"; - -// Re-export the booking participation helper for direct use -export { getBookingParticipation }; - -// Export the unified CalComAPIService object for backward compatibility -export const CalComAPIService = { - // Auth - setAccessToken, - setRefreshTokenFunction, - clearAuth, - setTokenRefreshCallback, - - // User - getCurrentUser, - updateUserProfile, - getUserProfile, - getUsername, - clearUserProfile, - - // Debug - testRawBookingsAPI, - - // Event Types - deleteEventType, - createEventType, - getEventTypes, - getEventTypeById, - updateEventType, - - // Bookings - cancelBooking, - rescheduleBooking, - confirmBooking, - declineBooking, - markAbsent, - addGuests, - updateLocation, - updateLocationV2, - getRecordings, - getConferencingSessions, - getTranscripts, - getBookingByUid, - getBookings, - - // Schedules - getSchedules, - createSchedule, - getScheduleById, - updateSchedule, - duplicateSchedule, - deleteSchedule, - - // Conferencing - getConferencingOptions, - - // Webhooks - getWebhooks, - createWebhook, - updateWebhook, - deleteWebhook, - getEventTypeWebhooks, - createEventTypeWebhook, - updateEventTypeWebhook, - deleteEventTypeWebhook, - - // Private Links - getEventTypePrivateLinks, - createEventTypePrivateLink, - updateEventTypePrivateLink, - deleteEventTypePrivateLink, -}; diff --git a/companion/services/calcom/private-links.ts b/companion/services/calcom/private-links.ts deleted file mode 100644 index e4cc8b5ccc..0000000000 --- a/companion/services/calcom/private-links.ts +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Private links functions for Cal.com API - */ - -import type { CreatePrivateLinkInput, PrivateLink, UpdatePrivateLinkInput } from "../types"; - -import { makeRequest } from "./request"; -import { sanitizePayload } from "./utils"; - -/** - * Get all private links for an event type - */ -export async function getEventTypePrivateLinks(eventTypeId: number): Promise { - try { - const response = await makeRequest<{ status: string; data: PrivateLink[] }>( - `/event-types/${eventTypeId}/private-links` - ); - - if (response?.data && Array.isArray(response.data)) { - return response.data; - } - - return []; - } catch (error) { - console.error("getEventTypePrivateLinks error"); - throw error; - } -} - -/** - * Create a private link for an event type - */ -export async function createEventTypePrivateLink( - eventTypeId: number, - input: CreatePrivateLinkInput = {} -): Promise { - try { - const sanitizedInput = sanitizePayload(input as Record); - const response = await makeRequest<{ status: string; data: PrivateLink }>( - `/event-types/${eventTypeId}/private-links`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(sanitizedInput), - } - ); - - if (response?.data) { - return response.data; - } - - throw new Error("Invalid response from create private link API"); - } catch (error) { - console.error("createEventTypePrivateLink error"); - throw error; - } -} - -/** - * Update a private link - */ -export async function updateEventTypePrivateLink( - eventTypeId: number, - linkId: number, - updates: UpdatePrivateLinkInput -): Promise { - try { - const sanitizedUpdates = sanitizePayload(updates as Record); - const response = await makeRequest<{ status: string; data: PrivateLink }>( - `/event-types/${eventTypeId}/private-links/${linkId}`, - { - method: "PATCH", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(sanitizedUpdates), - } - ); - - if (response?.data) { - return response.data; - } - - throw new Error("Invalid response from update private link API"); - } catch (error) { - console.error("updateEventTypePrivateLink error"); - throw error; - } -} - -/** - * Delete a private link - */ -export async function deleteEventTypePrivateLink( - eventTypeId: number, - linkId: number -): Promise { - try { - await makeRequest(`/event-types/${eventTypeId}/private-links/${linkId}`, { - method: "DELETE", - }); - } catch (error) { - console.error("deleteEventTypePrivateLink error"); - throw error; - } -} diff --git a/companion/services/calcom/request.ts b/companion/services/calcom/request.ts deleted file mode 100644 index e78cdcd657..0000000000 --- a/companion/services/calcom/request.ts +++ /dev/null @@ -1,133 +0,0 @@ -/** - * Core HTTP request functionality for Cal.com API - */ - -import { fetchWithTimeout } from "@/utils/network"; -import { safeLogError } from "@/utils/safeLogger"; - -import { - clearAuth, - getAuthConfig, - getAuthHeader, - getRefreshTokenFunction, - getTokenRefreshCallback, -} from "./auth"; -import { safeParseErrorJson, safeParseJson } from "./utils"; - -export const API_BASE_URL = "https://api.cal.com/v2"; -export const REQUEST_TIMEOUT_MS = 30000; - -/** - * Test function for bookings API specifically - */ -export async function testRawBookingsAPI(): Promise { - try { - const url = `${API_BASE_URL}/bookings?status=upcoming&status=unconfirmed&limit=50`; - - const response = await fetchWithTimeout( - url, - { - headers: { - Authorization: getAuthHeader(), - "Content-Type": "application/json", - "cal-api-version": "2024-08-13", - }, - }, - REQUEST_TIMEOUT_MS - ); - - const responseText = await response.text(); - - if (responseText?.trim()) { - const _responseJson = safeParseJson(responseText); - if (!_responseJson) { - safeLogError("[CalComAPIService] Failed to parse bookings response", { - responseLength: responseText.length, - }); - } - } - } catch (_error) { - safeLogError("[CalComAPIService] testRawBookingsAPI failed", { - error: _error, - }); - } -} - -/** - * Make an authenticated request to the Cal.com API - */ -export async function makeRequest( - endpoint: string, - options: RequestInit = {}, - apiVersion: string = "2024-08-13", - isRetry: boolean = false -): Promise { - const url = `${API_BASE_URL}${endpoint}`; - - const response = await fetchWithTimeout( - url, - { - ...options, - headers: { - Authorization: getAuthHeader(), - "Content-Type": "application/json", - "cal-api-version": apiVersion, - ...options.headers, - }, - }, - REQUEST_TIMEOUT_MS - ); - - if (!response.ok) { - const errorBody = await response.text(); - - // Parse error for better user messages - let errorMessage = response.statusText; - - const errorJson = safeParseErrorJson(errorBody); - if (errorJson) { - errorMessage = errorJson?.error?.message || errorJson?.message || response.statusText; - } else { - // If JSON parsing fails, use the raw error body - errorMessage = errorBody || response.statusText; - } - - // Handle specific error cases - if (response.status === 401) { - const authConfig = getAuthConfig(); - const refreshTokenFunction = getRefreshTokenFunction(); - const tokenRefreshCallback = getTokenRefreshCallback(); - - if (!isRetry && authConfig.refreshToken && refreshTokenFunction && tokenRefreshCallback) { - try { - const newTokens = await refreshTokenFunction(authConfig.refreshToken); - - authConfig.accessToken = newTokens.accessToken; - if (newTokens.refreshToken) { - authConfig.refreshToken = newTokens.refreshToken; - } - - // Notify AuthContext to update stored tokens - await tokenRefreshCallback(newTokens.accessToken, newTokens.refreshToken); - - // Retry the original request with the new token - return makeRequest(endpoint, options, apiVersion, true); - } catch (refreshError) { - safeLogError("Token refresh failed:", refreshError); - clearAuth(); - throw new Error("Authentication failed. Please sign in again."); - } - } - - if (errorMessage.includes("expired")) { - throw new Error("Your authentication has expired. Please sign in again."); - } - throw new Error("Authentication failed. Please check your credentials."); - } - - // Include status code in error message for graceful error handling downstream - throw new Error(`API Error: ${response.status} ${errorMessage}`); - } - - return response.json(); -} diff --git a/companion/services/calcom/schedules.ts b/companion/services/calcom/schedules.ts deleted file mode 100644 index 4912d4e90e..0000000000 --- a/companion/services/calcom/schedules.ts +++ /dev/null @@ -1,211 +0,0 @@ -/** - * Schedule functions for Cal.com API - */ - -import type { Schedule } from "../types"; - -import { makeRequest } from "./request"; -import { sanitizePayload } from "./utils"; - -/** - * Get all schedules for the authenticated user - */ -export async function getSchedules(): Promise { - const response = await makeRequest<{ status: string; data: Schedule[] }>( - "/schedules", - { - headers: { - "cal-api-version": "2024-06-11", - }, - }, - "2024-06-11" - ); - - let schedulesArray: Schedule[] = []; - - // Handle response structure: { status: "success", data: [...] } - if (response && response.status === "success" && response.data && Array.isArray(response.data)) { - schedulesArray = response.data; - } else if (Array.isArray(response)) { - // Fallback: response might be array directly - schedulesArray = response; - } else if (response?.data && Array.isArray(response.data)) { - // Fallback: response.data might be array - schedulesArray = response.data; - } - - return schedulesArray; -} - -/** - * Get a schedule by ID - */ -export async function getScheduleById(scheduleId: number): Promise { - try { - const response = await makeRequest( - `/schedules/${scheduleId}`, - { - headers: { - "cal-api-version": "2024-06-11", - }, - }, - "2024-06-11" - ); - - if (response && typeof response === "object") { - const resp = response as Record; - if (resp.data && typeof resp.data === "object") { - return resp.data as Schedule; - } - if (resp.id) { - return response as Schedule; - } - } - - return null; - } catch (error) { - console.error("getScheduleById error"); - throw error; - } -} - -/** - * Create a new schedule - */ -export async function createSchedule(input: { - name: string; - timeZone: string; - isDefault?: boolean; - availability?: Array<{ - days: string[]; - startTime: string; - endTime: string; - }>; - overrides?: Array<{ - date: string; - startTime: string; - endTime: string; - }>; -}): Promise { - try { - const sanitizedInput = sanitizePayload(input as Record); - - const response = await makeRequest<{ status: string; data: Schedule }>( - "/schedules", - { - method: "POST", - headers: { - "Content-Type": "application/json", - "cal-api-version": "2024-06-11", - }, - body: JSON.stringify(sanitizedInput), - }, - "2024-06-11" - ); - - if (response?.data) { - return response.data; - } - - throw new Error("Invalid response from create schedule API"); - } catch (error) { - console.error("createSchedule error"); - throw error; - } -} - -/** - * Update a schedule - */ -export async function updateSchedule( - scheduleId: number, - updates: { - isDefault?: boolean; - name?: string; - timeZone?: string; - availability?: Array<{ - days: string[]; - startTime: string; - endTime: string; - }>; - overrides?: Array<{ - date: string; - startTime: string; - endTime: string; - }>; - } -): Promise { - try { - // Sanitize the updates to remove null values - const sanitizedUpdates = sanitizePayload(updates as Record); - const response = await makeRequest<{ status: string; data: Schedule }>( - `/schedules/${scheduleId}`, - { - method: "PATCH", - headers: { - "Content-Type": "application/json", - "cal-api-version": "2024-06-11", - }, - body: JSON.stringify(sanitizedUpdates), - }, - "2024-06-11" - ); - - if (response?.data) { - return response.data; - } - - throw new Error("Invalid response from update schedule API"); - } catch (error) { - console.error("updateSchedule error"); - throw error; - } -} - -/** - * Duplicate a schedule - */ -export async function duplicateSchedule(scheduleId: number): Promise { - try { - const response = await makeRequest<{ status: string; data: Schedule }>( - `/atoms/schedules/${scheduleId}/duplicate`, - { - method: "POST", - headers: { - "cal-api-version": "2024-06-11", - }, - }, - "2024-06-11" - ); - - if (response?.data) { - return response.data; - } - - throw new Error("Invalid response from duplicate schedule API"); - } catch (error) { - console.error("duplicateSchedule error"); - throw error; - } -} - -/** - * Delete a schedule - */ -export async function deleteSchedule(scheduleId: number): Promise { - try { - await makeRequest<{ status: string }>( - `/schedules/${scheduleId}`, - { - method: "DELETE", - headers: { - "cal-api-version": "2024-06-11", - }, - }, - "2024-06-11" - ); - } catch (error) { - console.error("deleteSchedule error"); - throw error; - } -} diff --git a/companion/services/calcom/user.ts b/companion/services/calcom/user.ts deleted file mode 100644 index 73eea4b62c..0000000000 --- a/companion/services/calcom/user.ts +++ /dev/null @@ -1,116 +0,0 @@ -/** - * User profile functions for Cal.com API - */ - -import type { UserProfile } from "../types"; - -import { makeRequest } from "./request"; - -// Module-level state for user profile caching -let _userProfile: UserProfile | null = null; -// In-flight promise to prevent concurrent /me API calls -let _userProfilePromise: Promise | null = null; - -/** - * Get current user profile from API - */ -export async function getCurrentUser(): Promise { - const response = await makeRequest<{ status: string; data: UserProfile }>( - "/me", - { - headers: { - "cal-api-version": "2024-06-11", - }, - }, - "2024-06-11" - ); - return response.data; -} - -/** - * Update current user profile - */ -export async function updateUserProfile(updates: { - email?: string; - name?: string; - timeFormat?: number; - defaultScheduleId?: number; - weekStart?: string; - timeZone?: string; - locale?: string; - avatarUrl?: string; - bio?: string; - metadata?: Record; -}): Promise { - try { - const response = await makeRequest<{ status: string; data: UserProfile }>( - "/me", - { - method: "PATCH", - headers: { - "Content-Type": "application/json", - "cal-api-version": "2024-06-11", - }, - body: JSON.stringify(updates), - }, - "2024-06-11" - ); - - if (response?.data) { - // Update cached profile - _userProfile = response.data; - return response.data; - } - - throw new Error("Invalid response from update user profile API"); - } catch (error) { - console.error("updateUserProfile error"); - throw error; - } -} - -/** - * Get and cache user profile with in-flight deduplication - * This prevents multiple concurrent callers from each making a /me API call - */ -export async function getUserProfile(): Promise { - // Return cached profile if available - if (_userProfile) { - return _userProfile; - } - - // If there's already an in-flight request, wait for it instead of making a new one - if (_userProfilePromise) { - return _userProfilePromise; - } - - // Create a new request and cache the promise to deduplicate concurrent calls - _userProfilePromise = getCurrentUser() - .then((profile) => { - _userProfile = profile; - _userProfilePromise = null; - return profile; - }) - .catch((error) => { - _userProfilePromise = null; - throw error; - }); - - return _userProfilePromise; -} - -/** - * Clear cached profile (useful for logout) - */ -export function clearUserProfile(): void { - _userProfile = null; - _userProfilePromise = null; -} - -/** - * Helper to get username from user profile - */ -export async function getUsername(): Promise { - const profile = await getUserProfile(); - return profile.username; -} diff --git a/companion/services/calcom/utils.ts b/companion/services/calcom/utils.ts deleted file mode 100644 index 3bc9aac034..0000000000 --- a/companion/services/calcom/utils.ts +++ /dev/null @@ -1,149 +0,0 @@ -/** - * Utility functions for JSON parsing and payload sanitization - */ - -/** - * Safely parse JSON response with structure validation. - * Validates that the input is a non-null object before returning. - * This prevents prototype pollution and ensures type safety. - */ -export function safeParseJson(jsonString: string): Record | null { - if (typeof jsonString !== "string" || !jsonString.trim()) { - return null; - } - - try { - const parsed: unknown = JSON.parse(jsonString); - - // Validate it's a plain object (not null, array, or primitive) - if ( - parsed !== null && - typeof parsed === "object" && - !Array.isArray(parsed) && - Object.getPrototypeOf(parsed) === Object.prototype - ) { - return parsed as Record; - } - - // Also accept arrays as valid JSON responses - if (Array.isArray(parsed)) { - return { data: parsed } as Record; - } - - return null; - } catch { - return null; - } -} - -/** - * Safely parse JSON error response with structure validation. - * Validates the expected error response structure before returning. - */ -export function safeParseErrorJson( - jsonString: string -): { error?: { message?: string }; message?: string } | null { - if (typeof jsonString !== "string" || !jsonString.trim()) { - return null; - } - - try { - const parsed: unknown = JSON.parse(jsonString); - - // Validate it's a plain object - if ( - parsed === null || - typeof parsed !== "object" || - Array.isArray(parsed) || - Object.getPrototypeOf(parsed) !== Object.prototype - ) { - return null; - } - - const obj = parsed as Record; - - // Validate expected error structure - must have error or message property with correct types - const hasValidErrorProp = - obj.error === undefined || - (typeof obj.error === "object" && - obj.error !== null && - ((obj.error as Record).message === undefined || - typeof (obj.error as Record).message === "string")); - - const hasValidMessageProp = obj.message === undefined || typeof obj.message === "string"; - - if (hasValidErrorProp && hasValidMessageProp) { - return obj as { error?: { message?: string }; message?: string }; - } - - return null; - } catch { - return null; - } -} - -/** - * Sanitizes a payload before sending to the API. - * - Removes keys with null values for array fields (API expects arrays or field to be omitted) - * - Removes keys with undefined values - * - Recursively sanitizes nested objects - */ -export function sanitizePayload(payload: Record): Record { - const sanitized: Record = {}; - - // Fields that should NEVER be sent as null - API expects array or omit entirely - const arrayFields = [ - "lengthInMinutesOptions", - "multipleDuration", - "locations", - "bookingFields", - "hosts", - "children", - "customInputs", - ]; - - // Fields that can be null (to clear the value) - const nullableFields = [ - "description", - "successRedirectUrl", - "slotInterval", - "eventName", - "timeZone", - "interfaceLanguage", - ]; - - for (const [key, value] of Object.entries(payload)) { - // Skip undefined values - if (value === undefined) continue; - - // Handle null values - if (value === null) { - // For array fields, skip entirely (don't send null) - if (arrayFields.includes(key)) { - console.warn(`Skipping null value for array field: ${key}`); - continue; - } - // For nullable fields, allow null - if (nullableFields.includes(key)) { - sanitized[key] = null; - continue; - } - // For other fields, skip null to be safe - console.warn(`Skipping null value for field: ${key}`); - continue; - } - - // Recursively sanitize nested objects (but not arrays) - if (typeof value === "object" && !Array.isArray(value)) { - const sanitizedNested = sanitizePayload(value as Record); - // Only include if the nested object has values - if (Object.keys(sanitizedNested).length > 0) { - sanitized[key] = sanitizedNested; - } - } else { - sanitized[key] = value; - } - } - - return sanitized; -} diff --git a/companion/services/calcom/webhooks.ts b/companion/services/calcom/webhooks.ts deleted file mode 100644 index 1a587addaa..0000000000 --- a/companion/services/calcom/webhooks.ts +++ /dev/null @@ -1,196 +0,0 @@ -/** - * Webhook functions for Cal.com API - */ - -import type { CreateWebhookInput, UpdateWebhookInput, Webhook } from "../types"; - -import { makeRequest } from "./request"; -import { sanitizePayload } from "./utils"; - -/** - * Get all global webhooks - */ -export async function getWebhooks(): Promise { - try { - const response = await makeRequest<{ status: string; data: Webhook[] }>("/webhooks"); - - if (response?.data && Array.isArray(response.data)) { - return response.data; - } - - return []; - } catch (error) { - console.error("getWebhooks error"); - throw error; - } -} - -/** - * Create a global webhook - */ -export async function createWebhook(input: CreateWebhookInput): Promise { - try { - const sanitizedInput = sanitizePayload(input as unknown as Record); - const response = await makeRequest<{ status: string; data: Webhook }>("/webhooks", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(sanitizedInput), - }); - - if (response?.data) { - return response.data; - } - - throw new Error("Invalid response from create webhook API"); - } catch (error) { - console.error("createWebhook error"); - throw error; - } -} - -/** - * Update a global webhook - */ -export async function updateWebhook( - webhookId: string, - updates: UpdateWebhookInput -): Promise { - try { - const sanitizedUpdates = sanitizePayload(updates as Record); - const response = await makeRequest<{ status: string; data: Webhook }>( - `/webhooks/${webhookId}`, - { - method: "PATCH", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(sanitizedUpdates), - } - ); - - if (response?.data) { - return response.data; - } - - throw new Error("Invalid response from update webhook API"); - } catch (error) { - console.error("updateWebhook error"); - throw error; - } -} - -/** - * Delete a global webhook - */ -export async function deleteWebhook(webhookId: string): Promise { - try { - await makeRequest(`/webhooks/${webhookId}`, { - method: "DELETE", - }); - } catch (error) { - console.error("deleteWebhook error"); - throw error; - } -} - -/** - * Get webhooks for a specific event type - */ -export async function getEventTypeWebhooks(eventTypeId: number): Promise { - try { - const response = await makeRequest<{ status: string; data: Webhook[] }>( - `/event-types/${eventTypeId}/webhooks` - ); - - if (response?.data && Array.isArray(response.data)) { - return response.data; - } - - return []; - } catch (error) { - console.error("getEventTypeWebhooks error"); - throw error; - } -} - -/** - * Create a webhook for a specific event type - */ -export async function createEventTypeWebhook( - eventTypeId: number, - input: CreateWebhookInput -): Promise { - try { - const sanitizedInput = sanitizePayload(input as unknown as Record); - const response = await makeRequest<{ status: string; data: Webhook }>( - `/event-types/${eventTypeId}/webhooks`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(sanitizedInput), - } - ); - - if (response?.data) { - return response.data; - } - - throw new Error("Invalid response from create event type webhook API"); - } catch (error) { - console.error("createEventTypeWebhook error"); - throw error; - } -} - -/** - * Update an event type webhook - */ -export async function updateEventTypeWebhook( - eventTypeId: number, - webhookId: string, - updates: UpdateWebhookInput -): Promise { - try { - const sanitizedUpdates = sanitizePayload(updates as Record); - const response = await makeRequest<{ status: string; data: Webhook }>( - `/event-types/${eventTypeId}/webhooks/${webhookId}`, - { - method: "PATCH", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(sanitizedUpdates), - } - ); - - if (response?.data) { - return response.data; - } - - throw new Error("Invalid response from update event type webhook API"); - } catch (error) { - console.error("updateEventTypeWebhook error"); - throw error; - } -} - -/** - * Delete an event type webhook - */ -export async function deleteEventTypeWebhook( - eventTypeId: number, - webhookId: string -): Promise { - try { - await makeRequest(`/event-types/${eventTypeId}/webhooks/${webhookId}`, { - method: "DELETE", - }); - } catch (error) { - console.error("deleteEventTypeWebhook error"); - throw error; - } -} diff --git a/companion/services/oauthService.ts b/companion/services/oauthService.ts deleted file mode 100644 index 9265a4ad1c..0000000000 --- a/companion/services/oauthService.ts +++ /dev/null @@ -1,655 +0,0 @@ -/// -import { fromByteArray } from "base64-js"; -import type * as AuthSession from "expo-auth-session"; -import * as Crypto from "expo-crypto"; -import * as WebBrowser from "expo-web-browser"; -import { Platform } from "react-native"; - -import { fetchWithTimeout } from "@/utils/network"; -import { safeLogWarn } from "@/utils/safeLogger"; - -WebBrowser.maybeCompleteAuthSession(); - -// Message types for extension communication -const EXTENSION_MESSAGE_TYPES = { - OAUTH_REQUEST: "cal-extension-oauth-request", - OAUTH_RESULT: "cal-extension-oauth-result", - TOKEN_EXCHANGE_REQUEST: "cal-extension-token-exchange-request", - TOKEN_EXCHANGE_RESULT: "cal-extension-token-exchange-result", - SYNC_TOKENS: "cal-extension-sync-tokens", - SYNC_TOKENS_RESULT: "cal-extension-sync-tokens-result", - CLEAR_TOKENS: "cal-extension-clear-tokens", - CLEAR_TOKENS_RESULT: "cal-extension-clear-tokens-result", - REQUEST_SESSION: "cal-extension-request-session", - SESSION_TOKEN: "cal-extension-session-token", -} as const; - -let extensionSessionToken: string | null = null; -let sessionTokenPromise: Promise | null = null; - -const OAUTH_TIMEOUT_MS = 5 * 60 * 1000; -const TOKEN_EXCHANGE_TIMEOUT_MS = 30 * 1000; -const SESSION_TOKEN_TIMEOUT_MS = 5000; - -async function getExtensionSessionToken(): Promise { - if (extensionSessionToken) { - return extensionSessionToken; - } - - if (sessionTokenPromise) { - return sessionTokenPromise; - } - - if (typeof window === "undefined" || window.parent === window) { - return null; - } - - sessionTokenPromise = new Promise((resolve) => { - const timeoutId = setTimeout(() => { - window.removeEventListener("message", messageHandler); - sessionTokenPromise = null; - resolve(""); - }, SESSION_TOKEN_TIMEOUT_MS); - - const messageHandler = (event: MessageEvent) => { - if (event.source !== window.parent) { - return; - } - if (event.data.type !== EXTENSION_MESSAGE_TYPES.SESSION_TOKEN) { - return; - } - - clearTimeout(timeoutId); - window.removeEventListener("message", messageHandler); - - const token = event.data.sessionToken || ""; - extensionSessionToken = token; - sessionTokenPromise = null; - resolve(token); - }; - - window.addEventListener("message", messageHandler); - window.parent.postMessage({ type: EXTENSION_MESSAGE_TYPES.REQUEST_SESSION }, "*"); - }); - - return sessionTokenPromise; -} - -export interface OAuthTokens { - accessToken: string; - refreshToken?: string; - tokenType: string; - expiresAt?: number; - scope?: string; -} - -export interface OAuthConfig { - clientId: string; - redirectUri: string; - calcomBaseUrl: string; -} - -export class CalComOAuthService { - private config: OAuthConfig; - private codeVerifier: string | null = null; - - constructor(config: OAuthConfig) { - this.config = config; - } - - private async generatePKCEParams() { - const codeVerifier = this.generateRandomBase64Url(); - const codeChallenge = await this.generateCodeChallenge(codeVerifier); - const state = this.generateRandomBase64Url(); - - this.codeVerifier = codeVerifier; - - return { codeVerifier, codeChallenge, state }; - } - - private async generateCodeChallenge(codeVerifier: string): Promise { - const base64Hash = await Crypto.digestStringAsync( - Crypto.CryptoDigestAlgorithm.SHA256, - codeVerifier, - { encoding: Crypto.CryptoEncoding.BASE64 } - ); - return base64Hash.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); - } - - private generateRandomBase64Url(): string { - const bytes = new Uint8Array(Crypto.getRandomBytes(32)); - const base64 = - typeof btoa !== "undefined" ? btoa(String.fromCharCode(...bytes)) : fromByteArray(bytes); - return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); - } - - private buildAuthorizationUrl(codeChallenge: string, state: string): string { - const params = new URLSearchParams({ - client_id: this.config.clientId, - response_type: "code", - redirect_uri: this.config.redirectUri, - state, - code_challenge: codeChallenge, - code_challenge_method: "S256", - }); - - if (Platform.OS === "ios") { - params.append("register", "false"); - } - - return `${this.config.calcomBaseUrl}/auth/oauth2/authorize?${params.toString()}`; - } - - private parseCallbackUrl(url: string): Record { - const parsed = new URL(url); - const params: Record = {}; - parsed.searchParams.forEach((v, k) => { - params[k] = v; - }); - return params; - } - - private isMobileApp(): boolean { - return Platform.OS !== "web"; - } - - private isRunningInIframe(): boolean { - return Platform.OS === "web" && typeof window !== "undefined" && window.parent !== window; - } - - async startAuthorizationFlow(): Promise { - const { codeChallenge, state } = await this.generatePKCEParams(); - - if (Platform.OS === "web" && typeof window !== "undefined") { - const storedTokens = this.checkForStoredCallback(state); - if (storedTokens) { - return storedTokens; - } - } - - const result = await this.getAuthorizationResult(codeChallenge, state); - - if (result.type !== "success") { - throw new Error("OAuth flow failed"); - } - - const { code, state: returnedState } = result.params; - - if (!code) { - throw new Error("No authorization code received"); - } - - if (returnedState !== state) { - throw new Error("Invalid state parameter"); - } - - return this.exchangeCodeForTokens(code, returnedState); - } - - private checkForStoredCallback(state: string): Promise | null { - const storedCode = window.localStorage.getItem(`oauth_callback_code_${state}`); - const storedState = window.localStorage.getItem(`oauth_callback_state_${state}`); - - if (!storedCode || !storedState) { - return null; - } - - if (storedState !== state) { - throw new Error("Invalid state parameter"); - } - - window.localStorage.removeItem(`oauth_callback_code_${state}`); - window.localStorage.removeItem(`oauth_callback_state_${state}`); - - return this.exchangeCodeForTokens(storedCode, storedState); - } - - private async getAuthorizationResult( - codeChallenge: string, - state: string - ): Promise<{ type: "success"; params: Record } | { type: "error" }> { - const authUrl = this.buildAuthorizationUrl(codeChallenge, state); - - if (this.isMobileApp()) { - return this.getMobileAuthResult(authUrl); - } - - return this.getExtensionAuthResult(authUrl); - } - - private async getMobileAuthResult( - authUrl: string - ): Promise<{ type: "success"; params: Record } | { type: "error" }> { - const result = await WebBrowser.openAuthSessionAsync(authUrl, this.config.redirectUri, { - preferEphemeralSession: true, - }); - - if (result.type === "success") { - return { type: "success", params: this.parseCallbackUrl(result.url) }; - } - - return { type: "error" }; - } - - private async getExtensionAuthResult( - authUrl: string - ): Promise<{ type: "success"; params: Record } | { type: "error" }> { - try { - const responseUrl = await this.launchExtensionAuthFlow(authUrl); - return { type: "success", params: this.parseCallbackUrl(responseUrl) }; - } catch { - return { type: "error" }; - } - } - - private async launchExtensionAuthFlow(authUrl: string): Promise { - return new Promise((resolve, reject) => { - // Try Chrome/Chromium identity API first - if (typeof chrome !== "undefined" && chrome.identity) { - chrome.identity.launchWebAuthFlow({ url: authUrl, interactive: true }, (responseUrl) => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)); - } else if (responseUrl) { - resolve(responseUrl); - } else { - reject(new Error("OAuth cancelled")); - } - }); - return; - } - - // Try Firefox/Safari browser.identity API (Promise-based) - if (typeof browser !== "undefined" && browser?.identity) { - try { - browser.identity - .launchWebAuthFlow({ url: authUrl, interactive: true }) - .then((responseUrl: string | undefined) => { - if (responseUrl) { - resolve(responseUrl); - } else { - reject(new Error("OAuth cancelled")); - } - }) - .catch((error: Error) => { - reject(new Error(`OAuth flow failed: ${error.message}`)); - }); - return; - } catch { - // Fall through to iframe-based flow - } - } - - if (this.isRunningInIframe()) { - this.requestOAuthViaPostMessage(authUrl, resolve, reject); - return; - } - - reject(new Error("Extension context not available")); - }); - } - - private requestOAuthViaPostMessage( - authUrl: string, - resolve: (url: string) => void, - reject: (error: Error) => void - ): void { - const timeoutId = setTimeout(() => { - window.removeEventListener("message", messageHandler); - reject(new Error("OAuth flow timed out")); - }, OAUTH_TIMEOUT_MS); - - const messageHandler = (event: MessageEvent) => { - // Validate message source - must come from parent window (content script) - if (event.source !== window.parent) { - return; - } - if (event.data.type !== EXTENSION_MESSAGE_TYPES.OAUTH_RESULT) { - return; - } - - clearTimeout(timeoutId); - window.removeEventListener("message", messageHandler); - - if (event.data.success && event.data.responseUrl) { - resolve(event.data.responseUrl); - } else { - reject(new Error(event.data.error || "OAuth flow failed")); - } - }; - - window.addEventListener("message", messageHandler); - // Using "*" for targetOrigin because parent is the host page (e.g., gmail.com). - // Security: source validation above ensures responses only come from parent window. - window.parent.postMessage({ type: EXTENSION_MESSAGE_TYPES.OAUTH_REQUEST, authUrl }, "*"); - } - - private async exchangeCodeForTokens(code: string, state?: string): Promise { - if (!this.codeVerifier) { - throw new Error("Missing code verifier"); - } - - const tokenRequest = { - grant_type: "authorization_code", - client_id: this.config.clientId, - code, - redirect_uri: this.config.redirectUri, - code_verifier: this.codeVerifier, - }; - - const tokenEndpoint = `${this.config.calcomBaseUrl}/api/auth/oauth/token`; - - if (this.isRunningInIframe()) { - return this.exchangeCodeForTokensViaExtension(tokenRequest, tokenEndpoint, state); - } - - return this.exchangeCodeForTokensDirect(tokenRequest, tokenEndpoint); - } - - private async exchangeCodeForTokensDirect( - tokenRequest: Record, - tokenEndpoint: string - ): Promise { - const response = await fetchWithTimeout( - tokenEndpoint, - { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - }, - body: new URLSearchParams(tokenRequest).toString(), - }, - 30000 - ); - - if (!response.ok) { - throw new Error("Token exchange failed"); - } - - const data = await response.json(); - - return { - accessToken: data.access_token, - refreshToken: data.refresh_token, - tokenType: data.token_type ?? "Bearer", - expiresAt: data.expires_in ? Date.now() + data.expires_in * 1000 : undefined, - scope: data.scope, - }; - } - - private async exchangeCodeForTokensViaExtension( - tokenRequest: Record, - tokenEndpoint: string, - state?: string - ): Promise { - return new Promise((resolve, reject) => { - const timeoutId = setTimeout(() => { - window.removeEventListener("message", messageHandler); - reject(new Error("Token exchange timed out")); - }, TOKEN_EXCHANGE_TIMEOUT_MS); - - const messageHandler = (event: MessageEvent) => { - // Validate message source - must come from parent window (content script) - if (event.source !== window.parent) { - return; - } - if (event.data.type !== EXTENSION_MESSAGE_TYPES.TOKEN_EXCHANGE_RESULT) { - return; - } - - clearTimeout(timeoutId); - window.removeEventListener("message", messageHandler); - - if (event.data.success && event.data.tokens) { - resolve(event.data.tokens); - } else { - reject(new Error(event.data.error || "Token exchange failed")); - } - }; - - window.addEventListener("message", messageHandler); - // See comment in requestOAuthViaPostMessage for security rationale - window.parent.postMessage( - { - type: EXTENSION_MESSAGE_TYPES.TOKEN_EXCHANGE_REQUEST, - tokenRequest, - tokenEndpoint, - state, - }, - "*" - ); - }); - } - - getDiscoveryEndpoints(): AuthSession.DiscoveryDocument { - return { - authorizationEndpoint: `${this.config.calcomBaseUrl}/auth/oauth2/authorize`, - tokenEndpoint: `${this.config.calcomBaseUrl}/api/auth/oauth/token`, - revocationEndpoint: `${this.config.calcomBaseUrl}/api/auth/oauth/revoke`, - }; - } - - isTokenExpired(tokens: OAuthTokens): boolean { - if (!tokens.expiresAt) return false; - return Date.now() >= tokens.expiresAt - 5 * 60 * 1000; - } - - clearPKCEParams(): void { - this.codeVerifier = null; - } - - async syncTokensToExtension(tokens: OAuthTokens): Promise { - if (!this.isRunningInIframe()) { - return; - } - - const sessionToken = await getExtensionSessionToken(); - if (!sessionToken) { - safeLogWarn("No session token available for token sync"); - return; - } - - return new Promise((resolve) => { - const timeoutId = setTimeout(() => { - window.removeEventListener("message", messageHandler); - resolve(); - }, 5000); - - const messageHandler = (event: MessageEvent) => { - if (event.source !== window.parent) { - return; - } - if (event.data.type !== EXTENSION_MESSAGE_TYPES.SYNC_TOKENS_RESULT) { - return; - } - - clearTimeout(timeoutId); - window.removeEventListener("message", messageHandler); - - if (event.data.success) { - resolve(); - } else { - safeLogWarn("Failed to sync tokens to extension", event.data.error); - resolve(); - } - }; - - window.addEventListener("message", messageHandler); - window.parent.postMessage( - { type: EXTENSION_MESSAGE_TYPES.SYNC_TOKENS, tokens, sessionToken }, - "*" - ); - }); - } - - async clearTokensFromExtension(): Promise { - if (!this.isRunningInIframe()) { - return; - } - - const sessionToken = await getExtensionSessionToken(); - if (!sessionToken) { - safeLogWarn("No session token available for token clear"); - return; - } - - return new Promise((resolve) => { - const timeoutId = setTimeout(() => { - window.removeEventListener("message", messageHandler); - resolve(); - }, 5000); - - const messageHandler = (event: MessageEvent) => { - if (event.source !== window.parent) { - return; - } - if (event.data.type !== EXTENSION_MESSAGE_TYPES.CLEAR_TOKENS_RESULT) { - return; - } - - clearTimeout(timeoutId); - window.removeEventListener("message", messageHandler); - - if (event.data.success) { - resolve(); - } else { - safeLogWarn("Failed to clear tokens from extension", event.data.error); - resolve(); - } - }; - - window.addEventListener("message", messageHandler); - window.parent.postMessage({ type: EXTENSION_MESSAGE_TYPES.CLEAR_TOKENS, sessionToken }, "*"); - }); - } - - async refreshAccessToken(refreshToken: string): Promise { - const tokenRequest = { - grant_type: "refresh_token", - client_id: this.config.clientId, - refresh_token: refreshToken, - }; - - const refreshTokenEndpoint = `${this.config.calcomBaseUrl}/api/auth/oauth/refreshToken`; - - if (this.isRunningInIframe()) { - const tokens = await this.exchangeCodeForTokensViaExtension( - tokenRequest, - refreshTokenEndpoint - ); - await this.syncTokensToExtension(tokens); - return tokens; - } - - const result = await this.exchangeCodeForTokensDirect(tokenRequest, refreshTokenEndpoint); - return result; - } -} - -/** - * Browser type for OAuth configuration selection. - * Used to determine which OAuth client credentials to use. - */ -type BrowserType = "chrome" | "firefox" | "safari" | "edge" | "brave" | "unknown"; - -/** - * Detects the current browser type for OAuth configuration. - * This is used in web/extension context to select the appropriate OAuth credentials. - */ -function detectBrowserType(): BrowserType { - if (Platform.OS !== "web" || typeof navigator === "undefined") { - return "unknown"; - } - - const userAgent = navigator.userAgent.toLowerCase(); - - // Check for Brave first (it identifies as Chrome but has Brave-specific properties) - if (navigator.brave && typeof navigator.brave.isBrave === "function") { - return "brave"; - } - - // Check for Edge (Chromium-based Edge includes "Edg/" in user agent) - if (userAgent.includes("edg/")) { - return "edge"; - } - - // Check for Firefox - if (userAgent.includes("firefox")) { - return "firefox"; - } - - // Check for Safari (must check after Chrome since Chrome also includes "safari") - if ( - userAgent.includes("safari") && - !userAgent.includes("chrome") && - !userAgent.includes("chromium") - ) { - return "safari"; - } - - // Check for Chrome (or other Chromium-based browsers) - if (userAgent.includes("chrome") || userAgent.includes("chromium")) { - return "chrome"; - } - - return "unknown"; -} - -/** - * Gets browser-specific OAuth configuration. - * Falls back to default (Chrome) config if browser-specific config is not available. - */ -function getBrowserSpecificOAuthConfig(): { clientId: string; redirectUri: string } { - // Default values (Chrome/Brave) - const defaultClientId = process.env.EXPO_PUBLIC_CALCOM_OAUTH_CLIENT_ID || ""; - const defaultRedirectUri = process.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI || ""; - - // For mobile apps, always use default config - if (Platform.OS !== "web") { - return { clientId: defaultClientId, redirectUri: defaultRedirectUri }; - } - - const browserType = detectBrowserType(); - - switch (browserType) { - case "firefox": - return { - clientId: process.env.EXPO_PUBLIC_CALCOM_OAUTH_CLIENT_ID_FIREFOX || defaultClientId, - redirectUri: - process.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI_FIREFOX || defaultRedirectUri, - }; - - case "safari": - return { - clientId: process.env.EXPO_PUBLIC_CALCOM_OAUTH_CLIENT_ID_SAFARI || defaultClientId, - redirectUri: process.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI_SAFARI || defaultRedirectUri, - }; - - case "edge": - return { - clientId: process.env.EXPO_PUBLIC_CALCOM_OAUTH_CLIENT_ID_EDGE || defaultClientId, - redirectUri: process.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI_EDGE || defaultRedirectUri, - }; - default: - // Chrome, Brave, and unknown browsers use the default configuration - return { clientId: defaultClientId, redirectUri: defaultRedirectUri }; - } -} - -export function createCalComOAuthService(overrides: Partial = {}): CalComOAuthService { - // Get browser-specific OAuth config - const browserConfig = getBrowserSpecificOAuthConfig(); - - const config: OAuthConfig = { - clientId: browserConfig.clientId, - redirectUri: browserConfig.redirectUri, - calcomBaseUrl: "https://app.cal.com", - ...overrides, - }; - - if (!config.clientId || !config.redirectUri) { - throw new Error("OAuth configuration incomplete"); - } - - return new CalComOAuthService(config); -} diff --git a/companion/services/types/bookings.types.ts b/companion/services/types/bookings.types.ts deleted file mode 100644 index 508e357e90..0000000000 --- a/companion/services/types/bookings.types.ts +++ /dev/null @@ -1,187 +0,0 @@ -export type BookingStatus = "accepted" | "pending" | "cancelled" | "rejected"; - -export interface Booking { - id: number; - uid: string; - title: string; - description?: string; - startTime: string; - endTime: string; - start?: string; - end?: string; - eventTypeId: number; - eventType?: { - id: number; - title: string; - slug: string; - }; - hosts?: Array<{ - id?: number | string; - name?: string; - email?: string; - username?: string; - timeZone?: string; - }>; - user?: { - id: number; - email: string; - name: string; - timeZone: string; - }; - attendees?: Array<{ - id?: number | string; - email: string; - name: string; - timeZone: string; - noShow?: boolean; - absent?: boolean; - }>; - status: BookingStatus; - paid?: boolean; - payment?: Array<{ - id: number; - success: boolean; - paymentOption: string; - }>; - rescheduled?: boolean; - fromReschedule?: string; - recurringEventId?: string; - recurringBookingUid?: string; - requiresConfirmation?: boolean; - smsReminderNumber?: string; - location?: string; - cancellationReason?: string; - rejectionReason?: string; - responses?: Record; - // Additional fields from API - guests?: string[]; - bookingFieldsResponses?: Record; - rescheduledFromUid?: string; - rescheduledToUid?: string; - reschedulingReason?: string; - cancelledByEmail?: string; - absentHost?: boolean; - duration?: number; - meetingUrl?: string; -} - -export interface GetBookingsResponse { - status: "success"; - data: Booking[]; -} - -export interface BookingParticipationResult { - isOrganizer: boolean; - isHost: boolean; - isAttendee: boolean; - isParticipating: boolean; -} - -// Mark No Show / Absent Types -export interface MarkAbsentAttendee { - email: string; - absent: boolean; -} - -export interface MarkAbsentRequest { - attendees: MarkAbsentAttendee[]; -} - -export interface MarkAbsentResponse { - status: "success" | "error"; - data: Booking; -} - -// ============================================================================ -// Add Guests Types -// ============================================================================ - -export interface AddGuestInput { - email: string; - name?: string; -} - -export interface AddGuestsRequest { - guests: AddGuestInput[]; -} - -export interface AddGuestsResponse { - status: "success" | "error"; - data: Booking; -} - -// ============================================================================ -// Update Location Types -// ============================================================================ - -export interface UpdateLocationRequest { - location: string; -} - -export interface UpdateLocationResponse { - status: "success" | "error"; - data: Booking; -} - -// ============================================================================ -// Recordings Types -// ============================================================================ - -export interface BookingRecording { - id: string; - roomName: string; - startTime: string; - endTime?: string; - downloadUrl: string; - duration?: number; -} - -export interface GetRecordingsResponse { - status: "success" | "error"; - data: BookingRecording[]; - message?: string; -} - -// ============================================================================ -// Conferencing Sessions Types -// ============================================================================ - -export interface ConferencingSessionParticipant { - id: string; - userId?: string; - userName?: string; - joinTime: string; - duration?: number; -} - -export interface ConferencingSession { - id: string; - roomName: string; - startTime: string; - endTime?: string; - duration?: number; - maxParticipants?: number; - participants?: ConferencingSessionParticipant[]; -} - -export interface GetConferencingSessionsResponse { - status: "success" | "error"; - data: ConferencingSession[]; -} - -// ============================================================================ -// Transcripts Types -// ============================================================================ - -export interface BookingTranscript { - id: string; - roomName: string; - startTime: string; - downloadUrl: string; -} - -export interface GetTranscriptsResponse { - status: "success" | "error"; - data: BookingTranscript[]; - message?: string; -} diff --git a/companion/services/types/event-types.types.ts b/companion/services/types/event-types.types.ts deleted file mode 100644 index bd63ee3c19..0000000000 --- a/companion/services/types/event-types.types.ts +++ /dev/null @@ -1,357 +0,0 @@ -// Event Type Creation/Update Types -export interface BookingLimitsCount { - day?: number; - week?: number; - month?: number; - year?: number; -} - -export interface BookingLimitsDuration { - day?: number; - week?: number; - month?: number; - year?: number; -} - -export interface BookerActiveBookingsLimit { - count: number; -} - -export interface BusinessDaysWindow { - type: "businessDays"; - value: number; - rolling?: boolean; - rollingWindow?: number; -} - -export interface CalendarDaysWindow { - type: "calendarDays"; - value: number; - rolling?: boolean; - rollingWindow?: number; -} - -export interface RangeWindow { - type: "range"; - value: [string, string]; // ISO date strings -} - -export type BookingWindow = - | BusinessDaysWindow - | CalendarDaysWindow - | RangeWindow - | { disabled: true }; - -export interface BookerLayouts { - enabledLayouts: Array<"month_view" | "week_view" | "column_view">; - defaultLayout: "month_view" | "week_view" | "column_view"; -} - -export interface ConfirmationPolicy { - type?: "always"; - noticeThreshold?: { - count: number; - unit: "hours" | "minutes"; - }; -} - -export interface Recurrence { - interval: number; - occurrences: number; - frequency: "daily" | "weekly" | "monthly" | "yearly"; -} - -export interface Seats { - seatsPerTimeSlot?: number; - showAttendeeInfo?: boolean; - showAvailabilityCount?: boolean; - seatsShowAvailabilityCount?: boolean; -} - -export interface EventTypeColor { - lightEventTypeColor?: string; - darkEventTypeColor?: string; -} - -export interface EmailSettings { - additionalGuestsEmails?: string[]; -} - -// Disable Cancelling settings (API V2 format) -export interface DisableCancelling { - disabled: boolean; -} - -// Disable Rescheduling settings (API V2 format) -export interface DisableRescheduling { - disabled: boolean; - minutesBefore?: number; // Optional: disable when less than X minutes before -} - -// Cal Video Settings -export interface CalVideoSettings { - disableRecordingForOrganizer?: boolean; - disableRecordingForGuests?: boolean; - redirectUrlOnExit?: string; - enableAutomaticRecordingForOrganizer?: boolean; - enableAutomaticTranscription?: boolean; - disableTranscriptionForGuests?: boolean; - disableTranscriptionForOrganizer?: boolean; - sendTranscriptionEmails?: boolean; -} - -// Booking Field Types -export interface BaseBookingField { - name: string; - type: string; - label?: string; - placeholder?: string; - required?: boolean; - hidden?: boolean; -} - -export interface NameDefaultField extends BaseBookingField { - type: "name"; - variant?: "fullName" | "firstAndLastName"; -} - -export interface EmailDefaultField extends BaseBookingField { - type: "email"; -} - -export interface PhoneField extends BaseBookingField { - type: "phone"; -} - -export interface AddressField extends BaseBookingField { - type: "address"; -} - -export interface TextField extends BaseBookingField { - type: "text"; -} - -export interface NumberField extends BaseBookingField { - type: "number"; -} - -export interface TextAreaField extends BaseBookingField { - type: "textarea"; -} - -export interface SelectField extends BaseBookingField { - type: "select" | "multiselect"; - options?: Array<{ label: string; value: string }>; -} - -export interface CheckboxField extends BaseBookingField { - type: "checkbox"; -} - -export interface RadioField extends BaseBookingField { - type: "radio" | "radioInput"; - options?: Array<{ label: string; value: string }>; -} - -export interface BooleanField extends BaseBookingField { - type: "boolean"; -} - -export type BookingField = - | NameDefaultField - | EmailDefaultField - | PhoneField - | AddressField - | TextField - | NumberField - | TextAreaField - | SelectField - | CheckboxField - | RadioField - | BooleanField; - -export interface EventType { - id: number; - title: string; - slug: string; - description?: string; - length: number; // Deprecated: use lengthInMinutes instead - lengthInMinutes?: number; // API returns this field - lengthInMinutesOptions?: number[]; - - // Locations - locations?: Array<{ - type: string; - address?: string; - link?: string; - phone?: string; - integration?: string; - credentialId?: number; - public?: boolean; - }>; - - // Pricing - price?: number; - currency?: string; - - // Basic settings - disableGuests?: boolean; - lockTimeZoneToggleOnBookingPage?: boolean; - requiresConfirmation?: boolean; - requiresBookerEmailVerification?: boolean; - hideCalendarNotes?: boolean; - successRedirectUrl?: string; - forwardParamsSuccessRedirect?: boolean; - - // Schedule and buffers - scheduleId?: number; - slotInterval?: number; - minimumBookingNotice?: number; - beforeEventBuffer?: number; - afterEventBuffer?: number; - - // Limits - bookingLimitsCount?: BookingLimitsCount | { disabled: true }; - bookingLimitsDuration?: BookingLimitsDuration | { disabled: true }; - bookerActiveBookingsLimit?: BookerActiveBookingsLimit | { disabled: true }; - onlyShowFirstAvailableSlot?: boolean; - bookingWindow?: BookingWindow; - offsetStart?: number; - - // Advanced - bookerLayouts?: BookerLayouts; - confirmationPolicy?: ConfirmationPolicy | { disabled: true }; - - // Recurring - recurrence?: Recurrence | { disabled: true }; - - // Seats - seats?: Seats | { disabled: true }; - - // Customization - bookingFields?: BookingField[]; - hidden?: boolean; - eventTypeColor?: EventTypeColor; - - // Ownership - userId?: number; - teamId?: number; - hosts?: Array<{ - userId: number; - isFixed: boolean; - }>; - users?: Array<{ - id: number; - name?: string; - username?: string; - avatarUrl?: string; - brandColor?: string | null; - darkBrandColor?: string | null; - weekStart?: string; - metadata?: Record; - }>; - - // Metadata - metadata?: Record; - - // Booking action settings (API V2 format) - disableRescheduling?: DisableRescheduling; - disableCancelling?: DisableCancelling; - minimumRescheduleNotice?: number; - allowReschedulingPastBookings?: boolean; - allowReschedulingCancelledBookings?: boolean; - - // Additional properties from API responses - hideCalendarEventDetails?: boolean; - hideOrganizerEmail?: boolean; - customReplyToEmail?: string; - color?: { - lightThemeHex?: string; - darkThemeHex?: string; - }; - - // Cal Video Settings - calVideoSettings?: CalVideoSettings; - - // Interface language (API V2) - interfaceLanguage?: string; - - // Optimized slots (API V2) - showOptimizedSlots?: boolean; - - // Booking URL (API V2) - full booking URL for this event type - bookingUrl?: string; -} - -export interface CreateEventTypeInput { - // Basic fields - title: string; - slug: string; - description?: string; - lengthInMinutes: number; - lengthInMinutesOptions?: number[]; - - // Locations - locations?: Array<{ - type: string; - address?: string; - link?: string; - phone?: string; - integration?: string; - credentialId?: number; - public?: boolean; - }>; - - // Schedule - scheduleId?: number; - - // Booking settings - disableGuests?: boolean; - slotInterval?: number; - minimumBookingNotice?: number; - beforeEventBuffer?: number; - afterEventBuffer?: number; - - // Limits - bookingLimitsCount?: BookingLimitsCount | { disabled: true }; - bookingLimitsDuration?: BookingLimitsDuration | { disabled: true }; - bookerActiveBookingsLimit?: BookerActiveBookingsLimit | { disabled: true }; - onlyShowFirstAvailableSlot?: boolean; - - // Booking window - bookingWindow?: BookingWindow; - - // Advanced settings - offsetStart?: number; - bookerLayouts?: BookerLayouts; - confirmationPolicy?: ConfirmationPolicy | { disabled: true }; - recurrence?: Recurrence | { disabled: true }; - seats?: Seats | { disabled: true }; - - // Customization - bookingFields?: BookingField[]; - hidden?: boolean; - eventTypeColor?: EventTypeColor; - emailSettings?: EmailSettings; - - // Additional fields - requiresConfirmation?: boolean; - requiresBookerEmailVerification?: boolean; - hideCalendarNotes?: boolean; - lockTimeZoneToggleOnBookingPage?: boolean; - successRedirectUrl?: string; - forwardParamsSuccessRedirect?: boolean; - metadata?: Record; - - // API V2 new fields - disableCancelling?: DisableCancelling; - disableRescheduling?: DisableRescheduling; - calVideoSettings?: CalVideoSettings; - interfaceLanguage?: string; - showOptimizedSlots?: boolean; -} - -export interface GetEventTypesResponse { - status: "success"; - data: EventType[]; -} diff --git a/companion/services/types/index.ts b/companion/services/types/index.ts deleted file mode 100644 index dad7218772..0000000000 --- a/companion/services/types/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Re-export all types for easy importing - -export * from "./bookings.types"; -export * from "./event-types.types"; -export * from "./private-links.types"; -export * from "./schedules.types"; -export * from "./users.types"; -export * from "./webhooks.types"; diff --git a/companion/services/types/private-links.types.ts b/companion/services/types/private-links.types.ts deleted file mode 100644 index 1b224f8701..0000000000 --- a/companion/services/types/private-links.types.ts +++ /dev/null @@ -1,25 +0,0 @@ -// Private Link Types -export interface PrivateLink { - id: number; - link: string; - enabled: boolean; - eventTypeId: number; -} - -export interface CreatePrivateLinkInput { - enabled?: boolean; -} - -export interface UpdatePrivateLinkInput { - enabled?: boolean; -} - -export interface GetPrivateLinksResponse { - status: string; - data: PrivateLink[]; -} - -export interface GetPrivateLinkResponse { - status: string; - data: PrivateLink; -} diff --git a/companion/services/types/schedules.types.ts b/companion/services/types/schedules.types.ts deleted file mode 100644 index be64af6f53..0000000000 --- a/companion/services/types/schedules.types.ts +++ /dev/null @@ -1,31 +0,0 @@ -export interface ScheduleAvailability { - days: string[]; - startTime: string; - endTime: string; -} - -export interface ScheduleOverride { - date: string; - startTime: string; - endTime: string; -} - -export interface Schedule { - id: number; - ownerId: number; - name: string; - timeZone: string; - availability: ScheduleAvailability[]; - isDefault: boolean; - overrides: ScheduleOverride[]; -} - -export interface GetSchedulesResponse { - status: "success"; - data: Schedule[]; -} - -export interface GetScheduleResponse { - status: "success"; - data: Schedule | null; -} diff --git a/companion/services/types/users.types.ts b/companion/services/types/users.types.ts deleted file mode 100644 index 0f942f084d..0000000000 --- a/companion/services/types/users.types.ts +++ /dev/null @@ -1,37 +0,0 @@ -export interface UserProfile { - id: number; - username: string; - name: string; - email: string; - bio?: string; - avatarUrl?: string; - timeZone?: string; - weekStart?: string; - timeFormat?: number; - defaultScheduleId?: number; - locale?: string; - organizationId?: number; - organization?: { - id: number; - slug?: string; - name?: string; - isPlatform: boolean; - }; - metadata?: Record; - brandColor?: string; - darkBrandColor?: string; - theme?: string; -} - -export interface ConferencingOption { - id: number; - type: string; - userId: number | null; - teamId: number | null; - appId: string | null; - subscriptionId: number | null; - paymentStatus: string | null; - billingCycleStart: string | null; - invalid: boolean; - delegationCredentialId: string | null; -} diff --git a/companion/services/types/webhooks.types.ts b/companion/services/types/webhooks.types.ts deleted file mode 100644 index 30b47e673b..0000000000 --- a/companion/services/types/webhooks.types.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Webhook Types -export interface Webhook { - id: string; - subscriberUrl: string; - eventTriggers: string[]; - active: boolean; - payloadTemplate?: string; - secret?: string; - eventTypeId?: number; -} - -export interface CreateWebhookInput { - subscriberUrl: string; - eventTriggers: string[]; - active?: boolean; - payloadTemplate?: string; - secret?: string; -} - -export interface UpdateWebhookInput { - subscriberUrl?: string; - eventTriggers?: string[]; - active?: boolean; - payloadTemplate?: string; - secret?: string; -} - -export interface GetWebhooksResponse { - status: string; - data: Webhook[]; -} - -export interface GetWebhookResponse { - status: string; - data: Webhook; -} diff --git a/companion/services/webAuth.ts b/companion/services/webAuth.ts deleted file mode 100644 index 8afebe90aa..0000000000 --- a/companion/services/webAuth.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { Platform } from "react-native"; - -import { fetchWithTimeout } from "@/utils/network"; - -import type { UserProfile } from "./types/users.types"; - -const REQUEST_TIMEOUT_MS = 30000; - -export interface WebSessionInfo { - isLoggedIn: boolean; - accessToken?: string; - refreshToken?: string; - userInfo?: UserProfile; -} - -// Check if running on web -function isWeb(): boolean { - return Platform.OS === "web"; -} - -// Get cookies from the browser -function getCookies(): { [key: string]: string } { - if (!isWeb()) return {}; - - const cookies: { [key: string]: string } = {}; - if (typeof document !== "undefined") { - document.cookie.split(";").forEach((cookie) => { - const [name, value] = cookie.trim().split("="); - if (name && value) { - cookies[name] = decodeURIComponent(value); - } - }); - } - return cookies; -} - -// Check for Cal.com session cookies -async function checkCalComSession(): Promise { - if (!isWeb()) { - return { isLoggedIn: false }; - } - - try { - // Check for common Cal.com session cookies - const cookies = getCookies(); - - // Look for Cal.com authentication cookies - // Common patterns: next-auth.session-token, __Secure-next-auth.session-token, etc. - const sessionCookieNames = [ - "next-auth.session-token", - "__Secure-next-auth.session-token", - "__Host-next-auth.session-token", - "next-auth.csrf-token", - "cal-session", - "session-token", - ]; - - const foundSessionCookie = sessionCookieNames.find((name) => cookies[name]); - - if (foundSessionCookie) { - // Try to validate the session with Cal.com API - const sessionInfo = await validateWebSession(); - return sessionInfo; - } - - return { isLoggedIn: false }; - } catch (_error) { - return { isLoggedIn: false }; - } -} - -// Validate web session by calling Cal.com API with cookies -async function validateWebSession(): Promise { - if (!isWeb()) { - return { isLoggedIn: false }; - } - - try { - // First try to call the NextAuth session endpoint - const sessionResponse = await fetchWithTimeout( - "https://app.cal.com/api/auth/session", - { - method: "GET", - credentials: "include", // Include cookies - headers: { - "Content-Type": "application/json", - }, - }, - REQUEST_TIMEOUT_MS - ); - - if (sessionResponse.ok) { - const sessionData = await sessionResponse.json(); - - if (sessionData?.user) { - // User is logged in via web session - return { - isLoggedIn: true, - userInfo: sessionData.user, - }; - } - } - - // Try the internal Cal.com me endpoint (this might work with cookies) - const meResponse = await fetchWithTimeout( - "https://app.cal.com/api/me", - { - method: "GET", - credentials: "include", - headers: { - "Content-Type": "application/json", - }, - }, - REQUEST_TIMEOUT_MS - ); - - if (meResponse.ok) { - const userData = await meResponse.json(); - - if (userData && (userData.id || userData.user || userData.data)) { - return { - isLoggedIn: true, - userInfo: userData.user || userData.data || userData, - }; - } - } - - // Try to check if user is logged in by attempting to access a protected page - const dashboardResponse = await fetchWithTimeout( - "https://app.cal.com/api/trpc/viewer.me", - { - method: "GET", - credentials: "include", - headers: { - "Content-Type": "application/json", - }, - }, - REQUEST_TIMEOUT_MS - ); - - if (dashboardResponse.ok) { - const dashboardData = await dashboardResponse.json(); - - if (dashboardData?.result?.data) { - return { - isLoggedIn: true, - userInfo: dashboardData.result.data, - }; - } - } - - return { isLoggedIn: false }; - } catch (_error) { - return { isLoggedIn: false }; - } -} - -// Try to get API tokens from web session -async function getTokensFromWebSession(): Promise<{ accessToken?: string; refreshToken?: string }> { - if (!isWeb()) { - return {}; - } - - try { - // Some Cal.com implementations might expose tokens via specific endpoints - // This is a fallback approach - in practice, web sessions might not expose tokens directly - - // Check localStorage/sessionStorage for any stored tokens - if (typeof window !== "undefined") { - const localStorage = window.localStorage; - const sessionStorage = window.sessionStorage; - - const possibleTokenKeys = [ - "cal-access-token", - "cal-auth-token", - "next-auth.token", - "auth-token", - "access-token", - ]; - - for (const key of possibleTokenKeys) { - const localToken = localStorage.getItem(key); - const sessionToken = sessionStorage.getItem(key); - - if (localToken || sessionToken) { - return { - accessToken: localToken || sessionToken || undefined, - }; - } - } - } - - return {}; - } catch (_error) { - return {}; - } -} - -// Redirect to Cal.com web login instead of using WebView -function redirectToWebLogin(): void { - if (!isWeb()) return; - - // For web, redirect directly to Cal.com login - const currentUrl = window.location.href; - const loginUrl = `https://app.cal.com/auth/signin?callbackUrl=${encodeURIComponent(currentUrl)}`; - - window.location.href = loginUrl; -} - -// Export as object to satisfy noStaticOnlyClass rule -export const WebAuthService = { - isWeb, - getCookies, - checkCalComSession, - validateWebSession, - getTokensFromWebSession, - redirectToWebLogin, -}; diff --git a/companion/tailwind.config.js b/companion/tailwind.config.js deleted file mode 100644 index 1e201380e0..0000000000 --- a/companion/tailwind.config.js +++ /dev/null @@ -1,94 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { - content: ["./app/**/*.{js,jsx,ts,tsx}", "./components/**/*.{js,jsx,ts,tsx}"], - presets: [require("nativewind/preset")], - darkMode: "media", - theme: { - extend: { - colors: { - border: "hsl(var(--border))", - input: "hsl(var(--input))", - ring: "hsl(var(--ring))", - background: "hsl(var(--background))", - foreground: "hsl(var(--foreground))", - primary: { - DEFAULT: "hsl(var(--primary))", - foreground: "hsl(var(--primary-foreground))", - }, - secondary: { - DEFAULT: "hsl(var(--secondary))", - foreground: "hsl(var(--secondary-foreground))", - }, - destructive: { - DEFAULT: "hsl(var(--destructive))", - foreground: "hsl(var(--destructive-foreground))", - }, - muted: { - DEFAULT: "hsl(var(--muted))", - foreground: "hsl(var(--muted-foreground))", - }, - accent: { - DEFAULT: "hsl(var(--accent))", - foreground: "hsl(var(--accent-foreground))", - }, - popover: { - DEFAULT: "hsl(var(--popover))", - foreground: "hsl(var(--popover-foreground))", - }, - card: { - DEFAULT: "hsl(var(--card))", - foreground: "hsl(var(--card-foreground))", - }, - cal: { - text: { - DEFAULT: "#333333", - dark: "#FFFFFF", - secondary: "#666666", - secondaryDark: "#A3A3A3", - muted: "#8E8E93", - mutedDark: "#A3A3A3", - emphasis: "#3C3F44", - }, - bg: { - DEFAULT: "#FFFFFF", - dark: "#000000", - secondary: "#F8F9FA", - secondaryDark: "#171717", - muted: "#F2F2F7", - mutedDark: "#262626", - emphasis: "#E5E5EA", - emphasisDark: "#404040", - }, - border: { - DEFAULT: "#E5E5EA", - dark: "#4D4D4D", - light: "#C6C6C8", - subtle: "#D1D5DB", - subtleDark: "#262626", - }, - accent: { - DEFAULT: "#007AFF", - success: "#34C759", - warning: "#FF9500", - error: "#FF3B30", - destructive: "#800020", - destructiveDark: "#FF453A", - }, - brand: { - DEFAULT: "#292929", - black: "#000000", - }, - }, - }, - borderRadius: { - lg: "var(--radius)", - md: "calc(var(--radius) - 2px)", - sm: "calc(var(--radius) - 4px)", - }, - spacing: { - 18: "4.5rem", - }, - }, - }, - plugins: [require("tailwindcss-animate")], -}; 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 deleted file mode 100644 index ae598628b0..0000000000 Binary files a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@1x.png and /dev/null 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 deleted file mode 100644 index ff9d54105a..0000000000 Binary files a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@2x.png and /dev/null 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 deleted file mode 100644 index ba7913561e..0000000000 Binary files a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@3x.png and /dev/null 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 deleted file mode 100644 index d2b59a92be..0000000000 Binary files a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@1x.png and /dev/null 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 deleted file mode 100644 index 597bdb4bd4..0000000000 Binary files a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@2x.png and /dev/null 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 deleted file mode 100644 index 2f0e2b6213..0000000000 Binary files a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@3x.png and /dev/null 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 deleted file mode 100644 index ff9d54105a..0000000000 Binary files a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@1x.png and /dev/null 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 deleted file mode 100644 index 3b831397fe..0000000000 Binary files a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@2x.png and /dev/null 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 deleted file mode 100644 index 6cd063f2dc..0000000000 Binary files a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@3x.png and /dev/null 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 deleted file mode 100644 index 6cd063f2dc..0000000000 Binary files a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-60x60@2x.png and /dev/null 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 deleted file mode 100644 index 523900936e..0000000000 Binary files a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-60x60@3x.png and /dev/null 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 deleted file mode 100644 index 69908aedf3..0000000000 Binary files a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-76x76@1x.png and /dev/null 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 deleted file mode 100644 index 04580c4483..0000000000 Binary files a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-76x76@2x.png and /dev/null 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 deleted file mode 100644 index 74e8e26bb2..0000000000 Binary files a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-83.5x83.5@2x.png and /dev/null differ diff --git a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/Contents.json b/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 6188260aba..0000000000 --- a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "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 deleted file mode 100644 index 2732229faf..0000000000 Binary files a/companion/targets/widget/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png and /dev/null differ diff --git a/companion/targets/widget/Info.plist b/companion/targets/widget/Info.plist deleted file mode 100644 index 5510804d0e..0000000000 --- a/companion/targets/widget/Info.plist +++ /dev/null @@ -1,11 +0,0 @@ - - - - - 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 deleted file mode 100644 index 290dd3e41a..0000000000 --- a/companion/targets/widget/expo-target.config.js +++ /dev/null @@ -1,9 +0,0 @@ -/** @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 deleted file mode 100644 index 935884dc88..0000000000 --- a/companion/targets/widget/generated.entitlements +++ /dev/null @@ -1,10 +0,0 @@ - - - - - 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 deleted file mode 100644 index 93caa87500..0000000000 --- a/companion/targets/widget/index.swift +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 56e00d58fa..0000000000 --- a/companion/targets/widget/widgets.swift +++ /dev/null @@ -1,431 +0,0 @@ -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/tsconfig.extension.json b/companion/tsconfig.extension.json deleted file mode 100644 index 74a5a2b5dd..0000000000 --- a/companion/tsconfig.extension.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "ESNext", - "moduleResolution": "bundler", - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - "types": ["chrome"], - "baseUrl": ".", - "paths": { - "@/*": ["./*"], - "@/components/*": ["./components/*"], - "@/hooks/*": ["./hooks/*"], - "@/utils/*": ["./utils/*"], - "@/services/*": ["./services/*"], - "@/contexts/*": ["./contexts/*"], - "@/config/*": ["./config/*"], - "@/types/*": ["./types/*"] - } - }, - "include": ["extension/**/*.ts", "extension/**/*.tsx", "services/types/**/*.ts", "types/**/*.ts"], - "exclude": ["node_modules", ".output"] -} diff --git a/companion/tsconfig.json b/companion/tsconfig.json deleted file mode 100644 index 38e5b82283..0000000000 --- a/companion/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "types": ["nativewind/types"], - "strict": true, - "baseUrl": ".", - "paths": { - "@/*": ["./*"], - "@/components/*": ["./components/*"], - "@/hooks/*": ["./hooks/*"], - "@/utils/*": ["./utils/*"], - "@/services/*": ["./services/*"], - "@/contexts/*": ["./contexts/*"], - "@/config/*": ["./config/*"], - "@/types/*": ["./types/*"] - } - }, - "extends": "expo/tsconfig.base", - "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts", "nativewind-env.d.ts"], - "exclude": ["extension/**/*", ".output/**/*"] -} diff --git a/companion/types/attendee.ts b/companion/types/attendee.ts deleted file mode 100644 index 80350a23bd..0000000000 --- a/companion/types/attendee.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Booking } from "@/services/types/bookings.types"; - -/** - * Attendee type extracted from Booking for reuse across the codebase. - * This provides a proper type for individual attendees instead of using `any`. - */ -export type Attendee = NonNullable[number]; diff --git a/companion/types/browser.d.ts b/companion/types/browser.d.ts deleted file mode 100644 index 904e03f379..0000000000 --- a/companion/types/browser.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Ambient declarations for browser extension APIs - * These provide proper typing for Firefox/Safari browser namespace - * and Brave-specific navigator properties used in oauthService.ts - */ - -// Firefox/Safari use 'browser' namespace instead of 'chrome' -// This is a WebExtension API that mirrors chrome.* APIs -// Firefox/Safari browser.identity API is Promise-based (unlike Chrome's callback-based API) -declare const browser: - | { - identity?: { - launchWebAuthFlow: (options: { - url: string; - interactive: boolean; - }) => Promise; - }; - runtime?: { - lastError?: { message?: string }; - }; - } - | undefined; - -// Brave adds isBrave() to navigator -interface Navigator { - brave?: { - isBrave: () => Promise; - }; -} diff --git a/companion/types/index.ts b/companion/types/index.ts deleted file mode 100644 index a91fe02076..0000000000 --- a/companion/types/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Central export for all custom types - */ - -export * from "./locations"; diff --git a/companion/types/locations.ts b/companion/types/locations.ts deleted file mode 100644 index cba406bc37..0000000000 --- a/companion/types/locations.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Location types for event type management - * These types are used across the companion app for handling event type locations - */ - -/** - * Valid location types supported by the Cal.com API - */ -export type LocationType = - | "integration" - | "address" - | "link" - | "phone" - | "attendeeAddress" - | "attendeePhone" - | "attendeeDefined"; - -/** - * Valid integration types for conferencing apps - */ -export type IntegrationType = - | "cal-video" - | "google-meet" - | "zoom" - | "office365-video" - | "msteams" - | "webex" - | "jitsi"; - -/** - * Represents a location item in the UI - */ -export interface LocationItem { - /** Unique identifier for the location (used for list rendering) */ - id: string; - /** The type of location */ - type: LocationType; - /** Integration app ID (for conferencing apps) */ - integration?: string; - /** Physical address (for address type) */ - address?: string; - /** Meeting link URL (for link type) */ - link?: string; - /** Phone number (for phone types) */ - phone?: string; - /** Whether the location is public */ - public?: boolean; - /** Display name shown in the UI */ - displayName: string; - /** Icon URL for the location */ - iconUrl: string | null; -} - -/** - * Location object as returned from the API - */ -export interface ApiLocation { - type: string; - integration?: string; - address?: string; - link?: string; - phone?: string; - public?: boolean; -} - -/** - * Location input for API requests - */ -export interface ApiLocationInput { - type: string; - integration?: string; - address?: string; - link?: string; - phone?: string; - public?: boolean; -} - -/** - * Location option for dropdown selection - */ -export interface LocationOption { - label: string; - value: string; - iconUrl: string | null; - category?: string; -} - -/** - * Grouped location options for dropdown - */ -export interface LocationOptionGroup { - category: string; - options: LocationOption[]; -} diff --git a/companion/utils/alerts.ts b/companion/utils/alerts.ts deleted file mode 100644 index b7f6e1f9b8..0000000000 --- a/companion/utils/alerts.ts +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Alert Utilities - * - * Helper functions for showing alerts with environment-aware and platform-aware behavior. - * On iOS/Android, uses native Alert.alert(). - * On web (browser extension), uses the global toast system for centered notifications. - * Error alerts are only shown in development mode to avoid confusing users in production. - */ - -import { Alert, Platform } from "react-native"; - -type ToastType = "success" | "error" | "info"; - -/** - * Global toast reference (set by ToastProvider) - * This allows the alert utilities to show toasts on web without requiring React context. - */ -let globalShowToast: ((title: string, message: string, type?: ToastType) => void) | null = null; - -/** - * Set the global toast function. Called by ToastProvider on mount. - * @param fn - The showToast function from ToastProvider, or null to clear - */ -export const setGlobalToastFunction = (fn: typeof globalShowToast) => { - globalShowToast = fn; -}; - -/** - * Show an error alert only in development mode. - * On web, shows a centered toast notification. - * In production, errors are silently logged to console. - * - * @param title - The alert title - * @param message - The error message to display - */ -export const showErrorAlert = (title: string, message: string) => { - if (Platform.OS === "web" && globalShowToast) { - if (__DEV__) { - globalShowToast(title, message, "error"); - } else { - console.error(`[${title}] ${message}`); - } - return; - } - - if (__DEV__) { - Alert.alert(title, message); - } else { - console.error(`[${title}] ${message}`); - } -}; - -/** - * Show a confirmation alert (always shown - user-initiated actions) - * This is a direct reference to Alert.alert for consistency. - * Note: On web, this will not work - use a modal or custom confirmation dialog instead. - */ -export const showConfirmAlert = Alert.alert; - -/** - * Show a success alert (always shown - user feedback) - * On web, shows a centered toast notification. - * - * @param title - The alert title - * @param message - The success message to display - */ -export const showSuccessAlert = (title: string, message: string) => { - if (Platform.OS === "web" && globalShowToast) { - globalShowToast(title, message, "success"); - return; - } - - Alert.alert(title, message); -}; - -/** - * Show an info alert (always shown - informational feedback) - * On web, shows a centered toast notification. - * - * @param title - The alert title - * @param message - The info message to display - */ -export const showInfoAlert = (title: string, message: string) => { - if (Platform.OS === "web" && globalShowToast) { - globalShowToast(title, message, "info"); - return; - } - - Alert.alert(title, message); -}; - -/** - * Show a standard "Feature not available" alert for companion app limitations. - */ -export const showNotAvailableAlert = () => { - Alert.alert( - "Feature not available", - "This feature is not available on app yet. To use, please visit app.cal.com.", - [{ text: "OK", style: "cancel" }] - ); -}; diff --git a/companion/utils/booking-actions.ts b/companion/utils/booking-actions.ts deleted file mode 100644 index 8a843cd3b7..0000000000 --- a/companion/utils/booking-actions.ts +++ /dev/null @@ -1,686 +0,0 @@ -/** - * Centralized Booking Action Gating Utility - * - * This module provides a single source of truth for booking action visibility and enabled state. - * It matches the web's semantics from apps/web/components/booking/actions/bookingActions.ts - * - * Parity Scope (In Scope Now): - * - cancel, reschedule, reschedule_request, change_location, add_guests - * - view_recordings, meeting_session_details, mark_no_show - * - * Deferred: - * - report, reroute, reassign, charge_card - */ -import type { Booking, BookingStatus } from "@/services/types/bookings.types"; -import type { EventType } from "@/services/types/event-types.types"; - -// ============================================================================ -// Types -// ============================================================================ - -export interface BookingActionResult { - visible: boolean; - enabled: boolean; - disabledReason?: DisabledReason; - needsData?: string[]; -} - -export type DisabledReason = - | "BOOKING_IN_PAST" - | "BOOKING_CANCELLED" - | "BOOKING_REJECTED" - | "BOOKING_PENDING" - | "WITHIN_MINIMUM_NOTICE" - | "RESCHEDULING_DISABLED" - | "CANCELLING_DISABLED" - | "GUESTS_DISABLED" - | "NOT_CAL_VIDEO" - | "NO_RECORDINGS" - | "NO_ATTENDEE_DATA" - | "SEATED_BOOKING" - | "NOT_ORGANIZER" - | "OFFLINE"; - -export interface BookingActionsResult { - reschedule: BookingActionResult; - rescheduleRequest: BookingActionResult; - cancel: BookingActionResult; - changeLocation: BookingActionResult; - addGuests: BookingActionResult; - viewRecordings: BookingActionResult; - meetingSessionDetails: BookingActionResult; - markNoShow: BookingActionResult; -} - -export interface BookingActionContext { - booking: Booking | NormalizedBooking; - eventType?: EventType | null; - currentUserId?: number; - currentUserEmail?: string; - isOnline?: boolean; -} - -export interface NormalizedBooking { - id: number; - uid: string; - title: string; - status: BookingStatus; - startTime: Date; - endTime: Date; - location?: string; - isRecorded?: boolean; - rescheduled?: boolean; - fromReschedule?: string; - recurringEventId?: string; - eventTypeId?: number; - user?: { - id: number; - email: string; - name: string; - }; - hosts?: Array<{ - id?: number | string; - email?: string; - name?: string; - }>; - attendees?: Array<{ - id?: number | string; - email: string; - name: string; - noShow?: boolean; - }>; - seatsReferences?: Array<{ - referenceUid: string; - attendee?: { - email: string; - }; - }>; - payment?: Array<{ - id: number; - success: boolean; - paymentOption: string; - }>; -} - -export type { BookingStatus } from "@/services/types/bookings.types"; - -// ============================================================================ -// Normalization Functions -// ============================================================================ - -/** - * Normalizes a booking object from API response to a consistent format. - * - Normalizes status to lowercase (API v2 format) - * - Converts time strings to Date objects - * - Ensures consistent field names - */ -export function normalizeBooking(booking: Booking): NormalizedBooking { - const status = (booking.status?.toLowerCase() || "pending") as BookingStatus; - - // Normalize times - prefer startTime/endTime, fallback to start/end - const startTimeStr = booking.startTime || booking.start || ""; - const endTimeStr = booking.endTime || booking.end || ""; - - return { - id: booking.id, - uid: booking.uid, - title: booking.title, - status, - startTime: new Date(startTimeStr), - endTime: new Date(endTimeStr), - location: booking.location, - isRecorded: (booking as { isRecorded?: boolean }).isRecorded, - rescheduled: booking.rescheduled, - fromReschedule: booking.fromReschedule, - recurringEventId: booking.recurringEventId, - eventTypeId: booking.eventTypeId, - user: booking.user, - hosts: booking.hosts, - attendees: booking.attendees, - seatsReferences: ( - booking as { seatsReferences?: Array<{ referenceUid: string; attendee: { email: string } }> } - ).seatsReferences, - payment: booking.payment, - }; -} - -// ============================================================================ -// Time Computation Functions (matching web's exact definitions) -// ============================================================================ - -/** - * Check if booking is in the past. - * Web definition: new Date(booking.endTime) < new Date() - */ -export function isBookingInPast(booking: NormalizedBooking, now: Date = new Date()): boolean { - return booking.endTime < now; -} - -/** - * Check if booking is upcoming. - * Web definition: new Date(booking.endTime) >= new Date() - */ -export function isBookingUpcoming(booking: NormalizedBooking, now: Date = new Date()): boolean { - return booking.endTime >= now; -} - -/** - * Check if booking is currently ongoing. - * Web definition: isUpcoming && new Date() >= new Date(booking.startTime) - */ -export function isBookingOngoing(booking: NormalizedBooking, now: Date = new Date()): boolean { - return isBookingUpcoming(booking, now) && now >= booking.startTime; -} - -/** - * Check if booking is cancelled. - */ -export function isBookingCancelled(booking: NormalizedBooking): boolean { - return booking.status === "cancelled"; -} - -/** - * Check if booking is rejected. - */ -export function isBookingRejected(booking: NormalizedBooking): boolean { - return booking.status === "rejected"; -} - -/** - * Check if booking is pending (unconfirmed). - */ -export function isBookingPending(booking: NormalizedBooking): boolean { - return booking.status === "pending"; -} - -/** - * Check if booking is confirmed (accepted). - */ -export function isBookingConfirmed(booking: NormalizedBooking): boolean { - return booking.status === "accepted"; -} - -// ============================================================================ -// Role Detection Functions -// ============================================================================ - -/** - * Check if the current user is the organizer of the booking. - */ -export function isUserOrganizer( - booking: NormalizedBooking, - userId?: number, - userEmail?: string -): boolean { - if (!booking.user) return false; - - if (userId && booking.user.id === userId) return true; - if (userEmail && booking.user.email?.toLowerCase() === userEmail.toLowerCase()) return true; - - return false; -} - -/** - * Check if the current user is a host of the booking. - */ -export function isUserHost( - booking: NormalizedBooking, - userId?: number, - userEmail?: string -): boolean { - if (!booking.hosts || booking.hosts.length === 0) return false; - - return booking.hosts.some((host) => { - if (userId && host.id !== undefined && String(host.id) === String(userId)) return true; - if (userEmail && host.email?.toLowerCase() === userEmail.toLowerCase()) return true; - return false; - }); -} - -/** - * Check if the current user is an attendee of the booking. - */ -export function isUserAttendee( - booking: NormalizedBooking, - userId?: number, - userEmail?: string -): boolean { - if (!booking.attendees || booking.attendees.length === 0) return false; - - return booking.attendees.some((attendee) => { - if (userId && attendee.id !== undefined && String(attendee.id) === String(userId)) return true; - if (userEmail && attendee.email?.toLowerCase() === userEmail.toLowerCase()) return true; - return false; - }); -} - -/** - * Get the user's seat reference UID if they are an attendee of a seated booking. - */ -export function getUserSeatReferenceUid( - booking: NormalizedBooking, - userEmail?: string -): string | undefined { - if (!booking.seatsReferences || !userEmail) return undefined; - - const userSeat = booking.seatsReferences.find( - (seat) => seat.attendee?.email?.toLowerCase() === userEmail.toLowerCase() - ); - - return userSeat?.referenceUid; -} - -/** - * Check if booking is a seated booking. - */ -export function isSeatedBooking(booking: NormalizedBooking): boolean { - return Boolean(booking.seatsReferences && booking.seatsReferences.length > 0); -} - -// ============================================================================ -// Location Detection Functions -// ============================================================================ - -/** - * Check if the booking location is Cal Video. - * Web definition: !booking.location || booking.location === "integrations:daily" || location.trim() === "" - */ -export function isCalVideoLocation(booking: NormalizedBooking): boolean { - const location = booking.location; - - if (!location) return true; - if (location === "integrations:daily") return true; - if (typeof location === "string" && location.trim() === "") return true; - if (location.includes("cal.com/video") || location.includes("cal-video")) return true; - - return false; -} - -// ============================================================================ -// Minimum Reschedule Notice Check -// ============================================================================ - -/** - * Check if the booking is within the minimum reschedule notice period. - * This should only apply to non-organizers (attendees). - */ -export function isWithinMinimumRescheduleNotice( - bookingStartTime: Date, - minimumRescheduleNotice: number | null | undefined, - now: Date = new Date() -): boolean { - if (!minimumRescheduleNotice || minimumRescheduleNotice <= 0) return false; - - const noticeEndTime = new Date(bookingStartTime.getTime() - minimumRescheduleNotice * 60 * 1000); - return now >= noticeEndTime; -} - -// ============================================================================ -// Main Action Gating Function -// ============================================================================ - -/** - * Helper to check if a booking is already normalized (has Date objects for times) - */ -function isNormalizedBooking(booking: Booking | NormalizedBooking): booking is NormalizedBooking { - return booking.startTime instanceof Date; -} - -/** - * Get the visibility and enabled state for all booking actions. - * This is the main entry point for action gating. - * Accepts either a raw Booking or NormalizedBooking and normalizes internally. - */ -export function getBookingActions(context: BookingActionContext): BookingActionsResult { - const { - booking: rawBooking, - eventType, - currentUserId, - currentUserEmail, - isOnline = true, - } = context; - const now = new Date(); - - // Normalize booking if needed (convert string times to Date objects) - const booking: NormalizedBooking = isNormalizedBooking(rawBooking) - ? rawBooking - : normalizeBooking(rawBooking); - - // Compute booking state flags - const isPast = isBookingInPast(booking, now); - const isOngoing = isBookingOngoing(booking, now); - const isCancelled = isBookingCancelled(booking); - const isRejected = isBookingRejected(booking); - const isPending = isBookingPending(booking); - const isConfirmed = isBookingConfirmed(booking); - const isCalVideo = isCalVideoLocation(booking); - const isSeated = isSeatedBooking(booking); - - // Compute user role flags - const isOrganizer = isUserOrganizer(booking, currentUserId, currentUserEmail); - const isHost = isUserHost(booking, currentUserId, currentUserEmail); - const isOrganizerOrHost = isOrganizer || isHost; - - // Get event type settings (with defaults) - const disableRescheduling = eventType?.disableRescheduling ?? false; - const disableCancelling = eventType?.disableCancelling ?? false; - const disableGuests = eventType?.disableGuests ?? false; - const minimumRescheduleNotice = eventType?.minimumRescheduleNotice ?? null; - const allowReschedulingPastBookings = eventType?.allowReschedulingPastBookings ?? false; - - // Check minimum reschedule notice (only applies to non-organizers) - const withinMinimumNotice = - !isOrganizerOrHost && - isWithinMinimumRescheduleNotice(booking.startTime, minimumRescheduleNotice, now); - - // ============================================================================ - // Reschedule Action - // Visible for upcoming, non-cancelled, non-pending bookings - // ============================================================================ - const isUpcoming = !isPast; - const canReschedule = - (isUpcoming || allowReschedulingPastBookings) && - !isCancelled && - !isRejected && - !isPending && - !disableRescheduling; - const reschedule: BookingActionResult = { - visible: isUpcoming || allowReschedulingPastBookings, - enabled: canReschedule, - needsData: eventType ? undefined : ["eventType"], - }; - - if (!isOnline) { - reschedule.enabled = false; - reschedule.disabledReason = "OFFLINE"; - } else if (isCancelled) { - reschedule.visible = false; - reschedule.enabled = false; - reschedule.disabledReason = "BOOKING_CANCELLED"; - } else if (isRejected) { - reschedule.visible = false; - reschedule.enabled = false; - reschedule.disabledReason = "BOOKING_REJECTED"; - } else if (isPending) { - reschedule.visible = false; - reschedule.enabled = false; - reschedule.disabledReason = "BOOKING_PENDING"; - } else if (isPast && !allowReschedulingPastBookings) { - reschedule.visible = false; - reschedule.enabled = false; - reschedule.disabledReason = "BOOKING_IN_PAST"; - } else if (disableRescheduling) { - reschedule.enabled = false; - reschedule.disabledReason = "RESCHEDULING_DISABLED"; - } else if (withinMinimumNotice) { - reschedule.enabled = false; - reschedule.disabledReason = "WITHIN_MINIMUM_NOTICE"; - } - - // ============================================================================ - // Reschedule Request Action (for organizers to request attendee to reschedule) - // Visible only for organizers/hosts with upcoming, non-cancelled, non-pending bookings - // ============================================================================ - const canRequestReschedule = - isOrganizerOrHost && - (isUpcoming || allowReschedulingPastBookings) && - !isCancelled && - !isRejected && - !isPending && - !disableRescheduling && - !isSeated; - const rescheduleRequest: BookingActionResult = { - visible: isOrganizerOrHost && (isUpcoming || allowReschedulingPastBookings), - enabled: canRequestReschedule, - needsData: eventType ? undefined : ["eventType"], - }; - - if (!isOrganizerOrHost) { - rescheduleRequest.visible = false; - rescheduleRequest.enabled = false; - rescheduleRequest.disabledReason = "NOT_ORGANIZER"; - } else if (!isOnline) { - rescheduleRequest.enabled = false; - rescheduleRequest.disabledReason = "OFFLINE"; - } else if (isCancelled) { - rescheduleRequest.visible = false; - rescheduleRequest.enabled = false; - rescheduleRequest.disabledReason = "BOOKING_CANCELLED"; - } else if (isRejected) { - rescheduleRequest.visible = false; - rescheduleRequest.enabled = false; - rescheduleRequest.disabledReason = "BOOKING_REJECTED"; - } else if (isPending) { - rescheduleRequest.visible = false; - rescheduleRequest.enabled = false; - rescheduleRequest.disabledReason = "BOOKING_PENDING"; - } else if (isPast && !allowReschedulingPastBookings) { - rescheduleRequest.visible = false; - rescheduleRequest.enabled = false; - rescheduleRequest.disabledReason = "BOOKING_IN_PAST"; - } else if (disableRescheduling) { - rescheduleRequest.enabled = false; - rescheduleRequest.disabledReason = "RESCHEDULING_DISABLED"; - } else if (isSeated) { - // Web disables reschedule_request for seated bookings - rescheduleRequest.enabled = false; - rescheduleRequest.disabledReason = "SEATED_BOOKING"; - } - - // ============================================================================ - // Cancel Action - // Visible for upcoming, non-cancelled bookings - // ============================================================================ - const canCancel = isUpcoming && !isCancelled && !isRejected && !disableCancelling; - const cancel: BookingActionResult = { - visible: canCancel, - enabled: canCancel, - }; - - if (!canCancel) { - if (isCancelled) { - cancel.visible = false; - cancel.enabled = false; - cancel.disabledReason = "BOOKING_CANCELLED"; - } else if (isRejected) { - cancel.visible = false; - cancel.enabled = false; - cancel.disabledReason = "BOOKING_REJECTED"; - } else if (isPast) { - cancel.visible = false; - cancel.enabled = false; - cancel.disabledReason = "BOOKING_IN_PAST"; - } else if (disableCancelling) { - cancel.enabled = false; - cancel.disabledReason = "CANCELLING_DISABLED"; - } - } else if (!isOnline) { - cancel.enabled = false; - cancel.disabledReason = "OFFLINE"; - } - - // ============================================================================ - // Change Location Action - // Visible for upcoming, non-cancelled, non-pending bookings - // ============================================================================ - const canChangeLocation = isUpcoming && !isCancelled && !isRejected && !isPending; - const changeLocation: BookingActionResult = { - visible: canChangeLocation, - enabled: canChangeLocation, - }; - - if (!canChangeLocation) { - changeLocation.visible = false; - changeLocation.enabled = false; - if (isCancelled) { - changeLocation.disabledReason = "BOOKING_CANCELLED"; - } else if (isRejected) { - changeLocation.disabledReason = "BOOKING_REJECTED"; - } else if (isPending) { - changeLocation.disabledReason = "BOOKING_PENDING"; - } else if (isPast) { - changeLocation.disabledReason = "BOOKING_IN_PAST"; - } - } else if (!isOnline) { - changeLocation.enabled = false; - changeLocation.disabledReason = "OFFLINE"; - } - - // ============================================================================ - // Add Guests Action - // Visible for upcoming, non-cancelled, non-pending bookings (if guests enabled) - // ============================================================================ - const canAddGuests = isUpcoming && !isCancelled && !isRejected && !isPending && !disableGuests; - const addGuests: BookingActionResult = { - visible: canAddGuests, - enabled: canAddGuests, - needsData: eventType ? undefined : ["eventType"], - }; - - if (disableGuests) { - addGuests.visible = false; - addGuests.enabled = false; - addGuests.disabledReason = "GUESTS_DISABLED"; - } else if (!canAddGuests) { - addGuests.visible = false; - addGuests.enabled = false; - if (isCancelled) { - addGuests.disabledReason = "BOOKING_CANCELLED"; - } else if (isRejected) { - addGuests.disabledReason = "BOOKING_REJECTED"; - } else if (isPending) { - addGuests.disabledReason = "BOOKING_PENDING"; - } else if (isPast) { - addGuests.disabledReason = "BOOKING_IN_PAST"; - } - } else if (!isOnline) { - addGuests.enabled = false; - addGuests.disabledReason = "OFFLINE"; - } - - // ============================================================================ - // View Recordings Action - // Only visible for past Cal Video bookings - // Note: We show this for all past Cal Video bookings, not just those with isRecorded=true, - // because the isRecorded field may not always be reliable. The view-recordings screen - // will handle showing an empty state if there are no recordings. - // ============================================================================ - const canViewRecordings = isCalVideo && isPast && isConfirmed && !isCancelled && !isRejected; - const viewRecordings: BookingActionResult = { - visible: canViewRecordings, - enabled: canViewRecordings, - }; - - if (!isCalVideo) { - viewRecordings.visible = false; - viewRecordings.enabled = false; - viewRecordings.disabledReason = "NOT_CAL_VIDEO"; - } else if (!isPast) { - viewRecordings.visible = false; - viewRecordings.enabled = false; - viewRecordings.disabledReason = "BOOKING_IN_PAST"; - } else if (!isConfirmed || isCancelled || isRejected) { - viewRecordings.visible = false; - viewRecordings.enabled = false; - viewRecordings.disabledReason = isCancelled - ? "BOOKING_CANCELLED" - : isRejected - ? "BOOKING_REJECTED" - : "BOOKING_PENDING"; - } - - // ============================================================================ - // Meeting Session Details Action - // Only visible for past Cal Video bookings - // ============================================================================ - const canViewSessionDetails = isCalVideo && isPast && isConfirmed && !isCancelled && !isRejected; - const meetingSessionDetails: BookingActionResult = { - visible: canViewSessionDetails, - enabled: canViewSessionDetails, - }; - - if (!isCalVideo) { - meetingSessionDetails.visible = false; - meetingSessionDetails.enabled = false; - meetingSessionDetails.disabledReason = "NOT_CAL_VIDEO"; - } else if (!isPast) { - meetingSessionDetails.visible = false; - meetingSessionDetails.enabled = false; - meetingSessionDetails.disabledReason = "BOOKING_IN_PAST"; - } else if (!isConfirmed || isCancelled || isRejected) { - meetingSessionDetails.visible = false; - meetingSessionDetails.enabled = false; - meetingSessionDetails.disabledReason = isCancelled - ? "BOOKING_CANCELLED" - : isRejected - ? "BOOKING_REJECTED" - : "BOOKING_PENDING"; - } - - // ============================================================================ - // Mark No-Show Action - // Visible only for past or ongoing bookings (not for future/upcoming bookings) - // This matches the booking list behavior where Mark No-Show is not shown for upcoming - // ============================================================================ - const canMarkNoShow = (isPast || isOngoing) && !isPending && !isCancelled && !isRejected; - const markNoShow: BookingActionResult = { - visible: canMarkNoShow, - enabled: canMarkNoShow, - }; - - if (!canMarkNoShow) { - markNoShow.visible = false; - markNoShow.enabled = false; - if (isPending) { - markNoShow.disabledReason = "BOOKING_PENDING"; - } else if (isCancelled) { - markNoShow.disabledReason = "BOOKING_CANCELLED"; - } else if (isRejected) { - markNoShow.disabledReason = "BOOKING_REJECTED"; - } else { - markNoShow.disabledReason = "BOOKING_IN_PAST"; // Actually means booking is in future - } - } else if (!isOnline) { - markNoShow.enabled = false; - markNoShow.disabledReason = "OFFLINE"; - } else if (!booking.attendees || booking.attendees.length === 0) { - markNoShow.enabled = false; - markNoShow.disabledReason = "NO_ATTENDEE_DATA"; - } - - return { - reschedule, - rescheduleRequest, - cancel, - changeLocation, - addGuests, - viewRecordings, - meetingSessionDetails, - markNoShow, - }; -} - -// ============================================================================ -// Helper function to get disabled reason message -// ============================================================================ - -export function getDisabledReasonMessage(reason: DisabledReason): string { - const messages: Record = { - BOOKING_IN_PAST: "This booking is in the past", - BOOKING_CANCELLED: "This booking has been cancelled", - BOOKING_REJECTED: "This booking has been rejected", - BOOKING_PENDING: "This booking is pending confirmation", - WITHIN_MINIMUM_NOTICE: "Within minimum reschedule notice period", - RESCHEDULING_DISABLED: "Rescheduling is disabled for this event type", - CANCELLING_DISABLED: "Cancelling is disabled for this event type", - GUESTS_DISABLED: "Adding guests is disabled for this event type", - NOT_CAL_VIDEO: "Only available for Cal Video meetings", - NO_RECORDINGS: "No recordings available for this meeting", - NO_ATTENDEE_DATA: "Attendee information not available", - SEATED_BOOKING: "Not available for seated bookings", - NOT_ORGANIZER: "Only available for organizers", - OFFLINE: "You are currently offline", - }; - - return messages[reason] || "Action not available"; -} diff --git a/companion/utils/booking.ts b/companion/utils/booking.ts deleted file mode 100644 index 0b8d8fc35f..0000000000 --- a/companion/utils/booking.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Booking Utilities - * - * Shared utility functions for working with bookings. - */ - -import type { Booking } from "@/services/calcom"; - -/** - * Extract the meeting URL from a booking. - * - * Checks both the booking's responses.videoCallUrl and location fields - * for a valid HTTP(S) URL that can be used to join the meeting. - * - * @param booking - The booking to extract the meeting URL from - * @returns The meeting URL if found, null otherwise - * - * @example - * ```tsx - * const meetingUrl = getMeetingUrl(booking); - * if (meetingUrl) { - * openInDefaultBrowser(meetingUrl, "meeting link"); - * } - * ``` - */ -export const getMeetingUrl = (booking: Booking | null): string | null => { - if (!booking) return null; - - // Check metadata for videoCallUrl first - const videoCallUrl = booking.responses?.videoCallUrl; - if (typeof videoCallUrl === "string" && videoCallUrl.startsWith("http")) { - return videoCallUrl; - } - - // Check location - const location = booking.location; - if (typeof location === "string" && location.startsWith("http")) { - return location; - } - - return null; -}; diff --git a/companion/utils/bookings-utils.ts b/companion/utils/bookings-utils.ts deleted file mode 100644 index 46544466c9..0000000000 --- a/companion/utils/bookings-utils.ts +++ /dev/null @@ -1,499 +0,0 @@ -import type { BookingFilter } from "@/hooks"; -import type { Booking } from "@/services/calcom"; - -import { safeLogError, safeLogWarn } from "./safeLogger"; - -export const getEmptyStateContent = (activeFilter: BookingFilter) => { - switch (activeFilter) { - case "upcoming": - return { - icon: "calendar-outline" as const, - title: "No upcoming bookings", - text: "As soon as someone books a time with you it will show up here.", - }; - case "unconfirmed": - return { - icon: "calendar-outline" as const, - title: "No unconfirmed bookings", - text: "Your unconfirmed bookings will show up here.", - }; - case "past": - return { - icon: "calendar-outline" as const, - title: "No past bookings", - text: "Your past bookings will show up here.", - }; - case "cancelled": - return { - icon: "calendar-outline" as const, - title: "No cancelled bookings", - text: "Your canceled bookings will show up here.", - }; - case "recurring": - return { - icon: "repeat-outline" as const, - title: "No recurring bookings", - text: "Your recurring bookings will show up here.", - }; - default: - return { - icon: "calendar-outline" as const, - title: "No bookings found", - text: "Your bookings will appear here.", - }; - } -}; - -/** - * Format a date string to a time string in 12-hour format - * @param dateString ISO date string - * @returns Formatted time string (e.g., "2:30 PM") - */ -export const formatTime = (dateString: string): string => { - if (!dateString) { - return ""; - } - try { - const date = new Date(dateString); - if (Number.isNaN(date.getTime())) { - safeLogWarn("Invalid date string provided to formatTime", { dateString }); - return ""; - } - return date.toLocaleTimeString("en-US", { - hour: "numeric", - minute: "2-digit", - hour12: true, - }); - } catch (error) { - safeLogError("Error formatting time:", error); - return ""; - } -}; - -/** - * Format a date string to a human-readable date - * @param dateString ISO date string - * @param isUpcoming Whether the booking is upcoming (affects format) - * @returns Formatted date string - */ -export const formatDate = (dateString: string, isUpcoming: boolean): string => { - if (!dateString) { - return ""; - } - try { - const date = new Date(dateString); - if (Number.isNaN(date.getTime())) { - safeLogWarn("Invalid date string provided to formatDate", { dateString }); - return ""; - } - const bookingYear = date.getFullYear(); - const currentYear = new Date().getFullYear(); - const isDifferentYear = bookingYear !== currentYear; - - if (isUpcoming) { - // Format: "ddd, D MMM" or "ddd, D MMM YYYY" if different year - const weekday = date.toLocaleDateString("en-US", { weekday: "short" }); - const day = date.getDate(); - const month = date.toLocaleDateString("en-US", { month: "short" }); - if (isDifferentYear) { - return `${weekday}, ${day} ${month} ${bookingYear}`; - } - return `${weekday}, ${day} ${month}`; - } else { - // Format: "D MMMM YYYY" for past bookings - return date.toLocaleDateString("en-US", { - day: "numeric", - month: "long", - year: "numeric", - }); - } - } catch (error) { - safeLogError("Error formatting date:", error); - return ""; - } -}; - -/** - * Format a date string to month and year - * @param dateString ISO date string - * @returns Formatted month and year string (e.g., "December 2024" or "This month") - */ -export const formatMonthYear = (dateString: string): string => { - if (!dateString) return ""; - try { - const date = new Date(dateString); - if (Number.isNaN(date.getTime())) { - return ""; - } - const now = new Date(); - const isCurrentMonth = - date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth(); - - if (isCurrentMonth) { - return "This month"; - } - - return date.toLocaleDateString("en-US", { - month: "long", - year: "numeric", - }); - } catch (_error) { - return ""; - } -}; - -/** - * Get a unique key for a month and year from a date string - * @param dateString ISO date string - * @returns Key in format "YYYY-M" (e.g., "2024-11") - */ -export const getMonthYearKey = (dateString: string): string => { - if (!dateString) return ""; - try { - const date = new Date(dateString); - if (Number.isNaN(date.getTime())) { - return ""; - } - return `${date.getFullYear()}-${date.getMonth()}`; - } catch (_error) { - return ""; - } -}; - -/** - * Format recurrence pattern into human-readable text - * @param bookings Array of bookings in the recurring series - * @returns Formatted recurrence text (e.g., "Every week for 5 occurrences") - */ -export const formatRecurrencePattern = (bookings: Booking[]): string | null => { - if (bookings.length === 0) return null; - - // Try to determine frequency from booking dates - if (bookings.length < 2) { - return `${bookings.length} occurrence`; - } - - // Sort bookings by date - const sortedBookings = [...bookings].sort((a, b) => { - const aTime = new Date(a.start || a.startTime || "").getTime(); - const bTime = new Date(b.start || b.startTime || "").getTime(); - return aTime - bTime; - }); - - // Calculate interval between first two bookings - const first = new Date(sortedBookings[0].start || sortedBookings[0].startTime || ""); - const second = new Date(sortedBookings[1].start || sortedBookings[1].startTime || ""); - const diffMs = second.getTime() - first.getTime(); - const diffDays = Math.round(diffMs / (1000 * 60 * 60 * 24)); - - let frequency = ""; - if (diffDays === 1) { - frequency = "day"; - } else if (diffDays === 7) { - frequency = "week"; - } else if (diffDays >= 28 && diffDays <= 31) { - frequency = "month"; - } else if (diffDays >= 365 && diffDays <= 366) { - frequency = "year"; - } else { - // Unknown frequency, just show count - return `${bookings.length} occurrences`; - } - - return `Every ${frequency} for ${bookings.length} occurrences`; -}; - -/** - * Represents a group of recurring bookings with the same recurringBookingUid - */ -export interface RecurringBookingGroup { - recurringBookingUid: string; - bookings: Booking[]; - firstUpcoming: Booking; - remainingCount: number; - hasUnconfirmed: boolean; - recurrenceText: string | null; -} - -/** - * Type definition for list items (used in FlatList) - */ -export type ListItem = - | { type: "monthHeader"; monthYear: string; key: string } - | { type: "booking"; booking: Booking; key: string } - | { type: "recurringGroup"; group: RecurringBookingGroup; key: string }; - -/** - * Group bookings by month for display in a sectioned list - * @param bookings Array of bookings to group - * @returns Array of list items with month headers and bookings - */ -export const groupBookingsByMonth = (bookings: Booking[]): ListItem[] => { - const grouped: ListItem[] = []; - let currentMonthYear: string | null = null; - - bookings.forEach((booking) => { - const startTime = booking.start || booking.startTime || ""; - if (!startTime) return; - - const monthYearKey = getMonthYearKey(startTime); - const monthYear = formatMonthYear(startTime); - - if (monthYearKey !== currentMonthYear) { - currentMonthYear = monthYearKey; - grouped.push({ - type: "monthHeader", - monthYear, - key: `month-${monthYearKey}`, - }); - } - - grouped.push({ - type: "booking", - booking, - key: booking.id.toString(), - }); - }); - - return grouped; -}; - -/** - * Group recurring bookings by their recurringBookingUid - * @param bookings Array of recurring bookings to group - * @returns Array of RecurringBookingGroup objects - */ -export const groupRecurringBookings = (bookings: Booking[]): RecurringBookingGroup[] => { - const now = new Date(); - const groupMap = new Map(); - - // Group bookings by recurringBookingUid - bookings.forEach((booking) => { - const uid = booking.recurringBookingUid; - if (!uid) return; - - if (!groupMap.has(uid)) { - groupMap.set(uid, []); - } - groupMap.get(uid)?.push(booking); - }); - - // Convert map to RecurringBookingGroup array - const groups: RecurringBookingGroup[] = []; - - groupMap.forEach((groupBookings, recurringBookingUid) => { - // Sort bookings by start time (ascending) - const sortedBookings = [...groupBookings].sort((a, b) => { - const aStart = new Date(a.start || a.startTime || "").getTime(); - const bStart = new Date(b.start || b.startTime || "").getTime(); - return aStart - bStart; - }); - - // Find first upcoming (non-cancelled) booking - const upcomingBookings = sortedBookings.filter((b) => { - const startTime = new Date(b.start || b.startTime || ""); - const isCancelled = b.status?.toLowerCase() === "cancelled"; - const isRejected = b.status?.toLowerCase() === "rejected"; - return startTime >= now && !isCancelled && !isRejected; - }); - - const firstUpcoming = upcomingBookings[0] || sortedBookings[0]; - - // Count remaining (non-cancelled, non-rejected) bookings - reuse upcomingBookings - const remainingCount = upcomingBookings.length; - - // Check if any booking requires confirmation - const hasUnconfirmed = sortedBookings.some( - (b) => - b.status?.toLowerCase() === "pending" || - b.status?.toLowerCase() === "requires_confirmation" || - b.requiresConfirmation - ); - - groups.push({ - recurringBookingUid, - bookings: sortedBookings, - firstUpcoming, - remainingCount, - hasUnconfirmed, - recurrenceText: formatRecurrencePattern(sortedBookings), - }); - }); - - // Sort groups by first upcoming booking date - return groups.sort((a, b) => { - const aStart = new Date(a.firstUpcoming.start || a.firstUpcoming.startTime || "").getTime(); - const bStart = new Date(b.firstUpcoming.start || b.firstUpcoming.startTime || "").getTime(); - return aStart - bStart; - }); -}; - -/** - * Search/filter bookings by query string - * @param bookings Array of bookings to filter - * @param searchQuery Search query string - * @returns Filtered bookings - */ -export const searchBookings = (bookings: Booking[], searchQuery: string): Booking[] => { - if (searchQuery.trim() === "") { - return bookings; - } - - const searchLower = searchQuery.toLowerCase(); - return bookings.filter( - (booking) => - // Search in booking title - booking.title?.toLowerCase().includes(searchLower) || - // Search in booking description - booking.description - ?.toLowerCase() - .includes(searchLower) || - // Search in event type title - booking.eventType?.title - ?.toLowerCase() - .includes(searchLower) || - // Search in attendee names - booking.attendees?.some((attendee) => attendee.name?.toLowerCase().includes(searchLower)) || - // Search in attendee emails - booking.attendees?.some((attendee) => attendee.email?.toLowerCase().includes(searchLower)) || - // Search in location - booking.location - ?.toLowerCase() - .includes(searchLower) || - // Search in user name - booking.user?.name - ?.toLowerCase() - .includes(searchLower) || - // Search in user email - booking.user?.email - ?.toLowerCase() - .includes(searchLower) - ); -}; - -/** - * Filter bookings by event type ID - * @param bookings Array of bookings to filter - * @param eventTypeId Event type ID to filter by (null means no filter) - * @returns Filtered bookings - */ -export const filterByEventType = (bookings: Booking[], eventTypeId: number | null): Booking[] => { - if (eventTypeId === null) { - return bookings; - } - return bookings.filter((booking) => booking.eventTypeId === eventTypeId); -}; - -/** - * Format a date string to a complete date/time string - * @param dateString ISO date string - * @returns Formatted date/time string (e.g., "Mon, Dec 25, 2:30 PM") - */ -export const formatDateTime = (dateString: string): string => { - const date = new Date(dateString); - return date.toLocaleDateString("en-US", { - weekday: "short", - month: "short", - day: "numeric", - hour: "numeric", - minute: "2-digit", - }); -}; - -/** - * Get color for booking status - * @param status Booking status string - * @returns Hex color code - */ -export const getStatusColor = (status: string): string => { - // API v2 2024-08-13 returns status in lowercase, so normalize to uppercase for comparison - const normalizedStatus = status.toLowerCase(); - switch (normalizedStatus) { - case "accepted": - return "#34C759"; - case "pending": - return "#FF9500"; - case "cancelled": - return "#FF3B30"; - case "rejected": - return "#FF3B30"; - default: - return "#666"; - } -}; - -/** - * Format status text for display - * @param status Booking status string - * @returns Capitalized status text - */ -export const formatStatusText = (status: string): string => { - // Capitalize first letter for display - return status.charAt(0).toUpperCase() + status.slice(1).toLowerCase(); -}; - -/** - * Get formatted display string for host and attendees - * @param booking Booking object - * @param currentUserEmail Current user's email (optional) - * @returns Formatted string or null if no host/attendees - */ -export const getHostAndAttendeesDisplay = ( - booking: Booking, - currentUserEmail?: string -): string | null => { - const hasHostOrAttendees = - (booking.hosts && booking.hosts.length > 0) || - booking.user || - (booking.attendees && booking.attendees.length > 0); - - if (!hasHostOrAttendees) return null; - - const hostEmail = booking.hosts?.[0]?.email?.toLowerCase() || booking.user?.email?.toLowerCase(); - const isCurrentUserHost = - currentUserEmail && hostEmail && currentUserEmail.toLowerCase() === hostEmail; - - const hostName = isCurrentUserHost - ? "You" - : booking.hosts?.[0]?.name || - booking.hosts?.[0]?.email || - booking.user?.name || - booking.user?.email; - - const attendeesDisplay = - booking.attendees && booking.attendees.length > 0 - ? booking.attendees.length === 1 - ? booking.attendees[0].name || booking.attendees[0].email - : booking.attendees - .slice(0, 2) - .map((att) => att.name || att.email) - .join(", ") + - (booking.attendees.length > 2 ? ` and ${booking.attendees.length - 2} more` : "") - : null; - - if (hostName && attendeesDisplay) { - return `${hostName} and ${attendeesDisplay}`; - } else if (hostName) { - return hostName; - } else if (attendeesDisplay) { - return attendeesDisplay; - } - return null; -}; - -/** - * Get shareable booking details message - * @param booking Booking object - * @returns Formatted booking details string - */ -export const getBookingDetailsMessage = (booking: Booking): string => { - const attendeesList = booking.attendees?.map((att) => att.name).join(", ") || "No attendees"; - const startTime = booking.start || booking.startTime || ""; - const endTime = booking.end || booking.endTime || ""; - - return `${booking.description ? `${booking.description}\n\n` : ""}Time: ${formatDateTime( - startTime - )} - ${formatTime(endTime)}\nAttendees: ${attendeesList}\nStatus: ${formatStatusText(booking.status)}${ - booking.location ? `\nLocation: ${booking.location}` : "" - }`; -}; diff --git a/companion/utils/browser.ts b/companion/utils/browser.ts deleted file mode 100644 index 16d04147b6..0000000000 --- a/companion/utils/browser.ts +++ /dev/null @@ -1,153 +0,0 @@ -/** - * Browser Utilities - * - * Centralized utility for opening links in the in-app browser. - * Configured for session sharing with Safari/Chrome to maintain login state. - */ - -import * as WebBrowser from "expo-web-browser"; -import { Linking, Platform } from "react-native"; - -import { showErrorAlert } from "./alerts"; - -/** - * Handle errors from browser functions in a consistent way. - * - * @param error - The error that occurred - * @param functionName - Name of the function for debug logging - * @param fallbackMessage - Optional message to show in error alert (defaults to "link") - */ -const handleBrowserError = ( - error: unknown, - functionName: string, - fallbackMessage?: string -): void => { - console.error(`Failed to open link in ${functionName}`); - if (__DEV__) { - const message = error instanceof Error ? error.message : String(error); - const stack = error instanceof Error ? error.stack : undefined; - console.debug(`[${functionName}] failed`, { message, stack, fallbackMessage }); - } - showErrorAlert("Error", `Failed to open ${fallbackMessage || "link"}. Please try again.`); -}; - -/** - * Configuration options for in-app browser - */ -export interface BrowserOptions { - /** iOS: Toolbar color (hex string) */ - toolbarColor?: string; - /** iOS: Controls color (hex string) */ - controlsColor?: string; -} - -/** - * Appends ?standalone=true to app.cal.com URLs on iOS. - * This hides navigation elements when pages are opened in the in-app browser, - * which is required for Apple App Store compliance. - * - * @param url - The URL to process - * @returns The URL with standalone=true appended if it's an app.cal.com URL on iOS - */ -const appendStandaloneParam = (url: string): string => { - // Only apply to iOS - if (Platform.OS !== "ios") { - return url; - } - - try { - const urlObj = new URL(url); - - // Only apply to app.cal.com URLs - if (urlObj.hostname !== "app.cal.com") { - return url; - } - - // Don't add if already present - if (urlObj.searchParams.has("standalone")) { - return url; - } - - urlObj.searchParams.set("standalone", "true"); - return urlObj.toString(); - } catch { - // If URL parsing fails, return original - return url; - } -}; - -/** - * Open a URL in the in-app browser with session sharing enabled. - * - * Session sharing allows cookies to be shared between the in-app browser - * and Safari (iOS) or Chrome (Android). This means users who authenticate - * via OAuth will remain logged in when opening Cal.com links. - * - * On iOS, app.cal.com URLs automatically get ?standalone=true appended - * to hide navigation elements for Apple App Store compliance. - * - * @param url - The URL to open - * @param fallbackMessage - Optional message to show in error alert (defaults to "link") - * @param options - Optional browser customization options - * - * @example - * ```tsx - * // Open a link with session sharing - * await openInAppBrowser("https://app.cal.com"); - * - * // With custom error message - * await openInAppBrowser("https://app.cal.com/settings", "Settings page"); - * - * // With custom toolbar color - * await openInAppBrowser("https://app.cal.com", "Cal.com", { toolbarColor: "#111827" }); - * ``` - */ -export const openInAppBrowser = async ( - url: string, - fallbackMessage?: string, - options?: BrowserOptions -): Promise => { - try { - // Append standalone=true for app.cal.com URLs on iOS - const processedUrl = appendStandaloneParam(url); - - // Configure browser options - // Session sharing happens automatically when using Safari View Controller (iOS) - // or Chrome Custom Tabs (Android) - no special configuration needed - const browserOptions: WebBrowser.WebBrowserOpenOptions = { - ...(options?.toolbarColor && { toolbarColor: options.toolbarColor }), - ...(options?.controlsColor && { controlsColor: options.controlsColor }), - }; - - await WebBrowser.openBrowserAsync(processedUrl, browserOptions); - } catch (error) { - handleBrowserError(error, "openInAppBrowser", fallbackMessage); - } -}; - -/** - * Open a URL in the device's default browser (Safari on iOS, Chrome on Android). - * - * Unlike openInAppBrowser, this opens the URL in the actual browser app - * rather than an in-app browser view. This is useful for video meeting links - * where users may want to use their browser's native features or extensions. - * - * @param url - The URL to open - * @param fallbackMessage - Optional message to show in error alert (defaults to "link") - * - * @example - * ```tsx - * // Open a meeting link in the default browser - * await openInDefaultBrowser("https://meet.cal.com/abc123", "meeting link"); - * ``` - */ -export const openInDefaultBrowser = async ( - url: string, - fallbackMessage?: string -): Promise => { - try { - await Linking.openURL(url); - } catch (error) { - handleBrowserError(error, "openInDefaultBrowser", fallbackMessage); - } -}; diff --git a/companion/utils/deep-links.ts b/companion/utils/deep-links.ts deleted file mode 100644 index 39b1879360..0000000000 --- a/companion/utils/deep-links.ts +++ /dev/null @@ -1,165 +0,0 @@ -/** - * Deep Links Utility - * - * This module provides utilities for opening Cal.com web pages from the mobile app. - * Used for features that require server-side behavior that cannot be safely reproduced - * client-side (e.g., request reschedule). - */ -import * as Linking from "expo-linking"; -import * as WebBrowser from "expo-web-browser"; -import { Alert, Platform } from "react-native"; - -import { showErrorAlert } from "@/utils/alerts"; - -// Default Cal.com web URL - can be overridden for self-hosted instances -const DEFAULT_CAL_WEB_URL = "https://app.cal.com"; - -/** - * Get the Cal.com web URL from environment or use default - */ -function getCalWebUrl(): string { - // In a real implementation, this would read from environment config - // For now, use the default Cal.com URL - return DEFAULT_CAL_WEB_URL; -} - -/** - * Appends ?standalone=true to Cal.com URLs on iOS. - * This hides navigation elements when pages are opened in the in-app browser, - * which is required for Apple App Store compliance. - * - * @param url - The URL to process - * @returns The URL with standalone=true appended if it's a Cal.com URL on iOS - */ -function appendStandaloneParam(url: string): string { - // Only apply to iOS - if (Platform.OS !== "ios") { - return url; - } - - try { - const urlObj = new URL(url); - - // Only apply to app.cal.com URLs - if (urlObj.hostname !== "app.cal.com") { - return url; - } - - // Don't add if already present - if (urlObj.searchParams.has("standalone")) { - return url; - } - - urlObj.searchParams.set("standalone", "true"); - return urlObj.toString(); - } catch { - // If URL parsing fails, return original - return url; - } -} - -/** - * Open a booking detail page in the web browser. - * This is used for actions that require the full web experience. - * - * @param bookingUid - The unique identifier of the booking - */ -export async function openBookingInWeb(bookingUid: string): Promise { - const webUrl = getCalWebUrl(); - const url = appendStandaloneParam(`${webUrl}/booking/${bookingUid}`); - - try { - await WebBrowser.openBrowserAsync(url, { - presentationStyle: WebBrowser.WebBrowserPresentationStyle.FULL_SCREEN, - }); - } catch { - console.error("Failed to open booking in web browser"); - // Fallback to system browser - try { - await Linking.openURL(url); - } catch { - showErrorAlert("Error", "Failed to open booking in browser. Please try again."); - } - } -} - -/** - * Open the request reschedule flow in the web browser. - * Since request reschedule is a server-driven operation with complex side effects - * (emails, webhooks, calendar updates), we open the web page where the user can - * trigger it safely. - * - * Note: The web flow uses a dialog (RescheduleDialog.tsx) that calls - * trpc.viewer.bookings.requestReschedule.useMutation. There's no direct URL route - * for request reschedule, so we open the booking detail page. - * - * @param bookingUid - The unique identifier of the booking - */ -export async function openRequestRescheduleInWeb(bookingUid: string): Promise { - // Show an informational alert before opening the web page - Alert.alert( - "Request Reschedule", - "You will be redirected to the web app to request a reschedule. This ensures all notifications and calendar updates are handled correctly.", - [ - { text: "Cancel", style: "cancel" }, - { - text: "Open in Browser", - onPress: () => openBookingInWeb(bookingUid), - }, - ] - ); -} - -/** - * Open the reschedule page for an attendee to pick a new time. - * This is the public reschedule page that attendees use. - * - * @param bookingUid - The unique identifier of the booking - * @param rescheduleUid - The reschedule token/uid (optional) - */ -export async function openReschedulePage( - bookingUid: string, - rescheduleUid?: string -): Promise { - const webUrl = getCalWebUrl(); - const url = appendStandaloneParam( - rescheduleUid ? `${webUrl}/reschedule/${rescheduleUid}` : `${webUrl}/booking/${bookingUid}` - ); - - try { - await WebBrowser.openBrowserAsync(url, { - presentationStyle: WebBrowser.WebBrowserPresentationStyle.FULL_SCREEN, - }); - } catch { - console.error("Failed to open reschedule page in web browser"); - try { - await Linking.openURL(url); - } catch { - showErrorAlert("Error", "Failed to open reschedule page. Please try again."); - } - } -} - -/** - * Open the cancel booking page in the web browser. - * This is a fallback for cases where the API cancel doesn't work. - * - * @param bookingUid - The unique identifier of the booking - */ -export async function openCancelBookingInWeb(bookingUid: string): Promise { - const webUrl = getCalWebUrl(); - const url = appendStandaloneParam(`${webUrl}/booking/${bookingUid}`); - - try { - await WebBrowser.openBrowserAsync(url, { - presentationStyle: WebBrowser.WebBrowserPresentationStyle.FULL_SCREEN, - }); - } catch { - console.error("Failed to open cancel booking page in web browser"); - try { - await Linking.openURL(url); - } catch { - showErrorAlert("Error", "Failed to open booking page. Please try again."); - } - } -} diff --git a/companion/utils/defaultLocations.ts b/companion/utils/defaultLocations.ts deleted file mode 100644 index ef2969b451..0000000000 --- a/companion/utils/defaultLocations.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Default location types that are available alongside conferencing apps - * These match the pattern from packages/app-store/locations.ts - */ - -export enum DefaultLocationType { - AttendeeInPerson = "attendeeInPerson", - InPerson = "inPerson", - Phone = "phone", - UserPhone = "userPhone", - Link = "link", - Conferencing = "conferencing", - SomewhereElse = "somewhereElse", -} - -export type DefaultLocation = { - type: DefaultLocationType; - label: string; - iconUrl: string; - category: "in person" | "conferencing" | "other" | "phone"; - organizerInputType?: "text" | "phone" | null; - organizerInputPlaceholder?: string; - organizerInputLabel?: string; - messageForOrganizer?: string; -}; - -/** - * Default locations available in Cal.com - * Icons are served from https://app.cal.com/{iconPath} - */ -export const defaultLocations: DefaultLocation[] = [ - { - type: DefaultLocationType.AttendeeInPerson, - label: "In Person (Attendee Address)", - iconUrl: "https://app.cal.com/map-pin-dark.svg", - category: "in person", - }, - { - type: DefaultLocationType.SomewhereElse, - label: "Custom Attendee Location", - iconUrl: "https://app.cal.com/message-pin.svg", - category: "other", - }, - { - type: DefaultLocationType.InPerson, - label: "In Person (Organizer Address)", - iconUrl: "https://app.cal.com/map-pin-dark.svg", - category: "in person", - organizerInputType: "text", - organizerInputPlaceholder: "Enter address or place", - organizerInputLabel: "Address", - messageForOrganizer: "Provide an Address or Place", - }, - { - type: DefaultLocationType.Link, - label: "Link Meeting", - iconUrl: "https://app.cal.com/link.svg", - category: "other", - organizerInputType: "text", - organizerInputPlaceholder: "https://meet.example.com/join/123456", - organizerInputLabel: "Meeting Link", - messageForOrganizer: "Provide a Meeting Link", - }, - { - type: DefaultLocationType.Phone, - label: "Attendee Phone Number", - iconUrl: "https://app.cal.com/phone.svg", - category: "phone", - organizerInputType: null, - messageForOrganizer: "Cal will ask your invitee to enter a phone number before scheduling.", - }, - { - type: DefaultLocationType.UserPhone, - label: "Phone Call", - iconUrl: "https://app.cal.com/phone.svg", - category: "phone", - organizerInputType: "phone", - organizerInputPlaceholder: "Enter phone number", - organizerInputLabel: "Phone Number", - messageForOrganizer: "Provide your phone number with country code", - }, -]; - -/** - * Get icon URL for a default location type - */ -export function getDefaultLocationIconUrl(locationType: string): string | null { - const location = defaultLocations.find((loc) => loc.type === locationType); - return location ? location.iconUrl : null; -} - -/** - * Check if a location type is a default location (not a conferencing app) - */ -export function isDefaultLocation(locationType: string): boolean { - return defaultLocations.some((loc) => loc.type === locationType); -} diff --git a/companion/utils/error.ts b/companion/utils/error.ts deleted file mode 100644 index 5e7ef6e907..0000000000 --- a/companion/utils/error.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Determines if the error should be displayed to the user. - * Filters out authentication errors (which are handled by auth redirect) - * and only shows errors in development mode. - * - * @param queryError - The error from a query, if any - * @param context - A description of what was being loaded (e.g., "event types", "bookings") - * @returns A user-friendly error message or null if the error should not be displayed - * - * @example - * ```tsx - * const { error: queryError } = useEventTypes(); - * const error = getDisplayError(queryError, "event types"); - * - * if (error) { - * return ; - * } - * ``` - */ -export function getDisplayError( - queryError: Error | null | undefined, - context: string -): string | null { - if (!queryError) return null; - - const isAuthError = - queryError.message?.includes("Authentication") || - queryError.message?.includes("sign in") || - queryError.message?.includes("401"); - - if (isAuthError) return null; - - return __DEV__ ? `Failed to load ${context}.` : null; -} diff --git a/companion/utils/eventTypeParsers.ts b/companion/utils/eventTypeParsers.ts deleted file mode 100644 index ff7cf86d03..0000000000 --- a/companion/utils/eventTypeParsers.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Helper functions to parse form values into API-compatible formats - * Used for event type form handling - */ - -/** - * Parse buffer time string to minutes - * @param buffer - Buffer time string (e.g., "15 minutes", "30") - * @returns Number of minutes - */ -export const parseBufferTime = (buffer: string): number => { - const match = buffer.match(/(\d+)/); - return match ? parseInt(match[1], 10) : 0; -}; - -/** - * Parse minimum notice value and unit to total minutes - * @param value - Numeric value as string - * @param unit - Unit ("Minutes", "Hours", "Days") - * @returns Total minutes - */ -export const parseMinimumNotice = (value: string, unit: string): number => { - const val = parseInt(value, 10) || 0; - if (unit === "Hours") return val * 60; - if (unit === "Days") return val * 24 * 60; - return val; // Minutes -}; - -/** - * Parse frequency unit string to API format - * @param unit - Unit string (e.g., "Weeks", "Monthly") - * @returns Normalized unit ("day", "week", "month", "year") or null - */ -export const parseFrequencyUnit = (unit: string): string | null => { - const normalized = unit.toLowerCase(); - if (normalized.includes("day")) return "day"; - if (normalized.includes("week")) return "week"; - if (normalized.includes("month")) return "month"; - if (normalized.includes("year")) return "year"; - return null; -}; - -/** - * Parse slot interval string to minutes - * @param interval - Interval string (e.g., "15 minutes", "30") - * @returns Number of minutes - */ -export const parseSlotInterval = (interval: string): number => { - const match = interval.match(/(\d+)/); - return match ? parseInt(match[1], 10) : 0; -}; diff --git a/companion/utils/formatters.ts b/companion/utils/formatters.ts deleted file mode 100644 index 29c97bfb0b..0000000000 --- a/companion/utils/formatters.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Generic formatting utilities - * These are reusable formatting functions used across the app - */ - -/** - * Format duration in minutes to a human-readable string - * @param minutes - Duration in minutes (number or string) - * @returns Formatted string like "30m", "1h", "1h 30m" - * - * @example - * formatDuration(30) // "30m" - * formatDuration(60) // "1h" - * formatDuration(90) // "1h 30m" - */ -export const formatDuration = (minutes: number | string | undefined): string => { - const mins = typeof minutes === "string" ? parseInt(minutes, 10) || 0 : minutes || 0; - if (mins <= 0) return "0m"; - if (mins < 60) return `${mins}m`; - const hours = Math.floor(mins / 60); - const remainingMins = mins % 60; - return remainingMins > 0 ? `${hours}h ${remainingMins}m` : `${hours}h`; -}; - -/** - * Truncate text to a maximum length with ellipsis - * @param text - The text to truncate - * @param maxLength - Maximum length (default: 20) - * @returns Truncated text with "..." if it exceeds maxLength - * - * @example - * truncateTitle("Very long title here", 10) // "Very long..." - */ -export const truncateTitle = (text: string, maxLength: number = 20): string => { - return text.length > maxLength ? `${text.substring(0, maxLength)}...` : text; -}; - -/** - * Format an app ID to a display name - * Converts kebab-case to Title Case - * - * @param appId - The app identifier (e.g., "google-meet", "cal-video") - * @returns Formatted display name (e.g., "Google Meet", "Cal Video") - * - * @example - * formatAppIdToDisplayName("google-meet") // "Google Meet" - * formatAppIdToDisplayName("cal-video") // "Cal Video" - */ -export const formatAppIdToDisplayName = (appId: string): string => { - return appId - .split("-") - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(" "); -}; diff --git a/companion/utils/getAppIconUrl.ts b/companion/utils/getAppIconUrl.ts deleted file mode 100644 index 21d316c794..0000000000 --- a/companion/utils/getAppIconUrl.ts +++ /dev/null @@ -1,123 +0,0 @@ -/** - * Maps conferencing app types or appIds to their icon URLs. - * Icons are publicly accessible at: https://app.cal.com/app-store/{dirName}/icon.svg - */ - -const APP_TYPE_TO_DIR_NAME: Record = { - // Video conferencing apps - zoom_video: "zoomvideo", - google_video: "googlevideo", - office365_video: "office365video", - teams_video: "office365video", - daily_video: "dailyvideo", - jitsi_video: "jitsivideo", - huddle01_video: "huddle01video", - tandem_video: "tandemvideo", - whereby_video: "whereby", - webex_video: "webex", - sylapsvideo_video: "sylapsvideo", - shimmervideo_video: "shimmervideo", - riverside_video: "riverside", - roam_video: "roam", - ping_video: "ping", - mirotalk_video: "mirotalk", - nextcloudtalk_conferencing: "nextcloudtalk", - jelly_conferencing: "jelly", - "horizon-workrooms_video": "horizon-workrooms", - "element-call_conferencing": "element-call", - eightxeight_video: "eightxeight", - discord_video: "discord", - dialpad_video: "dialpad", - demodesk_video: "demodesk", - campfire_video: "campfire", - facetime_video: "facetime", - whatsapp_video: "whatsapp", - telegram_video: "telegram", - skype_video: "skype", - signal_video: "signal", - sirius_video: "sirius_video", - sirius_video_video: "sirius_video", // Handle double _video suffix - salesroom_video: "salesroom", -}; - -const APP_ID_TO_DIR_NAME: Record = { - zoom: "zoomvideo", - "google-meet": "googlevideo", - msteams: "office365video", - office365: "office365video", - teams: "office365video", - "cal-video": "dailyvideo", - "daily-video": "dailyvideo", - daily: "dailyvideo", - jitsi: "jitsivideo", - huddle01: "huddle01video", - tandem: "tandemvideo", - whereby: "whereby", - webex: "webex", - sylapsvideo: "sylapsvideo", - shimmervideo: "shimmervideo", - riverside: "riverside", - roam: "roam", - ping: "ping", - mirotalk: "mirotalk", - nextcloudtalk: "nextcloudtalk", - jelly: "jelly", - "horizon-workrooms": "horizon-workrooms", - "element-call": "element-call", - eightxeight: "eightxeight", - discord: "discord", - dialpad: "dialpad", - demodesk: "demodesk", - campfire: "campfire", - facetime: "facetime", - whatsapp: "whatsapp", - telegram: "telegram", - skype: "skype", - signal: "signal", - sirius_video: "sirius_video", - salesroom: "salesroom", -}; - -/** - * Generates the icon URL for a conferencing app based on its type or appId. - * Uses publicly available icons from https://app.cal.com/app-store/{dirName}/icon.svg - * Special cases are handled (e.g., Google Meet uses logo.webp) - * - * @param type - The credential type (e.g., "zoom_video", "google_video") - * @param appId - The app identifier (e.g., "zoom", "google-meet") - * @returns The full icon URL or null if the app is not found - * - * @example - * getAppIconUrl("zoom_video", "zoom") // "https://app.cal.com/app-store/zoomvideo/icon.svg" - * getAppIconUrl("google_video", "google-meet") // "https://app.cal.com/app-store/googlevideo/logo.webp" - */ -export function getAppIconUrl(type: string, appId: string | null): string | null { - let dirName: string | null = null; - let iconFileName = "icon.svg"; - - // Try to find by appId first (more specific) - if (appId && APP_ID_TO_DIR_NAME[appId]) { - dirName = APP_ID_TO_DIR_NAME[appId]; - } else if (APP_TYPE_TO_DIR_NAME[type]) { - // Fallback to type - dirName = APP_TYPE_TO_DIR_NAME[type]; - } - - if (!dirName) { - return null; - } - - // Special cases for icon file names - if (appId === "google-meet" || type === "google_video") { - // Google Meet uses logo.webp instead of icon.svg - iconFileName = "logo.webp"; - } else if (appId === "riverside" || type === "riverside_video") { - // Riverside uses icon-dark.svg - iconFileName = "icon-dark.svg"; - } else if (appId === "sirius_video" || type === "sirius_video" || type === "sirius_video_video") { - // Sirius Video uses icon-dark.svg - iconFileName = "icon-dark.svg"; - } - - return `https://app.cal.com/app-store/${dirName}/${iconFileName}`; -} diff --git a/companion/utils/getAvatarUrl.ts b/companion/utils/getAvatarUrl.ts deleted file mode 100644 index d3fb78aaf1..0000000000 --- a/companion/utils/getAvatarUrl.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { z } from "zod"; - -const CAL_URL = "https://cal.com"; -const AVATAR_FALLBACK = "/avatar.png"; - -/** - * Base64 image data URL regex pattern - * Matches: data:image/[type];base64,[base64data] - */ -const BASE64_IMAGE_REGEX = - /^data:image\/[^;]+;base64,(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/; - -/** - * Normalizes avatar URL to a format that can be used by React Native Image component. - * Handles three formats: - * 1. Absolute HTTPS URLs - returns as-is - * 2. Base64 data URLs (data:image/...) - returns as-is - * 3. Relative URLs (e.g., /api/avatar/[uuid]) - prefixes with CAL_URL - * - * @param avatarUrl - The avatar URL from the API (can be absolute URL, base64, or relative path) - * @returns A normalized URL that can be used by React Native Image component, or fallback URL - * - * @example - * getAvatarUrl("https://example.com/avatar.jpg") // "https://example.com/avatar.jpg" - * getAvatarUrl("data:image/png;base64,iVBORw0KG...") // "data:image/png;base64,iVBORw0KG..." - * getAvatarUrl("/api/avatar/123") // "https://cal.com/api/avatar/123" - * getAvatarUrl(undefined) // "https://cal.com/avatar.png" - */ -export const getAvatarUrl = (avatarUrl: string | null | undefined): string => { - if (!avatarUrl) { - return CAL_URL + AVATAR_FALLBACK; - } - - // Check if it's a base64 data URL - if (BASE64_IMAGE_REGEX.test(avatarUrl)) { - return avatarUrl; - } - - // Check if it's already an absolute URL (HTTPS or HTTP) - const isAbsoluteUrl = z.string().url().safeParse(avatarUrl).success; - if (isAbsoluteUrl) { - return avatarUrl; - } - - // Treat as relative URL and prefix with CAL_URL - // Ensure the relative URL starts with / - const relativePath = avatarUrl.startsWith("/") ? avatarUrl : `/${avatarUrl}`; - return CAL_URL + relativePath; -}; diff --git a/companion/utils/getEventDuration.ts b/companion/utils/getEventDuration.ts deleted file mode 100644 index 0657af55f5..0000000000 --- a/companion/utils/getEventDuration.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { EventType } from "@/hooks"; - -export const getEventDuration = (eventType: EventType): number => { - // Prefer lengthInMinutes (API field), fallback to length for backwards compatibility - return eventType.lengthInMinutes ?? eventType.length ?? 0; -}; diff --git a/companion/utils/gmailGoogleChipParser.ts b/companion/utils/gmailGoogleChipParser.ts deleted file mode 100644 index c829cfdd7b..0000000000 --- a/companion/utils/gmailGoogleChipParser.ts +++ /dev/null @@ -1,160 +0,0 @@ -/** - * Parser for Google Calendar scheduling chips in Gmail - * Extracts time slots from the Gmail ad-hoc scheduling chip - */ - -export interface GoogleTimeSlot { - date: string; // "Wed, 26 November" - startTime: string; // "3:30 pm" - endTime: string; // "4:30 pm" - durationMinutes: number; // 60 - isoDate: string; // "2025-11-27" - isoTimestamp: string; // "2025-11-27T05:30:00.000Z" - googleUrl: string; // Original Google Calendar URL -} - -export interface ParsedGoogleChip { - scheduleId: string; - timezone: string; // "Asia/Kolkata" - timezoneOffset: string; // "GMT+05:30" - slots: GoogleTimeSlot[]; - detectedDuration: number; // 15, 30, 45, or 60 -} - -/** - * Parse Google Calendar scheduling chip from Gmail - */ -export function parseGoogleChip(chipElement: HTMLElement): ParsedGoogleChip | null { - try { - // 1. Get schedule ID from data attribute - const scheduleId = chipElement.getAttribute("data-ad-hoc-schedule-id"); - if (!scheduleId) { - console.warn("No schedule ID found in Google chip"); - return null; - } - - // 2. Parse timezone - first try to get IANA timezone from data attribute - let timezone = "UTC"; - let timezoneOffset = "GMT+00:00"; - - // Try to get IANA timezone from data-ad-hoc-v2-params - const paramsAttr = chipElement.getAttribute("data-ad-hoc-v2-params"); - if (paramsAttr) { - // The timezone is at the end of the params string, like: "Asia/Kolkata" - // Try multiple patterns as Gmail might format it differently - let tzMatch = paramsAttr.match(/"([^&]+)"\]/); - - // Also try without HTML entities - if (!tzMatch) { - tzMatch = paramsAttr.match(/"([^"]+)"\]/); - } - - // Also try with escaped quotes - if (!tzMatch) { - tzMatch = paramsAttr.match(/\\"([^\\]+)\\"\]/); - } - - if (tzMatch?.[1]) { - timezone = tzMatch[1]; // e.g., "Asia/Kolkata" - } - } - - // Also get the display timezone offset from the UI text - const timezoneText = chipElement.querySelector("td")?.textContent?.trim() || ""; - const timezoneMatch = timezoneText.match(/^(.+?)\s*-\s*(.+?)\s*\((.+?)\)$/); - - if (timezoneMatch) { - timezoneOffset = timezoneMatch[3].trim(); // e.g., "GMT+05:30" - } - - // 3. Find all time slot links - const slotLinks = Array.from(chipElement.querySelectorAll("a[href*='slotStartTime']")); - - if (slotLinks.length === 0) { - console.warn("No time slot links found in Google chip"); - return null; - } - - const slots: GoogleTimeSlot[] = []; - let detectedDuration = 60; // Default - - slotLinks.forEach((link) => { - const href = (link as HTMLAnchorElement).href; - const url = new URL(href); - - // Extract parameters - const slotStartTime = url.searchParams.get("slotStartTime"); - const slotDurationMinutes = url.searchParams.get("slotDurationMinutes"); - - if (!slotStartTime || !slotDurationMinutes) { - return; - } - - const durationMinutes = parseInt(slotDurationMinutes, 10); - detectedDuration = durationMinutes; // Use the duration from slots - - // Convert Unix timestamp (milliseconds) to Date - const startDate = new Date(parseInt(slotStartTime, 10)); - const endDate = new Date(startDate.getTime() + durationMinutes * 60 * 1000); - - // Format date and time using the chip's timezone for consistency - const dateOptions: Intl.DateTimeFormatOptions = { - weekday: "short", - day: "numeric", - month: "long", - timeZone: timezone, // Use chip's timezone - }; - const timeOptions: Intl.DateTimeFormatOptions = { - hour: "numeric", - minute: "2-digit", - hour12: true, - timeZone: timezone, // Use chip's timezone - }; - - const date = startDate.toLocaleDateString("en-US", dateOptions); - const startTime = startDate - .toLocaleTimeString("en-US", timeOptions) - .toLowerCase() - .replace(/\s/g, " "); - const endTime = endDate - .toLocaleTimeString("en-US", timeOptions) - .toLowerCase() - .replace(/\s/g, " "); - - // ISO format for Cal.com - const isoDate = startDate.toISOString().split("T")[0]; // "2025-11-27" - const isoTimestamp = startDate.toISOString(); // "2025-11-27T05:30:00.000Z" - - slots.push({ - date, - startTime, - endTime, - durationMinutes, - isoDate, - isoTimestamp, - googleUrl: href, - }); - }); - - if (slots.length === 0) { - console.warn("No valid slots parsed from Google chip"); - return null; - } - - return { - scheduleId, - timezone, - timezoneOffset, - slots, - detectedDuration, - }; - } catch (error) { - console.error("Error parsing Google chip"); - if (__DEV__) { - const message = error instanceof Error ? error.message : String(error); - const stack = error instanceof Error ? error.stack : undefined; - console.debug("[gmailGoogleChipParser] parse failed", { message, stack }); - } - return null; - } -} diff --git a/companion/utils/index.ts b/companion/utils/index.ts deleted file mode 100644 index 273ad3f488..0000000000 --- a/companion/utils/index.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Utils Index - * - * Central export point for all utility functions. - * Import utilities from this file for clean imports: - * - * @example - * ```tsx - * import { formatDuration, secureStorage, parseBufferTime } from '../utils'; - * ``` - */ - -// Alert utilities -export { setGlobalToastFunction, showErrorAlert, showInfoAlert, showSuccessAlert } from "./alerts"; -// Browser utilities -export { openInAppBrowser } from "./browser"; -export { openInDefaultBrowser } from "./browser"; -// Default location utilities -export { - type DefaultLocation, - DefaultLocationType, - defaultLocations, - getDefaultLocationIconUrl, - isDefaultLocation, -} from "./defaultLocations"; -// Event type parser utilities -export { - parseBufferTime, - parseFrequencyUnit, - parseMinimumNotice, - parseSlotInterval, -} from "./eventTypeParsers"; -// Formatting utilities -export { formatAppIdToDisplayName, formatDuration, truncateTitle } from "./formatters"; -// App icon utilities -export { getAppIconUrl } from "./getAppIconUrl"; -// Avatar utilities -export { getAvatarUrl } from "./getAvatarUrl"; -// Gmail Google Chip parser (for extension) -export { - type GoogleTimeSlot, - type ParsedGoogleChip, - parseGoogleChip, -} from "./gmailGoogleChipParser"; -// Location helper utilities -export { - buildLocationOptions, - createLocationItemFromOption, - displayNameToLocationValue, - formatAppIdToDisplayName as formatAppId, // Alias for backward compatibility - generateLocationId, - getLocationDisplayName, - getLocationIconUrl, - getLocationInputLabel, - getLocationInputPlaceholder, - getLocationInputType, - locationRequiresInput, - mapApiLocationToItem, - mapItemToApiLocation, - validateLocationItem, -} from "./locationHelpers"; -// Network utilities -export { fetchWithTimeout, isOnline } from "./network"; -// Query persistence utilities -export { clearQueryCache, createQueryPersister, getCacheMetadata } from "./queryPersister"; -// Safe logging utilities -export { getSafeErrorMessage, safeLogError, safeLogInfo, safeLogWarn } from "./safeLogger"; -// Slug utilities -export { slugify } from "./slugify"; -// Storage utilities -export { - generalStorage, - isChromeStorageAvailable, - type StorageAdapter, - secureStorage, -} from "./storage"; diff --git a/companion/utils/locationHelpers.ts b/companion/utils/locationHelpers.ts deleted file mode 100644 index 74336b49ca..0000000000 --- a/companion/utils/locationHelpers.ts +++ /dev/null @@ -1,512 +0,0 @@ -/** - * Helper functions for handling event type locations - * Provides utilities for converting between API format and UI format - */ - -import type { - ApiLocation, - ApiLocationInput, - LocationItem, - LocationOption, - LocationOptionGroup, -} from "@/types/locations"; -import { DefaultLocationType, defaultLocations } from "./defaultLocations"; -import { formatAppIdToDisplayName } from "./formatters"; -import { getAppIconUrl } from "./getAppIconUrl"; - -// Re-export formatAppIdToDisplayName for backward compatibility -export { formatAppIdToDisplayName } from "./formatters"; - -/** - * Generate a unique ID for a location item - */ -export function generateLocationId(): string { - return `loc_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; -} - -/** - * Map an API location response to a UI LocationItem - */ -export function mapApiLocationToItem(apiLocation: ApiLocation): LocationItem { - const id = generateLocationId(); - - // Handle integration type (conferencing apps) - if (apiLocation.type === "integration" && apiLocation.integration) { - const iconUrl = getAppIconUrl("", apiLocation.integration); - return { - id, - type: "integration", - integration: apiLocation.integration, - displayName: formatAppIdToDisplayName(apiLocation.integration), - iconUrl, - public: apiLocation.public, - }; - } - - // Handle address type - if (apiLocation.type === "address") { - return { - id, - type: "address", - address: apiLocation.address || "", - displayName: "In Person (Organizer Address)", - iconUrl: "https://app.cal.com/map-pin-dark.svg", - public: apiLocation.public, - }; - } - - // Handle attendeeAddress type - if (apiLocation.type === "attendeeAddress") { - return { - id, - type: "attendeeAddress", - displayName: "In Person (Attendee Address)", - iconUrl: "https://app.cal.com/map-pin-dark.svg", - }; - } - - // Handle link type - if (apiLocation.type === "link") { - return { - id, - type: "link", - link: apiLocation.link || "", - displayName: "Link Meeting", - iconUrl: "https://app.cal.com/link.svg", - public: apiLocation.public, - }; - } - - // Handle phone type (organizer phone) - if (apiLocation.type === "phone") { - return { - id, - type: "phone", - phone: apiLocation.phone || "", - displayName: "Phone Call", - iconUrl: "https://app.cal.com/phone.svg", - public: apiLocation.public, - }; - } - - // Handle attendeePhone type - if (apiLocation.type === "attendeePhone") { - return { - id, - type: "attendeePhone", - displayName: "Attendee Phone Number", - iconUrl: "https://app.cal.com/phone.svg", - }; - } - - // Handle attendeeDefined type - if (apiLocation.type === "attendeeDefined") { - return { - id, - type: "attendeeDefined", - displayName: "Custom Attendee Location", - iconUrl: "https://app.cal.com/message-pin.svg", - }; - } - - // Fallback for unknown types - return { - id, - type: apiLocation.type as LocationItem["type"], - displayName: apiLocation.type, - iconUrl: null, - }; -} - -/** - * Map a UI LocationItem to API location format for saving - */ -export function mapItemToApiLocation(item: LocationItem): ApiLocationInput { - // Handle integration type - if (item.type === "integration" && item.integration) { - return { - type: "integration", - integration: item.integration, - }; - } - - // Handle address type - if (item.type === "address") { - return { - type: "address", - address: item.address || "", - public: item.public ?? true, - }; - } - - // Handle attendeeAddress type - if (item.type === "attendeeAddress") { - return { - type: "attendeeAddress", - }; - } - - // Handle link type - if (item.type === "link") { - return { - type: "link", - link: item.link || "", - public: item.public ?? true, - }; - } - - // Handle phone type (organizer phone) - if (item.type === "phone") { - return { - type: "phone", - phone: item.phone || "", - public: item.public ?? true, - }; - } - - // Handle attendeePhone type - if (item.type === "attendeePhone") { - return { - type: "attendeePhone", - }; - } - - // Handle attendeeDefined type - if (item.type === "attendeeDefined") { - return { - type: "attendeeDefined", - }; - } - - // Fallback - return { - type: item.type, - }; -} - -/** - * Get the display name for a location type - */ -export function getLocationDisplayName(locationType: string, integration?: string): string { - // Handle integration type - if (locationType === "integration" && integration) { - return formatAppIdToDisplayName(integration); - } - - // Check default locations - const typeToDisplayName: Record = { - address: "In Person (Organizer Address)", - attendeeAddress: "In Person (Attendee Address)", - link: "Link Meeting", - phone: "Phone Call", - attendeePhone: "Attendee Phone Number", - attendeeDefined: "Custom Attendee Location", - }; - - return typeToDisplayName[locationType] || locationType; -} - -/** - * Get the icon URL for a location type - */ -export function getLocationIconUrl(locationType: string, integration?: string): string | null { - // Handle integration type - if (locationType === "integration" && integration) { - return getAppIconUrl("", integration); - } - - // Check default location icons - const typeToIcon: Record = { - address: "https://app.cal.com/map-pin-dark.svg", - attendeeAddress: "https://app.cal.com/map-pin-dark.svg", - link: "https://app.cal.com/link.svg", - phone: "https://app.cal.com/phone.svg", - attendeePhone: "https://app.cal.com/phone.svg", - attendeeDefined: "https://app.cal.com/message-pin.svg", - }; - - return typeToIcon[locationType] || null; -} - -/** - * Create a new LocationItem from a location option selection - */ -export function createLocationItemFromOption( - optionValue: string, - optionLabel: string -): LocationItem { - const id = generateLocationId(); - - // Handle integration options (format: "integrations:app-id") - if (optionValue.startsWith("integrations:")) { - const integration = optionValue.replace("integrations:", ""); - return { - id, - type: "integration", - integration, - displayName: optionLabel, - iconUrl: getAppIconUrl("", integration), - }; - } - - // Handle default location types - const defaultLocation = defaultLocations.find((loc) => loc.type === optionValue); - if (defaultLocation) { - const item: LocationItem = { - id, - type: mapDefaultLocationTypeToApiType(defaultLocation.type), - displayName: defaultLocation.label, - iconUrl: defaultLocation.iconUrl, - }; - - // Add input fields for specific types - if (defaultLocation.type === DefaultLocationType.InPerson) { - item.address = ""; - item.public = true; - } else if (defaultLocation.type === DefaultLocationType.Link) { - item.link = ""; - item.public = true; - } else if (defaultLocation.type === DefaultLocationType.UserPhone) { - item.phone = ""; - item.public = true; - } - - return item; - } - - // Fallback - return { - id, - type: optionValue as LocationItem["type"], - displayName: optionLabel, - iconUrl: null, - }; -} - -/** - * Map DefaultLocationType to API location type - */ -function mapDefaultLocationTypeToApiType(defaultType: string): LocationItem["type"] { - const typeMap: Record = { - [DefaultLocationType.AttendeeInPerson]: "attendeeAddress", - [DefaultLocationType.InPerson]: "address", - [DefaultLocationType.Phone]: "attendeePhone", - [DefaultLocationType.UserPhone]: "phone", - [DefaultLocationType.Link]: "link", - [DefaultLocationType.SomewhereElse]: "attendeeDefined", - }; - - return typeMap[defaultType] || (defaultType as LocationItem["type"]); -} - -/** - * Check if a location type requires additional input - */ -export function locationRequiresInput(locationType: LocationItem["type"]): boolean { - return ["address", "link", "phone"].includes(locationType); -} - -/** - * Get the input field type for a location - */ -export function getLocationInputType( - locationType: LocationItem["type"] -): "text" | "phone" | "url" | null { - switch (locationType) { - case "address": - return "text"; - case "link": - return "url"; - case "phone": - return "phone"; - default: - return null; - } -} - -/** - * Get the input placeholder for a location type - */ -export function getLocationInputPlaceholder(locationType: LocationItem["type"]): string { - switch (locationType) { - case "address": - return "Enter address or place"; - case "link": - return "https://meet.example.com/join/123456"; - case "phone": - return "Enter phone number"; - default: - return ""; - } -} - -/** - * Get the input label for a location type - */ -export function getLocationInputLabel(locationType: LocationItem["type"]): string { - switch (locationType) { - case "address": - return "Address"; - case "link": - return "Meeting Link"; - case "phone": - return "Phone Number"; - default: - return ""; - } -} - -/** - * Validate a location item before saving - */ -export function validateLocationItem(item: LocationItem): { valid: boolean; error?: string } { - // Integration types don't need additional validation - if (item.type === "integration") { - if (!item.integration) { - return { valid: false, error: "Integration type is required" }; - } - return { valid: true }; - } - - // Address type requires address field - if (item.type === "address") { - if (!item.address || item.address.trim() === "") { - return { valid: false, error: "Address is required" }; - } - return { valid: true }; - } - - // Link type requires link field - if (item.type === "link") { - if (!item.link || item.link.trim() === "") { - return { valid: false, error: "Meeting link is required" }; - } - // URL validation with protocol restriction - try { - const url = new URL(item.link); - if (url.protocol !== "http:" && url.protocol !== "https:") { - return { valid: false, error: "Meeting link must use http or https" }; - } - } catch { - return { valid: false, error: "Invalid meeting link URL" }; - } - return { valid: true }; - } - - // Phone type requires phone field - if (item.type === "phone") { - if (!item.phone || item.phone.trim() === "") { - return { valid: false, error: "Phone number is required" }; - } - return { valid: true }; - } - - // Other types (attendeeAddress, attendeePhone, attendeeDefined) don't need input - return { valid: true }; -} - -/** - * Build location options for dropdown from conferencing options and default locations - */ -export function buildLocationOptions( - conferencingOptions: Array<{ type: string; appId: string }> -): LocationOptionGroup[] { - // Cal Video is always available as the default conferencing app - const calVideoOption: LocationOption = { - label: "Cal Video", - iconUrl: getAppIconUrl("daily_video", "cal-video"), - value: "integrations:cal-video", - category: "conferencing", - }; - - // Group conferencing apps under "conferencing" category - const conferencingAppOptions: LocationOption[] = conferencingOptions - .filter((option) => option.appId !== "cal-video" && option.type !== "daily_video") - .map((option) => ({ - label: formatAppIdToDisplayName(option.appId), - iconUrl: getAppIconUrl(option.type, option.appId), - value: `integrations:${option.appId}`, - category: "conferencing", - })); - - // Group default locations by their category - const grouped: Record = { - conferencing: [calVideoOption, ...conferencingAppOptions], - }; - - // Add default locations by category - defaultLocations.forEach((location) => { - const option: LocationOption = { - label: location.label, - iconUrl: location.iconUrl, - value: location.type, - category: location.category, - }; - - if (!grouped[location.category]) { - grouped[location.category] = []; - } - grouped[location.category].push(option); - }); - - // Convert to array of groups with proper ordering - const categoryOrder = ["conferencing", "in person", "phone", "other"]; - const categoryLabels: Record = { - conferencing: "Conferencing", - "in person": "In Person", - phone: "Phone", - other: "Other", - }; - - return categoryOrder - .filter((category) => grouped[category] && grouped[category].length > 0) - .map((category) => ({ - category: categoryLabels[category] || category, - options: grouped[category], - })); -} - -/** - * Convert a display name back to a location value for API - * Used when selecting a location from a dropdown - * - * @param displayName - The display name shown in UI (e.g., "Google Meet", "In Person (Organizer Address)") - * @param defaultLocationsList - List of default locations to check against - * @returns Location value object for API, or null if not found - */ -export const displayNameToLocationValue = ( - displayName: string, - defaultLocationsList: Array<{ label: string; type: string }> -): { - type: string; - integration?: string; - address?: string; - link?: string; - phone?: string; - public?: boolean; -} | null => { - // First check if it's a default location - const defaultLocation = defaultLocationsList.find((loc) => loc.label === displayName); - if (defaultLocation) { - // Map internal location types to API location types - switch (defaultLocation.type) { - case "attendeeInPerson": - return { type: "attendeeAddress" }; - case "inPerson": - return { type: "address", address: "", public: true }; - case "link": - return { type: "link", link: "", public: true }; - case "phone": - return { type: "attendeePhone" }; - case "userPhone": - return { type: "phone", phone: "", public: true }; - default: - return null; - } - } - - // Check if it's a conferencing app (formatted display name) - // e.g., "Google Meet", "Zoom", etc. - const appId = displayName.toLowerCase().replace(/\s+/g, "-"); - return { type: "integration", integration: appId }; -}; diff --git a/companion/utils/meetings-utils.ts b/companion/utils/meetings-utils.ts deleted file mode 100644 index 4beebfb979..0000000000 --- a/companion/utils/meetings-utils.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { getAppIconUrl } from "./getAppIconUrl"; - -// Helper to extract clean meeting URL from potentially wrapped URLs -function extractMeetingUrl(location: string): string { - // Check if it's a goo.gl redirect URL with embedded meet.google.com link - if (location.includes("goo.gl") && location.includes("meet.google.com")) { - // Extract the actual meet.google.com URL from the redirect - const meetMatch = location.match(/meet\.google\.com\/[a-z]+-[a-z]+-[a-z]+/i); - if (meetMatch) { - return `https://${meetMatch[0]}`; - } - } - return location; -} - -// Helper to detect meeting type from location URL and get icon/label -export const getMeetingInfo = ( - location?: string -): { appId: string; label: string; iconUrl: string | null; cleanUrl: string } | null => { - if (!location) return null; - - // Check if it's a URL - if (!location.match(/^https?:\/\//)) return null; - - const lowerLocation = location.toLowerCase(); - const cleanUrl = extractMeetingUrl(location); - - // Cal Video - if (lowerLocation.includes("cal.com/video") || lowerLocation.includes("cal.video")) { - return { - appId: "cal-video", - label: "Join Cal Video", - iconUrl: getAppIconUrl("daily_video", "cal-video"), - cleanUrl, - }; - } - - // Google Meet (including goo.gl redirect URLs) - if ( - lowerLocation.includes("meet.google.com") || - (lowerLocation.includes("goo.gl") && lowerLocation.includes("meet")) - ) { - return { - appId: "google-meet", - label: "Join Google Meet", - iconUrl: getAppIconUrl("google_video", "google-meet"), - cleanUrl, - }; - } - - // Zoom - if (lowerLocation.includes("zoom.us") || lowerLocation.includes("zoom.com")) { - return { - appId: "zoom", - label: "Join Zoom", - iconUrl: getAppIconUrl("zoom_video", "zoom"), - cleanUrl, - }; - } - - // Microsoft Teams - if (lowerLocation.includes("teams.microsoft.com") || lowerLocation.includes("teams.live.com")) { - return { - appId: "msteams", - label: "Join Microsoft Teams", - iconUrl: getAppIconUrl("office365_video", "msteams"), - cleanUrl, - }; - } - - // Webex - if (lowerLocation.includes("webex.com")) { - return { - appId: "webex", - label: "Join Webex", - iconUrl: getAppIconUrl("webex_video", "webex"), - cleanUrl, - }; - } - - // Jitsi - if (lowerLocation.includes("meet.jit.si") || lowerLocation.includes("jitsi")) { - return { - appId: "jitsi", - label: "Join Jitsi", - iconUrl: getAppIconUrl("jitsi_video", "jitsi"), - cleanUrl, - }; - } - - // Not a recognized conferencing app - return null; -}; diff --git a/companion/utils/network.ts b/companion/utils/network.ts deleted file mode 100644 index 884c7870ce..0000000000 --- a/companion/utils/network.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Network Utilities - * - * Helper functions for network-aware operations. - */ - -import NetInfo from "@react-native-community/netinfo"; -import { Alert } from "react-native"; - -/** - * Default timeout for fetch requests in milliseconds (30 seconds) - */ -const DEFAULT_FETCH_TIMEOUT = 30000; - -/** - * Fetch with timeout using AbortSignal. - * This prevents requests from hanging indefinitely. - * - * @param url - The URL to fetch - * @param options - Fetch options (RequestInit) - * @param timeoutMs - Timeout in milliseconds (default: 30000) - * @returns Promise - * @throws Error if the request times out or fails - * - * @example - * ```tsx - * const response = await fetchWithTimeout('https://api.example.com/data', { - * method: 'GET', - * headers: { 'Content-Type': 'application/json' } - * }); - * ``` - */ -export const fetchWithTimeout = async ( - url: string, - options: RequestInit = {}, - timeoutMs: number = DEFAULT_FETCH_TIMEOUT -): Promise => { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), timeoutMs); - - try { - const response = await fetch(url, { - ...options, - signal: controller.signal, - }); - return response; - } finally { - clearTimeout(timeoutId); - } -}; - -/** - * Check if the device is currently online - * - * @returns Promise - true if online, false if offline - */ -export const isOnline = async (): Promise => { - const netState = await NetInfo.fetch(); - return netState.isConnected === true && netState.isInternetReachable !== false; -}; - -/** - * Execute a refresh function only if online. - * Shows a friendly alert if offline and preserves cached data. - * - * @param refetchFn - The refetch function to call (e.g., from React Query) - * @returns Promise - * - * @example - * ```tsx - * const { refetch } = useBookings(); - * - * const onRefresh = () => offlineAwareRefresh(refetch); - * - * - * ``` - */ -export const offlineAwareRefresh = async (refetchFn: () => Promise): Promise => { - const online = await isOnline(); - - if (!online) { - Alert.alert("You're offline", "Can't refresh right now. Showing cached data.", [ - { text: "OK" }, - ]); - return; - } - - await refetchFn(); -}; diff --git a/companion/utils/normalizeMarkdown.ts b/companion/utils/normalizeMarkdown.ts deleted file mode 100644 index beff445ee8..0000000000 --- a/companion/utils/normalizeMarkdown.ts +++ /dev/null @@ -1,37 +0,0 @@ -export const normalizeMarkdown = (text: string): string => { - if (!text) return ""; - - return ( - text - // Remove HTML tags including
,
,

, etc. - .replace(/<[^>]*>/g, " ") - // Convert HTML entities - .replace(/&/g, "&") - .replace(/</g, "<") - .replace(/>/g, ">") - .replace(/"/g, '"') - .replace(/'/g, "'") - .replace(/ /g, " ") - // Convert markdown links [text](url) to just text - .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") - // Remove bold/italic markers **text** or *text* - .replace(/\*\*([^*]+)\*\*/g, "$1") - .replace(/\*([^*]+)\*/g, "$1") - // Remove inline code `text` - .replace(/`([^`]+)`/g, "$1") - // Remove strikethrough ~~text~~ - .replace(/~~([^~]+)~~/g, "$1") - // Remove heading markers # ## ### - .replace(/^#{1,6}\s+/gm, "") - // Remove blockquote markers > - .replace(/^>\s+/gm, "") - // Remove list markers - * + - .replace(/^[\s]*[-*+]\s+/gm, "") - // Remove numbered list markers 1. 2. etc - .replace(/^[\s]*\d+\.\s+/gm, "") - // Normalize multiple whitespace/newlines to single space - .replace(/\s+/g, " ") - // Trim whitespace - .trim() - ); -}; diff --git a/companion/utils/queryPersister.ts b/companion/utils/queryPersister.ts deleted file mode 100644 index 423464e225..0000000000 --- a/companion/utils/queryPersister.ts +++ /dev/null @@ -1,143 +0,0 @@ -/** - * React Query Cache Persister - * - * Uses the shared storage adapter from utils/storage.ts for cross-platform support. - */ - -import type { PersistedClient, Persister } from "@tanstack/react-query-persist-client"; -import { CACHE_CONFIG } from "@/config/cache.config"; -import { safeLogWarn } from "./safeLogger"; -import { generalStorage } from "./storage"; - -// Use the shared general storage adapter for cache persistence -const storage = generalStorage; - -/** - * Create a React Query persister that works across all platforms - * - * This persister: - * - Saves the query cache to platform-appropriate storage - * - Restores cache on app launch for instant data display - * - Handles serialization/deserialization of cache data - * - Respects cache expiration (maxAge) - */ -export const createQueryPersister = (): Persister => { - const storageKey = CACHE_CONFIG.persistence.storageKey; - const maxAge = CACHE_CONFIG.persistence.maxAge; - - return { - /** - * Persist the client state to storage - */ - persistClient: async (client: PersistedClient): Promise => { - try { - const serialized = JSON.stringify(client); - await storage.setItem(storageKey, serialized); - } catch (error) { - safeLogWarn("[QueryPersister] Failed to persist client:", error); - // Fail silently - persistence is a nice-to-have, not critical - } - }, - - /** - * Restore the client state from storage - */ - restoreClient: async (): Promise => { - try { - const serialized = await storage.getItem(storageKey); - if (!serialized) { - return undefined; - } - - const client = JSON.parse(serialized) as PersistedClient; - - // Validate timestamp exists and is a valid number - if (typeof client.timestamp !== "number" || Number.isNaN(client.timestamp)) { - safeLogWarn("[QueryPersister] Invalid or missing timestamp, discarding cache"); - await storage.removeItem(storageKey); - return undefined; - } - - // Check if the persisted cache has expired - const persistedAt = client.timestamp; - const now = Date.now(); - if (now - persistedAt > maxAge) { - // Cache is too old, discard it - await storage.removeItem(storageKey); - return undefined; - } - - return client; - } catch (error) { - safeLogWarn("[QueryPersister] Failed to restore client:", error); - // If restoration fails, start fresh - return undefined; - } - }, - - /** - * Remove the persisted client state - */ - removeClient: async (): Promise => { - try { - await storage.removeItem(storageKey); - } catch (error) { - safeLogWarn("[QueryPersister] Failed to remove client:", error); - } - }, - }; -}; - -/** - * Export the storage adapter for potential direct use - */ -export { storage }; - -/** - * Utility to clear all query cache from storage - * Useful for logout or cache reset scenarios - */ -export const clearQueryCache = async (): Promise => { - try { - await storage.removeItem(CACHE_CONFIG.persistence.storageKey); - } catch (error) { - safeLogWarn("[QueryPersister] Failed to clear cache:", error); - } -}; - -/** - * Get cache metadata (for debugging/status display) - */ -export const getCacheMetadata = async (): Promise<{ - exists: boolean; - timestamp?: number; - age?: number; - isExpired?: boolean; -} | null> => { - try { - const serialized = await storage.getItem(CACHE_CONFIG.persistence.storageKey); - if (!serialized) { - return { exists: false }; - } - - const client = JSON.parse(serialized) as PersistedClient; - - // Validate timestamp exists and is a valid number - if (typeof client.timestamp !== "number" || Number.isNaN(client.timestamp)) { - return { exists: true, isExpired: true }; // Treat invalid timestamp as expired - } - - const now = Date.now(); - const age = now - client.timestamp; - - return { - exists: true, - timestamp: client.timestamp, - age, - isExpired: age > CACHE_CONFIG.persistence.maxAge, - }; - } catch (error) { - safeLogWarn("[QueryPersister] Failed to get cache metadata:", error); - return null; - } -}; diff --git a/companion/utils/safeLogger.ts b/companion/utils/safeLogger.ts deleted file mode 100644 index 113fffd158..0000000000 --- a/companion/utils/safeLogger.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Safe Logger Utility - * - * Provides secure logging functions that only output in development mode. - * This prevents sensitive data from being exposed in production logs. - * - * Security: All error objects, stack traces, and potentially sensitive data - * should be logged using these functions to ensure they are not exposed in production. - */ - -/** - * Safely log an error message only in development mode. - * Use this instead of console.error when logging error objects that may contain sensitive data. - * - * @param message - The error message prefix - * @param error - Optional error object (will only be logged in __DEV__ mode) - */ -export const safeLogError = (message: string, error?: unknown): void => { - if (__DEV__) { - if (error !== undefined) { - console.error(message, error); - } else { - console.error(message); - } - } -}; - -/** - * Safely log a warning message only in development mode. - * Use this instead of console.warn when logging potentially sensitive data. - * - * @param message - The warning message prefix - * @param data - Optional additional data (will only be logged in __DEV__ mode) - */ -export const safeLogWarn = (message: string, data?: unknown): void => { - if (__DEV__) { - if (data !== undefined) { - console.warn(message, data); - } else { - console.warn(message); - } - } -}; - -/** - * Safely log an info message only in development mode. - * Use this instead of console.log when logging potentially sensitive data. - * - * @param message - The info message prefix - * @param data - Optional additional data (will only be logged in __DEV__ mode) - */ -export const safeLogInfo = (message: string, data?: unknown): void => { - if (__DEV__) { - if (data !== undefined) { - console.log(message, data); - } else { - console.log(message); - } - } -}; - -/** - * Get a safe error message for displaying to users. - * Returns a generic message instead of exposing raw error details. - * - * @param error - The error object - * @param fallbackMessage - The generic message to show to users - * @returns The fallback message (never exposes raw error details to users) - */ -export const getSafeErrorMessage = (_error: unknown, fallbackMessage: string): string => { - return fallbackMessage; -}; diff --git a/companion/utils/shadows.ts b/companion/utils/shadows.ts deleted file mode 100644 index 06c549d6da..0000000000 --- a/companion/utils/shadows.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Platform, type ViewStyle } from "react-native"; - -interface ShadowConfig { - color?: string; - offsetY?: number; - offsetX?: number; - opacity?: number; - radius?: number; - elevation?: number; -} - -export function createShadow(config: ShadowConfig = {}): ViewStyle { - const { - color = "#000", - offsetX = 0, - offsetY = 2, - opacity = 0.1, - radius = 8, - elevation = 4, - } = config; - - if (Platform.OS === "web") { - const _alpha = Math.round(opacity * 255) - .toString(16) - .padStart(2, "0"); - return { - boxShadow: `${offsetX}px ${offsetY}px ${radius}px rgba(0, 0, 0, ${opacity})`, - } as ViewStyle; - } - - return { - shadowColor: color, - shadowOffset: { width: offsetX, height: offsetY }, - shadowOpacity: opacity, - shadowRadius: radius, - elevation, - }; -} - -export const shadows = { - sm: () => createShadow({ offsetY: 1, radius: 3, opacity: 0.1, elevation: 2 }), - md: () => createShadow({ offsetY: 4, radius: 6, opacity: 0.1, elevation: 4 }), - lg: () => createShadow({ offsetY: 10, radius: 15, opacity: 0.15, elevation: 8 }), - xl: () => createShadow({ offsetY: 20, radius: 25, opacity: 0.25, elevation: 24 }), - none: (): ViewStyle => ({}), -}; diff --git a/companion/utils/slugify.ts b/companion/utils/slugify.ts deleted file mode 100644 index 01303b2896..0000000000 --- a/companion/utils/slugify.ts +++ /dev/null @@ -1,29 +0,0 @@ -// forDisplayingInput is used to allow user to type "-" at the end and not replace with empty space. -// For eg:- "test-slug" is the slug user wants to set but while typing "test-" would get replace to "test" because of replace(/-+$/, "") - -export const slugify = (str: string, forDisplayingInput?: boolean) => { - if (!str) { - return ""; - } - - const s = str - .toLowerCase() // Convert to lowercase - .trim() // Remove whitespace from both sides - .normalize("NFD") // Normalize to decomposed form for handling accents - .replace(/\p{Diacritic}/gu, "") // Remove any diacritics (accents) from characters - .replace(/[^.\p{L}\p{N}\p{Zs}\p{Emoji}]+/gu, "-") // Replace any non-alphanumeric characters (including Unicode and except "." period) with a dash - .replace(/[\s_#]+/g, "-") // Replace whitespace, # and underscores with a single dash - .replace(/^-+/, "") // Remove dashes from start - .replace(/\.{2,}/g, ".") // Replace consecutive periods with a single period - .replace(/^\.+/, "") // Remove periods from the start - .replace( - /([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g, - "" - ) // Removes emojis - .replace(/\s+/g, " ") - .replace(/-+/g, "-"); // Replace consecutive dashes with a single dash - - return forDisplayingInput ? s : s.replace(/-+$/, "").replace(/\.*$/, ""); // Remove dashes and period from end -}; - -export default slugify; diff --git a/companion/utils/storage.ts b/companion/utils/storage.ts deleted file mode 100644 index f15e1ee676..0000000000 --- a/companion/utils/storage.ts +++ /dev/null @@ -1,183 +0,0 @@ -/// - -/** - * Unified Storage Adapter - * - * Cross-platform storage abstraction that works with: - * - SecureStore for React Native (iOS/Android) - for sensitive data - * - AsyncStorage for React Native (iOS/Android) - for general data - * - chrome.storage for browser extensions - * - localStorage as fallback for web - * - * This is the single source of truth for storage operations across the app. - */ - -import AsyncStorage from "@react-native-async-storage/async-storage"; -import * as SecureStore from "expo-secure-store"; -import { Platform } from "react-native"; - -/** - * Check if chrome.storage is available (browser extension context) - */ -export const isChromeStorageAvailable = (): boolean => { - return ( - Platform.OS === "web" && - typeof chrome !== "undefined" && - chrome.storage !== undefined && - chrome.storage.local !== undefined - ); -}; - -/** - * Storage interface for type safety - */ -export interface StorageAdapter { - getItem: (key: string) => Promise; - setItem: (key: string, value: string) => Promise; - removeItem: (key: string) => Promise; -} - -/** - * Secure storage for sensitive data (tokens, credentials) - * Uses SecureStore on mobile, chrome.storage on extension, localStorage on web - */ -export const secureStorage = { - get: async (key: string): Promise => { - if (isChromeStorageAvailable()) { - return new Promise((resolve, reject) => { - chrome.storage.local.get([key], (result) => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)); - } else { - resolve((result[key] as string) ?? null); - } - }); - }); - } - if (Platform.OS === "web") { - return localStorage.getItem(key); - } - return await SecureStore.getItemAsync(key); - }, - - set: async (key: string, value: string): Promise => { - if (isChromeStorageAvailable()) { - return new Promise((resolve, reject) => { - chrome.storage.local.set({ [key]: value }, () => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)); - } else { - resolve(); - } - }); - }); - } - if (Platform.OS === "web") { - localStorage.setItem(key, value); - return; - } - await SecureStore.setItemAsync(key, value); - }, - - remove: async (key: string): Promise => { - if (isChromeStorageAvailable()) { - return new Promise((resolve, reject) => { - chrome.storage.local.remove(key, () => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)); - } else { - resolve(); - } - }); - }); - } - if (Platform.OS === "web") { - localStorage.removeItem(key); - return; - } - await SecureStore.deleteItemAsync(key); - }, - - removeAll: async (keys: string[]): Promise => { - if (isChromeStorageAvailable()) { - return new Promise((resolve, reject) => { - chrome.storage.local.remove(keys, () => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)); - } else { - resolve(); - } - }); - }); - } - if (Platform.OS === "web") { - for (const key of keys) { - localStorage.removeItem(key); - } - return; - } - await Promise.all(keys.map((key) => SecureStore.deleteItemAsync(key))); - }, -}; - -/** - * General storage for non-sensitive data (cache, preferences) - * Uses AsyncStorage on mobile, chrome.storage on extension, localStorage on web - */ -export const generalStorage: StorageAdapter = { - getItem: async (key: string): Promise => { - if (isChromeStorageAvailable()) { - return new Promise((resolve, reject) => { - chrome.storage.local.get([key], (result) => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)); - } else { - resolve((result[key] as string) ?? null); - } - }); - }); - } - if (Platform.OS === "web") { - return Promise.resolve(localStorage.getItem(key)); - } - return AsyncStorage.getItem(key); - }, - - setItem: async (key: string, value: string): Promise => { - if (isChromeStorageAvailable()) { - return new Promise((resolve, reject) => { - chrome.storage.local.set({ [key]: value }, () => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)); - } else { - resolve(); - } - }); - }); - } - if (Platform.OS === "web") { - localStorage.setItem(key, value); - return Promise.resolve(); - } - return AsyncStorage.setItem(key, value); - }, - - removeItem: async (key: string): Promise => { - if (isChromeStorageAvailable()) { - return new Promise((resolve, reject) => { - chrome.storage.local.remove(key, () => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)); - } else { - resolve(); - } - }); - }); - } - if (Platform.OS === "web") { - localStorage.removeItem(key); - return Promise.resolve(); - } - return AsyncStorage.removeItem(key); - }, -}; diff --git a/companion/utils/widgetStorage.android.ts b/companion/utils/widgetStorage.android.ts deleted file mode 100644 index f346e6f1f9..0000000000 --- a/companion/utils/widgetStorage.android.ts +++ /dev/null @@ -1,53 +0,0 @@ -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 deleted file mode 100644 index d145cb2892..0000000000 --- a/companion/utils/widgetStorage.ios.ts +++ /dev/null @@ -1,54 +0,0 @@ -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 deleted file mode 100644 index 3ba9f656ae..0000000000 --- a/companion/utils/widgetStorage.shared.ts +++ /dev/null @@ -1,106 +0,0 @@ -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 deleted file mode 100644 index 1dc6d7f12b..0000000000 --- a/companion/utils/widgetStorage.ts +++ /dev/null @@ -1,26 +0,0 @@ -// 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/vercel.json b/companion/vercel.json deleted file mode 100644 index eaddbca8ae..0000000000 --- a/companion/vercel.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "routes": [ - { - "handle": "filesystem" - }, - { - "src": "/.*", - "dest": "/index.html" - } - ] -} diff --git a/companion/widgets/UpcomingBookingsWidget.tsx b/companion/widgets/UpcomingBookingsWidget.tsx deleted file mode 100644 index 0df344eb95..0000000000 --- a/companion/widgets/UpcomingBookingsWidget.tsx +++ /dev/null @@ -1,246 +0,0 @@ -"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 deleted file mode 100644 index 63f3b9b6b5..0000000000 --- a/companion/widgets/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { UpcomingBookingsWidget } from "./UpcomingBookingsWidget"; -export { widgetTaskHandler } from "./widgetTaskHandler"; diff --git a/companion/widgets/widgetTaskHandler.tsx b/companion/widgets/widgetTaskHandler.tsx deleted file mode 100644 index 85d227c64b..0000000000 --- a/companion/widgets/widgetTaskHandler.tsx +++ /dev/null @@ -1,55 +0,0 @@ -"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; - } -} diff --git a/companion/wxt.config.ts b/companion/wxt.config.ts deleted file mode 100644 index 821d1bdc0c..0000000000 --- a/companion/wxt.config.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { defineConfig } from "wxt"; - -// BUILD_FOR_STORE=true is set by ext:build-prod, ext:zip-prod, etc. -// Forces production URL (https://companion.cal.com) and excludes localhost permissions -const isBuildForStore = process.env.BUILD_FOR_STORE === "true"; - -// BROWSER_TARGET is set during build to determine which OAuth credentials to use -// Values: "chrome" (default), "firefox", "safari", "edge" -const browserTarget = process.env.BROWSER_TARGET || "chrome"; - -/** - * Get browser-specific OAuth configuration. - * Falls back to default (Chrome) config if browser-specific config is not set. - */ -function getOAuthConfig() { - const defaultClientId = process.env.EXPO_PUBLIC_CALCOM_OAUTH_CLIENT_ID || ""; - const defaultRedirectUri = process.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI || ""; - - switch (browserTarget) { - case "firefox": - return { - clientId: process.env.EXPO_PUBLIC_CALCOM_OAUTH_CLIENT_ID_FIREFOX || defaultClientId, - redirectUri: - process.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI_FIREFOX || defaultRedirectUri, - }; - case "safari": - return { - clientId: process.env.EXPO_PUBLIC_CALCOM_OAUTH_CLIENT_ID_SAFARI || defaultClientId, - redirectUri: process.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI_SAFARI || defaultRedirectUri, - }; - case "edge": - return { - clientId: process.env.EXPO_PUBLIC_CALCOM_OAUTH_CLIENT_ID_EDGE || defaultClientId, - redirectUri: process.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI_EDGE || defaultRedirectUri, - }; - default: - return { - clientId: defaultClientId, - redirectUri: defaultRedirectUri, - }; - } -} - -export default defineConfig({ - hooks: { - "build:manifestGenerated": (_wxt, manifest) => { - manifest.browser_specific_settings ??= {}; - manifest.browser_specific_settings.gecko ??= {}; - // Stable extension ID for Firefox development - ensures consistent OAuth redirect URL - // This must be an email-like string for Firefox to accept it - manifest.browser_specific_settings.gecko.id = "companion@cal.com"; - manifest.browser_specific_settings.gecko.data_collection_permissions = { - required: ["none"], - }; - }, - }, - srcDir: "extension", - entrypointsDir: "entrypoints", - publicDir: "extension/public", - outDir: ".output", - manifest: { - name: browserTarget === "safari" ? "Cal.com" : "Cal.com Companion", - version: "1.7.6", - description: "Your calendar companion for quick booking and scheduling", - permissions: [ - "activeTab", - "storage", - // Safari doesn't support Chrome's identity API - ...(browserTarget !== "safari" ? ["identity"] : []), - ], - host_permissions: [ - "https://companion.cal.com/*", - "https://api.cal.com/*", - "https://app.cal.com/*", - "https://mail.google.com/*", - "https://calendar.google.com/*", - // Include localhost permission for dev builds (needed for iframe to load) - // ...(!isBuildForStore ? ["http://localhost:*/*"] : []), - ], - content_security_policy: { - extension_pages: !isBuildForStore - ? "script-src 'self'; object-src 'self'; frame-src 'self' https://companion.cal.com http://localhost:*" - : "script-src 'self'; object-src 'self'; frame-src 'self' https://companion.cal.com", - }, - action: { - default_title: "Cal.com Companion", - default_icon: { - "16": "icon-16.png", - "48": "icon-48.png", - "128": "icon-128.png", - }, - }, - icons: { - "16": "icon-16.png", - "48": "icon-48.png", - "128": "icon-128.png", - }, - }, - vite: () => { - // Determine companion URL based on build type - const devUrl = isBuildForStore ? "" : process.env.EXPO_PUBLIC_COMPANION_DEV_URL || ""; - const isLocalDev = Boolean(devUrl?.includes("localhost")); - - // Get OAuth config for the target browser - const oauthConfig = getOAuthConfig(); - - // Log build mode for clarity - if (isBuildForStore) { - console.log("\nšŸŖ STORE BUILD: Using https://companion.cal.com\n"); - } else if (isLocalDev) { - console.log(`\nšŸ”§ DEV BUILD: Using ${devUrl}\n`); - } - console.log(`🌐 Browser Target: ${browserTarget}\n`); - - return { - resolve: { - alias: { - "react-native": "react-native-web", - }, - }, - define: { - global: "globalThis", - __DEV__: JSON.stringify(false), - - // Browser target for runtime detection - "import.meta.env.BROWSER_TARGET": JSON.stringify(browserTarget), - - // Default OAuth config (Chrome/Brave) - always available for fallback - "import.meta.env.EXPO_PUBLIC_CALCOM_OAUTH_CLIENT_ID": JSON.stringify( - process.env.EXPO_PUBLIC_CALCOM_OAUTH_CLIENT_ID - ), - "import.meta.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI": JSON.stringify( - process.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI - ), - - // Browser-specific OAuth config (resolved based on BROWSER_TARGET) - "import.meta.env.EXPO_PUBLIC_CALCOM_OAUTH_CLIENT_ID_FIREFOX": JSON.stringify( - process.env.EXPO_PUBLIC_CALCOM_OAUTH_CLIENT_ID_FIREFOX - ), - "import.meta.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI_FIREFOX": JSON.stringify( - process.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI_FIREFOX - ), - "import.meta.env.EXPO_PUBLIC_CALCOM_OAUTH_CLIENT_ID_SAFARI": JSON.stringify( - process.env.EXPO_PUBLIC_CALCOM_OAUTH_CLIENT_ID_SAFARI - ), - "import.meta.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI_SAFARI": JSON.stringify( - process.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI_SAFARI - ), - "import.meta.env.EXPO_PUBLIC_CALCOM_OAUTH_CLIENT_ID_EDGE": JSON.stringify( - process.env.EXPO_PUBLIC_CALCOM_OAUTH_CLIENT_ID_EDGE - ), - "import.meta.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI_EDGE": JSON.stringify( - process.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI_EDGE - ), - - // Pre-resolved OAuth config for the build target (for convenience) - "process.env.EXPO_PUBLIC_CALCOM_OAUTH_CLIENT_ID": JSON.stringify(oauthConfig.clientId), - "process.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI": JSON.stringify( - oauthConfig.redirectUri - ), - - // Use devUrl which respects BUILD_FOR_STORE flag - "import.meta.env.EXPO_PUBLIC_COMPANION_DEV_URL": JSON.stringify(devUrl), - "import.meta.env.EXPO_PUBLIC_CACHE_STALE_TIME_MINUTES": JSON.stringify( - process.env.EXPO_PUBLIC_CACHE_STALE_TIME_MINUTES - ), - "import.meta.env.EXPO_PUBLIC_CACHE_GC_TIME_MINUTES": JSON.stringify( - process.env.EXPO_PUBLIC_CACHE_GC_TIME_MINUTES - ), - "import.meta.env.EXPO_PUBLIC_BOOKINGS_CACHE_STALE_TIME_MINUTES": JSON.stringify( - process.env.EXPO_PUBLIC_BOOKINGS_CACHE_STALE_TIME_MINUTES - ), - "import.meta.env.EXPO_PUBLIC_EVENT_TYPES_CACHE_STALE_TIME_MINUTES": JSON.stringify( - process.env.EXPO_PUBLIC_EVENT_TYPES_CACHE_STALE_TIME_MINUTES - ), - "import.meta.env.EXPO_PUBLIC_SCHEDULES_CACHE_STALE_TIME_MINUTES": JSON.stringify( - process.env.EXPO_PUBLIC_SCHEDULES_CACHE_STALE_TIME_MINUTES - ), - "import.meta.env.EXPO_PUBLIC_USER_PROFILE_CACHE_STALE_TIME_MINUTES": JSON.stringify( - process.env.EXPO_PUBLIC_USER_PROFILE_CACHE_STALE_TIME_MINUTES - ), - ...(process.env.NODE_ENV !== "production" && process.env.EXPO_PUBLIC_CAL_API_KEY - ? { - "import.meta.env.EXPO_PUBLIC_CAL_API_KEY": JSON.stringify( - process.env.EXPO_PUBLIC_CAL_API_KEY - ), - } - : {}), - }, - optimizeDeps: { - include: ["react-native-web"], - esbuildOptions: { - loader: { - ".js": "jsx", - }, - }, - }, - }; - }, -}); diff --git a/packages/lib/timeZones.ts b/packages/lib/timeZones.ts index ce9fcceb4b..44ee9fcd73 100644 --- a/packages/lib/timeZones.ts +++ b/packages/lib/timeZones.ts @@ -1,6 +1,6 @@ /** * IMPORTANT: If you update this timezone array, please also update the TIMEZONES array - * in companion/constants/timezones.ts to keep the companion apps in sync. + * in the companion repo (calcom/companion) constants/timezones.ts to keep the companion apps in sync. */ export const IntlSupportedTimeZones = [ "Africa/Abidjan",