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 };
+}