delete-account-form.tsx104 lines · main
1'use client';
2
3import { useState, type FormEvent } from 'react';
4
5interface Props {
6 email: string;
7}
8
9/**
10 * Account deletion form. Typed-email confirmation gates the click so an
11 * accidental "delete" can't go through. On success the api revokes the
12 * session — we just redirect to /signin?deleted=1 and the page renders
13 * the post-deletion banner.
14 */
15export function DeleteAccountForm({ email }: Props) {
16 const [confirmation, setConfirmation] = useState('');
17 const [reason, setReason] = useState('');
18 const [pending, setPending] = useState(false);
19 const [error, setError] = useState<string | null>(null);
20
21 async function onSubmit(e: FormEvent) {
22 e.preventDefault();
23 setError(null);
24 setPending(true);
25 try {
26 const res = await fetch('/api/v1/me/delete-account', {
27 method: 'POST',
28 headers: { 'content-type': 'application/json' },
29 body: JSON.stringify({
30 confirmation: confirmation.trim(),
31 reason: reason.trim() || undefined,
32 }),
33 });
34 if (!res.ok) {
35 const body = (await res.json().catch(() => ({}))) as {
36 message?: string;
37 code?: string;
38 };
39 throw new Error(body.message ?? body.code ?? `delete failed: ${res.status}`);
40 }
41 // Session is gone server-side; bounce to the post-deletion banner.
42 window.location.href = '/signin?deleted=1';
43 } catch (err) {
44 setError(err instanceof Error ? err.message : 'delete failed');
45 } finally {
46 setPending(false);
47 }
48 }
49
50 return (
51 <details className="mt-3 rounded-md border border-red-400/30 bg-red-400/5 p-5 font-mono text-sm">
52 <summary className="cursor-pointer text-red-400">delete account</summary>
53 <p className="mt-3 text-[var(--color-text-muted)]">
54 soft-deletes your account immediately. you have <strong>30 days</strong> to revert
55 via support before the data is hard-deleted. paid subscriptions are not
56 auto-cancelled — manage cancellation on polar separately.
57 </p>
58 <ul className="mt-3 list-disc pl-5 text-xs text-[var(--color-text-subtle)]">
59 <li>personal data on your account (legal name, address, vat, display name, image) is cleared.</li>
60 <li>orgs you solely own — and every project under them — are soft-deleted.</li>
61 <li>team orgs where you&apos;re not the only owner stay live; you&apos;re removed from membership.</li>
62 <li>api keys you own are revoked.</li>
63 </ul>
64 <form onSubmit={onSubmit} className="mt-4 flex flex-col gap-3">
65 <label className="flex flex-col gap-1">
66 <span className="text-xs text-[var(--color-text-muted)]">
67 type <code className="text-[var(--color-text)]">{email}</code> to confirm
68 </span>
69 <input
70 type="email"
71 value={confirmation}
72 onChange={(e) => setConfirmation(e.target.value)}
73 placeholder={email}
74 autoComplete="off"
75 className="rounded-md border border-[var(--color-border)] bg-[var(--color-bg)] px-3 py-2 text-sm outline-none focus:border-red-400"
76 />
77 </label>
78 <label className="flex flex-col gap-1">
79 <span className="text-xs text-[var(--color-text-muted)]">
80 why are you leaving? (optional — surfaced only in audit log)
81 </span>
82 <textarea
83 value={reason}
84 onChange={(e) => setReason(e.target.value)}
85 rows={3}
86 maxLength={2000}
87 placeholder="anything we can fix?"
88 className="rounded-md border border-[var(--color-border)] bg-[var(--color-bg)] px-3 py-2 text-sm outline-none focus:border-red-400"
89 />
90 </label>
91 {error ? (
92 <p className="rounded-md bg-red-400/10 px-3 py-2 text-xs text-red-400">{error}</p>
93 ) : null}
94 <button
95 type="submit"
96 disabled={pending || confirmation.trim().toLowerCase() !== email.toLowerCase()}
97 className="self-start rounded-md border border-red-500/40 px-4 py-2 text-sm text-red-400 transition hover:bg-red-500/10 disabled:opacity-30"
98 >
99 {pending ? 'deleting…' : 'permanently delete my account'}
100 </button>
101 </form>
102 </details>
103 );
104}