import { View, Text, StyleSheet, TouchableOpacity, Alert, ScrollView, Platform, useWindowDimensions, TextInput, Modal, ActivityIndicator, Linking, Switch, Image, ActionSheetIOS } from "react-native"; import { useState, useMemo, useEffect } from "react"; import { useAuthStore, useMatchStore, useMessageStore, usePacksStore, useSubscriptionStore } from "../../src/store"; import { Ionicons } from "@expo/vector-icons"; import { LinearGradient } from "expo-linear-gradient"; import Animated, { FadeInDown } from "react-native-reanimated"; import { supabase } from "../../src/lib/supabase"; import { resetSwipeTutorial } from "../../src/lib/swipeTutorialSeen"; import * as ImagePicker from 'expo-image-picker'; import * as FileSystem from 'expo-file-system'; import { decode } from 'base64-arraybuffer'; import { isBiometricAvailable, isBiometricEnabled, setBiometricEnabled, getBiometricType, } from "../../src/lib/biometricAuth"; import { router } from "expo-router"; import { GradientBackground, GlassCard } from "../../src/components/ui"; import { FeedbackModal } from "../../src/components/FeedbackModal"; import { Paywall } from "../../src/components/Paywall"; import { Events } from "../../src/lib/analytics"; import { colors, gradients, spacing, radius, typography, shadows } from "../../src/theme"; const MAX_CONTENT_WIDTH = 500; export default function ProfileScreen() { const { user, partner, couple, signOut, fetchCouple, fetchUser } = useAuthStore(); const { clearMatches } = useMatchStore(); const { clearMessages } = useMessageStore(); const { clearPacks, fetchPacks } = usePacksStore(); const { subscription, restorePurchases, isPurchasing } = useSubscriptionStore(); const { width } = useWindowDimensions(); const isWideScreen = width > MAX_CONTENT_WIDTH; const [showDeleteModal, setShowDeleteModal] = useState(false); const [deleteConfirmText, setDeleteConfirmText] = useState(""); const [isDeleting, setIsDeleting] = useState(false); const [showFeedbackModal, setShowFeedbackModal] = useState(false); const [showPaywall, setShowPaywall] = useState(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); const [biometricType, setBiometricType] = useState("Face ID"); const [isUpdatingBiometric, setIsUpdatingBiometric] = useState(false); const [isEditingName, setIsEditingName] = useState(false); const [newName, setNewName] = useState(user?.name || ""); const [isUpdatingName, setIsUpdatingName] = useState(false); const [isUploadingAvatar, setIsUploadingAvatar] = useState(false); // Check if user or partner has premium access const hasPremiumAccess = useMemo(() => { return user?.is_premium || partner?.is_premium || subscription.isProUser; }, [user?.is_premium, partner?.is_premium, subscription.isProUser]); // Update newName when user changes useEffect(() => { if (user?.name) { setNewName(user.name); } }, [user?.name]); // Check biometric availability on mount useEffect(() => { const checkBiometric = async () => { const available = await isBiometricAvailable(); setBiometricAvailable(available); if (available) { const enabled = await isBiometricEnabled(); setBiometricEnabledState(enabled); const type = await getBiometricType(); setBiometricType(type); } }; checkBiometric(); }, []); // Check if it's the user's own subscription (vs partner's) const isOwnSubscription = user?.is_premium && subscription.isProUser; // Format expiration date const formatExpirationDate = (date: Date | null) => { if (!date) return "Never"; return date.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", }); }; const handleRestorePurchases = async () => { const restored = await restorePurchases(); if (restored) { Alert.alert("Success", "Your purchases have been restored!"); } else { Alert.alert("No Purchases Found", "No previous purchases found to restore."); } }; const handleExplicitToggle = async (value: boolean) => { if (!user?.id) return; setShowExplicit(value); setIsUpdatingExplicit(true); try { const { error } = await supabase .from('profiles') .update({ show_explicit_content: value, updated_at: new Date().toISOString(), }) .eq('id', user.id); if (error) throw error; // Refresh user data and packs to reflect the change await fetchUser(); await fetchPacks(); Events.profileUpdated(["show_explicit_content"]); } catch (error) { // Revert on error setShowExplicit(!value); Alert.alert("Error", "Failed to update preference. Please try again."); } finally { setIsUpdatingExplicit(false); } }; const handleBiometricToggle = async (value: boolean) => { setBiometricEnabledState(value); setIsUpdatingBiometric(true); try { await setBiometricEnabled(value); } catch (error) { // Revert on error setBiometricEnabledState(!value); Alert.alert("Error", `Failed to ${value ? "enable" : "disable"} ${biometricType}. Please try again.`); } finally { setIsUpdatingBiometric(false); } }; const handleUpdateName = async () => { if (!user?.id) return; const trimmedName = newName.trim(); if (!trimmedName) { Alert.alert("Error", "Name cannot be empty"); return; } if (trimmedName === user.name) { setIsEditingName(false); return; } setIsUpdatingName(true); try { const { error } = await supabase .from('profiles') .update({ name: trimmedName, updated_at: new Date().toISOString(), }) .eq('id', user.id); if (error) throw error; await fetchUser(); setIsEditingName(false); Events.profileUpdated(["name"]); } catch (error) { Alert.alert("Error", "Failed to update name. Please try again."); setNewName(user.name || ""); } finally { setIsUpdatingName(false); } }; const handleCancelEditName = () => { setNewName(user?.name || ""); setIsEditingName(false); }; const handleAvatarPress = () => { if (Platform.OS === 'ios') { ActionSheetIOS.showActionSheetWithOptions( { options: ['Cancel', 'Take Photo', 'Choose from Library'], cancelButtonIndex: 0, }, (buttonIndex) => { if (buttonIndex === 1) { pickAvatar('camera'); } else if (buttonIndex === 2) { pickAvatar('library'); } } ); } else { Alert.alert( 'Change Profile Photo', 'Choose an option', [ { text: 'Cancel', style: 'cancel' }, { text: 'Take Photo', onPress: () => pickAvatar('camera') }, { text: 'Choose from Library', onPress: () => pickAvatar('library') }, ] ); } }; const pickAvatar = async (source: 'camera' | 'library') => { try { let result: ImagePicker.ImagePickerResult; if (source === 'camera') { const { status } = await ImagePicker.requestCameraPermissionsAsync(); if (status !== 'granted') { Alert.alert('Permission Denied', 'Camera access is required to take a photo.'); return; } result = await ImagePicker.launchCameraAsync({ mediaTypes: ['images'], allowsEditing: true, aspect: [1, 1], quality: 0.8, }); } else { const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (status !== 'granted') { Alert.alert('Permission Denied', 'Photo library access is required to choose a photo.'); return; } result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], allowsEditing: true, aspect: [1, 1], quality: 0.8, }); } if (!result.canceled && result.assets[0]) { await uploadAvatar(result.assets[0].uri); } } catch (error) { console.error('Error picking avatar:', error); Alert.alert('Error', 'Failed to pick image. Please try again.'); } }; const uploadAvatar = async (uri: string) => { if (!user?.id) return; setIsUploadingAvatar(true); try { let fileBody; let ext = 'jpg'; if (Platform.OS === 'web') { const response = await fetch(uri); const blob = await response.blob(); fileBody = blob; if (blob.type === 'image/png') ext = 'png'; else if (blob.type === 'image/webp') ext = 'webp'; } else { const base64 = await FileSystem.readAsStringAsync(uri, { encoding: FileSystem.EncodingType.Base64 }); fileBody = decode(base64); const uriExt = uri.split('.').pop()?.toLowerCase(); if (uriExt && ['jpg', 'jpeg', 'png', 'webp'].includes(uriExt)) { ext = uriExt === 'jpeg' ? 'jpg' : uriExt; } } const fileName = `${user.id}/${Date.now()}.${ext}`; const contentType = `image/${ext === 'jpg' ? 'jpeg' : ext}`; // Upload to storage const { error: uploadError } = await supabase.storage .from('avatars') .upload(fileName, fileBody, { contentType, upsert: true, }); if (uploadError) throw uploadError; // Get public URL const { data: { publicUrl } } = supabase.storage .from('avatars') .getPublicUrl(fileName); // Update profile with avatar URL const { error: updateError } = await supabase .from('profiles') .update({ avatar_url: publicUrl, updated_at: new Date().toISOString(), }) .eq('id', user.id); if (updateError) throw updateError; // Refresh user data await fetchUser(); Events.avatarUploaded(); } catch (error) { console.error('Error uploading avatar:', error); Alert.alert('Error', 'Failed to upload photo. Please try again.'); } finally { setIsUploadingAvatar(false); } }; const handleManageSubscription = () => { Linking.openURL("https://apps.apple.com/account/subscriptions"); }; const handleDeleteRelationship = async () => { if (deleteConfirmText !== "DELETE") return; setIsDeleting(true); try { const { error } = await supabase.functions.invoke("delete-relationship", { method: "DELETE", }); if (error) throw error; // Clear local stores clearMatches(); clearMessages(); clearPacks(); // Refresh user data (will also clear couple/partner since couple_id is now null) await useAuthStore.getState().fetchUser(); Events.relationshipEnded(); setShowDeleteModal(false); setDeleteConfirmText(""); Alert.alert("Success", "All relationship data has been deleted."); } catch (error) { Alert.alert("Error", "Failed to delete relationship data. Please try again."); } finally { setIsDeleting(false); } }; const handleUnpair = async () => { Alert.alert( "Unpair Partner", "Are you sure you want to unpair? This cannot be undone.", [ { text: "Cancel", style: "cancel" }, { text: "Unpair", style: "destructive", onPress: async () => { try { await supabase.functions.invoke("manage-couple", { method: "DELETE", }); await fetchCouple(); } catch (error) { Alert.alert("Error", "Failed to unpair"); } }, }, ] ); }; const handleSignOut = () => { Alert.alert( "Sign Out", "Are you sure you want to sign out?", [ { text: "Cancel", style: "cancel" }, { text: "Sign Out", style: "destructive", onPress: signOut, }, ] ); }; return ( {/* Header */} Profile {/* Profile Card */} {user?.avatar_url ? ( ) : ( {user?.name?.[0]?.toUpperCase() || "U"} )} {isUploadingAvatar ? ( ) : ( )} {isEditingName ? ( {isUpdatingName ? ( ) : ( )} ) : ( setIsEditingName(true)} activeOpacity={0.7} > {user?.name || "User"} )} {user?.email} {/* Partner Section */} Partner {partner ? ( {partner.name || partner.email || 'Your partner'} Connected ) : couple ? ( router.push("/(app)/pairing")} activeOpacity={0.7} > Waiting for partner Tap to view invite code ) : ( router.push("/(app)/pairing")} activeOpacity={0.7} > Not paired yet Tap to connect )} {/* Subscription Section */} Subscription {hasPremiumAccess ? ( Pro Member {isOwnSubscription ? `Renews ${formatExpirationDate(subscription.expirationDate)}` : "Via partner's subscription" } {isOwnSubscription && ( Manage )} ) : ( setShowPaywall(true)} activeOpacity={0.7} > Free Plan Upgrade to unlock all packs Upgrade )} {/* Restore Purchases Link */} {!hasPremiumAccess && ( {isPurchasing ? "Restoring..." : "Restore Purchases"} )} {/* Preferences Section */} Preferences Explicit Content Show 18+ question packs {biometricAvailable && ( <> {biometricType} Require unlock when opening app )} {/* Account Section */} Account Sign Out {/* Support Section */} Support setShowFeedbackModal(true)} activeOpacity={0.7} > Send Feedback Report bugs or request features {/* Debug Section - Only show in development builds */} {__DEV__ && ( Debug { await resetSwipeTutorial(); Alert.alert("Success", "Swipe tutorial has been reset. It will show again next time you visit the swipe screen."); }} activeOpacity={0.7} > Reset Swipe Tutorial Show the tutorial again )} {/* Danger Zone - Only show if in a couple */} {couple && ( Danger Zone Delete All Data Permanently delete your relationship including all matches, chats, and shared photos. setShowDeleteModal(true)} activeOpacity={0.7} > Delete )} {/* Version */} Sauci v1.0.0 {/* Feedback Modal */} setShowFeedbackModal(false)} /> {/* Paywall Modal */} setShowPaywall(false)} /> {/* Delete Confirmation Modal */} !isDeleting && setShowDeleteModal(false)} > Delete All Data? This action cannot be undone. This will permanently delete: Your couple connection All matches and responses All chat messages All shared photos Type DELETE to confirm: { setShowDeleteModal(false); setDeleteConfirmText(""); }} disabled={isDeleting} activeOpacity={0.7} > Cancel {isDeleting ? ( ) : ( <> Delete )} ); } const styles = StyleSheet.create({ container: { flex: 1, }, contentContainer: { paddingBottom: Platform.OS === 'ios' ? 100 : 80, }, contentContainerWide: { alignSelf: 'center', width: '100%', maxWidth: MAX_CONTENT_WIDTH, }, header: { paddingTop: 60, paddingHorizontal: spacing.lg, paddingBottom: spacing.lg, }, title: { ...typography.title1, color: colors.text, }, profileSection: { paddingHorizontal: spacing.lg, marginBottom: spacing.xl, }, profileContent: { alignItems: "center", paddingVertical: spacing.lg, }, avatarGradient: { width: 100, height: 100, borderRadius: 50, justifyContent: "center", alignItems: "center", ...shadows.lg, }, avatarInner: { width: 90, height: 90, borderRadius: 45, backgroundColor: colors.background, justifyContent: "center", alignItems: "center", }, avatarText: { fontSize: 36, fontWeight: "bold", color: colors.text, }, avatarTouchable: { position: "relative", }, avatarImage: { width: 96, height: 96, borderRadius: 48, }, avatarOverlay: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, borderRadius: 50, backgroundColor: "rgba(0, 0, 0, 0.5)", justifyContent: "center", alignItems: "center", }, avatarEditBadge: { position: "absolute", bottom: 0, right: 0, width: 32, height: 32, borderRadius: 16, backgroundColor: colors.primary, justifyContent: "center", alignItems: "center", borderWidth: 3, borderColor: colors.background, }, profileInfo: { alignItems: "center", marginTop: spacing.lg, }, nameContainer: { flexDirection: "row", alignItems: "center", gap: spacing.xs, }, name: { ...typography.title2, color: colors.text, marginBottom: spacing.xs, }, editNameIcon: { width: 24, height: 24, borderRadius: 12, backgroundColor: colors.glass.background, justifyContent: "center", alignItems: "center", marginBottom: spacing.xs, }, editNameContainer: { width: "100%", alignItems: "center", gap: spacing.sm, }, nameInput: { ...typography.title2, color: colors.text, textAlign: "center", backgroundColor: colors.glass.background, borderRadius: radius.md, borderWidth: 1, borderColor: colors.primary, paddingHorizontal: spacing.md, paddingVertical: spacing.sm, minWidth: 200, }, editNameButtons: { flexDirection: "row", gap: spacing.sm, }, cancelEditButton: { width: 36, height: 36, borderRadius: 18, backgroundColor: colors.glass.background, justifyContent: "center", alignItems: "center", borderWidth: 1, borderColor: colors.glass.border, }, saveEditButton: { width: 36, height: 36, borderRadius: 18, backgroundColor: colors.primary, justifyContent: "center", alignItems: "center", }, email: { ...typography.subhead, color: colors.textSecondary, }, section: { paddingHorizontal: spacing.lg, marginBottom: spacing.lg, }, sectionTitle: { ...typography.caption1, color: colors.textTertiary, textTransform: "uppercase", letterSpacing: 1.5, marginBottom: spacing.sm, marginLeft: spacing.xs, }, rowContainer: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", }, rowLeft: { flexDirection: "row", alignItems: "center", flex: 1, }, partnerIconGradient: { width: 44, height: 44, borderRadius: 22, justifyContent: "center", alignItems: "center", }, emptyPartnerIcon: { width: 44, height: 44, borderRadius: 22, backgroundColor: colors.glass.background, justifyContent: "center", alignItems: "center", }, rowTextContainer: { marginLeft: spacing.md, flex: 1, }, rowValue: { ...typography.body, color: colors.text, fontWeight: "600", }, rowValueMuted: { ...typography.body, color: colors.textSecondary, }, rowLabel: { ...typography.caption1, color: colors.textTertiary, marginTop: 2, }, preferencesDivider: { height: 1, backgroundColor: colors.glass.border, marginVertical: spacing.md, }, unlinkButton: { width: 36, height: 36, borderRadius: 18, backgroundColor: colors.glass.background, justifyContent: "center", alignItems: "center", }, chevronContainer: { width: 28, height: 28, borderRadius: 14, backgroundColor: colors.glass.background, justifyContent: "center", alignItems: "center", }, signOutRow: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", }, signOutIcon: { width: 44, height: 44, borderRadius: 22, backgroundColor: colors.errorLight, justifyContent: "center", alignItems: "center", }, signOutText: { ...typography.body, color: colors.error, fontWeight: "600", marginLeft: spacing.md, }, // Subscription styles manageButton: { paddingHorizontal: spacing.md, paddingVertical: spacing.sm, borderRadius: radius.md, backgroundColor: colors.glass.background, borderWidth: 1, borderColor: colors.glass.border, }, manageButtonText: { ...typography.subhead, color: colors.primary, fontWeight: "600", }, upgradeButton: { paddingHorizontal: spacing.md, paddingVertical: spacing.sm, borderRadius: radius.md, }, upgradeButtonText: { ...typography.subhead, color: colors.text, fontWeight: "600", }, restoreLink: { alignItems: "center", marginTop: spacing.sm, padding: spacing.sm, }, restoreLinkText: { ...typography.caption1, color: colors.textTertiary, }, versionContainer: { alignItems: "center", marginTop: spacing.lg, }, versionBadge: { flexDirection: "row", alignItems: "center", backgroundColor: colors.glass.background, paddingHorizontal: spacing.md, paddingVertical: spacing.sm, borderRadius: radius.full, gap: spacing.xs, }, version: { ...typography.caption1, color: colors.textTertiary, }, bottomSpacer: { height: spacing.lg, }, // Debug styles debugSectionTitle: { ...typography.caption1, color: colors.warning, textTransform: "uppercase", letterSpacing: 1.5, marginBottom: spacing.sm, marginLeft: spacing.xs, }, debugCard: { borderColor: 'rgba(243, 156, 18, 0.3)', borderWidth: 1, }, debugIcon: { width: 44, height: 44, borderRadius: 22, backgroundColor: colors.warningLight, justifyContent: "center", alignItems: "center", }, // Danger Zone styles dangerSectionTitle: { ...typography.caption1, color: colors.error, textTransform: "uppercase", letterSpacing: 1.5, marginBottom: spacing.sm, marginLeft: spacing.xs, }, dangerCard: { borderColor: 'rgba(231, 76, 60, 0.3)', borderWidth: 1, }, dangerContent: { gap: spacing.md, }, dangerInfo: { flexDirection: "row", alignItems: "flex-start", }, dangerIconContainer: { width: 44, height: 44, borderRadius: 22, backgroundColor: colors.errorLight, justifyContent: "center", alignItems: "center", }, dangerTextContainer: { flex: 1, marginLeft: spacing.md, }, dangerTitle: { ...typography.headline, color: colors.text, marginBottom: spacing.xs, }, dangerDescription: { ...typography.subhead, color: colors.textSecondary, lineHeight: 20, }, dangerButton: { flexDirection: "row", alignItems: "center", justifyContent: "center", backgroundColor: colors.errorLight, paddingVertical: spacing.sm, paddingHorizontal: spacing.md, borderRadius: radius.md, borderWidth: 1, borderColor: 'rgba(231, 76, 60, 0.3)', gap: spacing.xs, alignSelf: "flex-end", }, dangerButtonText: { ...typography.subhead, color: colors.error, fontWeight: "600", }, // Modal styles modalOverlay: { flex: 1, backgroundColor: "rgba(0, 0, 0, 0.8)", justifyContent: "center", alignItems: "center", padding: spacing.lg, }, modalContent: { backgroundColor: colors.backgroundLight, borderRadius: radius.xl, padding: spacing.lg, width: "100%", maxWidth: 400, borderWidth: 1, borderColor: colors.glass.border, }, modalHeader: { alignItems: "center", marginBottom: spacing.lg, }, modalIconContainer: { width: 64, height: 64, borderRadius: 32, backgroundColor: colors.errorLight, justifyContent: "center", alignItems: "center", marginBottom: spacing.md, }, modalTitle: { ...typography.title2, color: colors.text, textAlign: "center", }, modalDescription: { ...typography.body, color: colors.textSecondary, textAlign: "center", marginBottom: spacing.md, }, deleteList: { backgroundColor: colors.glass.background, borderRadius: radius.md, padding: spacing.md, marginBottom: spacing.lg, gap: spacing.sm, }, deleteListItem: { flexDirection: "row", alignItems: "center", gap: spacing.sm, }, deleteListText: { ...typography.subhead, color: colors.textSecondary, }, confirmLabel: { ...typography.subhead, color: colors.textSecondary, marginBottom: spacing.sm, }, confirmKeyword: { color: colors.error, fontWeight: "bold", }, confirmInput: { backgroundColor: colors.glass.background, borderRadius: radius.md, borderWidth: 1, borderColor: colors.glass.border, padding: spacing.md, ...typography.body, color: colors.text, marginBottom: spacing.lg, }, modalButtons: { flexDirection: "row", gap: spacing.md, }, cancelButton: { flex: 1, backgroundColor: colors.glass.background, paddingVertical: spacing.md, borderRadius: radius.md, alignItems: "center", borderWidth: 1, borderColor: colors.glass.border, }, cancelButtonText: { ...typography.body, color: colors.textSecondary, fontWeight: "600", }, confirmDeleteButton: { flex: 1, backgroundColor: colors.error, paddingVertical: spacing.md, borderRadius: radius.md, alignItems: "center", flexDirection: "row", justifyContent: "center", gap: spacing.xs, }, confirmDeleteButtonDisabled: { backgroundColor: colors.glass.background, opacity: 0.5, }, confirmDeleteButtonText: { ...typography.body, color: colors.text, fontWeight: "600", }, });