diff --git a/apps/mobile/app.json b/apps/mobile/app.json index ada0d16..3a173bf 100644 --- a/apps/mobile/app.json +++ b/apps/mobile/app.json @@ -41,6 +41,16 @@ "favicon": "./assets/favicon.png" }, "plugins": [ + "@react-native-firebase/app", + "@react-native-firebase/analytics", + [ + "expo-build-properties", + { + "ios": { + "useFrameworks": "static" + } + } + ], "expo-router", "expo-apple-authentication", [ diff --git a/apps/mobile/app/(app)/chat/[id].tsx b/apps/mobile/app/(app)/chat/[id].tsx index d2274b1..c8265c5 100644 --- a/apps/mobile/app/(app)/chat/[id].tsx +++ b/apps/mobile/app/(app)/chat/[id].tsx @@ -14,6 +14,7 @@ import { decode } from 'base64-arraybuffer'; import { Database } from "../../../src/types/supabase"; import { GradientBackground } from "../../../src/components/ui"; import { colors, gradients, spacing, radius, typography } from "../../../src/theme"; +import { Events } from "../../../src/lib/analytics"; type Message = Database["public"]["Tables"]["messages"]["Row"]; @@ -205,6 +206,8 @@ export default function ChatScreen() { if (error) { Alert.alert("Error", "Failed to send message"); setInputText(content); + } else { + Events.messageSent(); } }; @@ -283,6 +286,7 @@ export default function ChatScreen() { user_id: user.id, media_path: fileName, }); + Events.mediaUploaded(); } catch (error) { Alert.alert("Error", "Failed to upload image"); diff --git a/apps/mobile/app/(app)/onboarding.tsx b/apps/mobile/app/(app)/onboarding.tsx index 5e6b539..7a99b38 100644 --- a/apps/mobile/app/(app)/onboarding.tsx +++ b/apps/mobile/app/(app)/onboarding.tsx @@ -28,7 +28,9 @@ import { GlassButton } from '../../src/components/ui/GlassButton'; import { colors, gradients, spacing, typography, radius, shadows } from '../../src/theme'; import { useAuthStore } from '../../src/store'; import { supabase } from '../../src/lib/supabase'; +import { getProfileError } from '../../src/lib/errors'; import { registerForPushNotificationsAsync, savePushToken } from '../../src/lib/notifications'; +import { Events } from '../../src/lib/analytics'; import type { Gender } from '../../src/types'; const GENDER_OPTIONS: { value: Gender; label: string; icon: string }[] = [ @@ -59,6 +61,7 @@ export default function OnboardingScreen() { Keyboard.dismiss(); setError(null); setStage('gender'); + Events.onboardingStageComplete('name'); }; const handleGenderSelect = (selectedGender: Gender) => { @@ -72,10 +75,12 @@ export default function OnboardingScreen() { return; } setStage('explicit'); + Events.onboardingStageComplete('gender'); }; const handleExplicitContinue = () => { setStage('notifications'); + Events.onboardingStageComplete('explicit'); }; const handleEnableNotifications = async () => { @@ -124,10 +129,11 @@ export default function OnboardingScreen() { console.log('[Onboarding] Profile updated successfully, fetching user...'); await fetchUser(); console.log('[Onboarding] User fetched, navigating to home...'); + Events.onboardingComplete(); router.replace('/'); } catch (err: any) { console.error('Onboarding error:', err); - setError(err.message || 'Something went wrong'); + setError(getProfileError(err)); } finally { setIsLoading(false); } diff --git a/apps/mobile/app/(app)/pairing.tsx b/apps/mobile/app/(app)/pairing.tsx index 2a47441..6bb9f52 100755 --- a/apps/mobile/app/(app)/pairing.tsx +++ b/apps/mobile/app/(app)/pairing.tsx @@ -17,6 +17,8 @@ import Animated, { FadeInDown } from "react-native-reanimated"; import * as Clipboard from "expo-clipboard"; import { useAuthStore } from "../../src/store"; import { supabase } from "../../src/lib/supabase"; +import { getPairingError } from "../../src/lib/errors"; +import { Events } from "../../src/lib/analytics"; import { router } from "expo-router"; import { GradientBackground, GlassCard, GlassButton } from "../../src/components/ui"; import { colors, gradients, spacing, radius, typography, shadows } from "../../src/theme"; @@ -81,8 +83,9 @@ export default function PairingScreen() { await fetchUser(); // Refresh user to get couple_id await fetchCouple(); // Fetch couple data + Events.coupleCreated(); } catch (error: any) { - Alert.alert("Error", error.message); + Alert.alert("Error", getPairingError(error)); } finally { setIsSubmitting(false); } @@ -92,7 +95,7 @@ export default function PairingScreen() { // Validate invite code format (8 alphanumeric characters) const sanitizedCode = inviteCode.trim().toUpperCase(); if (!/^[A-Z0-9]{8}$/.test(sanitizedCode)) { - Alert.alert("Error", "Please enter a valid 8-character invite code"); + Alert.alert("Invalid Code", "Please enter a valid 8-character invite code."); return; } @@ -118,12 +121,13 @@ export default function PairingScreen() { await fetchUser(); // Refresh user to get couple_id await fetchCouple(); // Fetch couple data + Events.coupleJoined(); Alert.alert("Success", "You are now paired!", [ { text: "Let's Go", onPress: () => router.replace("/(app)/") } ]); } catch (error: any) { - Alert.alert("Error", error.message); + Alert.alert("Error", getPairingError(error)); } finally { setIsSubmitting(false); } @@ -142,6 +146,7 @@ export default function PairingScreen() { await Share.share({ message: `Join me on Sauci! Use my invite code to pair up: ${couple.invite_code}`, }); + Events.codeShared(); } catch (error) { console.error(error); } @@ -169,8 +174,9 @@ export default function PairingScreen() { await fetchUser(); await fetchCouple(); + Events.pairingCancelled(); } catch (error: any) { - Alert.alert("Error", error.message); + Alert.alert("Error", getPairingError(error)); } finally { setIsSubmitting(false); } diff --git a/apps/mobile/app/(app)/profile.tsx b/apps/mobile/app/(app)/profile.tsx index aa8876b..82e53ed 100755 --- a/apps/mobile/app/(app)/profile.tsx +++ b/apps/mobile/app/(app)/profile.tsx @@ -34,7 +34,7 @@ export default function ProfileScreen() { const [isDeleting, setIsDeleting] = useState(false); const [showFeedbackModal, setShowFeedbackModal] = useState(false); const [showPaywall, setShowPaywall] = useState(false); - const [showExplicit, setShowExplicit] = useState(user?.show_explicit_content ?? false); + const [showExplicit, setShowExplicit] = useState(user?.show_explicit_content ?? true); const [isUpdatingExplicit, setIsUpdatingExplicit] = useState(false); const [biometricAvailable, setBiometricAvailable] = useState(false); const [biometricEnabled, setBiometricEnabledState] = useState(false); diff --git a/apps/mobile/app/(app)/swipe.tsx b/apps/mobile/app/(app)/swipe.tsx index 27993ef..95660b1 100755 --- a/apps/mobile/app/(app)/swipe.tsx +++ b/apps/mobile/app/(app)/swipe.tsx @@ -11,6 +11,7 @@ import { hasSeenSwipeTutorial, markSwipeTutorialSeen } from "../../src/lib/swipe import SwipeCard from "../../src/components/SwipeCard"; import SwipeTutorial from "../../src/components/SwipeTutorial"; import { GradientBackground, GlassCard, GlassButton } from "../../src/components/ui"; +import { Events } from "../../src/lib/analytics"; import { colors, gradients, spacing, typography, radius, shadows } from "../../src/theme"; export default function SwipeScreen() { @@ -102,6 +103,7 @@ export default function SwipeScreen() { // Handle skip - don't submit a response, just store it for later if (direction === "down") { await skipQuestion(question.id); + Events.questionSkipped(); return; } @@ -127,6 +129,8 @@ export default function SwipeScreen() { if (error) { console.error("Submit response error:", error); + } else { + Events.questionAnswered(answer, question.pack_id); } } catch (error) { console.error("Failed to submit response", error); diff --git a/apps/mobile/app/(auth)/forgot-password.tsx b/apps/mobile/app/(auth)/forgot-password.tsx index 6d333d2..0b3076c 100644 --- a/apps/mobile/app/(auth)/forgot-password.tsx +++ b/apps/mobile/app/(auth)/forgot-password.tsx @@ -17,6 +17,7 @@ import Animated, { } from "react-native-reanimated"; import { Ionicons } from "@expo/vector-icons"; import { supabase } from "../../src/lib/supabase"; +import { getAuthError } from "../../src/lib/errors"; import { GradientBackground, GlassCard, GlassButton, GlassInput } from "../../src/components/ui"; import { colors, spacing, radius, typography } from "../../src/theme"; @@ -71,7 +72,7 @@ export default function ForgotPasswordScreen() { setIsLoading(false); if (resetError) { - showError(resetError.message); + showError(getAuthError(resetError)); } else { setIsEmailSent(true); setResendCooldown(60); @@ -92,7 +93,7 @@ export default function ForgotPasswordScreen() { if (resendError) { if (Platform.OS !== 'web') { - Alert.alert("Error", resendError.message); + Alert.alert("Error", getAuthError(resendError)); } } else { setResendCooldown(60); diff --git a/apps/mobile/app/(auth)/login.tsx b/apps/mobile/app/(auth)/login.tsx index 09cd6cc..d774d23 100755 --- a/apps/mobile/app/(auth)/login.tsx +++ b/apps/mobile/app/(auth)/login.tsx @@ -29,6 +29,8 @@ if (Platform.OS === "ios") { } } import { supabase } from "../../src/lib/supabase"; +import { getAuthError } from "../../src/lib/errors"; +import { Events } from "../../src/lib/analytics"; import { useAuthStore } from "../../src/store"; import { GradientBackground, GlassCard, GlassButton, GlassInput } from "../../src/components/ui"; import { colors, spacing, radius, typography } from "../../src/theme"; @@ -96,7 +98,7 @@ export default function LoginScreen() { if (resendError) { if (Platform.OS !== 'web') { - Alert.alert("Error", resendError.message); + Alert.alert("Error", getAuthError(resendError)); } } else { // Start 60 second cooldown @@ -125,7 +127,7 @@ export default function LoginScreen() { Alert.alert("Not yet verified", "Please check your email and click the verification link."); } } else { - Alert.alert("Error", signInError.message); + Alert.alert("Error", getAuthError(signInError)); } } // If successful, the auth state change will handle the redirect @@ -175,9 +177,10 @@ export default function LoginScreen() { setIsLoading(false); if (authError) { - showError(authError.message); + showError(getAuthError(authError)); } else { setIsMagicLinkSent(true); + Events.signIn("magic_link"); } }; @@ -207,7 +210,7 @@ export default function LoginScreen() { setIsLoading(false); if (signUpError) { - showError(signUpError.message); + showError(getAuthError(signUpError)); } else if (data.user && data.user.identities && data.user.identities.length === 0) { // User already exists - Supabase returns success but empty identities // for security (prevents email enumeration) @@ -218,6 +221,7 @@ export default function LoginScreen() { email: email.trim(), password, }); + Events.signUp("password"); } } else { const { error: signInError } = await supabase.auth.signInWithPassword({ @@ -227,7 +231,9 @@ export default function LoginScreen() { setIsLoading(false); - if (signInError) { + if (!signInError) { + Events.signIn("password"); + } else if (signInError) { // Check if the error is due to unverified email if (signInError.message.includes("Email not confirmed")) { // Show the verification pending screen @@ -236,7 +242,7 @@ export default function LoginScreen() { password, }); } else { - showError(signInError.message); + showError(getAuthError(signInError)); } } } @@ -264,7 +270,9 @@ export default function LoginScreen() { if (error) { console.log("[Apple Sign In] Supabase error:", error.message); - Alert.alert("Error", error.message); + Alert.alert("Error", getAuthError(error)); + } else { + Events.signIn("apple"); } } else { Alert.alert("Error", "No identity token received from Apple"); @@ -274,7 +282,7 @@ export default function LoginScreen() { if (e.code === "ERR_REQUEST_CANCELED") { // User canceled - do nothing } else { - Alert.alert("Apple Sign In Error", `${e.code || 'Unknown'}: ${e.message || "Apple sign in failed"}`); + Alert.alert("Error", "Apple sign in failed. Please try again."); } } } else { @@ -288,7 +296,7 @@ export default function LoginScreen() { }); if (error) { - Alert.alert("Error", error.message); + Alert.alert("Error", getAuthError(error)); } } }; diff --git a/apps/mobile/app/(auth)/reset-password.tsx b/apps/mobile/app/(auth)/reset-password.tsx index 8ecd218..7fb7a1e 100644 --- a/apps/mobile/app/(auth)/reset-password.tsx +++ b/apps/mobile/app/(auth)/reset-password.tsx @@ -16,6 +16,7 @@ import Animated, { } from "react-native-reanimated"; import { Ionicons } from "@expo/vector-icons"; import { supabase } from "../../src/lib/supabase"; +import { getAuthError } from "../../src/lib/errors"; import { GradientBackground, GlassCard, GlassButton, GlassInput } from "../../src/components/ui"; import { colors, spacing, radius, typography } from "../../src/theme"; @@ -59,14 +60,7 @@ export default function ResetPasswordScreen() { setIsLoading(false); if (updateError) { - // Handle common errors - if (updateError.message.includes("same as")) { - showError("New password must be different from your current password"); - } else if (updateError.message.includes("session")) { - showError("Your reset link has expired. Please request a new one."); - } else { - showError(updateError.message); - } + showError(getAuthError(updateError)); } else { setIsSuccess(true); } diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx index 39a4b04..d1f8113 100755 --- a/apps/mobile/app/_layout.tsx +++ b/apps/mobile/app/_layout.tsx @@ -5,6 +5,10 @@ import "../src/polyfills/web"; import { initSentry, setUserContext, clearUserContext } from "../src/lib/sentry"; initSentry(); +// Initialize Firebase Analytics +import { initAnalytics, setUserId, clearUserId } from "../src/lib/analytics"; +initAnalytics(); + // Import push notification utilities import { clearPushToken, @@ -153,6 +157,8 @@ export default function RootLayout() { // Set Sentry user context for error tracking if (session?.user) { setUserContext(session.user.id, session.user.email); + // Set Analytics user ID + setUserId(session.user.id); } } else if (event === "SIGNED_OUT") { // Clear push token before clearing user state @@ -162,6 +168,7 @@ export default function RootLayout() { } setUser(null); clearUserContext(); + clearUserId(); // Clear React Query cache queryClient.clear(); } diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 97d59d3..d8a8f3d 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -15,6 +15,8 @@ "@expo/ngrok": "^4.1.0", "@expo/vector-icons": "~14.0.4", "@react-native-async-storage/async-storage": "1.23.1", + "@react-native-firebase/analytics": "^23.7.0", + "@react-native-firebase/app": "^23.7.0", "@react-navigation/bottom-tabs": "^7.0.0", "@react-navigation/native": "^7.0.0", "@react-navigation/native-stack": "^7.0.0", @@ -25,6 +27,7 @@ "expo": "~52.0.0", "expo-apple-authentication": "~7.1.3", "expo-blur": "~14.0.3", + "expo-build-properties": "^0.13.3", "expo-clipboard": "~7.0.1", "expo-constants": "~17.0.0", "expo-dev-client": "~5.0.20", diff --git a/apps/mobile/src/components/Paywall.tsx b/apps/mobile/src/components/Paywall.tsx index 4d12c89..cccce3d 100644 --- a/apps/mobile/src/components/Paywall.tsx +++ b/apps/mobile/src/components/Paywall.tsx @@ -14,6 +14,7 @@ import { LinearGradient } from "expo-linear-gradient"; import { useSubscriptionStore } from "../store"; import { colors, gradients, spacing, radius, typography } from "../theme"; import type { PurchasesPackage } from "../lib/revenuecat"; +import { Events } from "../lib/analytics"; interface PaywallProps { visible: boolean; @@ -58,6 +59,9 @@ export function Paywall({ visible, onClose, onSuccess }: PaywallProps) { if (visible && !offerings) { fetchOfferings(); } + if (visible) { + Events.paywallShown("paywall"); + } }, [visible, offerings, fetchOfferings]); useEffect(() => { @@ -73,8 +77,10 @@ export function Paywall({ visible, onClose, onSuccess }: PaywallProps) { const handlePurchase = async () => { if (!selectedPackage) return; + Events.purchaseInitiated(selectedPackage.packageType || "unknown"); const success = await purchasePackage(selectedPackage); if (success) { + Events.purchaseCompleted(selectedPackage.packageType || "unknown"); onSuccess?.(); onClose(); } @@ -83,6 +89,7 @@ export function Paywall({ visible, onClose, onSuccess }: PaywallProps) { const handleRestore = async () => { const restored = await restorePurchases(); if (restored) { + Events.purchaseRestored(); onSuccess?.(); onClose(); } diff --git a/apps/mobile/src/lib/analytics.ts b/apps/mobile/src/lib/analytics.ts new file mode 100644 index 0000000..039bc55 --- /dev/null +++ b/apps/mobile/src/lib/analytics.ts @@ -0,0 +1,142 @@ +import analytics from "@react-native-firebase/analytics"; +import { Platform } from "react-native"; + +let isInitialized = false; + +export function initAnalytics() { + if (__DEV__) { + console.log("[Analytics] Disabled in development mode"); + return; + } + + if (isInitialized) { + return; + } + + isInitialized = true; + console.log("[Analytics] Firebase Analytics initialized"); +} + +// Log a custom event +export async function logEvent( + name: string, + params?: Record +) { + if (__DEV__) { + console.log("[Analytics] Event:", name, params); + return; + } + + try { + await analytics().logEvent(name, params); + } catch (error) { + console.warn("[Analytics] Failed to log event:", error); + } +} + +// Set user ID for tracking +export async function setUserId(userId: string) { + if (__DEV__) { + console.log("[Analytics] Set user ID:", userId); + return; + } + + try { + await analytics().setUserId(userId); + } catch (error) { + console.warn("[Analytics] Failed to set user ID:", error); + } +} + +// Clear user ID on logout +export async function clearUserId() { + if (__DEV__) { + console.log("[Analytics] Cleared user ID"); + return; + } + + try { + await analytics().setUserId(null); + } catch (error) { + console.warn("[Analytics] Failed to clear user ID:", error); + } +} + +// Set user properties +export async function setUserProperties( + properties: Record +) { + if (__DEV__) { + console.log("[Analytics] Set user properties:", properties); + return; + } + + try { + for (const [key, value] of Object.entries(properties)) { + await analytics().setUserProperty(key, value); + } + } catch (error) { + console.warn("[Analytics] Failed to set user properties:", error); + } +} + +// Log screen view +export async function logScreenView(screenName: string, screenClass?: string) { + if (__DEV__) { + console.log("[Analytics] Screen view:", screenName); + return; + } + + try { + await analytics().logScreenView({ + screen_name: screenName, + screen_class: screenClass ?? screenName, + }); + } catch (error) { + console.warn("[Analytics] Failed to log screen view:", error); + } +} + +// 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"), + + // Subscription events + paywallShown: (source: string) => logEvent("paywall_shown", { source }), + purchaseInitiated: (packageType: string) => + logEvent("purchase_initiated", { package_type: packageType }), + purchaseCompleted: (packageType: string) => + logEvent("purchase", { 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 }), +}; diff --git a/apps/mobile/src/lib/errors.ts b/apps/mobile/src/lib/errors.ts new file mode 100644 index 0000000..0bdcb18 --- /dev/null +++ b/apps/mobile/src/lib/errors.ts @@ -0,0 +1,134 @@ +/** + * Centralized error handling utility + * Maps technical Supabase/API errors to user-friendly messages + */ + +type ErrorCategory = 'auth' | 'pairing' | 'profile' | 'network' | 'generic'; + +interface ErrorMapping { + pattern: RegExp | string; + message: string; +} + +const AUTH_ERROR_MAPPINGS: ErrorMapping[] = [ + { pattern: /invalid login credentials/i, message: "Incorrect email or password. Please try again." }, + { pattern: /email not confirmed/i, message: "Please verify your email before signing in." }, + { pattern: /user already registered/i, message: "An account with this email already exists." }, + { pattern: /invalid email/i, message: "Please enter a valid email address." }, + { pattern: /password.*too short/i, message: "Password must be at least 8 characters." }, + { pattern: /password.*too weak/i, message: "Please choose a stronger password." }, + { pattern: /rate limit/i, message: "Too many attempts. Please wait a moment and try again." }, + { pattern: /email rate limit/i, message: "Too many emails sent. Please wait before requesting another." }, + { pattern: /same as/i, message: "New password must be different from your current password." }, + { pattern: /session.*expired/i, message: "Your session has expired. Please log in again." }, + { pattern: /invalid.*token/i, message: "This link has expired. Please request a new one." }, + { pattern: /otp.*expired/i, message: "This code has expired. Please request a new one." }, + { pattern: /signups.*disabled/i, message: "Sign ups are currently disabled. Please try again later." }, + { pattern: /user not found/i, message: "No account found with this email." }, + { pattern: /provider.*disabled/i, message: "This sign-in method is currently unavailable." }, +]; + +const PAIRING_ERROR_MAPPINGS: ErrorMapping[] = [ + { pattern: /invalid.*invite.*code/i, message: "This invite code is invalid. Please check and try again." }, + { pattern: /invite.*code.*not.*found/i, message: "This invite code doesn't exist. Please check with your partner." }, + { pattern: /already.*paired/i, message: "You're already paired with a partner." }, + { pattern: /couple.*full/i, message: "This couple already has two partners." }, + { pattern: /cannot.*join.*own/i, message: "You can't join your own couple." }, +]; + +const PROFILE_ERROR_MAPPINGS: ErrorMapping[] = [ + { pattern: /profile.*not.*found/i, message: "Profile not found. Please try logging in again." }, + { pattern: /duplicate.*name/i, message: "This name is already taken." }, +]; + +const NETWORK_ERROR_MAPPINGS: ErrorMapping[] = [ + { pattern: /network.*error/i, message: "Connection problem. Please check your internet and try again." }, + { pattern: /fetch.*failed/i, message: "Couldn't connect to server. Please try again." }, + { pattern: /timeout/i, message: "Request timed out. Please try again." }, + { pattern: /abort/i, message: "Request was cancelled. Please try again." }, +]; + +const GENERIC_FALLBACK = "Something went wrong. Please try again."; + +/** + * Get a user-friendly error message from a Supabase/API error + */ +export function getUserFriendlyError( + error: Error | { message: string } | string | null | undefined, + category: ErrorCategory = 'generic' +): string { + if (!error) return GENERIC_FALLBACK; + + const errorMessage = typeof error === 'string' ? error : error.message; + if (!errorMessage) return GENERIC_FALLBACK; + + // Check category-specific mappings first + const categoryMappings = getCategoryMappings(category); + for (const mapping of categoryMappings) { + if (matchesPattern(errorMessage, mapping.pattern)) { + return mapping.message; + } + } + + // Check network errors (applies to all categories) + for (const mapping of NETWORK_ERROR_MAPPINGS) { + if (matchesPattern(errorMessage, mapping.pattern)) { + return mapping.message; + } + } + + // Check auth errors if in auth category + if (category === 'auth') { + for (const mapping of AUTH_ERROR_MAPPINGS) { + if (matchesPattern(errorMessage, mapping.pattern)) { + return mapping.message; + } + } + } + + // Return generic fallback instead of raw error + return GENERIC_FALLBACK; +} + +function getCategoryMappings(category: ErrorCategory): ErrorMapping[] { + switch (category) { + case 'auth': + return AUTH_ERROR_MAPPINGS; + case 'pairing': + return PAIRING_ERROR_MAPPINGS; + case 'profile': + return PROFILE_ERROR_MAPPINGS; + case 'network': + return NETWORK_ERROR_MAPPINGS; + default: + return []; + } +} + +function matchesPattern(message: string, pattern: RegExp | string): boolean { + if (typeof pattern === 'string') { + return message.toLowerCase().includes(pattern.toLowerCase()); + } + return pattern.test(message); +} + +/** + * Helper specifically for auth errors + */ +export function getAuthError(error: Error | { message: string } | string | null | undefined): string { + return getUserFriendlyError(error, 'auth'); +} + +/** + * Helper specifically for pairing/couple errors + */ +export function getPairingError(error: Error | { message: string } | string | null | undefined): string { + return getUserFriendlyError(error, 'pairing'); +} + +/** + * Helper specifically for profile errors + */ +export function getProfileError(error: Error | { message: string } | string | null | undefined): string { + return getUserFriendlyError(error, 'profile'); +} diff --git a/apps/mobile/src/store/index.ts b/apps/mobile/src/store/index.ts index e268611..944ea69 100755 --- a/apps/mobile/src/store/index.ts +++ b/apps/mobile/src/store/index.ts @@ -257,7 +257,7 @@ export const usePacksStore = create((set, get) => ({ set({ isLoading: true }); // Get user's explicit content preference - const showExplicit = useAuthStore.getState().user?.show_explicit_content ?? false; + const showExplicit = useAuthStore.getState().user?.show_explicit_content ?? true; // Fetch categories const { data: categories } = await supabase diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 35b8468..840e27f 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -2,6 +2,7 @@ import Navbar from '@/components/Navbar' import Hero from '@/components/Hero' import Features from '@/components/Features' import HowItWorks from '@/components/HowItWorks' +import Pricing from '@/components/Pricing' import CTA from '@/components/CTA' import Footer from '@/components/Footer' @@ -12,6 +13,7 @@ export default function Home() { +