diff --git a/.env.example b/.env.example index 10366bf1..1832b102 100644 --- a/.env.example +++ b/.env.example @@ -20,6 +20,11 @@ AUTH_DISCORD_SECRET='your_discord_client_secret' # Expo app API URL (for local development, set to localhost:3000) EXPO_PUBLIC_API_URL=http://localhost:3000 +# PostHog product analytics (optional) — analytics are disabled if the key is unset. +# Project API key + host from https://posthog.com (Project Settings) +EXPO_PUBLIC_POSTHOG_KEY= +EXPO_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com + # Google Custom Search API (for article images) # API Key: https://console.cloud.google.com/apis/credentials # Search Engine: https://programmablesearchengine.google.com/ diff --git a/apps/expo/package.json b/apps/expo/package.json index b5a191c9..22ec13c7 100644 --- a/apps/expo/package.json +++ b/apps/expo/package.json @@ -37,8 +37,10 @@ "expo-dev-client": "~5.2.4", "expo-font": "~13.3.2", "expo-image": "~2.4.1", + "expo-image-picker": "~16.0.6", "expo-linear-gradient": "~14.1.5", "expo-linking": "~7.1.7", + "expo-location": "~18.1.6", "expo-network": "~7.1.5", "expo-router": "~5.1.11", "expo-secure-store": "~14.2.4", @@ -49,6 +51,7 @@ "expo-web-browser": "~14.2.0", "fuse.js": "^7.1.0", "nativewind": "5.0.0-preview.3", + "posthog-react-native": "^3.3.0", "react": "19.0.0", "react-dom": "19.0.0", "react-native": "~0.79.6", diff --git a/apps/expo/src/app/_layout.tsx b/apps/expo/src/app/_layout.tsx index 1af371cb..86ae7c8b 100644 --- a/apps/expo/src/app/_layout.tsx +++ b/apps/expo/src/app/_layout.tsx @@ -28,6 +28,7 @@ import { InriaSerif_700Bold_Italic, } from "@expo-google-fonts/inria-serif"; import { QueryClientProvider } from "@tanstack/react-query"; +import { PostHogProvider } from "posthog-react-native"; import { useTheme } from "~/styles"; import { queryClient } from "~/utils/api"; @@ -87,7 +88,9 @@ export default function RootLayout() { void loadFonts(); }, []); - return ( + const posthogKey = process.env.EXPO_PUBLIC_POSTHOG_KEY; + + const tree = ( ); + + if (posthogKey) { + return ( + + {tree} + + ); + } + + return tree; } diff --git a/apps/expo/src/app/settings/edit-profile.tsx b/apps/expo/src/app/settings/edit-profile.tsx index cceac303..f71ec30e 100644 --- a/apps/expo/src/app/settings/edit-profile.tsx +++ b/apps/expo/src/app/settings/edit-profile.tsx @@ -1,11 +1,19 @@ import { useState } from "react"; -import { StyleSheet, TextInput, TouchableOpacity, View } from "react-native"; +import { + Alert, + StyleSheet, + TextInput, + TouchableOpacity, + View, +} from "react-native"; +import * as ImagePicker from "expo-image-picker"; import { useMutation, useQuery } from "@tanstack/react-query"; import { Text } from "~/components/Themed"; import { Avatar, GhostButton, Icon, ScreenShell } from "~/components/ui"; import { colors, fontBody, hair, planes } from "~/styles"; import { queryClient, trpc } from "~/utils/api"; +import { authClient } from "~/utils/auth"; function getInitials(name: string): string { return name @@ -22,6 +30,7 @@ export default function EditProfileScreen() { const [name, setName] = useState(""); const [email, setEmail] = useState(""); + const [nameError, setNameError] = useState(null); const [synced, setSynced] = useState(false); if (sessionUser && !synced) { @@ -39,14 +48,63 @@ export default function EditProfileScreen() { }, }); + const deleteAccount = useMutation(trpc.user.deleteAccount.mutationOptions()); + + const handleChangePhoto = async () => { + const perm = await ImagePicker.requestMediaLibraryPermissionsAsync(); + if (!perm.granted) { + Alert.alert( + "Permission needed", + "Allow photo library access to change your profile photo.", + ); + return; + } + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsEditing: true, + aspect: [1, 1], + quality: 0.5, + base64: true, + }); + const asset = result.assets?.[0]; + if (!result.canceled && asset?.base64) { + const dataUri = `data:image/jpeg;base64,${asset.base64}`; + updateProfile.mutate({ image: dataUri }); + } + }; + + const handleDeleteAccount = () => { + Alert.alert( + "Delete account", + "This permanently deletes your account and all your data. This cannot be undone.", + [ + { text: "Cancel", style: "cancel" }, + { + text: "Delete", + style: "destructive", + onPress: () => + deleteAccount.mutate(undefined, { + onSuccess: () => void authClient.signOut(), + }), + }, + ], + ); + }; + const fields = [ { label: "DISPLAY NAME", value: name, set: setName }, { label: "EMAIL", value: email, set: undefined }, ]; const handleSave = () => { - if (name && name !== sessionUser?.name) { - updateProfile.mutate({ name }); + const trimmed = name.trim(); + if (trimmed.length < 1) { + setNameError("Name can't be empty."); + return; + } + setNameError(null); + if (trimmed && trimmed !== sessionUser?.name) { + updateProfile.mutate({ name: trimmed }); } }; @@ -72,27 +130,42 @@ export default function EditProfileScreen() { label="Change photo" color={colors.bill} style={{ marginTop: 10, height: 32 }} + onPress={handleChangePhoto} /> - {fields.map((f) => ( - - {f.label} - - - ))} + {fields.map((f) => { + const set = f.set; + return ( + + {f.label} + { + set(v); + setNameError(null); + } + : undefined + } + editable={!!set} + placeholderTextColor={colors.textSecondary} + autoCapitalize="none" + /> + {f.label === "DISPLAY NAME" && nameError && ( + {nameError} + )} + + ); + })} ); @@ -133,4 +206,11 @@ const s = StyleSheet.create({ fontFamily: "AlbertSans-Regular", fontSize: 16, }, + error: { + fontFamily: fontBody.semibold, + fontSize: 12, + color: colors.red[500], + marginTop: 6, + paddingLeft: 4, + }, }); diff --git a/apps/expo/src/app/settings/feedback.tsx b/apps/expo/src/app/settings/feedback.tsx index 2e0a37e2..4748a98f 100644 --- a/apps/expo/src/app/settings/feedback.tsx +++ b/apps/expo/src/app/settings/feedback.tsx @@ -1,10 +1,20 @@ import { useState } from "react"; -import { StyleSheet, TextInput, TouchableOpacity, View } from "react-native"; +import { + Alert, + Platform, + StyleSheet, + TextInput, + TouchableOpacity, + View, +} from "react-native"; +import Constants from "expo-constants"; +import { useMutation } from "@tanstack/react-query"; import type { IconName } from "~/components/ui"; import { Text } from "~/components/Themed"; import { Icon, Kicker, PrimaryButton, ScreenShell } from "~/components/ui"; import { colors, fontBody, hair, planes } from "~/styles"; +import { trpc } from "~/utils/api"; const CATS: { id: string; label: string; icon: IconName }[] = [ { id: "bug", label: "Bug report", icon: "flag" }, @@ -16,6 +26,31 @@ export default function FeedbackScreen() { const [cat, setCat] = useState("bug"); const [text, setText] = useState(""); + const submitFeedback = useMutation( + trpc.user.submitFeedback.mutationOptions(), + ); + + const handleSubmit = () => { + if (!text.trim()) return; + submitFeedback.mutate( + { + category: cat as "bug" | "idea" | "content", + message: text, + os: Platform.OS, + appVersion: Constants.expoConfig?.version ?? "0.1.1", + }, + { + onSuccess: () => { + setText(""); + Alert.alert("Thanks!", "Your feedback was sent to the team."); + }, + onError: () => { + Alert.alert("Couldn't send", "Please try again."); + }, + }, + ); + }; + return ( What's on your mind? @@ -76,9 +111,16 @@ export default function FeedbackScreen() { multiline textAlignVertical="top" /> - App version 2.4.0 attached automatically. + + App version {Constants.expoConfig?.version ?? "0.1.1"} attached + automatically. + - + ); } diff --git a/apps/expo/src/app/settings/help.tsx b/apps/expo/src/app/settings/help.tsx index c52f868b..9103dd84 100644 --- a/apps/expo/src/app/settings/help.tsx +++ b/apps/expo/src/app/settings/help.tsx @@ -1,10 +1,12 @@ import { useState } from "react"; -import { StyleSheet, TouchableOpacity, View } from "react-native"; +import { Linking, StyleSheet, TouchableOpacity, View } from "react-native"; import { Text } from "~/components/Themed"; import { Card, Icon, ScreenShell, SearchInput } from "~/components/ui"; import { colors, fontBody } from "~/styles"; +const SUPPORT_EMAIL = "hello@billion.app"; + const FAQS = [ { q: "Where does Billion get its information?", @@ -57,14 +59,25 @@ export default function HelpScreen() { ))} - - - - Still stuck? - Reach our team directly - - - + + void Linking.openURL( + `mailto:${SUPPORT_EMAIL}?subject=${encodeURIComponent( + "Billion support request", + )}`, + ) + } + > + + + + Still stuck? + Reach our team directly + + + + ); } diff --git a/apps/expo/src/app/settings/privacy.tsx b/apps/expo/src/app/settings/privacy.tsx index ad0c01bf..7d1cdf75 100644 --- a/apps/expo/src/app/settings/privacy.tsx +++ b/apps/expo/src/app/settings/privacy.tsx @@ -1,6 +1,8 @@ import { useState } from "react"; -import { StyleSheet, View } from "react-native"; +import { Alert, Share, StyleSheet, View } from "react-native"; +import * as Location from "expo-location"; import { useMutation, useQuery } from "@tanstack/react-query"; +import { usePostHog } from "posthog-react-native"; import type { IconName } from "~/components/ui"; import { Text } from "~/components/Themed"; @@ -52,6 +54,7 @@ const DEFAULTS: Record = { }; export default function PrivacyScreen() { + const posthog = usePostHog(); const settingsQuery = useQuery(trpc.user.getSettings.queryOptions()); const updateMutation = useMutation({ ...trpc.user.updateSettings.mutationOptions(), @@ -64,6 +67,7 @@ export default function PrivacyScreen() { const [state, setState] = useState>(DEFAULTS); const [synced, setSynced] = useState(false); + const [exporting, setExporting] = useState(false); if (settingsQuery.data && !synced) { setState({ @@ -76,12 +80,51 @@ export default function PrivacyScreen() { setSynced(true); } - const toggle = (k: Key) => { + const toggle = async (k: Key) => { const newVal = !state[k]; setState((p) => ({ ...p, [k]: newVal })); + + // Requesting location on the OS level when enabling the location toggle. + if (k === "location" && newVal) { + const { status } = await Location.requestForegroundPermissionsAsync(); + if (status !== Location.PermissionStatus.GRANTED) { + Alert.alert( + "Location permission denied", + "Enable location access in Settings to load your local ballot.", + ); + setState((p) => ({ ...p, location: false })); + updateMutation.mutate({ location: false }); + return; + } + } + + // Reflect the analytics preference in PostHog so the SDK respects opt-out. + if (k === "analytics") { + if (newVal) { + void posthog.optIn(); + } else { + void posthog.optOut(); + } + } + updateMutation.mutate({ [k]: newVal }); }; + const handleExport = async () => { + if (exporting) return; + setExporting(true); + try { + const data = await queryClient.fetchQuery( + trpc.user.requestDataExport.queryOptions(), + ); + await Share.share({ message: JSON.stringify(data, null, 2) }); + } catch { + Alert.alert("Export failed", "Please try again."); + } finally { + setExporting(false); + } + }; + return ( @@ -102,14 +145,15 @@ export default function PrivacyScreen() { {r.label} {r.sub} - toggle(r.k)} /> + void toggle(r.k)} /> ))} void handleExport()} /> ); diff --git a/apps/expo/src/components/ui/primitives.tsx b/apps/expo/src/components/ui/primitives.tsx index 5fe959f5..c6942363 100644 --- a/apps/expo/src/components/ui/primitives.tsx +++ b/apps/expo/src/components/ui/primitives.tsx @@ -118,17 +118,20 @@ export function PrimaryButton({ onPress, icon, style, + disabled, }: { label: string; onPress?: () => void; icon?: IconName; style?: ViewStyle; + disabled?: boolean; }) { return ( {label} {icon && } diff --git a/packages/api/src/router/user.ts b/packages/api/src/router/user.ts index 2261cca9..2c117c4c 100644 --- a/packages/api/src/router/user.ts +++ b/packages/api/src/router/user.ts @@ -1,12 +1,13 @@ import type { TRPCRouterRecord } from "@trpc/server"; import { z } from "zod/v4"; -import { and, desc, eq } from "@acme/db"; +import { and, desc, eq, gte } from "@acme/db"; import { db } from "@acme/db/client"; import { Bill, BlockedContent, CourtCase, + Feedback, GovernmentContent, SavedArticle, user, @@ -171,6 +172,20 @@ export const userRouter = { return { success: true }; }), + // --- Account Deletion --- + + deleteAccount: protectedProcedure.mutation(async ({ ctx }) => { + const userId = ctx.session.user.id; + await db.transaction(async (tx) => { + await tx.delete(SavedArticle).where(eq(SavedArticle.userId, userId)); + await tx.delete(BlockedContent).where(eq(BlockedContent.userId, userId)); + await tx.delete(UserPreference).where(eq(UserPreference.userId, userId)); + await tx.delete(UserSettings).where(eq(UserSettings.userId, userId)); + await tx.delete(user).where(eq(user.id, userId)); + }); + return { success: true }; + }), + // --- Saved Articles --- getSaved: protectedProcedure.query(async ({ ctx }) => { @@ -284,4 +299,99 @@ export const userRouter = { .limit(1); return { saved: !!row }; }), + + // --- Feedback --- + + submitFeedback: protectedProcedure + .input( + z.object({ + category: z.enum(["bug", "idea", "content"]), + message: z.string().min(1).max(5000), + os: z.string().max(20).optional(), + appVersion: z.string().max(20).optional(), + }), + ) + .mutation(async ({ ctx, input }) => { + const userId = ctx.session.user.id; + + // Dedup: skip if the same user submitted an identical message + // within the last 60 seconds. + const cutoff = new Date(Date.now() - 60_000); + const [recent] = await db + .select({ id: Feedback.id }) + .from(Feedback) + .where( + and( + eq(Feedback.userId, userId), + eq(Feedback.message, input.message), + gte(Feedback.createdAt, cutoff), + ), + ) + .limit(1); + if (recent) { + return { success: true, deduped: true }; + } + + await db.insert(Feedback).values({ + userId, + category: input.category, + message: input.message, + os: input.os, + appVersion: input.appVersion, + }); + return { success: true, deduped: false }; + }), + + // --- Data Export --- + + requestDataExport: protectedProcedure.query(async ({ ctx }) => { + const userId = ctx.session.user.id; + + const [profileRow] = await db + .select({ + name: user.name, + email: user.email, + createdAt: user.createdAt, + }) + .from(user) + .where(eq(user.id, userId)) + .limit(1); + + const [preferences] = await db + .select() + .from(UserPreference) + .where(eq(UserPreference.userId, userId)) + .limit(1); + + const [settings] = await db + .select() + .from(UserSettings) + .where(eq(UserSettings.userId, userId)) + .limit(1); + + const blocked = await db + .select() + .from(BlockedContent) + .where(eq(BlockedContent.userId, userId)); + + const savedArticles = await db + .select() + .from(SavedArticle) + .where(eq(SavedArticle.userId, userId)); + + const feedback = await db + .select() + .from(Feedback) + .where(eq(Feedback.userId, userId)); + + return { + exportedAt: new Date().toISOString(), + profile: profileRow ?? null, + preferences: preferences ?? null, + settings: settings ?? null, + blocked, + savedArticles, + feedback, + }; + }), } satisfies TRPCRouterRecord; diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 617ac1c1..114846cc 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -427,6 +427,23 @@ export const UserSettings = pgTable( }), ); +// User-submitted feedback +export const Feedback = pgTable( + "feedback", + (t) => ({ + id: t.uuid().notNull().primaryKey().defaultRandom(), + userId: t.text().notNull(), + category: t.varchar({ length: 20 }).notNull(), // "bug" | "idea" | "content" + message: t.text().notNull(), + os: t.varchar({ length: 20 }), // device OS, e.g. "ios" | "android" + appVersion: t.varchar({ length: 20 }), + createdAt: t.timestamp().defaultNow().notNull(), + }), + (table) => ({ + userIdx: index("feedback_user_id_idx").on(table.userId), + }), +); + // Legistar local government data cache tables export const LegistarBody = pgTable( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7fa67bdf..8dd24029 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -159,7 +159,7 @@ importers: version: 11.16.0(@tanstack/react-query@5.95.2(react@19.0.0))(@trpc/client@11.16.0(@trpc/server@11.16.0(typescript@6.0.2))(typescript@6.0.2))(@trpc/server@11.16.0(typescript@6.0.2))(react@19.0.0)(typescript@6.0.2) better-auth: specifier: 'catalog:' - version: 1.5.6(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@5.22.0))(better-sqlite3@12.8.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(better-sqlite3@12.8.0)(gel@2.0.0)(kysely@0.28.14)(mysql2@3.11.3)(pg@8.20.0)(postgres@3.4.4)(prisma@5.22.0))(mysql2@3.11.3)(next@16.2.1(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(pg@8.20.0)(prisma@5.22.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 1.5.6(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@5.22.0))(better-sqlite3@12.8.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(better-sqlite3@12.8.0)(gel@2.0.0)(kysely@0.28.14)(mysql2@3.11.3)(pg@8.20.0)(postgres@3.4.4)(prisma@5.22.0))(mysql2@3.11.3)(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(pg@8.20.0)(prisma@5.22.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) expo: specifier: ~53.0.27 version: 53.0.27(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@6.1.1)(babel-plugin-react-compiler@1.0.0)(bufferutil@4.1.0)(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0)(utf-8-validate@6.0.4) @@ -181,12 +181,18 @@ importers: expo-image: specifier: ~2.4.1 version: 2.4.1(expo@53.0.27)(react-native-web@0.20.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0) + expo-image-picker: + specifier: ~16.0.6 + version: 16.0.6(expo@53.0.27) expo-linear-gradient: specifier: ~14.1.5 version: 14.1.5(expo@53.0.27)(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0) expo-linking: specifier: ~7.1.7 version: 7.1.7(expo@53.0.27)(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0) + expo-location: + specifier: ~18.1.6 + version: 18.1.6(expo@53.0.27) expo-network: specifier: ~7.1.5 version: 7.1.5(expo@53.0.27)(react@19.0.0) @@ -217,6 +223,9 @@ importers: nativewind: specifier: 5.0.0-preview.3 version: 5.0.0-preview.3(react-native-css@3.0.6(@expo/metro-config@55.0.13(bufferutil@4.1.0)(expo@53.0.27)(typescript@6.0.2)(utf-8-validate@6.0.4))(lightningcss@1.32.0)(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0))(tailwindcss@4.2.2) + posthog-react-native: + specifier: ^3.3.0 + version: 3.16.1(@react-native-async-storage/async-storage@3.1.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0))(@react-navigation/native@7.2.2(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0))(expo-file-system@18.1.11(expo@53.0.27)(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4)))(react-native-safe-area-context@5.4.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0))(react-native-svg@15.11.2(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0)) react: specifier: 19.0.0 version: 19.0.0 @@ -325,7 +334,7 @@ importers: version: 11.16.0(@tanstack/react-query@5.95.2(react@19.0.0))(@trpc/client@11.16.0(@trpc/server@11.16.0(typescript@6.0.2))(typescript@6.0.2))(@trpc/server@11.16.0(typescript@6.0.2))(react@19.0.0)(typescript@6.0.2) better-auth: specifier: 'catalog:' - version: 1.5.6(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@5.22.0))(better-sqlite3@12.8.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(better-sqlite3@12.8.0)(gel@2.0.0)(kysely@0.28.14)(mysql2@3.11.3)(pg@8.20.0)(postgres@3.4.4)(prisma@5.22.0))(mysql2@3.11.3)(next@16.2.1(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(pg@8.20.0)(prisma@5.22.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 1.5.6(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@5.22.0))(better-sqlite3@12.8.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(better-sqlite3@12.8.0)(gel@2.0.0)(kysely@0.28.14)(mysql2@3.11.3)(pg@8.20.0)(postgres@3.4.4)(prisma@5.22.0))(mysql2@3.11.3)(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(pg@8.20.0)(prisma@5.22.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next: specifier: ^16.2.1 version: 16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -5419,6 +5428,16 @@ packages: expo: '*' react: 19.0.0 + expo-image-loader@5.0.0: + resolution: {integrity: sha512-Eg+5FHtyzv3Jjw9dHwu2pWy4xjf8fu3V0Asyy42kO+t/FbvW/vjUixpTjPtgKQLQh+2/9Nk4JjFDV6FwCnF2ZA==} + peerDependencies: + expo: '*' + + expo-image-picker@16.0.6: + resolution: {integrity: sha512-HN4xZirFjsFDIsWFb12AZh19fRzuvZjj2ll17cGr19VNRP06S/VPQU3Tdccn5vwUzQhOBlLu704CnNm278boiQ==} + peerDependencies: + expo: '*' + expo-image@2.4.1: resolution: {integrity: sha512-yHp0Cy4ylOYyLR21CcH6i70DeRyLRPc0yAIPFPn4BT/BpkJNaX5QMXDppcHa58t4WI3Bb8QRJRLuAQaeCtDF8A==} peerDependencies: @@ -5452,6 +5471,11 @@ packages: react: 19.0.0 react-native: '*' + expo-location@18.1.6: + resolution: {integrity: sha512-l5dQQ2FYkrBgNzaZN1BvSmdhhcztFOUucu2kEfDBMV4wSIuTIt/CKsho+F3RnAiWgvui1wb1WTTf80E8zq48hA==} + peerDependencies: + expo: '*' + expo-manifests@0.16.6: resolution: {integrity: sha512-1A+do6/mLUWF9xd3uCrlXr9QFDbjbfqAYmUy8UDLOjof1lMrOhyeC4Yi6WexA/A8dhZEpIxSMCKfn7G4aHAh4w==} peerDependencies: @@ -7039,6 +7063,45 @@ packages: resolution: {integrity: sha512-IbyN+9KslkqcXa8AO9fxpk97PA4pzewvpi2B3Dwy9u4zpV32QicaEdgmF3eSQUzdRk7ttDHQejNgAEr4XoeH4A==} engines: {node: '>=12'} + posthog-react-native@3.16.1: + resolution: {integrity: sha512-qrsQC552nY9emZbv2HmnPfckS1waFf5kG1LLGF1noUhgXWzNfLfwK20FIypjRR50O+j/vCFIzQBmoy4Sv/U2RA==} + peerDependencies: + '@react-native-async-storage/async-storage': '>=1.0.0' + '@react-navigation/native': '>= 5.0.0' + expo-application: '>= 4.0.0' + expo-device: '>= 4.0.0' + expo-file-system: '>= 13.0.0' + expo-localization: '>= 11.0.0' + posthog-react-native-session-replay: '>= 1.0.0' + react-native-device-info: '>= 10.0.0' + react-native-localize: '>= 3.0.0' + react-native-navigation: '>= 6.0.0' + react-native-safe-area-context: '>= 4.0.0' + react-native-svg: '>= 15.0.0' + peerDependenciesMeta: + '@react-native-async-storage/async-storage': + optional: true + '@react-navigation/native': + optional: true + expo-application: + optional: true + expo-device: + optional: true + expo-file-system: + optional: true + expo-localization: + optional: true + posthog-react-native-session-replay: + optional: true + react-native-device-info: + optional: true + react-native-localize: + optional: true + react-native-navigation: + optional: true + react-native-safe-area-context: + optional: true + prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} @@ -12373,40 +12436,6 @@ snapshots: - '@cloudflare/workers-types' - '@opentelemetry/api' - better-auth@1.5.6(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@5.22.0))(better-sqlite3@12.8.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(better-sqlite3@12.8.0)(gel@2.0.0)(kysely@0.28.14)(mysql2@3.11.3)(pg@8.20.0)(postgres@3.4.4)(prisma@5.22.0))(mysql2@3.11.3)(next@16.2.1(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(pg@8.20.0)(prisma@5.22.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): - dependencies: - '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0) - '@better-auth/drizzle-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.2(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(better-sqlite3@12.8.0)(gel@2.0.0)(kysely@0.28.14)(mysql2@3.11.3)(pg@8.20.0)(postgres@3.4.4)(prisma@5.22.0)) - '@better-auth/kysely-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(kysely@0.28.14) - '@better-auth/memory-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1) - '@better-auth/mongo-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1) - '@better-auth/prisma-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(@prisma/client@5.22.0(prisma@5.22.0))(prisma@5.22.0) - '@better-auth/telemetry': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0)) - '@better-auth/utils': 0.3.1 - '@better-fetch/fetch': 1.1.21 - '@noble/ciphers': 2.1.1 - '@noble/hashes': 2.0.1 - better-call: 1.3.2(zod@4.3.6) - defu: 6.1.4 - jose: 6.2.2 - kysely: 0.28.14 - nanostores: 1.2.0 - zod: 4.3.6 - optionalDependencies: - '@prisma/client': 5.22.0(prisma@5.22.0) - better-sqlite3: 12.8.0 - drizzle-kit: 0.31.10 - drizzle-orm: 0.45.2(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(better-sqlite3@12.8.0)(gel@2.0.0)(kysely@0.28.14)(mysql2@3.11.3)(pg@8.20.0)(postgres@3.4.4)(prisma@5.22.0) - mysql2: 3.11.3 - next: 16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - pg: 8.20.0 - prisma: 5.22.0 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - transitivePeerDependencies: - - '@cloudflare/workers-types' - - '@opentelemetry/api' - better-call@1.1.8(zod@4.3.6): dependencies: '@better-auth/utils': 0.3.1 @@ -13464,6 +13493,15 @@ snapshots: fontfaceobserver: 2.3.0 react: 19.0.0 + expo-image-loader@5.0.0(expo@53.0.27): + dependencies: + expo: 53.0.27(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@6.1.1)(babel-plugin-react-compiler@1.0.0)(bufferutil@4.1.0)(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0)(utf-8-validate@6.0.4) + + expo-image-picker@16.0.6(expo@53.0.27): + dependencies: + expo: 53.0.27(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@6.1.1)(babel-plugin-react-compiler@1.0.0)(bufferutil@4.1.0)(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0)(utf-8-validate@6.0.4) + expo-image-loader: 5.0.0(expo@53.0.27) + expo-image@2.4.1(expo@53.0.27)(react-native-web@0.20.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0): dependencies: expo: 53.0.27(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@6.1.1)(babel-plugin-react-compiler@1.0.0)(bufferutil@4.1.0)(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0)(utf-8-validate@6.0.4) @@ -13495,6 +13533,10 @@ snapshots: - expo - supports-color + expo-location@18.1.6(expo@53.0.27): + dependencies: + expo: 53.0.27(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@6.1.1)(babel-plugin-react-compiler@1.0.0)(bufferutil@4.1.0)(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0)(utf-8-validate@6.0.4) + expo-manifests@0.16.6(expo@53.0.27): dependencies: '@expo/config': 11.0.13 @@ -15329,6 +15371,15 @@ snapshots: postgres@3.4.4: optional: true + posthog-react-native@3.16.1(@react-native-async-storage/async-storage@3.1.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0))(@react-navigation/native@7.2.2(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0))(expo-file-system@18.1.11(expo@53.0.27)(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4)))(react-native-safe-area-context@5.4.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0))(react-native-svg@15.11.2(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0)): + dependencies: + react-native-svg: 15.11.2(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0) + optionalDependencies: + '@react-native-async-storage/async-storage': 3.1.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0) + '@react-navigation/native': 7.2.2(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0) + expo-file-system: 18.1.11(expo@53.0.27)(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4)) + react-native-safe-area-context: 5.4.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0) + prebuild-install@7.1.3: dependencies: detect-libc: 2.1.2