import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; import { View, Text, TouchableOpacity, ActivityIndicator, Modal, Animated, StyleSheet, KeyboardAvoidingView, Platform, Image, ScrollView, Dimensions, } from 'react-native'; import { useDubsTheme } from './theme'; import { useDubs } from '../provider'; import { useAuth } from '../hooks/useAuth'; import { usePushNotifications } from '../hooks/usePushNotifications'; import { AvatarEditor, getAvatarUrl, generateSeed, parseAvatarUrl } from './AvatarEditor'; function truncateAddress(address: string, chars = 4): string { if (address.length <= chars * 2 + 3) return address; return `${address.slice(0, chars)}...${address.slice(-chars)}`; } // ── Props ── export interface UserProfileSheetProps { visible: boolean; onDismiss: () => void; user: { walletAddress: string; username?: string; avatar?: string | null; createdAt?: string; }; onAvatarUpdated?: (newAvatarUrl: string) => void; onDisconnect?: () => void; } export function UserProfileSheet({ visible, onDismiss, user, onAvatarUpdated, onDisconnect, }: UserProfileSheetProps) { const t = useDubsTheme(); const { client } = useDubs(); const { refreshUser } = useAuth(); const push = usePushNotifications(); const overlayOpacity = useRef(new Animated.Value(0)).current; const parsed = useMemo(() => parseAvatarUrl(user.avatar), [user.avatar]); const [avatarStyle, setAvatarStyle] = useState(parsed.style); const [avatarSeed, setAvatarSeed] = useState(parsed.seed); const [bgColor, setBgColor] = useState(parsed.bg); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); // Re-sync when user prop changes useEffect(() => { const p = parseAvatarUrl(user.avatar); setAvatarStyle(p.style); setAvatarSeed(p.seed); setBgColor(p.bg); }, [user.avatar]); // Animate overlay useEffect(() => { Animated.timing(overlayOpacity, { toValue: visible ? 1 : 0, duration: 250, useNativeDriver: true, }).start(); }, [visible, overlayOpacity]); // Reset error when sheet opens useEffect(() => { if (visible) setError(null); }, [visible]); const currentAvatarUrl = getAvatarUrl(avatarStyle, avatarSeed, bgColor); const saveAvatar = useCallback(async (newUrl: string) => { setSaving(true); setError(null); try { await client.updateProfile({ avatar: newUrl }); await refreshUser(); onAvatarUpdated?.(newUrl); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to update avatar'); } finally { setSaving(false); } }, [client, refreshUser, onAvatarUpdated]); const handleStyleChange = useCallback((style: string) => { setAvatarStyle(style); saveAvatar(getAvatarUrl(style, avatarSeed, bgColor)); }, [avatarSeed, bgColor, saveAvatar]); const handleShuffle = useCallback(() => { const newSeed = generateSeed(); setAvatarSeed(newSeed); saveAvatar(getAvatarUrl(avatarStyle, newSeed, bgColor)); }, [avatarStyle, bgColor, saveAvatar]); const handleBgChange = useCallback((color: string) => { setBgColor(color); saveAvatar(getAvatarUrl(avatarStyle, avatarSeed, color)); }, [avatarStyle, avatarSeed, saveAvatar]); return ( {/* Drag handle */} {/* Header */} Profile {'\u2715'} {/* Avatar section */} {saving && ( )} {user.username ? ( {user.username} ) : null} {truncateAddress(user.walletAddress, 6)} {/* Change Avatar */} Change Avatar Shuffle {/* Error */} {error ? ( {error} ) : null} {/* Push Notifications */} {push.enabled && ( Push Notifications {push.hasPermission ? 'Enabled' : 'Disabled'} {!push.hasPermission && ( {push.loading ? ( ) : ( Enable )} )} )} {/* Push error */} {push.enabled && push.error ? ( {push.error.message} ) : null} {/* Disconnect */} {onDisconnect ? ( Disconnect Wallet ) : null} ); } const styles = StyleSheet.create({ overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.5)', }, overlayTap: { flex: 1, }, keyboardView: { flex: 1, justifyContent: 'flex-end', }, sheetPositioner: { justifyContent: 'flex-end', }, sheet: { borderTopLeftRadius: 24, borderTopRightRadius: 24, paddingHorizontal: 20, paddingBottom: 0, height: Dimensions.get('window').height * 0.7, }, handleRow: { alignItems: 'center', paddingTop: 10, paddingBottom: 8, }, handle: { width: 36, height: 4, borderRadius: 2, opacity: 0.4, }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 12, }, headerTitle: { fontSize: 20, fontWeight: '700', }, closeButton: { fontSize: 20, padding: 4, }, scrollContent: { flexGrow: 0, }, scrollContentInner: { paddingBottom: 8, }, avatarSection: { alignItems: 'center', paddingTop: 8, paddingBottom: 20, gap: 8, }, avatarContainer: { width: 120, height: 120, borderRadius: 60, borderWidth: 2, overflow: 'hidden', }, avatar: { width: '100%', height: '100%', }, avatarLoading: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.35)', justifyContent: 'center', alignItems: 'center', }, username: { fontSize: 18, fontWeight: '700', }, walletAddress: { fontSize: 13, fontFamily: 'monospace', }, section: { marginBottom: 20, gap: 12, }, sectionHeaderRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, sectionLabel: { fontSize: 14, fontWeight: '600', }, shuffleButton: { paddingHorizontal: 14, paddingVertical: 6, borderRadius: 12, borderWidth: 1, }, shuffleText: { fontSize: 13, fontWeight: '600', }, errorBox: { marginBottom: 16, borderRadius: 12, borderWidth: 1, padding: 12, }, errorText: { fontSize: 13, fontWeight: '500', }, notifRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', borderRadius: 16, borderWidth: 1, paddingHorizontal: 16, paddingVertical: 14, marginBottom: 16, }, notifLeft: { gap: 2, }, notifLabel: { fontSize: 15, fontWeight: '600', }, notifStatus: { fontSize: 13, }, enableButton: { paddingHorizontal: 18, paddingVertical: 10, borderRadius: 12, }, enableText: { color: '#FFFFFF', fontSize: 14, fontWeight: '700', }, disconnectButton: { height: 52, borderRadius: 16, borderWidth: 1.5, justifyContent: 'center', alignItems: 'center', }, disconnectText: { fontSize: 16, fontWeight: '700', }, });