diff --git a/mobile/.env.example b/mobile/.env.example index e1a0236..d6a6da9 100644 --- a/mobile/.env.example +++ b/mobile/.env.example @@ -23,3 +23,10 @@ EXPO_PUBLIC_API_URL="https://pawfeed.org" # value of NEXT_PUBLIC_SUPABASE_URL from the repo root's .env.local. Safe to # embed client-side (it's the public storage origin, not a service key). EXPO_PUBLIC_SUPABASE_URL="https://xxxxxxxxxxxx.supabase.co" + +# Self-hosted GlitchTip (Sentry-protocol-compatible) DSN — use a separate +# GlitchTip project from the web app's so mobile crashes/breadcrumbs don't +# mix into the same event stream. A DSN is safe to ship client-side (it only +# lets a client send events, not read them). Optional: error reporting is a +# no-op if unset, it doesn't block the app from running. +EXPO_PUBLIC_SENTRY_DSN="" diff --git a/mobile/app.json b/mobile/app.json index 4f1bcbd..c8f7169 100644 --- a/mobile/app.json +++ b/mobile/app.json @@ -3,7 +3,7 @@ "name": "PawFeed", "slug": "pawfeed-mobile", "scheme": "pawfeed", - "version": "1.0.2", + "version": "1.0.6", "orientation": "portrait", "icon": "./assets/icon.png", "userInterfaceStyle": "automatic", @@ -13,7 +13,7 @@ }, "android": { "package": "org.pawfeed.app", - "versionCode": 3, + "versionCode": 7, "googleServicesFile": "./google-services.json", "adaptiveIcon": { "backgroundColor": "#fafafa", @@ -32,7 +32,6 @@ "expo-router", "expo-secure-store", "expo-font", - "expo-dev-client", "expo-image", "expo-system-ui", [ @@ -57,9 +56,15 @@ "image": "./assets/splash-icon.png", "imageWidth": 200, "resizeMode": "contain", - "backgroundColor": "#fafafa" + "backgroundColor": "#fafafa", + "dark": { + "image": "./assets/splash-icon.png", + "backgroundColor": "#111111" + } } - ] + ], + "expo-web-browser", + "@sentry/react-native" ], "extra": { "router": {}, diff --git a/mobile/app/(app)/(tabs)/explore.tsx b/mobile/app/(app)/(tabs)/explore.tsx index e5c3ec4..a17d255 100644 --- a/mobile/app/(app)/(tabs)/explore.tsx +++ b/mobile/app/(app)/(tabs)/explore.tsx @@ -62,11 +62,6 @@ export default function ExploreScreen() { numColumns={2} columnWrapperStyle={styles.row} contentContainerStyle={styles.list} - // FlatList wraps ListHeaderComponent in its own internal container, - // a sibling of the row containers — zIndex on `filters` itself only - // ranks it among ITS OWN children, so the breed dropdown's absolute - // overlay still painted underneath the grid without this. - ListHeaderComponentStyle={styles.headerWrapper} renderItem={({ item }) => } onEndReachedThreshold={0.5} onEndReached={() => { @@ -191,10 +186,6 @@ function makeStyles(colors: ColorTokens) { name: { ...typography.body, fontFamily: typography.title.fontFamily }, followers: { ...typography.label, textTransform: "none", letterSpacing: 0 }, footer: { marginVertical: 16 }, - // zIndex above the grid rows below it, so the breed dropdown (position: - // absolute inside OptionPicker) overlays the grid instead of being - // painted behind it — rows have no explicit zIndex (default 0). - headerWrapper: { zIndex: 10 }, filters: { gap: 10, marginBottom: 12 }, speciesTabs: { gap: 8 }, tab: { paddingHorizontal: 14, height: 36, borderRadius: radii.lg, justifyContent: "center", backgroundColor: colors.whisperGray }, diff --git a/mobile/app/(app)/milestone-new.tsx b/mobile/app/(app)/milestone-new.tsx index c7675fd..920eafc 100644 --- a/mobile/app/(app)/milestone-new.tsx +++ b/mobile/app/(app)/milestone-new.tsx @@ -9,13 +9,12 @@ import { useTRPC } from "../../src/api/trpc"; import { useActivePet } from "../../src/context/ActivePetContext"; import { useTheme } from "../../src/context/ThemeContext"; import { uploadImageToSupabase } from "../../src/lib/upload"; +import { resizeForUpload } from "../../src/lib/resize-image"; import { MILESTONE_META, USER_INITIATED_MILESTONE_TYPES, type CreatableMilestoneType } from "../../src/lib/milestone-meta"; import { TextField } from "../../src/components/TextField"; import { Button } from "../../src/components/Button"; import { radii, getTypography, type ColorTokens } from "../../src/theme/tokens"; -const ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/webp"]; - /** * Port of web's MilestoneForm — a milestone is a structured Post * (type=MILESTONE), created for the active pet only. Restricted to the 5 @@ -66,13 +65,13 @@ export default function NewMilestoneScreen() { try { let imageKey: string | undefined; if (asset) { - const mimeType = ALLOWED_MIME_TYPES.includes(asset.mimeType ?? "") ? (asset.mimeType as string) : "image/jpeg"; + const resized = await resizeForUpload(asset.uri, asset.width, asset.height); const { presignedUrl, key } = await getPresignedUrlMutation.mutateAsync({ petId: activePetId, - contentType: mimeType as "image/jpeg" | "image/png" | "image/webp", - fileSize: asset.fileSize ?? 0, + contentType: resized.mimeType, + fileSize: resized.fileSize, }); - await uploadImageToSupabase(asset.uri, presignedUrl, mimeType); + await uploadImageToSupabase(resized.uri, presignedUrl, resized.mimeType); imageKey = key; } createMutation.mutate({ diff --git a/mobile/app/(app)/pets/[petId]/edit.tsx b/mobile/app/(app)/pets/[petId]/edit.tsx index 1d85a7c..a2b7220 100644 --- a/mobile/app/(app)/pets/[petId]/edit.tsx +++ b/mobile/app/(app)/pets/[petId]/edit.tsx @@ -115,11 +115,10 @@ export default function EditPetScreen() { } return ( - // FlatList, not ScrollView: BreedPicker nests its own vertical - // ScrollView, and two same-axis ScrollViews stacked inside each other - // silently eat the inner one's scroll gesture on Android — see - // onboarding/pet.tsx's identical fix for the same OptionPicker-based - // component. + // FlatList, not ScrollView — kept from the original nested-ScrollView + // fix (see onboarding/pet.tsx's comment); OptionPicker's dropdown now + // renders in a Modal instead, so this is no longer strictly required + // for that reason, just the established scroll container here. { - const mimeType = asset.mimeType ?? "image/jpeg"; try { + const resized = await resizeForUpload(asset.uri, asset.width, asset.height); const { presignedUrl, key } = await getPresignedUrlMutation.mutateAsync({ petId: activePetId!, - contentType: mimeType as "image/jpeg" | "image/png" | "image/webp", - fileSize: asset.fileSize ?? 0, + contentType: resized.mimeType, + fileSize: resized.fileSize, }); - await uploadImageToSupabase(asset.uri, presignedUrl, mimeType); + await uploadImageToSupabase(resized.uri, presignedUrl, resized.mimeType); setImages((prev) => { const updated = [...prev]; updated[index] = { ...updated[index], status: "done", key }; diff --git a/mobile/app/(app)/story-new.tsx b/mobile/app/(app)/story-new.tsx index de1a2dc..bc02e61 100644 --- a/mobile/app/(app)/story-new.tsx +++ b/mobile/app/(app)/story-new.tsx @@ -8,11 +8,10 @@ import { ImagePlus, Camera } from "lucide-react-native"; import { useTRPC } from "../../src/api/trpc"; import { useActivePet } from "../../src/context/ActivePetContext"; import { uploadImageToSupabase } from "../../src/lib/upload"; +import { resizeForUpload } from "../../src/lib/resize-image"; import { Button } from "../../src/components/Button"; import { colors, radii, typography } from "../../src/theme/tokens"; -const ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/webp"]; - /** * Photo-only story creation (matches web's own "Phase 2" note in * stories.ts: "Photo-only in Phase 2" — video stories are a Mux upload @@ -65,15 +64,15 @@ export default function NewStoryScreen() { const handleSubmit = useCallback(async () => { if (!asset || !activePetId) return; setError(null); - const mimeType = ALLOWED_MIME_TYPES.includes(asset.mimeType ?? "") ? (asset.mimeType as string) : "image/jpeg"; try { + const resized = await resizeForUpload(asset.uri, asset.width, asset.height); const { presignedUrl, key } = await getPresignedUrlMutation.mutateAsync({ petId: activePetId, - contentType: mimeType as "image/jpeg" | "image/png" | "image/webp", - fileSize: asset.fileSize ?? 0, + contentType: resized.mimeType, + fileSize: resized.fileSize, }); - await uploadImageToSupabase(asset.uri, presignedUrl, mimeType); + await uploadImageToSupabase(resized.uri, presignedUrl, resized.mimeType); createMutation.mutate({ petId: activePetId, storageKey: key, blurhash: null }); } catch { setError(t("onboarding.uploadError")); diff --git a/mobile/app/_layout.tsx b/mobile/app/_layout.tsx index 382d9cc..9d0dc24 100644 --- a/mobile/app/_layout.tsx +++ b/mobile/app/_layout.tsx @@ -1,9 +1,13 @@ import { useEffect, useMemo, useState } from "react"; -import { ActivityIndicator, View } from "react-native"; +import { ActivityIndicator, View, Text, Pressable, useColorScheme } from "react-native"; import { Slot, useRouter, useSegments } from "expo-router"; import { ClerkProvider, useAuth } from "@clerk/expo"; import { SafeAreaProvider } from "react-native-safe-area-context"; import { StatusBar } from "expo-status-bar"; +import * as WebBrowser from "expo-web-browser"; +import * as Sentry from "@sentry/react-native"; +import * as Updates from "expo-updates"; +import { useTranslation } from "react-i18next"; import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client"; import { useFonts, @@ -17,9 +21,27 @@ import { env } from "../src/lib/env"; import { TRPCProvider, createMobileTRPCClient, makeQueryClient } from "../src/api/trpc"; import { queryPersister } from "../src/lib/storage"; import { ActivePetProvider, useActivePet } from "../src/context/ActivePetContext"; -import { ThemeProvider, useTheme } from "../src/context/ThemeContext"; +import { ThemeProvider, useTheme, resolveInitialColors } from "../src/context/ThemeContext"; import { usePushNotifications } from "../src/hooks/usePushNotifications"; -import { colors } from "../src/theme/tokens"; +import { fontFamily } from "../src/theme/tokens"; + +// Required once, module-scope, for expo-web-browser to resolve the pending +// AuthSession promise when the OAuth browser tab redirects back into the +// app (Google SSO via useSSO()) — otherwise the promise from +// WebBrowser.openAuthSessionAsync() never settles. +WebBrowser.maybeCompleteAuthSession(); + +// Self-hosted GlitchTip (Sentry-protocol-compatible) — see .env.example. +// A no-op when the DSN isn't set (local dev). Deliberately minimal: no +// sourcemap upload wired into the build (that already fails against this +// GlitchTip instance for the web app — CSRF 403, see CLAUDE.md), just error +// + breadcrumb capture so a repeat of an unreproducible field bug (e.g. the +// stuck-loading-screen report below) has something to look at next time. +if (env.sentryDsn) { + Sentry.init({ dsn: env.sentryDsn, tracesSampleRate: 0.1 }); +} + +const AUTH_LOAD_TIMEOUT_MS = 10_000; /** * Redirects between the (auth) and (app) route groups based on Clerk's @@ -104,10 +126,101 @@ function OnboardingRedirect() { return null; } +/** + * Rendered before ThemeProvider mounts (pre-fonts, pre-auth-resolved), so it + * can't use useTheme() — resolves the same local-preference-or-system guess + * ThemeProvider uses for its own first frame, instead of always hardcoding + * the light palette regardless of device theme. + */ function LoadingScreen() { + const systemScheme = useColorScheme(); + const themeColors = resolveInitialColors(systemScheme); return ( - - + + + + ); +} + +/** + * True once `waiting` has stayed true for AUTH_LOAD_TIMEOUT_MS straight — + * resets if `waiting` goes false first. Exists because Clerk's `isLoaded` + * has no built-in timeout: a hung silent token-refresh (the stored session + * JWT is expired on a reopen days later, so Clerk has to hit the network + * before it can resolve) previously left the loading spinner in + * app/_layout.tsx spinning forever, with no error and no way to recover + * short of force-closing the app (field report: stuck loading screen on a + * second-day reopen, found.md TODO Neu5). + */ +function useTimedOut(waiting: boolean, timeoutMs: number): boolean { + const [timedOut, setTimedOut] = useState(false); + + useEffect(() => { + if (!waiting) return; + const id = setTimeout(() => setTimedOut(true), timeoutMs); + return () => clearTimeout(id); + }, [waiting, timeoutMs]); + + // `timedOut` only ever flips true, so mask a stale true from a previous + // wait cycle with the current `waiting` value instead of resetting state + // synchronously inside the effect above. + return waiting && timedOut; +} + +/** + * Shown instead of an indefinite spinner once useTimedOut fires. Rendered + * pre-ThemeProvider like LoadingScreen, so it can't use useTheme() or the + * themed Button component — same reason LoadingScreen builds its own View + * instead. + */ +function AuthTimeoutScreen() { + const { t } = useTranslation(); + const systemScheme = useColorScheme(); + const colors = resolveInitialColors(systemScheme); + const [isRetrying, setIsRetrying] = useState(false); + + // Captured on mount, not only on a retry tap — a user who just force-closes + // instead of tapping retry would otherwise leave zero trace of the timeout. + useEffect(() => { + Sentry.captureMessage("Auth load timeout — showed retry screen", "warning"); + }, []); + + const onRetry = async () => { + setIsRetrying(true); + Sentry.captureMessage("Auth load timeout — user tapped retry", "warning"); + try { + await Updates.reloadAsync(); + } catch { + // No OTA runtime to reload against (e.g. a dev build) — nothing more + // we can do from here; leave the retry button re-enabled. + setIsRetrying(false); + } + }; + + return ( + + + {t("common.authLoadTimeout")} + + + {isRetrying ? ( + + ) : ( + {t("common.retry")} + )} + ); } @@ -115,9 +228,10 @@ function LoadingScreen() { function InitialLayout() { const { isLoaded } = useAuth(); useProtectedRoute(); + const authTimedOut = useTimedOut(!isLoaded, AUTH_LOAD_TIMEOUT_MS); if (!isLoaded) { - return ; + return authTimedOut ? : ; } return ( @@ -127,7 +241,7 @@ function InitialLayout() { ); } -export default function RootLayout() { +function RootLayout() { const [fontsLoaded] = useFonts({ Inter_400Regular, Inter_600SemiBold, @@ -147,3 +261,9 @@ export default function RootLayout() { ); } + +// Sentry.wrap adds an error boundary around the root component so an +// otherwise-uncaught render error reports to GlitchTip instead of silently +// leaving a blank/frozen screen — a no-op when Sentry.init() above never ran +// (no DSN configured). +export default Sentry.wrap(RootLayout); diff --git a/mobile/app/onboarding/pet.tsx b/mobile/app/onboarding/pet.tsx index de507ef..81bd60b 100644 --- a/mobile/app/onboarding/pet.tsx +++ b/mobile/app/onboarding/pet.tsx @@ -48,13 +48,12 @@ export default function OnboardingPetScreen() { } return ( - // FlatList, not ScrollView, as the scroll container: PetCreateForm's - // Rassen-Dropdown (OptionPicker) nests its own vertical ScrollView, and - // two plain same-axis ScrollViews stacked inside each other silently eat - // the inner one's scroll gestures on Android (nestedScrollEnabled does - // not reliably fix real-ScrollView-in-ScrollView nesting) — a FlatList - // outer container is exactly explore.tsx's already-working fix for the - // same OptionPicker component (see its ListHeaderComponent usage there). + // FlatList, not ScrollView, as the scroll container — kept from the + // original nested-ScrollView fix (see git history) even though + // OptionPicker's dropdown now renders in a Modal instead of nesting a + // ScrollView here, so it's no longer strictly required for that + // specific reason; still the established, working scroll container for + // this screen. =18" + } + }, + "node_modules/@sentry-internal/feedback": { + "version": "10.37.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.37.0.tgz", + "integrity": "sha512-P0PVlfrDvfvCYg2KPIS7YUG/4i6ZPf8z1MicXx09C9Cz9W9UhSBh/nii13eBdDtLav2BFMKhvaFMcghXHX03Hw==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.37.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay": { + "version": "10.37.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.37.0.tgz", + "integrity": "sha512-snuk12ZaDerxesSnetNIwKoth/51R0y/h3eXD/bGtXp+hnSkeXN5HanI/RJl297llRjn4zJYRShW9Nx86Ay0Dw==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "10.37.0", + "@sentry/core": "10.37.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "10.37.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.37.0.tgz", + "integrity": "sha512-PyIYSbjLs+L5essYV0MyIsh4n5xfv2eV7l0nhUoPJv9Bak3kattQY3tholOj0EP3SgKgb+8HSZnmazgF++Hbog==", + "license": "MIT", + "dependencies": { + "@sentry-internal/replay": "10.37.0", + "@sentry/core": "10.37.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/babel-plugin-component-annotate": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.8.0.tgz", + "integrity": "sha512-cy/9Eipkv23MsEJ4IuB4dNlVwS9UqOzI3Eu+QPake5BVFgPYCX0uP0Tr3Z43Ime6Rb+BiDnWC51AJK9i9afHYw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@sentry/browser": { + "version": "10.37.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.37.0.tgz", + "integrity": "sha512-kheqJNqGZP5TSBCPv4Vienv1sfZwXKHQDYR+xrdHHYdZqwWuZMJJW/cLO9XjYAe+B9NnJ4UwJOoY4fPvU+HQ1Q==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "10.37.0", + "@sentry-internal/feedback": "10.37.0", + "@sentry-internal/replay": "10.37.0", + "@sentry-internal/replay-canvas": "10.37.0", + "@sentry/core": "10.37.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/cli": { + "version": "2.58.4", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.58.4.tgz", + "integrity": "sha512-ArDrpuS8JtDYEvwGleVE+FgR+qHaOp77IgdGSacz6SZy6Lv90uX0Nu4UrHCQJz8/xwIcNxSqnN22lq0dH4IqTg==", + "hasInstallScript": true, + "license": "FSL-1.1-MIT", + "dependencies": { + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.7", + "progress": "^2.0.3", + "proxy-from-env": "^1.1.0", + "which": "^2.0.2" + }, + "bin": { + "sentry-cli": "bin/sentry-cli" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@sentry/cli-darwin": "2.58.4", + "@sentry/cli-linux-arm": "2.58.4", + "@sentry/cli-linux-arm64": "2.58.4", + "@sentry/cli-linux-i686": "2.58.4", + "@sentry/cli-linux-x64": "2.58.4", + "@sentry/cli-win32-arm64": "2.58.4", + "@sentry/cli-win32-i686": "2.58.4", + "@sentry/cli-win32-x64": "2.58.4" + } + }, + "node_modules/@sentry/cli-darwin": { + "version": "2.58.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.58.4.tgz", + "integrity": "sha512-kbTD+P4X8O+nsNwPxCywtj3q22ecyRHWff98rdcmtRrvwz8CKi/T4Jxn/fnn2i4VEchy08OWBuZAqaA5Kh2hRQ==", + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm": { + "version": "2.58.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.58.4.tgz", + "integrity": "sha512-rdQ8beTwnN48hv7iV7e7ZKucPec5NJkRdrrycMJMZlzGBPi56LqnclgsHySJ6Kfq506A2MNuQnKGaf/sBC9REA==", + "cpu": [ + "arm" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm64": { + "version": "2.58.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.58.4.tgz", + "integrity": "sha512-0g0KwsOozkLtzN8/0+oMZoOuQ0o7W6O+hx+ydVU1bktaMGKEJLMAWxOQNjsh1TcBbNIXVOKM/I8l0ROhaAb8Ig==", + "cpu": [ + "arm64" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-i686": { + "version": "2.58.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.58.4.tgz", + "integrity": "sha512-NseoIQAFtkziHyjZNPTu1Gm1opeQHt7Wm1LbLrGWVIRvUOzlslO9/8i6wETUZ6TjlQxBVRgd3Q0lRBG2A8rFYA==", + "cpu": [ + "x86", + "ia32" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-x64": { + "version": "2.58.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.58.4.tgz", + "integrity": "sha512-d3Arz+OO/wJYTqCYlSN3Ktm+W8rynQ/IMtSZLK8nu0ryh5mJOh+9XlXY6oDXw4YlsM8qCRrNquR8iEI1Y/IH+Q==", + "cpu": [ + "x64" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-arm64": { + "version": "2.58.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.58.4.tgz", + "integrity": "sha512-bqYrF43+jXdDBh0f8HIJU3tbvlOFtGyRjHB8AoRuMQv9TEDUfENZyCelhdjA+KwDKYl48R1Yasb4EHNzsoO83w==", + "cpu": [ + "arm64" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-i686": { + "version": "2.58.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.58.4.tgz", + "integrity": "sha512-3triFD6jyvhVcXOmGyttf+deKZcC1tURdhnmDUIBkiDPJKGT/N5xa4qAtHJlAB/h8L9jgYih9bvJnvvFVM7yug==", + "cpu": [ + "x86", + "ia32" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-x64": { + "version": "2.58.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.58.4.tgz", + "integrity": "sha512-cSzN4PjM1RsCZ4pxMjI0VI7yNCkxiJ5jmWncyiwHXGiXrV1eXYdQ3n1LhUYLZ91CafyprR0OhDcE+RVZ26Qb5w==", + "cpu": [ + "x64" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@sentry/cli/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@sentry/core": { + "version": "10.37.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.37.0.tgz", + "integrity": "sha512-hkRz7S4gkKLgPf+p3XgVjVm7tAfvcEPZxeACCC6jmoeKhGkzN44nXwLiqqshJ25RMcSrhfFvJa/FlBg6zupz7g==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/react": { + "version": "10.37.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.37.0.tgz", + "integrity": "sha512-XLnXJOHgsCeVAVBbO+9AuGlZWnCxLQHLOmKxpIr8wjE3g7dHibtug6cv8JLx78O4dd7aoCqv2TTyyKY9FLJ2EQ==", + "license": "MIT", + "dependencies": { + "@sentry/browser": "10.37.0", + "@sentry/core": "10.37.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.14.0 || 17.x || 18.x || 19.x" + } + }, + "node_modules/@sentry/react-native": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@sentry/react-native/-/react-native-7.11.0.tgz", + "integrity": "sha512-OiDaLCAGpRN18YG/o7IIwLhU0Xpb0tYKQ5QxkGHiwb+L3VHn+MqGCGfITYNdhqr06HHMvu9Lysm+UJxaNmGaJg==", + "license": "MIT", + "dependencies": { + "@sentry/babel-plugin-component-annotate": "4.8.0", + "@sentry/browser": "10.37.0", + "@sentry/cli": "2.58.4", + "@sentry/core": "10.37.0", + "@sentry/react": "10.37.0", + "@sentry/types": "10.37.0" + }, + "bin": { + "sentry-expo-upload-sourcemaps": "scripts/expo-upload-sourcemaps.js" + }, + "peerDependencies": { + "expo": ">=49.0.0", + "react": ">=17.0.0", + "react-native": ">=0.65.0" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, + "node_modules/@sentry/types": { + "version": "10.37.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-10.37.0.tgz", + "integrity": "sha512-umpnUKRC0AAbJrADg6SlFtqN2yzf7NHciCF9lkHau+ax2PIZ/NDmoG4RQujFVflVaVoD60Ly2t+CcPnYIWMPlw==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.37.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.12", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.12.tgz", @@ -4974,9 +5194,9 @@ } }, "node_modules/babel-preset-expo": { - "version": "57.0.10", - "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-57.0.10.tgz", - "integrity": "sha512-08bt6bqMZFoQuWO9gkt0EXotbqKHimELFGa1LlKJvY89iKsi4S0FJSFBFJ4pn9NZvofet48HIjYDQ0SZJ7bNrg==", + "version": "57.0.11", + "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-57.0.11.tgz", + "integrity": "sha512-R0NouDI3nzQUsjBh5TUJSDXLmuVzE2bp2YE0ywJkKxzrdWVjShpAlZMBuDeOQy6iKGB6ZCCvMqbQvpqgNMJ7iw==", "license": "MIT", "dependencies": { "@babel/generator": "^7.20.5", @@ -5025,7 +5245,7 @@ "peerDependencies": { "@babel/runtime": "^7.20.0", "expo": "*", - "expo-widgets": "^57.0.16", + "expo-widgets": "^57.0.18", "react-refresh": ">=0.14.0 <1.0.0" }, "peerDependenciesMeta": { @@ -5526,14 +5746,15 @@ } }, "node_modules/compression": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.2.tgz", + "integrity": "sha512-o8vI5RE5A6EVVOd9o41jKp41aJom+QTEO/Bx8MYNjexMo/Bv2WOjUfZr+aL0WnYSgymUy6zeguqLTsIhV0gMvQ==", "license": "MIT", "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", + "destroy": "1.2.0", "negotiator": "~0.6.4", "on-headers": "~1.1.0", "safe-buffer": "5.2.1", @@ -5541,6 +5762,10 @@ }, "engines": { "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/compression/node_modules/debug": { @@ -6025,31 +6250,31 @@ "license": "MIT" }, "node_modules/expo": { - "version": "57.0.20", - "resolved": "https://registry.npmjs.org/expo/-/expo-57.0.20.tgz", - "integrity": "sha512-H57GWLLpNPrltsrjvx6ROsMecoFQza7h9OzujUjBsaY+aDL+bj3GsZaH3T5v5B96R7txuidUwjUh5q5hi3qttg==", + "version": "57.0.22", + "resolved": "https://registry.npmjs.org/expo/-/expo-57.0.22.tgz", + "integrity": "sha512-YWUGqWtchsRQzcbU+Iqqt/1kFbZ9Pp3/eg+UenINahNt2RIaDElFJFFWLD1Yfex6WnBm6ESSE8h0/ja28hRqZg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.0", - "@expo/cli": "^57.0.22", + "@expo/cli": "^57.0.24", "@expo/config": "~57.0.9", "@expo/config-plugins": "~57.0.9", "@expo/devtools": "~57.0.1", "@expo/dom-webview": "~57.0.1", - "@expo/fingerprint": "^0.20.12", + "@expo/fingerprint": "^0.20.13", "@expo/local-build-cache-provider": "^57.0.8", "@expo/log-box": "^57.0.4", "@expo/metro": "~56.0.2", "@expo/metro-config": "~57.0.12", "@ungap/structured-clone": "^1.3.0", - "babel-preset-expo": "~57.0.10", - "expo-asset": "~57.0.16", - "expo-constants": "~57.0.17", - "expo-file-system": "~57.0.6", - "expo-font": "~57.0.3", - "expo-keep-awake": "~57.0.1", - "expo-modules-autolinking": "~57.0.12", - "expo-modules-core": "~57.0.16", + "babel-preset-expo": "~57.0.11", + "expo-asset": "~57.0.17", + "expo-constants": "~57.0.18", + "expo-file-system": "~57.0.7", + "expo-font": "~57.0.4", + "expo-keep-awake": "~57.0.2", + "expo-modules-autolinking": "~57.0.13", + "expo-modules-core": "~57.0.18", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", "whatwg-url-minimum": "^0.1.2" @@ -6087,22 +6312,22 @@ } }, "node_modules/expo-application": { - "version": "57.0.2", - "resolved": "https://registry.npmjs.org/expo-application/-/expo-application-57.0.2.tgz", - "integrity": "sha512-q31YwcXyymviAmdrtDfAg3Dld4VMxLCNAfgMHip7vZpPX4lzF/AfwsywqBJUAjQnJknzaNAkzqvMVjO5XmKYDA==", + "version": "57.0.3", + "resolved": "https://registry.npmjs.org/expo-application/-/expo-application-57.0.3.tgz", + "integrity": "sha512-t/CxeLio0NjY9/cOak1tQp62/hAUjzJxmhR4rVUR7K5bb9YoEtfl1ZekTn+Iuh+dYOMYzF0l72ODMSmY3CR1NA==", "license": "MIT", "peerDependencies": { "expo": "*" } }, "node_modules/expo-asset": { - "version": "57.0.16", - "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-57.0.16.tgz", - "integrity": "sha512-IBRfQdW3iFT+GOBERMZLZM1MUNyrjMgMskuD0elVZ1ae44858UFlTJkHO3f8vi+u5zuv7O7KofsiN8NMG/uWzw==", + "version": "57.0.17", + "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-57.0.17.tgz", + "integrity": "sha512-qSdWZBSfEg06o3GJMwcj36tw0vXkB6eYjqkQistWmyJkHxhUOKnsKJnev6OraXxLdoI+KOd1+qgp/ENx8gcqYA==", "license": "MIT", "dependencies": { "@expo/image-utils": "^0.11.5", - "expo-constants": "~57.0.17" + "expo-constants": "~57.0.18" }, "peerDependencies": { "expo": "*", @@ -6110,10 +6335,37 @@ "react-native": "*" } }, + "node_modules/expo-auth-session": { + "version": "57.0.12", + "resolved": "https://registry.npmjs.org/expo-auth-session/-/expo-auth-session-57.0.12.tgz", + "integrity": "sha512-WtUJMRjuw0PkT2Aei9qMy3S2FHPTJUSzvBbSncJvBTAXU09FDyJqLkkuJJmur7aF9E43PFxJkw0GTogQYIBgJQ==", + "license": "MIT", + "dependencies": { + "expo-application": "~57.0.3", + "expo-constants": "~57.0.18", + "expo-crypto": "~57.0.3", + "expo-linking": "~57.0.10", + "expo-web-browser": "~57.0.3", + "invariant": "^2.2.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-auth-session/node_modules/expo-crypto": { + "version": "57.0.3", + "resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-57.0.3.tgz", + "integrity": "sha512-SAWqEfF37nc8ot7bXsVmu9AFYwQdC2kGvH/eNAHiR3bmlcf1Vv8wu9AnGKT+a90r3T8YfwMpZP4QAYlo4vCn9w==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-constants": { - "version": "57.0.17", - "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-57.0.17.tgz", - "integrity": "sha512-cPWYBKN1SEbg2lXg2f8VkePJqGZrJPLvVdQDCTfbFu9sQHO1M31Y1zILReUuGnmt/VKeUz40ze579vR00xGu/A==", + "version": "57.0.18", + "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-57.0.18.tgz", + "integrity": "sha512-cLDwDOniPfuCZPHNY++cAY2ZkK97u5aobbBvilPK2LLmaWxR4ZpD31LWUIzUDLrwMPxLjmh1uURnbeYEf0Mivw==", "license": "MIT", "dependencies": { "@expo/env": "~2.4.3" @@ -6124,30 +6376,30 @@ } }, "node_modules/expo-dev-client": { - "version": "57.0.18", - "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-57.0.18.tgz", - "integrity": "sha512-cT+6iO4rRoguIxeONAPHF2wbAlc002tZya2M3xHlc6EWKptjsAL2Vr+kagYdLH/4vtLX7XKhWvOzaBf+rQpRFg==", + "version": "57.0.19", + "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-57.0.19.tgz", + "integrity": "sha512-7giMK6BLL685GECHem2UAQvS473ImZLi5l+Q5tq+XSyv6I9UNVPs0p07mKwgBzq8z7DR3d4nTl702ziFxTjO3Q==", "license": "MIT", "dependencies": { - "expo-dev-launcher": "~57.0.19", + "expo-dev-launcher": "~57.0.20", "expo-dev-menu": "~57.0.18", "expo-dev-menu-interface": "~57.0.0", - "expo-manifests": "~57.0.1", - "expo-updates-interface": "~57.0.1" + "expo-manifests": "~57.0.2", + "expo-updates-interface": "~57.0.2" }, "peerDependencies": { "expo": "*" } }, "node_modules/expo-dev-launcher": { - "version": "57.0.19", - "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-57.0.19.tgz", - "integrity": "sha512-IF/EJ3tcHqiyfBb5VtpAU3upBXRVTFVuVoWqEzrkZCesuMi35td4yQFvPtqXbMceaEErgFyiyxhW+L6AXVYapA==", + "version": "57.0.20", + "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-57.0.20.tgz", + "integrity": "sha512-F6oAHaDn9j3L/gyMGE+OalriX6PHZWnZLxpN5jc6crTrp8f6hSwGhmsHpAvdOzijfMjyi3R2UkcISFsCIIQYFA==", "license": "MIT", "dependencies": { "@expo/schema-utils": "^57.0.2", "expo-dev-menu": "~57.0.18", - "expo-manifests": "~57.0.1" + "expo-manifests": "~57.0.2" }, "peerDependencies": { "expo": "*", @@ -6177,15 +6429,15 @@ } }, "node_modules/expo-eas-client": { - "version": "57.0.3", - "resolved": "https://registry.npmjs.org/expo-eas-client/-/expo-eas-client-57.0.3.tgz", - "integrity": "sha512-cyC2AZg85DfAt4Ge7jIRmxmL62dUXob4EnrLZatrl+xJ6AuObqjMYf3hx5JYFfvro4ajkFo25qNREPTdiHCYQQ==", + "version": "57.0.4", + "resolved": "https://registry.npmjs.org/expo-eas-client/-/expo-eas-client-57.0.4.tgz", + "integrity": "sha512-VSPi6Kldu0EpPkC2e3bv3QNAUVDogbz1tTj7KgcdbsuUySt0hu24g4f1fJ/YjYzCw9G1l755x4QQctcX1j7UEA==", "license": "MIT" }, "node_modules/expo-file-system": { - "version": "57.0.6", - "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-57.0.6.tgz", - "integrity": "sha512-pm8PMYEW6BnVOCBJ7df9FcDmQtE1tqImuYphlfYe1ipQRLYtdCayRczJcbKIsnB3mqEyp4gC2gWmMbsAsAOjVg==", + "version": "57.0.7", + "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-57.0.7.tgz", + "integrity": "sha512-1vkZRadOsIputFDBcclqBa1pO3iXOO6iIsdqJipQWw4V5TSnWgmRTuOF7ro8IzrVue+PHbum++KCG3Tawem4cg==", "license": "MIT", "peerDependencies": { "expo": "*", @@ -6193,9 +6445,9 @@ } }, "node_modules/expo-font": { - "version": "57.0.3", - "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-57.0.3.tgz", - "integrity": "sha512-kiVUnc2A8vAvO2FfDJTsQa5BwmY+PAkof/1wRb5MOkcX1jtiaSTwz9gCUAyscMBFLundZDmZrLy1P7LZVC+NvA==", + "version": "57.0.4", + "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-57.0.4.tgz", + "integrity": "sha512-7WOMC2oA6xCfOm8bbofqWn2bIqyWFm7YLEnpc/gA1MJH0Ll0TfT/3+CqRkoW2eNRWMS8sxrz5WukUmSvmNquHA==", "license": "MIT", "dependencies": { "fontfaceobserver": "^2.1.0" @@ -6207,9 +6459,9 @@ } }, "node_modules/expo-glass-effect": { - "version": "57.0.1", - "resolved": "https://registry.npmjs.org/expo-glass-effect/-/expo-glass-effect-57.0.1.tgz", - "integrity": "sha512-m/n8maxqNcHk6ZDhuqXBfD5Kt1Iz3M8xykVgdB0iSCIXvF70IqWXmQhX8Psswhrp8eZ+3r0mAD0Jh/2gFA3QaA==", + "version": "57.0.3", + "resolved": "https://registry.npmjs.org/expo-glass-effect/-/expo-glass-effect-57.0.3.tgz", + "integrity": "sha512-aTh2J5eCxf+rSRMBJTnP8Ib5/k8DYFVDuEX1xqesN9F9aqTXocwMv/aQRVBNUs/iogDWhHIJz2pN97IsrrTcoA==", "license": "MIT", "peerDependencies": { "expo": "*", @@ -6218,9 +6470,9 @@ } }, "node_modules/expo-image": { - "version": "57.0.4", - "resolved": "https://registry.npmjs.org/expo-image/-/expo-image-57.0.4.tgz", - "integrity": "sha512-DLVNjqIMLb71wh0F27kRv7nA9i4g80fvkVugiimVgGnO7Iot+gRSv5VhrFJCvX/OlSzbGQqd/3ktlEaPGFqP7g==", + "version": "57.0.5", + "resolved": "https://registry.npmjs.org/expo-image/-/expo-image-57.0.5.tgz", + "integrity": "sha512-jOm5lofaIKXhQ7i0Hz3I5JuNULw/aghmpIPZXddwQcEtw8+wUhlozq3M7LbsOrcsBEapcG4jFs85DsfuFoz1tQ==", "license": "MIT", "dependencies": { "sf-symbols-typescript": "^2.2.0" @@ -6246,10 +6498,22 @@ "expo": "*" } }, + "node_modules/expo-image-manipulator": { + "version": "57.0.17", + "resolved": "https://registry.npmjs.org/expo-image-manipulator/-/expo-image-manipulator-57.0.17.tgz", + "integrity": "sha512-VpM8qAotTeSIobIAL8RAIDrZKL0jQBK/6O41NnwmdT96sd2wT263DlTbkvQ6RkcFrR9+NZiC5Um7FDBK+jFneg==", + "license": "MIT", + "dependencies": { + "expo-image-loader": "~57.0.1" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-image-picker": { - "version": "57.0.16", - "resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-57.0.16.tgz", - "integrity": "sha512-9CrY/hoBHI2GsOo/4AWz6cInRHhGhhCmbza3JYr4j3P/NWpthLlIm0IXSWpxadSYJx0a2FAti0f7XWMg95aPEg==", + "version": "57.0.17", + "resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-57.0.17.tgz", + "integrity": "sha512-qPcCkGSoqDLpmdXCLiLe09Je01gzBrwEgLd4SBI03Od/5t531vDTkzUmGemjm7vcsCTG2iqZMh2VZXBscAPO3g==", "license": "MIT", "dependencies": { "expo-image-loader": "~57.0.1" @@ -6259,18 +6523,28 @@ } }, "node_modules/expo-json-utils": { - "version": "57.0.1", - "resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-57.0.1.tgz", - "integrity": "sha512-cgTe1NqzQdYs/WN+3nIY5IZg8s0pb0xaTUbhYvxQDn137GbwRfHoGM2se3m3Vsl4Qu+B9G4RPEK5WJDEU2Do7g==", + "version": "57.0.2", + "resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-57.0.2.tgz", + "integrity": "sha512-9kYZECLKE3pYZQs+kt3+ggY83Lfryog9/UEo3HJKGcHUZeKKtlFBIPFjI4LzREubsvjP7AsvG0FdEX9EquIoAQ==", "license": "MIT" }, + "node_modules/expo-keep-awake": { + "version": "57.0.2", + "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-57.0.2.tgz", + "integrity": "sha512-GqgH746wtJImmsbyHmCQoi7cOslKuWQBbJHXr53S35Bpf5UvxCAztvnitUEd7D0QvFN2rvkOhylbZpgZZfBLlQ==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*" + } + }, "node_modules/expo-linking": { - "version": "57.0.9", - "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-57.0.9.tgz", - "integrity": "sha512-TQJe3NsnpZXApBg1n6DKaZ4TNhYm+iYWq7W9rTp+G44IBMZSUcI8ba+ffYC/LMY1qw+95AuBPIF1zTx1NQoTXw==", + "version": "57.0.10", + "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-57.0.10.tgz", + "integrity": "sha512-RzRi4l4pIYzjgoCJKnqkO+lUI+M2YX6qgcMIQkIbLAXdcWrykBoKYwHGlmAkdFHQm89GaYcq329yfxG+c1hv8w==", "license": "MIT", "dependencies": { - "expo-constants": "~57.0.17", + "expo-constants": "~57.0.18", "invariant": "^2.2.4" }, "peerDependencies": { @@ -6279,9 +6553,9 @@ } }, "node_modules/expo-localization": { - "version": "57.0.1", - "resolved": "https://registry.npmjs.org/expo-localization/-/expo-localization-57.0.1.tgz", - "integrity": "sha512-8Ffl4UTbOsQeGT0v5fxMbyPHyPMPnhSPDFQJa8p9rjJrthFoAtNi+fL6Ssmrvf1/7dmPq1mVY52MEt0TMEfgjA==", + "version": "57.0.2", + "resolved": "https://registry.npmjs.org/expo-localization/-/expo-localization-57.0.2.tgz", + "integrity": "sha512-sIKYH3Ivddoge568WY6j+Kp2MAAZzWWqEnY2bokLkJFwX366tGL1mEnoY2WUi/qHatFFPop2yqDdurzbhnnimQ==", "license": "MIT", "dependencies": { "rtl-detect": "^1.0.2" @@ -6292,21 +6566,21 @@ } }, "node_modules/expo-manifests": { - "version": "57.0.1", - "resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-57.0.1.tgz", - "integrity": "sha512-qB/mDG2dYdl+EvUeQuqP8KFYCFgFCQjJYdWIHo8SFBgDzMYmdF286DFY2M1M9Okr99wkb5M4tgA3aCcwv3aEQA==", + "version": "57.0.2", + "resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-57.0.2.tgz", + "integrity": "sha512-pPIIcKR73OShwlWMg8H90WRzKUd2krDdKlKAwuQV1FtUwE7H3hQEFw0ePFQY9yFoGMF/77ryYwNKfI5umMcCog==", "license": "MIT", "dependencies": { - "expo-json-utils": "~57.0.1" + "expo-json-utils": "~57.0.2" }, "peerDependencies": { "expo": "*" } }, "node_modules/expo-modules-autolinking": { - "version": "57.0.12", - "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-57.0.12.tgz", - "integrity": "sha512-Q8KAlq37nLKsQ+HsS9NpQVpd5jCgqtu694TDUNHBUBpV9ViD82mRBh8Uug/h68RG9xnLS+kuL4nYaCuFRghHjg==", + "version": "57.0.13", + "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-57.0.13.tgz", + "integrity": "sha512-Hj5NjZRlfccazhKab+wK19leRVUr1Y/PUERaRvegIbUSkq4GVxKq6lU9QHlKQyC16MdD/VS4iwsiLivB4o7lvQ==", "license": "MIT", "dependencies": { "@expo/require-utils": "^57.0.5", @@ -6318,47 +6592,26 @@ "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, - "node_modules/expo-modules-core": { - "version": "57.0.16", - "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-57.0.16.tgz", - "integrity": "sha512-vPbxDOdvG6Ucpws5ucdbQa+nNmdxKONS1fTg92sy0p4TnHX8hBiExtbrtUA/5+1t3FRCUuiy29/1nJEGdt3E4A==", - "license": "MIT", - "dependencies": { - "@expo/expo-modules-macros-plugin": "0.6.1", - "expo-modules-jsi": "~57.0.8", - "invariant": "^2.2.4" - }, - "peerDependencies": { - "react": "*", - "react-native": "*", - "react-native-worklets": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0" - }, - "peerDependenciesMeta": { - "react-native-worklets": { - "optional": true - } - } - }, "node_modules/expo-modules-jsi": { - "version": "57.0.8", - "resolved": "https://registry.npmjs.org/expo-modules-jsi/-/expo-modules-jsi-57.0.8.tgz", - "integrity": "sha512-MFwvKP0FXLkEET27KsKvOh/L7Dpu2d00UgzAlmKozJgtI23WTKSyKPxu+EOSs9peSgkHzKI0vQOhWjOseGBMkQ==", + "version": "57.1.0", + "resolved": "https://registry.npmjs.org/expo-modules-jsi/-/expo-modules-jsi-57.1.0.tgz", + "integrity": "sha512-a5ckeHfnbYfonhcHGkM0EU/a0Keh+/OufXy/HyFS+spr5Ib0N4Oh1ZsSYFQhp5xdOUFkVofKRhfH+VmcccIIpA==", "license": "MIT", "peerDependencies": { "react-native": "*" } }, "node_modules/expo-notifications": { - "version": "57.0.17", - "resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-57.0.17.tgz", - "integrity": "sha512-P/9ZqBOlFBVCZK3T5bhqORtWJhtVBcXQXe/MY1YWuKaxWgKvoGeEvD9ItWuo87jg2nD+eloVmFYqwdhhVCxPlA==", + "version": "57.0.18", + "resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-57.0.18.tgz", + "integrity": "sha512-EUyFHeP7pakTYO3UkxnK3CvzgrNSfe+IOxcCa8dX6DuSxSvIOcIzaQ2sNZ4XsvzmJzZPxl3MHKql2QVigDmg8A==", "license": "MIT", "dependencies": { "@expo/image-utils": "^0.11.5", "abort-controller": "^3.0.0", "badgin": "^1.1.5", - "expo-application": "~57.0.2", - "expo-constants": "~57.0.17" + "expo-application": "~57.0.3", + "expo-constants": "~57.0.18" }, "peerDependencies": { "expo": "*", @@ -6367,15 +6620,15 @@ } }, "node_modules/expo-router": { - "version": "57.0.19", - "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-57.0.19.tgz", - "integrity": "sha512-JovzZ066+qhykyJLC4hSEJpM8ToVWnJqbxmTN7USzARM0HgRR/g05a75f/EIsY4S//hbPGm6wGl/ObcCVtAX+A==", + "version": "57.0.21", + "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-57.0.21.tgz", + "integrity": "sha512-r7UDLZU0gtVm8FW4nv2pxSl9TDhKlf2shcBbfp1T9STXJ3xYl9llLH701qQkICuXqqt3vGpOZ57GXC1ioVWg4g==", "license": "MIT", "dependencies": { "@expo/log-box": "^57.0.4", "@expo/metro-runtime": "^57.0.15", "@expo/schema-utils": "^57.0.2", - "@expo/ui": "^57.0.16", + "@expo/ui": "^57.0.18", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-tabs": "^1.1.12", "@react-native-masked-view/masked-view": "^0.3.2", @@ -6383,12 +6636,12 @@ "color": "^4.2.3", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", - "expo-glass-effect": "^57.0.1", + "expo-glass-effect": "^57.0.3", "expo-server": "^57.0.3", - "expo-symbols": "^57.0.2", + "expo-symbols": "^57.0.3", "fast-deep-equal": "^3.1.3", "invariant": "^2.2.4", - "nanoid": "^3.3.8", + "nanoid": "^3.3.18", "query-string": "^7.1.3", "react-fast-compare": "^3.2.2", "react-is": "^19.1.0", @@ -6405,8 +6658,8 @@ "@expo/metro-runtime": "^57.0.15", "@testing-library/react-native": ">= 13.2.0", "expo": "*", - "expo-constants": "^57.0.17", - "expo-linking": "^57.0.9", + "expo-constants": "^57.0.18", + "expo-linking": "^57.0.10", "react": "*", "react-dom": "*", "react-native": "*", @@ -6445,9 +6698,9 @@ "license": "MIT" }, "node_modules/expo-secure-store": { - "version": "57.0.3", - "resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-57.0.3.tgz", - "integrity": "sha512-w7XkSQeUiYXPoKXo1jSrQqql7pyCSyIzOp2k0apsZZBE+RkoLTQIMjcbqc181y6KgupfNnYxkaKe+4MEg94+SA==", + "version": "57.0.4", + "resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-57.0.4.tgz", + "integrity": "sha512-7zBA1RThURTXdP4w65kBzZNbcSsfArqVd+p+ETAnPRWXO8J296Qd0YaCVyRpJ0HSwlCnGdvOnqO+NgSreHsOhA==", "license": "MIT", "peerDependencies": { "expo": "*" @@ -6463,9 +6716,9 @@ } }, "node_modules/expo-splash-screen": { - "version": "57.0.8", - "resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-57.0.8.tgz", - "integrity": "sha512-BEsrKg4niYBZa5AFzWRyGqxA4ZemtBOPwbCy+C336iGYYPuZYLEgWJGmm2xD1gRfCdrM/TP+l2ONd86Zvk3KyA==", + "version": "57.0.9", + "resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-57.0.9.tgz", + "integrity": "sha512-7cKDn7uBwg0RkcmY015/5j9st4msWJ4zQiPmQdVZoppW3cFSbv05n7xhM6CjUU466Tx4VRTqDF21HuIHkmnEtA==", "license": "MIT", "dependencies": { "@expo/config-plugins": "~57.0.9", @@ -6488,15 +6741,15 @@ } }, "node_modules/expo-structured-headers": { - "version": "57.0.0", - "resolved": "https://registry.npmjs.org/expo-structured-headers/-/expo-structured-headers-57.0.0.tgz", - "integrity": "sha512-//t9UNPbJSEysc2x4VKJG/u7Osvv5DYJWsET5bqt/B+qcD1by/JXvSQzX3Q/YAgA96xFPontrz6OAPLbO4JKEA==", + "version": "57.0.1", + "resolved": "https://registry.npmjs.org/expo-structured-headers/-/expo-structured-headers-57.0.1.tgz", + "integrity": "sha512-5VwmitVr2J716pOuHXWmEnHbZhZg63DBOZKH5tj0SR8Sps/3jSAf9wf9+UGTKcKgAxGf1PPwQ0lAQ2bG3RwjAg==", "license": "MIT" }, "node_modules/expo-symbols": { - "version": "57.0.2", - "resolved": "https://registry.npmjs.org/expo-symbols/-/expo-symbols-57.0.2.tgz", - "integrity": "sha512-qZ0iqOflm5lZGwRsQ5Y8sDksw3GAUKwHSX1bJBoocXf7gu14vafIXXYWte+JT9VfXAUGyKodJhLHP/GoOrcNWg==", + "version": "57.0.3", + "resolved": "https://registry.npmjs.org/expo-symbols/-/expo-symbols-57.0.3.tgz", + "integrity": "sha512-uuK5yHLVR7kExUCrO/ItBvp/kp1maQ9Nkb6KvNy5ZUQnN0NMKwuhJ4GvbfS9N3ACmvCjTT4gKvg4yH5UuKIYNA==", "license": "MIT", "dependencies": { "@expo-google-fonts/material-symbols": "^0.4.1", @@ -6510,9 +6763,9 @@ } }, "node_modules/expo-system-ui": { - "version": "57.0.3", - "resolved": "https://registry.npmjs.org/expo-system-ui/-/expo-system-ui-57.0.3.tgz", - "integrity": "sha512-3N3BoagGojEvQw0h7kL0Lvv2LrVjqEvODSNS+Nc9lj/kzaTaVx4K1HI8qqxeALMf5jui3rLP2b3Ai2pXotoNuA==", + "version": "57.0.4", + "resolved": "https://registry.npmjs.org/expo-system-ui/-/expo-system-ui-57.0.4.tgz", + "integrity": "sha512-z//rxQJzhTNA1PcKrUtWmrd21jAS3nEDu1b0n8B/7LTTR933OcsZYP1HnbaZifuo2ZH/h7nce2Ow61WZ1Ig2bg==", "license": "MIT", "dependencies": { "@react-native/normalize-colors": "0.86.3", @@ -6530,9 +6783,9 @@ } }, "node_modules/expo-updates": { - "version": "57.0.21", - "resolved": "https://registry.npmjs.org/expo-updates/-/expo-updates-57.0.21.tgz", - "integrity": "sha512-JwTmguNvBvISEz9ALwrT8yDLnYuSP4/6kUz0kXkQuC8izebtTGkRPR37Cv39KPheDDA1F+QPhNyvw4lXM/BPvA==", + "version": "57.0.22", + "resolved": "https://registry.npmjs.org/expo-updates/-/expo-updates-57.0.22.tgz", + "integrity": "sha512-W/wR3PZiIiu+DtbNFju0WXJF0eqR6m0CNorjmxoQ9/RGiplpIZTrFOBp74dwsU1DOk3qpsivfLHsotR+mGTwkQ==", "license": "MIT", "dependencies": { "@expo/code-signing-certificates": "^0.0.6", @@ -6541,10 +6794,10 @@ "arg": "^4.1.0", "chalk": "^4.1.2", "debug": "^4.3.4", - "expo-eas-client": "~57.0.3", - "expo-manifests": "~57.0.1", - "expo-structured-headers": "~57.0.0", - "expo-updates-interface": "~57.0.1", + "expo-eas-client": "~57.0.4", + "expo-manifests": "~57.0.2", + "expo-structured-headers": "~57.0.1", + "expo-updates-interface": "~57.0.2", "getenv": "^2.0.0", "glob": "^13.0.0", "ignore": "^5.3.1", @@ -6567,9 +6820,9 @@ } }, "node_modules/expo-updates-interface": { - "version": "57.0.1", - "resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-57.0.1.tgz", - "integrity": "sha512-+LUWwJ0gf/TEKMVdQAw/Gjih4dvrk+URgy24X9qEGKuuMDZqjBRm9T4yQyBVALGL5TTdPUaB6ILxx3lshm3pwQ==", + "version": "57.0.2", + "resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-57.0.2.tgz", + "integrity": "sha512-x3SA5toiHZEkW+5oeNdUM1Rvt6M3ExegQsSwS3Z1+S9gn9utGYTGBkb8MBQivhjIZiRlQYDIhauxA8kySQEWtQ==", "license": "MIT", "peerDependencies": { "expo": "*" @@ -6582,9 +6835,9 @@ "license": "MIT" }, "node_modules/expo-video": { - "version": "57.0.3", - "resolved": "https://registry.npmjs.org/expo-video/-/expo-video-57.0.3.tgz", - "integrity": "sha512-Z+rLdBSzICwoHm/HUxND5fm5nfgqiB+QPWAOKxmA8ScYwvxG1UGjZ6OaayvPc3GkT4aucsbycmFO0uUv/qnIhg==", + "version": "57.0.4", + "resolved": "https://registry.npmjs.org/expo-video/-/expo-video-57.0.4.tgz", + "integrity": "sha512-4YCWG1IJzUNpLX0xoussK0VOG524qgUiMbwJzrgpDuPUzOgR3ZqXSZxKsy3HlYLiuWdzIQzwfpbtSNim9F9D3Q==", "license": "MIT", "peerDependencies": { "expo": "*", @@ -6592,10 +6845,20 @@ "react-native": "*" } }, + "node_modules/expo-web-browser": { + "version": "57.0.3", + "resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-57.0.3.tgz", + "integrity": "sha512-+ecvhyS/PdWkpv4ai6FF1Trd6Y/4kn/eQ3O86mie2ecU3w+I8bKZh13kppY/jw1uSL7yikt03L5W/iiHojrFwQ==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, "node_modules/expo/node_modules/@expo/cli": { - "version": "57.0.22", - "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-57.0.22.tgz", - "integrity": "sha512-MdToR0ZZ1yjfkqbP1jcA7viS9duMPGq/C+qQ/21dtq+en583Ghp+O11BP/wujHa9ZGRtXimzuKPZDpPresVE0g==", + "version": "57.0.24", + "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-57.0.24.tgz", + "integrity": "sha512-CCBkvhPgAcrzJQ3daYbyOD6YuqKKkvOIyUfAbsUIvlpLomIlocYxFHYdmaC/KHqfCsbGFYSV/QKTSYshhSFNEA==", "license": "MIT", "dependencies": { "@expo/code-signing-certificates": "^0.0.6", @@ -6609,11 +6872,11 @@ "@expo/log-box": "^57.0.4", "@expo/metro": "~56.0.2", "@expo/metro-config": "~57.0.12", - "@expo/metro-file-map": "^57.0.2", + "@expo/metro-file-map": "^57.0.3", "@expo/osascript": "^2.7.1", "@expo/package-manager": "^1.13.1", "@expo/plist": "^0.8.1", - "@expo/prebuild-config": "^57.0.15", + "@expo/prebuild-config": "^57.0.16", "@expo/require-utils": "^57.0.5", "@expo/router-server": "^57.0.9", "@expo/schema-utils": "^57.0.2", @@ -6746,14 +7009,25 @@ "node": ">=8" } }, - "node_modules/expo/node_modules/expo-keep-awake": { - "version": "57.0.1", - "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-57.0.1.tgz", - "integrity": "sha512-28lkFImeXTS+bhAjuCFV7w7tW5bXg27BJVrxv+nC/nyYa86qEa0oFeHwqol6ha5k4pdVDQgBF09GM4A1k76Ssg==", + "node_modules/expo/node_modules/expo-modules-core": { + "version": "57.0.18", + "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-57.0.18.tgz", + "integrity": "sha512-ovB7C+GVQTlKAOIsOIHJVVXN9Mn0ljf2SYBS+afrRaGqq+HZEn1D3o+HReIVeH1NjCn/xDBb7I4HjldHs6luZA==", "license": "MIT", + "dependencies": { + "@expo/expo-modules-macros-plugin": "0.6.1", + "expo-modules-jsi": "~57.1.0", + "invariant": "^2.2.4" + }, "peerDependencies": { - "expo": "*", - "react": "*" + "react": "*", + "react-native": "*", + "react-native-worklets": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0" + }, + "peerDependenciesMeta": { + "react-native-worklets": { + "optional": true + } } }, "node_modules/expo/node_modules/mime-db": { @@ -6798,6 +7072,34 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/expo/node_modules/react-native-worklets": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.10.4.tgz", + "integrity": "sha512-gbk0CxMrGywEp3SJAHO+2JVaCWtl4WyAYBIxLZy6Y113+mHHuq3FuT6iV+VcaBuJzuuN34mtdZ4j5JIlBDzmeA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-class-properties": "^7.28.6", + "@babel/plugin-transform-classes": "^7.28.6", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", + "@babel/plugin-transform-optional-chaining": "^7.28.6", + "@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.28.5", + "@babel/types": "^7.27.1", + "convert-source-map": "^2.0.0", + "semver": "^7.7.4" + }, + "peerDependencies": { + "@babel/core": "*", + "@react-native/metro-config": "*", + "react": "*", + "react-native": "0.83 - 0.86" + } + }, "node_modules/expo/node_modules/ws": { "version": "8.21.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", @@ -8477,7 +8779,6 @@ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "license": "MIT", - "peer": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -9049,6 +9350,12 @@ "node": ">= 6" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/qrcode": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", @@ -10370,8 +10677,7 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tslib": { "version": "2.8.1", @@ -10612,6 +10918,119 @@ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/vaul/node_modules/@radix-ui/react-dialog": { + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.23.tgz", + "integrity": "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/vaul/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.19.tgz", + "integrity": "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-effect-event": "0.0.5" + }, + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/vaul/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.16.tgz", + "integrity": "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4" + }, + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/vaul/node_modules/@radix-ui/react-portal": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.17.tgz", + "integrity": "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/viem": { "version": "2.56.1", "resolved": "https://registry.npmjs.org/viem/-/viem-2.56.1.tgz", @@ -10775,8 +11194,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause", - "peer": true + "license": "BSD-2-Clause" }, "node_modules/whatwg-fetch": { "version": "3.6.20", @@ -10789,7 +11207,6 @@ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "license": "MIT", - "peer": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" diff --git a/mobile/package.json b/mobile/package.json index 484d3d0..3d07a30 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -5,28 +5,32 @@ "dependencies": { "@clerk/expo": "^4.6.1", "@expo-google-fonts/inter": "^0.4.2", + "@sentry/react-native": "~7.11.0", "@tanstack/query-sync-storage-persister": "^5.102.8", "@tanstack/react-query": "5.102.8", "@tanstack/react-query-persist-client": "^5.102.8", "@trpc/client": "11.17.0", "@trpc/tanstack-react-query": "11.17.0", - "expo": "~57.0.20", + "expo": "~57.0.22", + "expo-auth-session": "~57.0.12", "expo-constants": "~57.0.16", - "expo-dev-client": "~57.0.18", + "expo-dev-client": "~57.0.19", "expo-file-system": "~57.0.6", "expo-font": "~57.0.3", - "expo-image": "~57.0.4", - "expo-image-picker": "~57.0.16", - "expo-linking": "~57.0.9", - "expo-localization": "~57.0.1", - "expo-notifications": "~57.0.17", - "expo-router": "~57.0.19", - "expo-secure-store": "~57.0.3", - "expo-splash-screen": "~57.0.8", + "expo-image": "~57.0.5", + "expo-image-manipulator": "~57.0.17", + "expo-image-picker": "~57.0.17", + "expo-linking": "~57.0.10", + "expo-localization": "~57.0.2", + "expo-notifications": "~57.0.18", + "expo-router": "~57.0.21", + "expo-secure-store": "~57.0.4", + "expo-splash-screen": "~57.0.9", "expo-status-bar": "~57.0.1", - "expo-system-ui": "~57.0.3", - "expo-updates": "~57.0.21", - "expo-video": "~57.0.3", + "expo-system-ui": "~57.0.4", + "expo-updates": "~57.0.22", + "expo-video": "~57.0.4", + "expo-web-browser": "~57.0.3", "i18next": "^26.4.2", "lucide-react-native": "^1.41.0", "react": "19.2.3", diff --git a/mobile/src/components/GoogleIcon.tsx b/mobile/src/components/GoogleIcon.tsx new file mode 100644 index 0000000..dfa4872 --- /dev/null +++ b/mobile/src/components/GoogleIcon.tsx @@ -0,0 +1,25 @@ +import Svg, { Path } from "react-native-svg"; + +/** Standard four-color Google "G" mark per Google's sign-in button branding guidelines. */ +export function GoogleIcon({ size = 18 }: { size?: number }) { + return ( + + + + + + + ); +} diff --git a/mobile/src/components/GoogleSignInButton.tsx b/mobile/src/components/GoogleSignInButton.tsx new file mode 100644 index 0000000..5091502 --- /dev/null +++ b/mobile/src/components/GoogleSignInButton.tsx @@ -0,0 +1,126 @@ +import { useMemo, useState } from "react"; +import { Pressable, Text, StyleSheet } from "react-native"; +import { useRouter } from "expo-router"; +import { useTranslation } from "react-i18next"; +import { useSSO } from "@clerk/expo"; +import * as Sentry from "@sentry/react-native"; +import { useTheme } from "../context/ThemeContext"; +import { useWarmUpBrowser } from "../hooks/useWarmUpBrowser"; +import { radii, fontFamily, type ColorTokens } from "../theme/tokens"; +import { GoogleIcon } from "./GoogleIcon"; + +type Props = { + onError: (message: string) => void; + /** + * Whether the caller already has affirmative legal consent to attach if + * Clerk reports it as a missing requirement (see below). Pass the ticked + * checkbox value from sign-up.tsx; omit on sign-in.tsx, where there's no + * consent UI to source it from. + */ + legalAccepted?: boolean; + /** + * Called instead of completing the flow when Google authenticates someone + * with no existing PawFeed account and no `legalAccepted` was supplied — + * i.e. sign-in.tsx was used to (attempt to) create a brand-new account. + * Required on sign-in.tsx; sign-up.tsx doesn't need it because it gates + * the button on its own checkbox instead (see its `disabled` usage). + */ + onNeedsConsent?: () => void; + disabled?: boolean; +}; + +/** + * "Continue with Google" — shared by sign-in.tsx and sign-up.tsx since + * Clerk's SSO flow creates a session (and a new account, if none exists yet) + * in one step; there's no separate sign-up variant to build. + * + * Uses the stable `useSSO()` from `@clerk/expo` (not `/experimental`), the + * import path already used for useSignIn/useSignUp elsewhere in this app — + * see AGENTS.md on why this project's installed Clerk version puts its + * Futures-style API in the stable package rather than under `/experimental`. + * + * This Clerk instance requires legal consent at sign-up + * (`sign_up.legal_consent_enabled: true`, verified live via its public + * /v1/environment config — same check sign-up.tsx's own comment describes + * for the password flow). Google's transfer-based account creation inside + * useSSO() has no field for it, so a brand-new Google sign-up otherwise gets + * stuck on `missing_requirements` exactly like the password flow did before + * that fix — handled below via `signUp.update({ legalAccepted: true })` + * once we know consent was actually given (legalAccepted param truthy). + */ +export function GoogleSignInButton({ onError, legalAccepted, onNeedsConsent, disabled }: Props) { + const { t } = useTranslation(); + const { colors } = useTheme(); + const styles = useMemo(() => makeStyles(colors), [colors]); + const router = useRouter(); + const { startSSOFlow } = useSSO(); + const [isPending, setIsPending] = useState(false); + useWarmUpBrowser(); + + const onPress = async () => { + setIsPending(true); + try { + const ssoResult = await startSSOFlow({ strategy: "oauth_google" }); + const { setActive, signUp } = ssoResult; + let createdSessionId = ssoResult.createdSessionId; + + if (!createdSessionId && signUp?.status === "missing_requirements" && signUp.missingFields.includes("legal_accepted")) { + if (!legalAccepted) { + onNeedsConsent?.(); + return; + } + const updatedSignUp = await signUp.update({ legalAccepted: true }); + createdSessionId = updatedSignUp.createdSessionId; + } + + if (createdSessionId && setActive) { + await setActive({ session: createdSessionId }); + router.replace("/(app)"); + } + // No createdSessionId otherwise (e.g. the user cancelled the browser + // flow) — nothing to do, they're still looking at this screen. + } catch (err) { + // Previously only shown locally — a redirect/OAuth failure a tester + // hits and can't reproduce left no trace anywhere (found.md: "redirect + // problem beim Login mit Google, konnte ich nicht reproduzieren"). + Sentry.captureException(err); + onError(err instanceof Error ? err.message : t("auth.googleSignInError")); + } finally { + setIsPending(false); + } + }; + + return ( + [ + styles.base, + state.pressed && !isPending && !disabled && styles.pressed, + (isPending || disabled) && styles.disabled, + ]} + disabled={isPending || disabled} + onPress={onPress} + > + + {t("auth.googleButton")} + + ); +} + +function makeStyles(colors: ColorTokens) { + return StyleSheet.create({ + base: { + height: 48, + borderRadius: radii.lg, + borderWidth: 1, + borderColor: colors.hairlineGray, + backgroundColor: colors.pureWhite, + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + gap: 10, + }, + pressed: { transform: [{ translateY: 1 }], opacity: 0.92 }, + disabled: { opacity: 0.5 }, + text: { color: colors.ink, fontFamily: fontFamily.semiBold, fontSize: 14 }, + }); +} diff --git a/mobile/src/components/OptionPicker.tsx b/mobile/src/components/OptionPicker.tsx index ab71cba..a6b1121 100644 --- a/mobile/src/components/OptionPicker.tsx +++ b/mobile/src/components/OptionPicker.tsx @@ -1,5 +1,5 @@ -import { useMemo } from "react"; -import { View, Text, Pressable, ScrollView, ActivityIndicator, StyleSheet } from "react-native"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { View, Text, Pressable, ScrollView, ActivityIndicator, Modal, StyleSheet } from "react-native"; import { ChevronDown } from "lucide-react-native"; import { useTranslation } from "react-i18next"; import { useTheme } from "../context/ThemeContext"; @@ -21,24 +21,32 @@ type Props = { const BUTTON_HEIGHT = 48; const LIST_GAP = 4; +const MAX_LIST_HEIGHT = 220; + +type Anchor = { top: number; left: number; width: number }; /** * A dropdown-style button that expands into a scrollable option list — * extracted from onboarding/PetCreateForm once explore.tsx's breed filter - * needed the exact same thing. Deliberately a plain ScrollView + map, not a - * FlatList: this is meant to sit inside a screen-level ScrollView, and - * nesting a VirtualizedList inside a ScrollView with the same orientation - * breaks touch/scroll handling (RN's "VirtualizedLists should never be - * nested..." warning) — these option lists are at most a few dozen items, - * so virtualization buys nothing here anyway. + * needed the exact same thing. * - * The option list is position:absolute so it overlays whatever comes after - * it instead of pushing it down (found.md: the breed dropdown on Explore was - * shoving the pet grid down the screen). zIndex is applied to the whole - * component root — not just the list — because the sibling content it needs - * to overlay renders after it in the same parent's paint order, and RN only - * lets zIndex reorder stacking among siblings, not push a child above its - * own subtree's siblings from the inside. + * The option list renders inside a `Modal`, not as a `position: absolute` + * sibling — every consumer embeds this inside an outer FlatList (see + * onboarding/pet.tsx, edit.tsx, explore.tsx), and having *this* component's + * own vertical ScrollView nested inside that outer FlatList (itself a + * VirtualizedList/ScrollView under the hood) silently ate the inner + * ScrollView's touch gesture on Android — `nestedScrollEnabled` is a known + * pain point (Android-only, notoriously unreliable across RN/Android + * versions) rather than a real fix. A Modal renders into its own native + * layer entirely outside the JS view/scroll hierarchy, so this sidesteps + * the nested-same-axis-scroll problem structurally instead of working + * around it — found.md: reported again on Android 17 (Pixel 10 Pro XL) + * after the nestedScrollEnabled-based fix. + * + * Since a Modal covers the whole screen by default, the trigger button's + * on-screen position is measured (measureInWindow) when opening and used to + * position the option list right under the button, keeping the same + * inline-dropdown look the absolute-positioned version had. */ export function OptionPicker({ open, @@ -54,30 +62,62 @@ export function OptionPicker({ const { t } = useTranslation(); const { colors } = useTheme(); const styles = useMemo(() => makeStyles(colors), [colors]); + const triggerRef = useRef(null); + const [anchor, setAnchor] = useState(null); + + useEffect(() => { + // No need to reset `anchor` to null when closing — the Modal already + // isn't `visible` then, and this effect re-measures before anything + // renders with it on the next open, so a stale value in between is + // never actually seen. + if (!open) return; + triggerRef.current?.measureInWindow((x, y, width, height) => { + setAnchor({ top: y + height + LIST_GAP, left: x, width }); + }); + }, [open]); return ( - - + + {selectedName ?? placeholder} - {open && ( - - {isLoading ? ( - - ) : options.length === 0 ? ( - {emptyLabel ?? t("common.noResultsFound")} - ) : ( - - {options.map((option) => ( - onSelect(option)}> - {option.name} - - ))} - - )} - - )} + + {/* + Backdrop and list are SIBLINGS, not parent/child. The list must + not be a descendant of the backdrop Pressable at all — a Pressable + ANYWHERE above a ScrollView in the tree is still a competing + responder candidate for the scroll gesture, which is exactly why + the previous fix (only de-Pressable-ing the card itself, still + nested inside this backdrop) didn't actually fix the "not + scrollable" report. Keeping them as siblings means a touch inside + the list's bounds hit-tests into the list's own subtree and never + reaches the backdrop's Pressable underneath at all. + */} + + {anchor && ( + + {isLoading ? ( + + ) : options.length === 0 ? ( + {emptyLabel ?? t("common.noResultsFound")} + ) : ( + + {options.map((option) => ( + onSelect(option)}> + {option.name} + + ))} + + )} + + )} + ); } @@ -85,8 +125,6 @@ export function OptionPicker({ function makeStyles(colors: ColorTokens) { const typography: Typography = getTypography(colors); return StyleSheet.create({ - root: { zIndex: 0 }, - rootOpen: { zIndex: 30 }, button: { height: BUTTON_HEIGHT, borderRadius: radii.lg, @@ -101,15 +139,13 @@ function makeStyles(colors: ColorTokens) { buttonDisabled: { opacity: 0.5 }, value: { ...typography.body }, placeholder: { ...typography.body, color: colors.quietGray }, + backdrop: { flex: 1 }, list: { position: "absolute", - top: BUTTON_HEIGHT + LIST_GAP, - left: 0, - right: 0, borderWidth: 1, borderColor: colors.hairlineGray, borderRadius: radii.lg, - maxHeight: 220, + maxHeight: MAX_LIST_HEIGHT, overflow: "hidden", backgroundColor: colors.pureWhite, elevation: 8, @@ -118,7 +154,7 @@ function makeStyles(colors: ColorTokens) { shadowOpacity: 0.15, shadowRadius: 8, }, - scrollView: { maxHeight: 220 }, + scrollView: { maxHeight: MAX_LIST_HEIGHT }, row: { paddingHorizontal: 14, paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: colors.hairlineGray }, loading: { padding: 16 }, empty: { ...typography.body, color: colors.quietGray, padding: 16 }, diff --git a/mobile/src/components/onboarding/AvatarStep.tsx b/mobile/src/components/onboarding/AvatarStep.tsx index 63aa2c3..fc3f7b8 100644 --- a/mobile/src/components/onboarding/AvatarStep.tsx +++ b/mobile/src/components/onboarding/AvatarStep.tsx @@ -6,12 +6,11 @@ import { useMutation } from "@tanstack/react-query"; import { ImagePlus, Camera } from "lucide-react-native"; import { useTRPC } from "../../api/trpc"; import { uploadImageToSupabase } from "../../lib/upload"; +import { resizeForUpload } from "../../lib/resize-image"; import { PetAvatar } from "../PetAvatar"; import { Button } from "../Button"; import { colors, radii, typography } from "../../theme/tokens"; -const ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/webp"]; - type Props = { petId: string; petName: string; @@ -43,14 +42,14 @@ export function AvatarStep({ petId, petName, onDone }: Props) { setAsset(picked); setError(null); - const mimeType = ALLOWED_MIME_TYPES.includes(picked.mimeType ?? "") ? (picked.mimeType as string) : "image/jpeg"; try { + const resized = await resizeForUpload(picked.uri, picked.width, picked.height); const { presignedUrl, key } = await getPresignedUrlMutation.mutateAsync({ petId, - contentType: mimeType as "image/jpeg" | "image/png" | "image/webp", - fileSize: picked.fileSize ?? 0, + contentType: resized.mimeType, + fileSize: resized.fileSize, }); - await uploadImageToSupabase(picked.uri, presignedUrl, mimeType); + await uploadImageToSupabase(resized.uri, presignedUrl, resized.mimeType); await confirmUploadMutation.mutateAsync({ petId, key }); setUploadedKey(key); } catch { diff --git a/mobile/src/context/ThemeContext.tsx b/mobile/src/context/ThemeContext.tsx index 6685af8..e9d862b 100644 --- a/mobile/src/context/ThemeContext.tsx +++ b/mobile/src/context/ThemeContext.tsx @@ -6,9 +6,23 @@ import { useTRPC } from "../api/trpc"; import { storage } from "../lib/storage"; import { colors as lightColors, darkColors, getTypography, type ColorTokens, type Typography } from "../theme/tokens"; -const THEME_PREFERENCE_KEY = "theme_preference"; +export const THEME_PREFERENCE_KEY = "theme_preference"; type ThemePreference = "LIGHT" | "DARK" | null; +/** + * Same "local guess" logic ThemeProvider uses for its first-frame paint, + * exposed standalone so screens that render before ThemeProvider mounts + * (app/_layout.tsx's LoadingScreen, shown pre-auth/pre-fonts) can still + * pick the right palette instead of hardcoding the light one — the cause of + * a reported black-on-black flash right after login for a dark-mode user. + */ +export function resolveInitialColors(systemScheme: string | null | undefined): ColorTokens { + const stored = storage.getString(THEME_PREFERENCE_KEY); + const preference: ThemePreference = stored === "LIGHT" || stored === "DARK" ? stored : null; + const isDark = preference === "DARK" || (preference === null && systemScheme === "dark"); + return isDark ? darkColors : lightColors; +} + type ThemeContextValue = { colors: ColorTokens; typography: Typography; diff --git a/mobile/src/hooks/useWarmUpBrowser.ts b/mobile/src/hooks/useWarmUpBrowser.ts new file mode 100644 index 0000000..a8e3fd0 --- /dev/null +++ b/mobile/src/hooks/useWarmUpBrowser.ts @@ -0,0 +1,17 @@ +import { useEffect } from "react"; +import * as WebBrowser from "expo-web-browser"; + +/** + * Pre-warms the Android Custom Tab / iOS SFSafariViewController so the + * Google OAuth browser session opens without a cold-start delay — Clerk's + * documented pattern for useSSO(). No-op cleanup on unmount avoids leaking + * the warmed instance once the sign-in/sign-up screen is left. + */ +export function useWarmUpBrowser() { + useEffect(() => { + void WebBrowser.warmUpAsync(); + return () => { + void WebBrowser.coolDownAsync(); + }; + }, []); +} diff --git a/mobile/src/lib/env.ts b/mobile/src/lib/env.ts index 32be78b..c4b1ad9 100644 --- a/mobile/src/lib/env.ts +++ b/mobile/src/lib/env.ts @@ -17,4 +17,7 @@ export const env = { process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY ), supabaseUrl: requireEnv("EXPO_PUBLIC_SUPABASE_URL", process.env.EXPO_PUBLIC_SUPABASE_URL), + // Optional, not requireEnv() — error reporting should degrade to a no-op + // when unset (local dev without a DSN configured), not crash the app. + sentryDsn: process.env.EXPO_PUBLIC_SENTRY_DSN, }; diff --git a/mobile/src/lib/resize-image.ts b/mobile/src/lib/resize-image.ts new file mode 100644 index 0000000..a23df15 --- /dev/null +++ b/mobile/src/lib/resize-image.ts @@ -0,0 +1,39 @@ +import { ImageManipulator, SaveFormat } from "expo-image-manipulator"; +import { File } from "expo-file-system"; + +const MAX_DIMENSION = 1600; +const COMPRESS_QUALITY = 0.8; + +export type ResizedImage = { + uri: string; + mimeType: "image/jpeg"; + fileSize: number; +}; + +/** + * Downscales a picked photo to a feed-appropriate resolution before upload. + * expo-image-picker's `quality: 0.8` only re-encodes at the camera's native + * resolution (often 3000-4000px wide) — every future viewer then downloads + * that full-size original just to display it at card width. Supabase's + * Image Transform API would resize on read instead, but that's a Pro-plan + * feature this project doesn't have, so the fix has to happen once here, at + * write time. + * + * Always re-encodes to JPEG regardless of the source format — these are + * photos, not graphics that need PNG's lossless/alpha behavior, and JPEG + * compresses substantially further at an equivalent visual quality. + */ +export async function resizeForUpload(uri: string, width: number, height: number): Promise { + const needsResize = width > 0 && height > 0 && Math.max(width, height) > MAX_DIMENSION; + const context = ImageManipulator.manipulate(uri); + const withResize = needsResize + ? width >= height + ? context.resize({ width: MAX_DIMENSION }) + : context.resize({ height: MAX_DIMENSION }) + : context; + + const rendered = await withResize.renderAsync(); + const result = await rendered.saveAsync({ compress: COMPRESS_QUALITY, format: SaveFormat.JPEG }); + + return { uri: result.uri, mimeType: "image/jpeg", fileSize: new File(result.uri).size }; +}