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