import { useState, useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Separator } from '@/components/ui/separator'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'; import { useToast } from '@/hooks/use-toast'; import { useAuth } from '@/hooks/useAuth'; import { supabase } from '@/integrations/supabase/client'; import { User, Upload, Trash2, Mail, AlertCircle, X } from 'lucide-react'; import { PhotoUpload } from '@/components/upload/PhotoUpload'; import { notificationService } from '@/lib/notificationService'; import { EmailChangeDialog } from './EmailChangeDialog'; import { Badge } from '@/components/ui/badge'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { toast as sonnerToast } from 'sonner'; import { AccountDeletionDialog } from './AccountDeletionDialog'; import { DeletionStatusBanner } from './DeletionStatusBanner'; const profileSchema = z.object({ username: z.string().min(3).max(30).regex(/^[a-zA-Z0-9_-]+$/), display_name: z.string().max(50).optional(), bio: z.string().max(500).optional(), preferred_pronouns: z.string().max(20).optional(), show_pronouns: z.boolean(), preferred_language: z.string() }); type ProfileFormData = z.infer; export function AccountProfileTab() { const { user, profile, refreshProfile, pendingEmail, clearPendingEmail } = useAuth(); const { toast } = useToast(); const [loading, setLoading] = useState(false); const [avatarLoading, setAvatarLoading] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [showEmailDialog, setShowEmailDialog] = useState(false); const [showCancelEmailDialog, setShowCancelEmailDialog] = useState(false); const [cancellingEmail, setCancellingEmail] = useState(false); const [avatarUrl, setAvatarUrl] = useState(profile?.avatar_url || ''); const [avatarImageId, setAvatarImageId] = useState(profile?.avatar_image_id || ''); const [showDeletionDialog, setShowDeletionDialog] = useState(false); const [deletionRequest, setDeletionRequest] = useState(null); const form = useForm({ resolver: zodResolver(profileSchema), defaultValues: { username: profile?.username || '', display_name: profile?.display_name || '', bio: profile?.bio || '', preferred_pronouns: profile?.preferred_pronouns || '', show_pronouns: profile?.show_pronouns || false, preferred_language: profile?.preferred_language || 'en' } }); // Check for existing deletion request on mount useEffect(() => { const checkDeletionRequest = async () => { if (!user?.id) return; const { data, error } = await supabase .from('account_deletion_requests') .select('*') .eq('user_id', user.id) .eq('status', 'pending') .maybeSingle(); if (!error && data) { setDeletionRequest(data); } }; checkDeletionRequest(); }, [user?.id]); const onSubmit = async (data: ProfileFormData) => { if (!user) return; setLoading(true); try { const usernameChanged = profile?.username !== data.username; const { error } = await supabase .from('profiles') .update({ username: data.username, display_name: data.display_name || null, bio: data.bio || null, preferred_pronouns: data.preferred_pronouns || null, show_pronouns: data.show_pronouns, preferred_language: data.preferred_language, updated_at: new Date().toISOString() }) .eq('user_id', user.id); if (error) throw error; // Update Novu subscriber if username changed if (usernameChanged && notificationService.isEnabled()) { await notificationService.updateSubscriber({ subscriberId: user.id, email: user.email, firstName: data.username, // Send username as firstName to Novu }); } await refreshProfile(); toast({ title: 'Profile updated', description: 'Your profile has been successfully updated.' }); } catch (error: any) { toast({ title: 'Error', description: error.message || 'Failed to update profile', variant: 'destructive' }); } finally { setLoading(false); } }; const handleAvatarUpload = async (urls: string[], imageId?: string) => { if (!user || !urls[0]) return; const newAvatarUrl = urls[0]; const newImageId = imageId || ''; // Update local state immediately setAvatarUrl(newAvatarUrl); setAvatarImageId(newImageId); try { const { error } = await supabase .from('profiles') .update({ avatar_url: newAvatarUrl, avatar_image_id: newImageId, updated_at: new Date().toISOString() }) .eq('user_id', user.id); if (error) throw error; await refreshProfile(); toast({ title: 'Avatar updated', description: 'Your avatar has been successfully updated.' }); } catch (error: any) { // Revert local state on error setAvatarUrl(profile?.avatar_url || ''); setAvatarImageId(profile?.avatar_image_id || ''); toast({ title: 'Error', description: error.message || 'Failed to update avatar', variant: 'destructive' }); } }; const handleCancelEmailChange = async () => { if (!user?.email || !pendingEmail) return; setCancellingEmail(true); try { // Ensure we have a valid session with access token const { data: { session }, error: sessionError } = await supabase.auth.getSession(); if (sessionError || !session?.access_token) { console.error('Session error:', sessionError); throw new Error('Your session has expired. Please refresh the page and try again.'); } // Call the edge function with explicit authorization header const { data, error } = await supabase.functions.invoke('cancel-email-change', { method: 'POST', headers: { Authorization: `Bearer ${session.access_token}`, }, }); if (error) { console.error('Edge function error:', error); throw error; } if (!data?.success) { throw new Error(data?.error || 'Failed to cancel email change'); } // Clear the pending email state immediately without refreshing session clearPendingEmail(); // Update Novu subscriber back to current email if (notificationService.isEnabled()) { await notificationService.updateSubscriber({ subscriberId: user.id, email: user.email, firstName: profile?.username || user.email.split('@')[0], }); } sonnerToast.success('Email change cancelled', { description: 'Your email change request has been cancelled.' }); setShowCancelEmailDialog(false); await refreshProfile(); } catch (error: any) { sonnerToast.error('Failed to cancel email change', { description: error.message || 'An error occurred while cancelling the email change.' }); } finally { setCancellingEmail(false); } }; const handleDeletionRequested = async () => { // Refresh deletion request data const { data, error } = await supabase .from('account_deletion_requests') .select('*') .eq('user_id', user!.id) .eq('status', 'pending') .maybeSingle(); if (!error && data) { setDeletionRequest(data); } }; const handleDeletionCancelled = () => { setDeletionRequest(null); }; const isDeactivated = (profile as any)?.deactivated || false; return (
{/* Deletion Status Banner */} {deletionRequest && ( )} {/* Profile Picture */}

Profile Picture

{ toast({ title: "Upload Error", description: error, variant: "destructive" }); }} />
{/* Profile Information */}

Profile Information

{form.formState.errors.username && (

{form.formState.errors.username.message}

)}
{form.formState.errors.display_name && (

{form.formState.errors.display_name.message}

)}