import { useState, useEffect } from 'react'; import { invokeWithTracking } from '@/lib/edgeFunctionTracking'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/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 { 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 { useAuth } from '@/hooks/useAuth'; import { useProfile } from '@/hooks/useProfile'; import { supabase } from '@/integrations/supabase/client'; import { Trash2, Mail, AlertCircle, X, Check, Loader2 } 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 { AccountDeletionDialog } from './AccountDeletionDialog'; import { DeletionStatusBanner } from './DeletionStatusBanner'; import { EmailChangeStatus } from './EmailChangeStatus'; import { usernameSchema, displayNameSchema, bioSchema } from '@/lib/validation'; import { z } from 'zod'; import { AccountDeletionRequest } from '@/types/database'; import { handleError, handleSuccess, AppError } from '@/lib/errorHandler'; import { useAvatarUpload } from '@/hooks/useAvatarUpload'; import { useUsernameValidation } from '@/hooks/useUsernameValidation'; import { useAutoSave } from '@/hooks/useAutoSave'; import { formatDistanceToNow } from 'date-fns'; import { cn } from '@/lib/utils'; const profileSchema = z.object({ username: usernameSchema, display_name: displayNameSchema, bio: bioSchema }); type ProfileFormData = z.infer; export function AccountProfileTab() { const { user, pendingEmail, clearPendingEmail } = useAuth(); const { data: profile, refreshProfile } = useProfile(user?.id); const [loading, setLoading] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [showEmailDialog, setShowEmailDialog] = useState(false); const [showCancelEmailDialog, setShowCancelEmailDialog] = useState(false); const [cancellingEmail, setCancellingEmail] = useState(false); const [showDeletionDialog, setShowDeletionDialog] = useState(false); const [deletionRequest, setDeletionRequest] = useState(null); // Initialize avatar upload hook const { avatarUrl, avatarImageId, isUploading: avatarLoading, uploadAvatar } = useAvatarUpload( profile?.avatar_url || '', profile?.avatar_image_id || '', profile?.username || '' ); const form = useForm({ resolver: zodResolver(profileSchema), defaultValues: { username: profile?.username || '', display_name: profile?.display_name || '', bio: profile?.bio || '' } }); // Username validation const usernameValue = form.watch('username'); const usernameValidation = useUsernameValidation(usernameValue, profile?.username); // Auto-save (disabled for now - can be enabled per user preference) const formData = form.watch(); const { isSaving, lastSaved } = useAutoSave({ data: formData, onSave: async (data) => { const isValid = await form.trigger(); if (!isValid || usernameValidation.isAvailable === false) return; await handleFormSubmit(data); }, debounceMs: 3000, enabled: false, // TODO: Get from user preferences isValid: usernameValidation.isAvailable !== false }); // Check for existing deletion request on mount (both pending and confirmed) useEffect(() => { const checkDeletionRequest = async () => { if (!user?.id) return; const { data, error } = await supabase .from('account_deletion_requests') .select('*') .eq('user_id', user.id) .in('status', ['pending', 'confirmed']) .maybeSingle(); if (!error && data) { setDeletionRequest(data); } }; checkDeletionRequest(); }, [user?.id]); const handleFormSubmit = async (data: ProfileFormData) => { if (!user) return; setLoading(true); try { // Use the update_profile RPC function with server-side validation const { data: result, error } = await supabase.rpc('update_profile', { p_username: data.username, p_display_name: data.display_name || null, p_bio: data.bio || null }); if (error) { // Handle rate limiting error if (error.code === 'P0001') { const resetTime = error.message.match(/Try again at (.+)$/)?.[1]; throw new AppError( error.message, 'RATE_LIMIT', `Too many profile updates. ${resetTime ? 'Try again at ' + new Date(resetTime).toLocaleTimeString() : 'Please wait a few minutes.'}` ); } throw error; } // Type the RPC result const rpcResult = result as unknown as { success: boolean; changes_count: number }; // Update Novu subscriber if username changed if (rpcResult?.changes_count > 0 && notificationService.isEnabled()) { await notificationService.updateSubscriber({ subscriberId: user.id, email: user.email, firstName: data.username, }); } await refreshProfile(); handleSuccess('Profile updated', 'Your profile has been successfully updated.'); } catch (error: unknown) { handleError(error, { action: 'Update profile', userId: user.id }); } finally { setLoading(false); } }; const onSubmit = async (data: ProfileFormData) => { await handleFormSubmit(data); }; const handleAvatarUpload = async (urls: string[], imageId?: string) => { await uploadAvatar(urls, imageId); await refreshProfile(); }; 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) { 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, requestId } = await invokeWithTracking( 'cancel-email-change', {}, user.id ); if (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], }); } handleSuccess('Email change cancelled', 'Your email change request has been cancelled.'); setShowCancelEmailDialog(false); await refreshProfile(); } catch (error: unknown) { handleError(error, { action: 'Cancel email change' }); } finally { setCancellingEmail(false); } }; const handleDeletionRequested = async () => { // Refresh deletion request data (check for both pending and confirmed) const { data, error } = await supabase .from('account_deletion_requests') .select('*') .eq('user_id', user!.id) .in('status', ['pending', 'confirmed']) .maybeSingle(); if (!error && data) { setDeletionRequest(data); } }; const handleDeletionCancelled = () => { setDeletionRequest(null); }; const isDeactivated = profile?.deactivated || false; return (
{/* Deletion Status Banner */} {deletionRequest && ( )} {/* Profile Picture + Account Info Grid */}
{/* Profile Picture */} Profile Picture Upload your profile picture { handleError(new Error(error), { action: 'Upload avatar' }); }} /> {/* Account Information */} Account Information View your account details {pendingEmail && ( setShowCancelEmailDialog(true)} /> )}

Email Address

{user?.email}

{pendingEmail ? ( Change Pending ) : user?.email_confirmed_at ? ( Verified ) : ( Pending Verification )}

Account Created

{profile?.created_at ? new Date(profile.created_at).toLocaleDateString() : 'N/A'}

{/* Profile Information */} Profile Information Update your public profile details
{usernameValidation.isChecking ? (

Checking availability...

) : usernameValidation.isAvailable === false && !form.formState.errors.username ? (

{usernameValidation.error}

) : usernameValidation.isAvailable === true && !form.formState.errors.username ? (

Username is available

) : form.formState.errors.username ? (

{form.formState.errors.username.message}

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

{form.formState.errors.display_name.message}

)}