import { useState, useEffect } from 'react'; import { useAuthStore } from '../../stores/authStore'; import { useInstanceStore } from '../../stores/instanceStore'; import { useSpaceStore } from '../../stores/spaceStore'; import { api } from '../../api/client'; import { deleteAccountOnRemotes, type FederationOpResult } from '../../utils/federationOps'; interface DeleteAccountModalProps { isOpen: boolean; onClose: () => void; } type Step = 'warning' | 'confirm' | 'federation' | 'complete'; interface OwnedSpaceInfo { id: string; name: string; members: { userId: string; username: string; displayName: string | null }[]; action: 'none' | 'transfer' | 'delete'; transferTo: string; } export function DeleteAccountModal({ isOpen, onClose }: DeleteAccountModalProps) { const user = useAuthStore((s) => s.user); const instances = useInstanceStore((s) => s.instances); const spaces = useSpaceStore((s) => s.spaces); const [step, setStep] = useState('warning'); const [ownedSpaces, setOwnedSpaces] = useState([]); const [confirmUsername, setConfirmUsername] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [error, setError] = useState(''); const [isLoading, setIsLoading] = useState(false); const [federationResults, setFederationResults] = useState([]); const [deletionComplete, setDeletionComplete] = useState(false); // Reset state when modal opens/closes useEffect(() => { if (isOpen) { setStep('warning'); setConfirmUsername(''); setConfirmPassword(''); setError(''); setIsLoading(false); setFederationResults([]); setDeletionComplete(false); // Build owned spaces list and fetch members for each if (user) { const owned = spaces.filter(s => s.ownerId === user.id); if (owned.length > 0) { Promise.all( owned.map(async (s) => { try { const members = await api.spaces.members(s.id); return { id: s.id, name: s.name, members: members .filter(m => m.userId !== user.id) .map(m => ({ userId: m.userId, username: m.user.username, displayName: m.user.displayName, })), action: 'none' as const, transferTo: '', }; } catch { return { id: s.id, name: s.name, members: [] as OwnedSpaceInfo['members'], action: 'none' as const, transferTo: '', }; } }) ).then(setOwnedSpaces); } } } }, [isOpen, user]); // Auto-redirect after deletion — must be before early return to maintain hooks order useEffect(() => { if (deletionComplete && step === 'complete') { const timer = setTimeout(() => { localStorage.removeItem('backspace_token'); window.location.href = '/login'; }, 3000); return () => clearTimeout(timer); } }, [deletionComplete, step]); if (!isOpen || !user) return null; const hasRemotes = instances.filter(i => i.status === 'connected').length > 0; const allOwnedHandled = ownedSpaces.every(s => s.action !== 'none'); const handleSpaceAction = (spaceId: string, action: 'transfer' | 'delete') => { setOwnedSpaces(prev => prev.map(s => s.id === spaceId ? { ...s, action, transferTo: action === 'transfer' ? s.transferTo : '' } : s )); }; const handleTransferTo = (spaceId: string, userId: string) => { setOwnedSpaces(prev => prev.map(s => s.id === spaceId ? { ...s, transferTo: userId } : s )); }; const handleContinueFromWarning = async () => { setError(''); setIsLoading(true); try { // Process owned spaces for (const space of ownedSpaces) { if (space.action === 'transfer' && space.transferTo) { await api.spaces.transferOwnership(space.id, space.transferTo); } else if (space.action === 'delete') { await api.spaces.delete(space.id); } } setStep('confirm'); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to process spaces'); } finally { setIsLoading(false); } }; const handleConfirmDelete = async () => { if (confirmUsername !== user.username) { setError('Username does not match'); return; } setError(''); setIsLoading(true); try { if (hasRemotes) { setStep('federation'); // Delete on remotes first const results = await deleteAccountOnRemotes(); setFederationResults(results); // Then delete home account await api.users.deleteAccount({ password: confirmPassword, username: confirmUsername }); setDeletionComplete(true); setStep('complete'); } else { // No remotes — direct delete via API (don't clear auth state yet — let the modal show "complete") await api.users.deleteAccount({ password: confirmPassword, username: confirmUsername }); setDeletionComplete(true); setStep('complete'); } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to delete account'); if (step === 'federation') { // Stay on federation step so user can see results } else { setStep('confirm'); } } finally { setIsLoading(false); } }; const handleDeleteAnyway = async () => { setError(''); setIsLoading(true); try { await api.users.deleteAccount({ password: confirmPassword, username: confirmUsername }); setDeletionComplete(true); setStep('complete'); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to delete account'); } finally { setIsLoading(false); } }; return (
{/* Header */}

Delete Account

{step !== 'complete' && ( )}
{/* Step 1: Warning & Space Handling */} {step === 'warning' && ( <>

This will permanently delete your account.

  • - All space memberships will be removed
  • - All friend connections will be removed
  • - All DM memberships will be removed
  • - Your messages will remain but be attributed to "Deleted User"
{ownedSpaces.length > 0 && (

You own {ownedSpaces.length} space{ownedSpaces.length > 1 ? 's' : ''}. Handle each before continuing:

{ownedSpaces.map(space => (
{space.name}
{space.action === 'transfer' && ( )}
))}
)} {error && (
{error}
)} )} {/* Step 2: Confirmation */} {step === 'confirm' && (
{ e.preventDefault(); handleConfirmDelete(); }} className="space-y-4">

This action is permanent and cannot be undone.

setConfirmUsername(e.target.value)} className="input-danger w-full" placeholder={user.username} />
setConfirmPassword(e.target.value)} className="input-danger w-full" placeholder="Enter your password" autoComplete="current-password" />
{error && (
{error}
)}
)} {/* Step 3: Federation Progress */} {step === 'federation' && ( <>

Removing your account from connected instances...

{instances.filter(i => i.status === 'connected').map(inst => { const result = federationResults.find(r => r.origin === inst.origin); return (
{inst.label || new URL(inst.origin).host} {!result ? ( ) : result.success ? ( ) : (
{result.error}
)}
); })}
{error && (
{error}
)} {federationResults.length > 0 && !deletionComplete && ( )} )} {/* Step 4: Complete */} {step === 'complete' && (

Account deleted

Redirecting to login...

)}
); }