mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-11-01 18:19:43 +00:00
update
This commit is contained in:
1 parent
c978a3bd28
commit
0f229211ea
19 files changed
+1625
-624
No files matched your search
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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<string, string | number | boolean>
|
||||
) {
|
||||
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<string, string | null>
|
||||
) {
|
||||
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 }),
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
@@ -257,7 +257,7 @@ export const usePacksStore = create<PacksState>((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
|
||||
|
||||
Reference in new issue
Block a user