diff --git a/.gitignore b/.gitignore
index 663039f..4ac02df 100755
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,7 @@ apps/mobile/.env.*
# Supabase
.supabase/
+supabase/.temp/
# IDE
.idea/
diff --git a/apps/mobile/android/app/build.gradle b/apps/mobile/android/app/build.gradle
index cf0e260..e291eae 100644
--- a/apps/mobile/android/app/build.gradle
+++ b/apps/mobile/android/app/build.gradle
@@ -80,8 +80,6 @@ def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInRelea
*/
def jscFlavor = 'org.webkit:android-jsc:+'
-apply from: new File(["node", "--print", "require('path').dirname(require.resolve('@sentry/react-native/package.json'))"].execute().text.trim(), "sentry.gradle")
-
android {
ndkVersion rootProject.ext.ndkVersion
@@ -93,8 +91,8 @@ android {
applicationId 'com.sauci.app'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 37
- versionName "1.0.3"
+ versionCode 42
+ versionName "1.0.4"
}
signingConfigs {
debug {
@@ -177,4 +175,5 @@ dependencies {
}
}
-apply plugin: 'com.google.gms.google-services'
\ No newline at end of file
+apply plugin: 'com.google.gms.google-services'
+apply plugin: 'com.google.firebase.crashlytics'
\ No newline at end of file
diff --git a/apps/mobile/android/build.gradle b/apps/mobile/android/build.gradle
index 1752a9e..e228627 100644
--- a/apps/mobile/android/build.gradle
+++ b/apps/mobile/android/build.gradle
@@ -15,6 +15,7 @@ buildscript {
mavenCentral()
}
dependencies {
+ classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.6'
classpath 'com.google.gms:google-services:4.4.1'
classpath('com.android.tools.build:gradle')
classpath('com.facebook.react:react-native-gradle-plugin')
diff --git a/apps/mobile/app.json b/apps/mobile/app.json
index 08b18cd..27138dd 100644
--- a/apps/mobile/app.json
+++ b/apps/mobile/app.json
@@ -2,7 +2,7 @@
"expo": {
"name": "Sauci",
"slug": "sauci",
- "version": "1.0.3",
+ "version": "1.0.4",
"orientation": "portrait",
"icon": "./assets/icon.png",
"scheme": "app.sauci",
@@ -16,10 +16,11 @@
"ios": {
"supportsTablet": false,
"bundleIdentifier": "com.sauci.app",
- "buildNumber": "33",
+ "buildNumber": "35",
"usesAppleSignIn": true,
"googleServicesFile": "./GoogleService-Info.plist",
"infoPlist": {
+ "GOOGLE_ANALYTICS_INAPURCHASE_COLLECTION_ENABLED": false,
"UIBackgroundModes": [
"remote-notification"
],
@@ -177,7 +178,7 @@
"backgroundColor": "#1a1a2e"
},
"package": "com.sauci.app",
- "versionCode": 37,
+ "versionCode": 42,
"googleServicesFile": "./google-services.json",
"permissions": [
"RECEIVE_BOOT_COMPLETED",
@@ -227,14 +228,7 @@
"faceIDPermission": "Sauci uses Face ID to unlock the app and keep your conversations private."
}
],
- [
- "@sentry/react-native/expo",
- {
- "organization": "molten-prints",
- "project": "sauci"
- }
- ],
- "@sentry/react-native",
+ "@react-native-firebase/crashlytics",
"expo-font",
"expo-asset",
[
diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx
index 7b4b577..c853a69 100755
--- a/apps/mobile/app/_layout.tsx
+++ b/apps/mobile/app/_layout.tsx
@@ -1,9 +1,9 @@
// Polyfills must be imported FIRST before any other React Native imports
import "../src/polyfills/web";
-// Initialize Sentry early to capture all errors
-import { initSentry, setUserContext, clearUserContext } from "../src/lib/sentry";
-initSentry();
+// Initialize Crashlytics early to capture all errors
+import { initCrashlytics, setUserContext, clearUserContext } from "../src/lib/crashlytics";
+initCrashlytics();
// Analytics imports (initialization deferred until app mounts)
import { initAnalytics, setUserId, clearUserId, logScreenView, Events } from "../src/lib/analytics";
@@ -212,7 +212,7 @@ export default function RootLayout() {
if (event === "SIGNED_IN" || event === "TOKEN_REFRESHED" || event === "USER_UPDATED") {
console.log('[Auth] Calling fetchUser...');
fetchUser();
- // Set Sentry user context for error tracking
+ // Set Crashlytics user context for error tracking
if (session?.user) {
setUserContext(session.user.id, session.user.email);
// Set Analytics user ID
diff --git a/apps/mobile/eas.json b/apps/mobile/eas.json
index 050c5e1..55c47c5 100644
--- a/apps/mobile/eas.json
+++ b/apps/mobile/eas.json
@@ -19,7 +19,6 @@
"env": {
"EXPO_PUBLIC_SUPABASE_URL": "https://ckjcrkjpmhqhiucifukx.supabase.co",
"EXPO_PUBLIC_SUPABASE_ANON_KEY": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNramNya2pwbWhxaGl1Y2lmdWt4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjcxMDc1ODEsImV4cCI6MjA4MjY4MzU4MX0.7FA_RmFXUjlmZF96O9xlAUfxHrQrSIZcJclNReLL4lI",
- "EXPO_PUBLIC_SENTRY_DSN": "https://03a91f8a1b1ecdce2d5f38a4c23ca9d5@o4510613894070272.ingest.de.sentry.io/4510613896560720",
"EXPO_PUBLIC_REVENUECAT_IOS_API_KEY": "appl_lfPyYonFQtVytftPAfXIrGsMNFu",
"EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEY": "goog_zoHtaRswsgvxtvYWIhZyvrPBGgh",
"EXPO_PUBLIC_REVENUECAT_ENTITLEMENT_ID": "Sauci Pro",
diff --git a/apps/mobile/firebase.json b/apps/mobile/firebase.json
index 8d6179b..46b6efc 100644
--- a/apps/mobile/firebase.json
+++ b/apps/mobile/firebase.json
@@ -1,6 +1,9 @@
{
"react-native": {
"analytics_auto_collection_enabled": true,
- "google_analytics_adid_collection_enabled": false
+ "google_analytics_adid_collection_enabled": false,
+ "crashlytics_auto_collection_enabled": true,
+ "crashlytics_debug_enabled": false,
+ "crashlytics_ndk_enabled": true
}
}
diff --git a/apps/mobile/ios/Sauci/Info.plist b/apps/mobile/ios/Sauci/Info.plist
index 1907934..035ae5c 100644
--- a/apps/mobile/ios/Sauci/Info.plist
+++ b/apps/mobile/ios/Sauci/Info.plist
@@ -21,7 +21,7 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 1.0.3
+ 1.0.4
CFBundleSignature
????
CFBundleURLTypes
@@ -47,7 +47,9 @@
CFBundleVersion
- 33
+ 35
+ GOOGLE_ANALYTICS_INAPURCHASE_COLLECTION_ENABLED
+
LSMinimumSystemVersion
12.0
LSRequiresIPhoneOS
diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index 56939e8..9314ec0 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -24,11 +24,11 @@
"@react-native-async-storage/async-storage": "1.23.1",
"@react-native-firebase/analytics": "^23.8.2",
"@react-native-firebase/app": "^23.8.2",
+ "@react-native-firebase/crashlytics": "^23.8.4",
"@react-native-google-signin/google-signin": "^16.1.1",
"@react-navigation/bottom-tabs": "^7.0.0",
"@react-navigation/native": "^7.0.0",
"@react-navigation/native-stack": "^7.0.0",
- "@sentry/react-native": "~6.10.0",
"@supabase/supabase-js": "^2.39.0",
"@tanstack/react-query": "^5.17.0",
"babel-preset-expo": "~12.0.0",
diff --git a/apps/mobile/src/lib/analytics.ts b/apps/mobile/src/lib/analytics.ts
index 79c7e52..fd3f486 100644
--- a/apps/mobile/src/lib/analytics.ts
+++ b/apps/mobile/src/lib/analytics.ts
@@ -72,35 +72,35 @@ function getAnalyticsInstance(): FirebaseAnalyticsTypes.Module | null {
return null;
}
}
-
-export async function initAnalytics() {
- if (isInitialized) {
- return;
- }
-
- // Skip initialization in non-RN environments (e.g., during tsc/Node.js)
- if (typeof window === "undefined" && typeof global !== "undefined" && !("__BUNDLE_START_TIME__" in global)) {
- return;
- }
-
- try {
+
+export async function initAnalytics() {
+ if (isInitialized) {
+ return;
+ }
+
+ // Skip initialization in non-RN environments (e.g., during tsc/Node.js)
+ if (typeof window === "undefined" && typeof global !== "undefined" && !("__BUNDLE_START_TIME__" in global)) {
+ return;
+ }
+
+ try {
const analytics = getAnalyticsInstance();
if (analytics) {
await setAnalyticsCollectionEnabled(analytics, true);
isInitialized = true;
console.log("[Analytics] Firebase Analytics initialized");
}
- } catch (error) {
- console.warn("[Analytics] Failed to initialize Firebase Analytics:", error);
- }
-}
-
-// Log a custom event
-export function logEvent(
- name: string,
- params?: Record
-) {
- try {
+ } catch (error) {
+ console.warn("[Analytics] Failed to initialize Firebase Analytics:", error);
+ }
+}
+
+// Log a custom event
+export function logEvent(
+ name: string,
+ params?: Record
+) {
+ try {
if (__DEV__) console.log("[Analytics] Event:", name, params);
const analytics = getAnalyticsInstance();
if (analytics) {
@@ -109,11 +109,11 @@ export function logEvent(
} catch (error) {
console.warn("[Analytics] Failed to log event:", error);
}
-}
-
-// Set user ID for tracking
-export function setUserId(userId: string) {
- try {
+}
+
+// Set user ID for tracking
+export function setUserId(userId: string) {
+ try {
if (__DEV__) console.log("[Analytics] Set user ID:", userId);
const analytics = getAnalyticsInstance();
if (analytics) {
@@ -122,11 +122,11 @@ export function setUserId(userId: string) {
} catch (error) {
console.warn("[Analytics] Failed to set user ID:", error);
}
-}
-
-// Clear user ID on logout
-export function clearUserId() {
- try {
+}
+
+// Clear user ID on logout
+export function clearUserId() {
+ try {
if (__DEV__) console.log("[Analytics] Cleared user ID");
const analytics = getAnalyticsInstance();
if (analytics) {
@@ -136,13 +136,13 @@ export function clearUserId() {
} catch (error) {
console.warn("[Analytics] Failed to clear user ID:", error);
}
-}
-
-// Set user properties
-export function setUserProperties(
- properties: Record
-) {
- try {
+}
+
+// Set user properties
+export function setUserProperties(
+ properties: Record
+) {
+ try {
if (__DEV__) console.log("[Analytics] Set user properties:", properties);
const analytics = getAnalyticsInstance();
if (analytics) {
@@ -154,11 +154,11 @@ export function setUserProperties(
} catch (error) {
console.warn("[Analytics] Failed to set user properties:", error);
}
-}
-
-// Log screen view
-export function logScreenView(screenName: string, screenClass?: string) {
- try {
+}
+
+// Log screen view
+export function logScreenView(screenName: string, screenClass?: string) {
+ try {
if (__DEV__) console.log("[Analytics] Screen view:", screenName);
const analytics = getAnalyticsInstance();
if (analytics) {
@@ -166,12 +166,12 @@ export function logScreenView(screenName: string, screenClass?: string) {
screen_name: screenName,
screen_class: screenClass ?? screenName,
});
- }
- } catch (error) {
- console.warn("[Analytics] Failed to log screen view:", error);
- }
-}
-
+ }
+ } catch (error) {
+ console.warn("[Analytics] Failed to log screen view:", error);
+ }
+}
+
// Flush events (useful before app backgrounding)
export function flush() {
// Firebase Analytics batches and sends events automatically
@@ -193,35 +193,35 @@ function safeParam(value: unknown, maxLength = 100): string {
// Pre-defined event helpers for common actions
export const Events = {
- // Auth events
- signUp: (method: string) => logEvent("sign_up", { method }),
- signIn: (method: string) => logEvent("login", { method }),
- signOut: () => logEvent("sign_out"),
-
- // Onboarding events
- onboardingStart: () => logEvent("onboarding_start"),
- onboardingStageComplete: (stage: string) =>
- logEvent("onboarding_stage_complete", { stage }),
- onboardingComplete: () => logEvent("onboarding_complete"),
-
- // Pairing events
- coupleCreated: () => logEvent("couple_created"),
- coupleJoined: () => logEvent("couple_joined"),
- codeShared: () => logEvent("invite_code_shared"),
- pairingCancelled: () => logEvent("pairing_cancelled"),
-
- // Swipe/question events
- questionAnswered: (answer: string, packId?: string) =>
- logEvent("question_answered", { answer, pack_id: packId ?? "unknown" }),
- questionSkipped: () => logEvent("question_skipped"),
- matchCreated: (matchType: string) => logEvent("match_created", { match_type: matchType }),
- allQuestionsExhausted: () => logEvent("all_questions_exhausted"),
-
- // Chat events
- matchViewed: () => logEvent("match_viewed"),
- messageSent: () => logEvent("message_sent"),
- mediaUploaded: () => logEvent("chat_media_uploaded"),
-
+ // Auth events
+ signUp: (method: string) => logEvent("sign_up", { method }),
+ signIn: (method: string) => logEvent("login", { method }),
+ signOut: () => logEvent("sign_out"),
+
+ // Onboarding events
+ onboardingStart: () => logEvent("onboarding_start"),
+ onboardingStageComplete: (stage: string) =>
+ logEvent("onboarding_stage_complete", { stage }),
+ onboardingComplete: () => logEvent("onboarding_complete"),
+
+ // Pairing events
+ coupleCreated: () => logEvent("couple_created"),
+ coupleJoined: () => logEvent("couple_joined"),
+ codeShared: () => logEvent("invite_code_shared"),
+ pairingCancelled: () => logEvent("pairing_cancelled"),
+
+ // Swipe/question events
+ questionAnswered: (answer: string, packId?: string) =>
+ logEvent("question_answered", { answer, pack_id: packId ?? "unknown" }),
+ questionSkipped: () => logEvent("question_skipped"),
+ matchCreated: (matchType: string) => logEvent("match_created", { match_type: matchType }),
+ allQuestionsExhausted: () => logEvent("all_questions_exhausted"),
+
+ // Chat events
+ matchViewed: () => logEvent("match_viewed"),
+ messageSent: () => logEvent("message_sent"),
+ mediaUploaded: () => logEvent("chat_media_uploaded"),
+
// Subscription events
paywallShown: (source: string) => logEvent("paywall_shown", { source }),
paywallClosed: (source: string, reason: string) =>
@@ -240,42 +240,42 @@ export const Events = {
error_message: safeParam(errorMessage),
}),
purchaseCompleted: (packageType: string) =>
- logEvent("purchase", { package_type: packageType }),
+ logEvent("purchase_completed", { package_type: packageType }),
purchaseRestored: () => logEvent("purchase_restored"),
-
- // Pack events
- packEnabled: (packId: string) => logEvent("pack_enabled", { pack_id: packId }),
- packDisabled: (packId: string) => logEvent("pack_disabled", { pack_id: packId }),
-
- // App lifecycle events
- appOpened: (type: "cold" | "warm") => logEvent("app_opened", { type }),
-
- // Profile events
- profileUpdated: (fields: string[]) => logEvent("profile_updated", { fields: fields.join(",") }),
- avatarUploaded: () => logEvent("avatar_uploaded"),
-
- // Notification events
- notificationPermissionGranted: () => logEvent("notification_permission_granted"),
- notificationPermissionDenied: () => logEvent("notification_permission_denied"),
-
- // Relationship events
- relationshipEnded: () => logEvent("relationship_ended"),
- accountDeleted: () => logEvent("account_deleted"),
-
- // Milestone events
- firstMatch: () => logEvent("first_match"),
- milestoneMatch: (count: number) => logEvent("milestone_match", { match_count: count }),
-
- // Invite events
- inviteCodeCopied: () => logEvent("invite_code_copied"),
-
- // Tutorial events
- tutorialStarted: (tutorial: "swipe" | "matches") =>
- logEvent("tutorial_started", { tutorial }),
- tutorialStepViewed: (tutorial: "swipe" | "matches", step: number, stepName: string) =>
- logEvent("tutorial_step_viewed", { tutorial, step, step_name: stepName }),
- tutorialCompleted: (tutorial: "swipe" | "matches") =>
- logEvent("tutorial_completed", { tutorial }),
- tutorialSkipped: (tutorial: "swipe" | "matches", atStep: number) =>
- logEvent("tutorial_skipped", { tutorial, at_step: atStep }),
-};
+
+ // Pack events
+ packEnabled: (packId: string) => logEvent("pack_enabled", { pack_id: packId }),
+ packDisabled: (packId: string) => logEvent("pack_disabled", { pack_id: packId }),
+
+ // App lifecycle events
+ appOpened: (type: "cold" | "warm") => logEvent("app_opened", { type }),
+
+ // Profile events
+ profileUpdated: (fields: string[]) => logEvent("profile_updated", { fields: fields.join(",") }),
+ avatarUploaded: () => logEvent("avatar_uploaded"),
+
+ // Notification events
+ notificationPermissionGranted: () => logEvent("notification_permission_granted"),
+ notificationPermissionDenied: () => logEvent("notification_permission_denied"),
+
+ // Relationship events
+ relationshipEnded: () => logEvent("relationship_ended"),
+ accountDeleted: () => logEvent("account_deleted"),
+
+ // Milestone events
+ firstMatch: () => logEvent("first_match"),
+ milestoneMatch: (count: number) => logEvent("milestone_match", { match_count: count }),
+
+ // Invite events
+ inviteCodeCopied: () => logEvent("invite_code_copied"),
+
+ // Tutorial events
+ tutorialStarted: (tutorial: "swipe" | "matches") =>
+ logEvent("tutorial_started", { tutorial }),
+ tutorialStepViewed: (tutorial: "swipe" | "matches", step: number, stepName: string) =>
+ logEvent("tutorial_step_viewed", { tutorial, step, step_name: stepName }),
+ tutorialCompleted: (tutorial: "swipe" | "matches") =>
+ logEvent("tutorial_completed", { tutorial }),
+ tutorialSkipped: (tutorial: "swipe" | "matches", atStep: number) =>
+ logEvent("tutorial_skipped", { tutorial, at_step: atStep }),
+};
diff --git a/apps/mobile/src/lib/crashlytics.ts b/apps/mobile/src/lib/crashlytics.ts
new file mode 100644
index 0000000..7a38748
--- /dev/null
+++ b/apps/mobile/src/lib/crashlytics.ts
@@ -0,0 +1,128 @@
+import { Platform } from "react-native";
+import Constants from "expo-constants";
+import crashlytics from "@react-native-firebase/crashlytics";
+
+let isInitialized = false;
+let crashlyticsUnavailable = false;
+let crashlyticsUnavailableLogged = false;
+
+function getErrorMessage(error: unknown): string {
+ if (!error) return "";
+ if (typeof error === "string") return error;
+ if (error instanceof Error) return error.message;
+ if (typeof (error as { message?: unknown }).message === "string") {
+ return (error as { message?: string }).message || "";
+ }
+ return "";
+}
+
+function isCrashlyticsSupported(): boolean {
+ if (Platform.OS === "web") return false;
+ if (Constants.appOwnership === "expo") return false;
+ return true;
+}
+
+function logCrashlyticsUnavailableOnce(reason: string) {
+ if (crashlyticsUnavailableLogged) return;
+ crashlyticsUnavailableLogged = true;
+ if (__DEV__) console.log("[Crashlytics] " + reason);
+}
+
+function getCrashlyticsInstance(): ReturnType | null {
+ if (crashlyticsUnavailable) {
+ return null;
+ }
+
+ if (!isCrashlyticsSupported()) {
+ crashlyticsUnavailable = true;
+ logCrashlyticsUnavailableOnce("Skipping Firebase Crashlytics on Expo Go or web.");
+ return null;
+ }
+
+ try {
+ return crashlytics();
+ } catch (error) {
+ const message = getErrorMessage(error);
+ if (message.includes("module could not be found")) {
+ crashlyticsUnavailable = true;
+ logCrashlyticsUnavailableOnce(
+ "Firebase Crashlytics native module missing. Rebuild with a dev client or native app."
+ );
+ return null;
+ }
+ console.warn("[Crashlytics] Failed to get crashlytics instance:", error);
+ return null;
+ }
+}
+
+export function initCrashlytics() {
+ if (isInitialized) return;
+
+ try {
+ const instance = getCrashlyticsInstance();
+ if (instance) {
+ instance.setCrashlyticsCollectionEnabled(true);
+ isInitialized = true;
+ console.log("[Crashlytics] Firebase Crashlytics initialized");
+ }
+ } catch (error) {
+ console.warn("[Crashlytics] Failed to initialize:", error);
+ }
+}
+
+export function captureError(error: Error, context?: Record) {
+ try {
+ const instance = getCrashlyticsInstance();
+ if (!instance) return;
+
+ if (context) {
+ for (const [key, value] of Object.entries(context)) {
+ instance.setAttribute(key, String(value ?? ""));
+ }
+ }
+ instance.recordError(error);
+ } catch (e) {
+ console.warn("[Crashlytics] Failed to capture error:", e);
+ }
+}
+
+export function setUserContext(userId: string, email?: string) {
+ try {
+ const instance = getCrashlyticsInstance();
+ if (!instance) return;
+
+ instance.setUserId(userId);
+ if (email) {
+ instance.setAttribute("email", email);
+ }
+ } catch (error) {
+ console.warn("[Crashlytics] Failed to set user context:", error);
+ }
+}
+
+export function clearUserContext() {
+ try {
+ const instance = getCrashlyticsInstance();
+ if (!instance) return;
+
+ instance.setUserId("");
+ instance.setAttribute("email", "");
+ } catch (error) {
+ console.warn("[Crashlytics] Failed to clear user context:", error);
+ }
+}
+
+export function addBreadcrumb(
+ message: string,
+ category: string,
+ data?: Record
+) {
+ try {
+ const instance = getCrashlyticsInstance();
+ if (!instance) return;
+
+ instance.log(`[${category}] ${message}${data ? " " + JSON.stringify(data) : ""}`);
+ } catch (error) {
+ console.warn("[Crashlytics] Failed to add breadcrumb:", error);
+ }
+}
diff --git a/apps/mobile/src/lib/notifications.ts b/apps/mobile/src/lib/notifications.ts
index 3b7c9f3..f7e0c1d 100644
--- a/apps/mobile/src/lib/notifications.ts
+++ b/apps/mobile/src/lib/notifications.ts
@@ -3,7 +3,7 @@ import Constants from "expo-constants";
import { Platform } from "react-native";
import { router } from "expo-router";
import { supabase } from "./supabase";
-import { captureError } from "./sentry";
+import { captureError } from "./crashlytics";
import { Events } from "./analytics";
import { useMatchStore } from "../store/matchStore";
diff --git a/apps/mobile/src/lib/sentry.ts b/apps/mobile/src/lib/sentry.ts
deleted file mode 100644
index 976c188..0000000
--- a/apps/mobile/src/lib/sentry.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import * as Sentry from "@sentry/react-native";
-import Constants from "expo-constants";
-
-const SENTRY_DSN = process.env.EXPO_PUBLIC_SENTRY_DSN;
-
-export function initSentry() {
- if (!SENTRY_DSN) {
- console.warn("Sentry DSN not configured - error tracking disabled");
- return;
- }
-
- Sentry.init({
- dsn: SENTRY_DSN,
- debug: __DEV__,
- enabled: !__DEV__, // Only enable in production
- environment: __DEV__ ? "development" : "production",
- release: Constants.expoConfig?.version ?? "1.0.0",
- dist: Constants.expoConfig?.ios?.buildNumber ?? Constants.expoConfig?.android?.versionCode?.toString(),
-
- // Performance monitoring
- tracesSampleRate: 0.2, // 20% of transactions for performance
-
- // Session tracking
- enableAutoSessionTracking: true,
- sessionTrackingIntervalMillis: 30000,
-
- // Capture unhandled promise rejections
- enableNative: true,
-
- // Filter out noisy errors
- beforeSend(event) {
- // Filter out network errors that are expected (e.g., user offline)
- if (event.exception?.values?.[0]?.value?.includes("Network request failed")) {
- return null;
- }
- return event;
- },
- });
-}
-
-// Helper to capture errors with context
-export function captureError(error: Error, context?: Record) {
- if (context) {
- Sentry.setContext("additional", context);
- }
- Sentry.captureException(error);
-}
-
-// Set user context when they log in
-export function setUserContext(userId: string, email?: string) {
- Sentry.setUser({
- id: userId,
- email,
- });
-}
-
-// Clear user context on logout
-export function clearUserContext() {
- Sentry.setUser(null);
-}
-
-// Add breadcrumb for debugging
-export function addBreadcrumb(message: string, category: string, data?: Record) {
- Sentry.addBreadcrumb({
- message,
- category,
- data,
- level: "info",
- });
-}
-
-export { Sentry };
diff --git a/apps/mobile/src/lib/supabase.ts b/apps/mobile/src/lib/supabase.ts
index 69a9ad2..4eeea47 100755
--- a/apps/mobile/src/lib/supabase.ts
+++ b/apps/mobile/src/lib/supabase.ts
@@ -58,7 +58,7 @@ if (!isTestEnv && (!supabaseUrl || !supabaseAnonKey)) {
if (!supabaseAnonKey) missing.push('EXPO_PUBLIC_SUPABASE_ANON_KEY');
const errorMsg = `Missing required environment variables: ${missing.join(', ')}. App cannot start.`;
console.error('[Supabase]', errorMsg);
- // In production, this will be caught by Sentry before crashing
+ // In production, this will be caught by Crashlytics before crashing
throw new Error(errorMsg);
}
diff --git a/package-lock.json b/package-lock.json
index b439d41..2469507 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -79,11 +79,11 @@
"@react-native-async-storage/async-storage": "1.23.1",
"@react-native-firebase/analytics": "^23.8.2",
"@react-native-firebase/app": "^23.8.2",
+ "@react-native-firebase/crashlytics": "^23.8.4",
"@react-native-google-signin/google-signin": "^16.1.1",
"@react-navigation/bottom-tabs": "^7.0.0",
"@react-navigation/native": "^7.0.0",
"@react-navigation/native-stack": "^7.0.0",
- "@sentry/react-native": "~6.10.0",
"@supabase/supabase-js": "^2.39.0",
"@tanstack/react-query": "^5.17.0",
"babel-preset-expo": "~12.0.0",
@@ -142,6 +142,168 @@
"typescript": "^5.3.3"
}
},
+ "apps/mobile/node_modules/@firebase/ai": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.7.0.tgz",
+ "integrity": "sha512-PwpCz+TtAMWICM7uQNO0mkSPpUKwrMV4NSwHkbVKDvPKoaQmSlO96vIz+Suw2Ao1EaUUsxYb5LGImHWt/fSnRQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/app-check-interop-types": "0.3.3",
+ "@firebase/component": "0.7.0",
+ "@firebase/logger": "0.5.0",
+ "@firebase/util": "1.13.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x",
+ "@firebase/app-types": "0.x"
+ }
+ },
+ "apps/mobile/node_modules/@firebase/app": {
+ "version": "0.14.7",
+ "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.7.tgz",
+ "integrity": "sha512-o3ZfnOx0AWBD5n/36p2zPoB0rDDxQP8H/A60zDLvvfRLtW8b3LfCyV97GKpJaAVV1JMMl/BC89EDzMyzxFZxTw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.7.0",
+ "@firebase/logger": "0.5.0",
+ "@firebase/util": "1.13.0",
+ "idb": "7.1.1",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "apps/mobile/node_modules/@firebase/app-compat": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.7.tgz",
+ "integrity": "sha512-MO+jfap8IBZQ+K8L2QCiHObyMgpYHrxo4Hc7iJgfb9hjGRW/z1y6LWVdT9wBBK+VJ7cRP2DjAiWQP+thu53hHA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/app": "0.14.7",
+ "@firebase/component": "0.7.0",
+ "@firebase/logger": "0.5.0",
+ "@firebase/util": "1.13.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "apps/mobile/node_modules/@firebase/firestore": {
+ "version": "4.10.0",
+ "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.10.0.tgz",
+ "integrity": "sha512-fgF6EbpoagGWh5Vwfu/7/jYgBFwUCwTlPNVF/aSjHcoEDRXpRsIqVfAFTp1LD+dWAUcAKEK3h+osk8spMJXtxA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.7.0",
+ "@firebase/logger": "0.5.0",
+ "@firebase/util": "1.13.0",
+ "@firebase/webchannel-wrapper": "1.0.5",
+ "@grpc/grpc-js": "~1.9.0",
+ "@grpc/proto-loader": "^0.7.8",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "apps/mobile/node_modules/@firebase/firestore-compat": {
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.4.tgz",
+ "integrity": "sha512-JvxxIgi+D5v9BecjLA1YomdyF7LA6CXhJuVK10b4GtRrB3m2O2hT1jJWbKYZYHUAjTaajkvnos+4U5VNxqkI2w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.7.0",
+ "@firebase/firestore": "4.10.0",
+ "@firebase/firestore-types": "3.0.3",
+ "@firebase/util": "1.13.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "apps/mobile/node_modules/@firebase/remote-config": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.8.0.tgz",
+ "integrity": "sha512-sJz7C2VACeE257Z/3kY9Ap2WXbFsgsDLfaGfZmmToKAK39ipXxFan+vzB9CSbF6mP7bzjyzEnqPcMXhAnYE6fQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.7.0",
+ "@firebase/installations": "0.6.19",
+ "@firebase/logger": "0.5.0",
+ "@firebase/util": "1.13.0",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "apps/mobile/node_modules/@firebase/remote-config-compat": {
+ "version": "0.2.21",
+ "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.21.tgz",
+ "integrity": "sha512-9+lm0eUycxbu8GO25JfJe4s6R2xlDqlVt0CR6CvN9E6B4AFArEV4qfLoDVRgIEB7nHKwvH2nYRocPWfmjRQTnw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.7.0",
+ "@firebase/logger": "0.5.0",
+ "@firebase/remote-config": "0.8.0",
+ "@firebase/remote-config-types": "0.5.0",
+ "@firebase/util": "1.13.0",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "apps/mobile/node_modules/@react-native-firebase/app": {
+ "version": "23.8.4",
+ "resolved": "https://registry.npmjs.org/@react-native-firebase/app/-/app-23.8.4.tgz",
+ "integrity": "sha512-xURXijgL5gH5O6X7aUXKS5TObDT/aqWpdJpyUNWvRtG2cUxDRcLaSfJnSJP0TREBFAP1Dntdms2kmt5l2oSYZg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "firebase": "12.8.0"
+ },
+ "peerDependencies": {
+ "expo": ">=47.0.0",
+ "react": "*",
+ "react-native": "*"
+ },
+ "peerDependenciesMeta": {
+ "expo": {
+ "optional": true
+ }
+ }
+ },
+ "apps/mobile/node_modules/@react-native-firebase/crashlytics": {
+ "version": "23.8.4",
+ "resolved": "https://registry.npmjs.org/@react-native-firebase/crashlytics/-/crashlytics-23.8.4.tgz",
+ "integrity": "sha512-5zGzbXQ7u24TqwTCRIqo63Sw2C6VgPdgEJnR+Iwh5G2NSt339wcbsxyLRaM5cP25M7Obve0JrUUG9bTDI2qIOQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "stacktrace-js": "^2.0.2"
+ },
+ "peerDependencies": {
+ "@react-native-firebase/app": "23.8.4",
+ "expo": ">=47.0.0"
+ },
+ "peerDependenciesMeta": {
+ "expo": {
+ "optional": true
+ }
+ }
+ },
"apps/mobile/node_modules/expo-sharing": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-13.0.1.tgz",
@@ -151,6 +313,66 @@
"expo": "*"
}
},
+ "apps/mobile/node_modules/firebase": {
+ "version": "12.8.0",
+ "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.8.0.tgz",
+ "integrity": "sha512-S1tCIR3ENecee0tY2cfTHfMkXqkitHfbsvqpCtvsT0Zi9vDB7A4CodAjHfHCjVvu/XtGy1LHLjOasVcF10rCVw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/ai": "2.7.0",
+ "@firebase/analytics": "0.10.19",
+ "@firebase/analytics-compat": "0.2.25",
+ "@firebase/app": "0.14.7",
+ "@firebase/app-check": "0.11.0",
+ "@firebase/app-check-compat": "0.4.0",
+ "@firebase/app-compat": "0.5.7",
+ "@firebase/app-types": "0.9.3",
+ "@firebase/auth": "1.12.0",
+ "@firebase/auth-compat": "0.6.2",
+ "@firebase/data-connect": "0.3.12",
+ "@firebase/database": "1.1.0",
+ "@firebase/database-compat": "2.1.0",
+ "@firebase/firestore": "4.10.0",
+ "@firebase/firestore-compat": "0.4.4",
+ "@firebase/functions": "0.13.1",
+ "@firebase/functions-compat": "0.4.1",
+ "@firebase/installations": "0.6.19",
+ "@firebase/installations-compat": "0.2.19",
+ "@firebase/messaging": "0.12.23",
+ "@firebase/messaging-compat": "0.2.23",
+ "@firebase/performance": "0.7.9",
+ "@firebase/performance-compat": "0.2.22",
+ "@firebase/remote-config": "0.8.0",
+ "@firebase/remote-config-compat": "0.2.21",
+ "@firebase/storage": "0.14.0",
+ "@firebase/storage-compat": "0.4.0",
+ "@firebase/util": "1.13.0"
+ }
+ },
+ "apps/mobile/node_modules/firebase/node_modules/@firebase/auth": {
+ "version": "1.12.0",
+ "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.12.0.tgz",
+ "integrity": "sha512-zkvLpsrxynWHk07qGrUDfCSqKf4AvfZGEqJ7mVCtYGjNNDbGE71k0Yn84rg8QEZu4hQw1BC0qDEHzpNVBcSVmA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.7.0",
+ "@firebase/logger": "0.5.0",
+ "@firebase/util": "1.13.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x",
+ "@react-native-async-storage/async-storage": "^2.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@react-native-async-storage/async-storage": {
+ "optional": true
+ }
+ }
+ },
"apps/mobile/node_modules/react-native-svg": {
"version": "15.8.0",
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.8.0.tgz",
@@ -4194,6 +4416,7 @@
"resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.6.1.tgz",
"integrity": "sha512-qJd9bpABqsanFnwdbjZEDbKKr1jRtuUZ+cHyNBLWsxobH4pd73QncvuO3XlMq4eKBLlg1f5jNdFpJ3G3ABu2Tg==",
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"@firebase/app-check-interop-types": "0.3.3",
"@firebase/component": "0.7.0",
@@ -4252,6 +4475,7 @@
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.6.tgz",
"integrity": "sha512-4uyt8BOrBsSq6i4yiOV/gG6BnnrvTeyymlNcaN/dKvyU1GoolxAafvIvaNP1RCGPlNab3OuE4MKUQuv2lH+PLQ==",
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"@firebase/component": "0.7.0",
"@firebase/logger": "0.5.0",
@@ -4318,6 +4542,7 @@
"resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.6.tgz",
"integrity": "sha512-YYGARbutghQY4zZUWMYia0ib0Y/rb52y72/N0z3vglRHL7ii/AaK9SA7S/dzScVOlCdnbHXz+sc5Dq+r8fwFAg==",
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"@firebase/app": "0.14.6",
"@firebase/component": "0.7.0",
@@ -4473,6 +4698,7 @@
"resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.9.3.tgz",
"integrity": "sha512-RVuvhcQzs1sD5Osr2naQS71H0bQMbSnib16uOWAKk3GaKb/WBPyCYSr2Ry7MqlxDP/YhwknUxECL07lw9Rq1nA==",
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"@firebase/component": "0.7.0",
"@firebase/logger": "0.5.0",
@@ -4494,6 +4720,7 @@
"resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.3.tgz",
"integrity": "sha512-1ylF/njF68Pmb6p0erP0U78XQv1w77Wap4bUmqZ7ZVkmN1oMgplyu0TyirWtCBoKFRV2+SUZfWXvIij/z39LYg==",
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"@firebase/component": "0.7.0",
"@firebase/firestore": "4.9.3",
@@ -4698,6 +4925,7 @@
"resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.7.0.tgz",
"integrity": "sha512-dX95X6WlW7QlgNd7aaGdjAIZUiQkgWgNS+aKNu4Wv92H1T8Ue/NDUjZHd9xb8fHxLXIHNZeco9/qbZzr500MjQ==",
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"@firebase/component": "0.7.0",
"@firebase/installations": "0.6.19",
@@ -4714,6 +4942,7 @@
"resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.20.tgz",
"integrity": "sha512-P/ULS9vU35EL9maG7xp66uljkZgcPMQOxLj3Zx2F289baTKSInE6+YIkgHEi1TwHoddC/AFePXPpshPlEFkbgg==",
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"@firebase/component": "0.7.0",
"@firebase/logger": "0.5.0",
@@ -7759,6 +7988,7 @@
"resolved": "https://registry.npmjs.org/@react-native-firebase/app/-/app-23.8.3.tgz",
"integrity": "sha512-woH8Y+9MInWK3SG2Y+V4bxi4406AuDDoUaB7x2WmEueUiJxVvlAvPJ/uGLPrqmUFyk0tiv2DXPlcEfBzxXUlZQ==",
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"firebase": "12.7.0"
},
@@ -8662,303 +8892,6 @@
"join-component": "^1.1.0"
}
},
- "node_modules/@sentry-internal/browser-utils": {
- "version": "8.54.0",
- "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.54.0.tgz",
- "integrity": "sha512-DKWCqb4YQosKn6aD45fhKyzhkdG7N6goGFDeyTaJFREJDFVDXiNDsYZu30nJ6BxMM7uQIaARhPAC5BXfoED3pQ==",
- "license": "MIT",
- "dependencies": {
- "@sentry/core": "8.54.0"
- },
- "engines": {
- "node": ">=14.18"
- }
- },
- "node_modules/@sentry-internal/feedback": {
- "version": "8.54.0",
- "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.54.0.tgz",
- "integrity": "sha512-nQqRacOXoElpE0L0ADxUUII0I3A94niqG9Z4Fmsw6057QvyrV/LvTiMQBop6r5qLjwMqK+T33iR4/NQI5RhsXQ==",
- "license": "MIT",
- "dependencies": {
- "@sentry/core": "8.54.0"
- },
- "engines": {
- "node": ">=14.18"
- }
- },
- "node_modules/@sentry-internal/replay": {
- "version": "8.54.0",
- "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.54.0.tgz",
- "integrity": "sha512-8xuBe06IaYIGJec53wUC12tY2q4z2Z0RPS2s1sLtbA00EvK1YDGuXp96IDD+HB9mnDMrQ/jW5f97g9TvPsPQUg==",
- "license": "MIT",
- "dependencies": {
- "@sentry-internal/browser-utils": "8.54.0",
- "@sentry/core": "8.54.0"
- },
- "engines": {
- "node": ">=14.18"
- }
- },
- "node_modules/@sentry-internal/replay-canvas": {
- "version": "8.54.0",
- "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.54.0.tgz",
- "integrity": "sha512-K/On3OAUBeq/TV2n+1EvObKC+WMV9npVXpVyJqCCyn8HYMm8FUGzuxeajzm0mlW4wDTPCQor6mK9/IgOquUzCw==",
- "license": "MIT",
- "dependencies": {
- "@sentry-internal/replay": "8.54.0",
- "@sentry/core": "8.54.0"
- },
- "engines": {
- "node": ">=14.18"
- }
- },
- "node_modules/@sentry/babel-plugin-component-annotate": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.2.2.tgz",
- "integrity": "sha512-D+SKQ266ra/wo87s9+UI/rKQi3qhGPCR8eSCDe0VJudhjHsqyNU+JJ5lnIGCgmZaWFTXgdBP/gdr1Iz1zqGs4Q==",
- "license": "MIT",
- "engines": {
- "node": ">= 14"
- }
- },
- "node_modules/@sentry/browser": {
- "version": "8.54.0",
- "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.54.0.tgz",
- "integrity": "sha512-BgUtvxFHin0fS0CmJVKTLXXZcke0Av729IVfi+2fJ4COX8HO7/HAP02RKaSQGmL2HmvWYTfNZ7529AnUtrM4Rg==",
- "license": "MIT",
- "dependencies": {
- "@sentry-internal/browser-utils": "8.54.0",
- "@sentry-internal/feedback": "8.54.0",
- "@sentry-internal/replay": "8.54.0",
- "@sentry-internal/replay-canvas": "8.54.0",
- "@sentry/core": "8.54.0"
- },
- "engines": {
- "node": ">=14.18"
- }
- },
- "node_modules/@sentry/cli": {
- "version": "2.42.4",
- "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.42.4.tgz",
- "integrity": "sha512-BoSZDAWJiz/40tu6LuMDkSgwk4xTsq6zwqYoUqLU3vKBR/VsaaQGvu6EWxZXORthfZU2/5Agz0+t220cge6VQw==",
- "hasInstallScript": true,
- "license": "BSD-3-Clause",
- "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.42.4",
- "@sentry/cli-linux-arm": "2.42.4",
- "@sentry/cli-linux-arm64": "2.42.4",
- "@sentry/cli-linux-i686": "2.42.4",
- "@sentry/cli-linux-x64": "2.42.4",
- "@sentry/cli-win32-i686": "2.42.4",
- "@sentry/cli-win32-x64": "2.42.4"
- }
- },
- "node_modules/@sentry/cli-darwin": {
- "version": "2.42.4",
- "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.42.4.tgz",
- "integrity": "sha512-PZV4Y97VDWBR4rIt0HkJfXaBXlebIN2s/FDzC3iHINZE5OG62CDFsnC4/lbGlf2/UZLDaGGIK7mYwSHhTvN+HQ==",
- "license": "BSD-3-Clause",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@sentry/cli-linux-arm": {
- "version": "2.42.4",
- "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.42.4.tgz",
- "integrity": "sha512-lBn0oeeg62h68/4Eo6zbPq99Idz5t0VRV48rEU/WKeM4MtQCvG/iGGQ3lBFW2yNiUBzXZIK9poXLEcgbwmcRVw==",
- "cpu": [
- "arm"
- ],
- "license": "BSD-3-Clause",
- "optional": true,
- "os": [
- "linux",
- "freebsd"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@sentry/cli-linux-arm64": {
- "version": "2.42.4",
- "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.42.4.tgz",
- "integrity": "sha512-Ex8vRnryyzC/9e43daEmEqPS+9uirY/l6Hw2lAvhBblFaL7PTWNx52H+8GnYGd9Zy2H3rWNyBDYfHwnErg38zA==",
- "cpu": [
- "arm64"
- ],
- "license": "BSD-3-Clause",
- "optional": true,
- "os": [
- "linux",
- "freebsd"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@sentry/cli-linux-i686": {
- "version": "2.42.4",
- "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.42.4.tgz",
- "integrity": "sha512-IBJg0aHjsLCL4LvcFa3cXIjA+4t5kPqBT9y+PoDu4goIFxYD8zl7mbUdGJutvJafTk8Akf4ss4JJXQBjg019zA==",
- "cpu": [
- "x86",
- "ia32"
- ],
- "license": "BSD-3-Clause",
- "optional": true,
- "os": [
- "linux",
- "freebsd"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@sentry/cli-linux-x64": {
- "version": "2.42.4",
- "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.42.4.tgz",
- "integrity": "sha512-gXI5OEiOSNiAEz7VCE6AZcAgHJ47mlgal3+NmbE8XcHmFOnyDws9FNie6PJAy8KZjXi3nqoBP9JVAbnmOix3uA==",
- "cpu": [
- "x64"
- ],
- "license": "BSD-3-Clause",
- "optional": true,
- "os": [
- "linux",
- "freebsd"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@sentry/cli-win32-i686": {
- "version": "2.42.4",
- "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.42.4.tgz",
- "integrity": "sha512-vZuR3UPHKqOMniyrijrrsNwn9usaRysXq78F6WV0cL0ZyPLAmY+KBnTDSFk1Oig2pURnzaTm+RtcZu2fc8mlzg==",
- "cpu": [
- "x86",
- "ia32"
- ],
- "license": "BSD-3-Clause",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@sentry/cli-win32-x64": {
- "version": "2.42.4",
- "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.42.4.tgz",
- "integrity": "sha512-OIBj3uaQ6nAERSm5Dcf8UIhyElEEwMNsZEEppQpN4IKl0mrwb/57AznM23Dvpu6GR8WGbVQUSolt879YZR5E9g==",
- "cpu": [
- "x64"
- ],
- "license": "BSD-3-Clause",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@sentry/core": {
- "version": "8.54.0",
- "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.54.0.tgz",
- "integrity": "sha512-03bWf+D1j28unOocY/5FDB6bUHtYlm6m6ollVejhg45ZmK9iPjdtxNWbrLsjT1WRym0Tjzowu+A3p+eebYEv0Q==",
- "license": "MIT",
- "engines": {
- "node": ">=14.18"
- }
- },
- "node_modules/@sentry/react": {
- "version": "8.54.0",
- "resolved": "https://registry.npmjs.org/@sentry/react/-/react-8.54.0.tgz",
- "integrity": "sha512-42T/fp8snYN19Fy/2P0Mwotu4gcdy+1Lx+uYCNcYP1o7wNGigJ7qb27sW7W34GyCCHjoCCfQgeOqDQsyY8LC9w==",
- "license": "MIT",
- "dependencies": {
- "@sentry/browser": "8.54.0",
- "@sentry/core": "8.54.0",
- "hoist-non-react-statics": "^3.3.2"
- },
- "engines": {
- "node": ">=14.18"
- },
- "peerDependencies": {
- "react": "^16.14.0 || 17.x || 18.x || 19.x"
- }
- },
- "node_modules/@sentry/react-native": {
- "version": "6.10.0",
- "resolved": "https://registry.npmjs.org/@sentry/react-native/-/react-native-6.10.0.tgz",
- "integrity": "sha512-B56vc+pnFHMiu3cabFb454v4qD0zObW6JVzJ5Gb6fIMdt93AFIJg10ZErzC+ump7xM4BOEROFFRuLiyvadvlPA==",
- "license": "MIT",
- "dependencies": {
- "@sentry/babel-plugin-component-annotate": "3.2.2",
- "@sentry/browser": "8.54.0",
- "@sentry/cli": "2.42.4",
- "@sentry/core": "8.54.0",
- "@sentry/react": "8.54.0",
- "@sentry/types": "8.54.0",
- "@sentry/utils": "8.54.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": "8.54.0",
- "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.54.0.tgz",
- "integrity": "sha512-wztdtr7dOXQKi0iRvKc8XJhJ7HaAfOv8lGu0yqFOFwBZucO/SHnu87GOPi8mvrTiy1bentQO5l+zXWAaMvG4uw==",
- "license": "MIT",
- "dependencies": {
- "@sentry/core": "8.54.0"
- },
- "engines": {
- "node": ">=14.18"
- }
- },
- "node_modules/@sentry/utils": {
- "version": "8.54.0",
- "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.54.0.tgz",
- "integrity": "sha512-JL8UDjrsKxKclTdLXfuHfE7B3KbrAPEYP7tMyN/xiO2vsF6D84fjwYyalO0ZMtuFZE6vpSze8ZOLEh6hLnPYsw==",
- "license": "MIT",
- "dependencies": {
- "@sentry/core": "8.54.0"
- },
- "engines": {
- "node": ">=14.18"
- }
- },
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@@ -10626,6 +10559,7 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"debug": "4"
@@ -15477,6 +15411,7 @@
"resolved": "https://registry.npmjs.org/firebase/-/firebase-12.7.0.tgz",
"integrity": "sha512-ZBZg9jFo8uH4Emd7caOqtalKJfDGHnHQSrCPiqRAdTFQd0wL3ERilUBfhnhBLnlernugkN/o7nJa0p+sE71Izg==",
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"@firebase/ai": "2.6.1",
"@firebase/analytics": "0.10.19",
@@ -15513,6 +15448,7 @@
"resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.12.0.tgz",
"integrity": "sha512-zkvLpsrxynWHk07qGrUDfCSqKf4AvfZGEqJ7mVCtYGjNNDbGE71k0Yn84rg8QEZu4hQw1BC0qDEHzpNVBcSVmA==",
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"@firebase/component": "0.7.0",
"@firebase/logger": "0.5.0",
@@ -16317,6 +16253,7 @@
"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==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"agent-base": "6",
@@ -21929,12 +21866,6 @@
"node": ">=12.0.0"
}
},
- "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/psl": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
@@ -23792,7 +23723,6 @@
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz",
"integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"stackframe": "^1.3.4"
@@ -23829,7 +23759,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz",
"integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"source-map": "0.5.6",
@@ -23840,7 +23769,6 @@
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz",
"integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==",
- "dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -23850,7 +23778,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz",
"integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"error-stack-parser": "^2.0.6",
diff --git a/supabase/migrations/20260127202703_add_discord_notification_triggers.sql b/supabase/migrations/20260127202703_add_discord_notification_triggers.sql
new file mode 100644
index 0000000..16314d7
--- /dev/null
+++ b/supabase/migrations/20260127202703_add_discord_notification_triggers.sql
@@ -0,0 +1,142 @@
+-- Helper function: sends a Discord notification via the send-discord-notification edge function
+CREATE OR REPLACE FUNCTION public.notify_discord_event(p_event text, p_payload jsonb)
+RETURNS void
+LANGUAGE plpgsql
+SECURITY DEFINER
+AS $$
+BEGIN
+ PERFORM net.http_post(
+ url := 'https://ckjcrkjpmhqhiucifukx.supabase.co/functions/v1/send-discord-notification',
+ headers := jsonb_build_object('Content-Type', 'application/json'),
+ body := jsonb_build_object(
+ 'event', p_event,
+ 'payload', p_payload
+ )
+ );
+END;
+$$;
+
+-- Trigger function: new user signup
+CREATE OR REPLACE FUNCTION public.notify_new_user_discord()
+RETURNS trigger
+LANGUAGE plpgsql
+SECURITY DEFINER
+AS $$
+BEGIN
+ PERFORM public.notify_discord_event(
+ 'new_user',
+ jsonb_build_object(
+ 'user_id', NEW.id,
+ 'name', NEW.name,
+ 'email', NEW.email,
+ 'created_at', NEW.created_at
+ )
+ );
+
+ RETURN NEW;
+END;
+$$;
+
+-- Trigger function: couple paired (fires when second partner joins)
+CREATE OR REPLACE FUNCTION public.notify_couple_paired_discord()
+RETURNS trigger
+LANGUAGE plpgsql
+SECURITY DEFINER
+AS $$
+DECLARE
+ v_member_count integer;
+ v_profiles jsonb;
+BEGIN
+ IF NEW.couple_id IS NULL OR OLD.couple_id IS NOT DISTINCT FROM NEW.couple_id THEN
+ RETURN NEW;
+ END IF;
+
+ SELECT COUNT(*)
+ INTO v_member_count
+ FROM public.profiles
+ WHERE couple_id = NEW.couple_id;
+
+ IF v_member_count = 2 THEN
+ SELECT jsonb_agg(
+ jsonb_build_object(
+ 'id', p.id,
+ 'name', p.name,
+ 'email', p.email
+ )
+ ORDER BY p.created_at
+ )
+ INTO v_profiles
+ FROM public.profiles p
+ WHERE p.couple_id = NEW.couple_id;
+
+ PERFORM public.notify_discord_event(
+ 'couple_paired',
+ jsonb_build_object(
+ 'couple_id', NEW.couple_id,
+ 'profiles', COALESCE(v_profiles, '[]'::jsonb),
+ 'paired_at', now()
+ )
+ );
+ END IF;
+
+ RETURN NEW;
+END;
+$$;
+
+-- Trigger function: feedback submitted
+CREATE OR REPLACE FUNCTION public.notify_feedback_submitted_discord()
+RETURNS trigger
+LANGUAGE plpgsql
+SECURITY DEFINER
+AS $$
+DECLARE
+ v_user jsonb;
+BEGIN
+ SELECT jsonb_build_object(
+ 'id', p.id,
+ 'name', p.name,
+ 'email', p.email
+ )
+ INTO v_user
+ FROM public.profiles p
+ WHERE p.id = NEW.user_id;
+
+ PERFORM public.notify_discord_event(
+ 'feedback_submitted',
+ jsonb_build_object(
+ 'feedback_id', NEW.id,
+ 'user_id', NEW.user_id,
+ 'type', NEW.type,
+ 'title', NEW.title,
+ 'description', NEW.description,
+ 'status', NEW.status,
+ 'screenshot_url', NEW.screenshot_url,
+ 'question_id', NEW.question_id,
+ 'created_at', NEW.created_at,
+ 'user', v_user
+ )
+ );
+
+ RETURN NEW;
+END;
+$$;
+
+-- Create triggers (drop first to be idempotent)
+DROP TRIGGER IF EXISTS on_profile_created_discord ON public.profiles;
+CREATE TRIGGER on_profile_created_discord
+ AFTER INSERT ON public.profiles
+ FOR EACH ROW
+ EXECUTE FUNCTION public.notify_new_user_discord();
+
+DROP TRIGGER IF EXISTS on_profile_couple_paired_discord ON public.profiles;
+CREATE TRIGGER on_profile_couple_paired_discord
+ AFTER UPDATE OF couple_id ON public.profiles
+ FOR EACH ROW
+ WHEN (OLD.couple_id IS DISTINCT FROM NEW.couple_id AND NEW.couple_id IS NOT NULL)
+ EXECUTE FUNCTION public.notify_couple_paired_discord();
+
+DROP TRIGGER IF EXISTS on_feedback_submitted_discord ON public.feedback;
+CREATE TRIGGER on_feedback_submitted_discord
+ AFTER INSERT ON public.feedback
+ FOR EACH ROW
+ EXECUTE FUNCTION public.notify_feedback_submitted_discord();