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();