feat: add delete account functionality in settings

Adds Account settings page with a danger zone section for permanent
account deletion. Handles Polar customer cleanup, subscription records,
and cascading user data deletion with confirmation dialog.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sahaj Jain
2026-02-18 17:46:33 +05:30
parent 025bdadf4f
commit cd0c58e3db
4 changed files with 193 additions and 1 deletions
+36
View File
@@ -0,0 +1,36 @@
"use server";
import { db } from "@/db";
import { user as userTable, subscription } from "@/db/schema";
import { eq } from "drizzle-orm";
import { getCurrentUserId } from "@/lib/shared";
import { logError } from "@/lib/shared";
import { actionError, actionSuccess, ErrorCode, type ActionResult } from "@/types/errors";
import { polar } from "@/lib/polar";
export async function deleteAccount(): Promise<ActionResult<boolean>> {
try {
const userId = await getCurrentUserId();
// Try to delete Polar customer (cancels subscriptions + revokes benefits)
// Free users won't have a Polar customer, so we catch and ignore errors
try {
await polar.customers.deleteExternal({ externalId: userId });
} catch (_e) {
// Expected for free users — no Polar customer exists
}
// Delete subscription records (no CASCADE on this table)
await db.delete(subscription).where(eq(subscription.userId, userId));
// Delete user — CASCADE handles: sessions, accounts, themes,
// communityThemes, communityThemeTags, themeLikes, aiUsage,
// oauthAuthorizationCode, oauthToken
await db.delete(userTable).where(eq(userTable.id, userId));
return actionSuccess(true);
} catch (error) {
logError(error as Error, { action: "deleteAccount" });
return actionError(ErrorCode.UNKNOWN_ERROR, "Failed to delete account. Please try again.");
}
}
@@ -0,0 +1,131 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { usePostHog } from "posthog-js/react";
import { AlertTriangle, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
AlertDialog,
AlertDialogContent,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogCancel,
} from "@/components/ui/alert-dialog";
import { Input } from "@/components/ui/input";
import { toast } from "@/hooks/use-toast";
import { authClient } from "@/lib/auth-client";
import { deleteAccount } from "@/actions/account";
const CONFIRMATION_TEXT = "DELETE";
export function DeleteAccountSection() {
const [open, setOpen] = useState(false);
const [confirmText, setConfirmText] = useState("");
const [isDeleting, setIsDeleting] = useState(false);
const router = useRouter();
const posthog = usePostHog();
const isConfirmed = confirmText === CONFIRMATION_TEXT;
const handleDelete = async () => {
if (!isConfirmed) return;
setIsDeleting(true);
const result = await deleteAccount();
if (result.success) {
posthog.reset();
await authClient.signOut();
router.push("/");
} else {
toast({
title: "Failed to delete account",
description: result.error.message,
variant: "destructive",
});
setIsDeleting(false);
}
};
return (
<>
<div className="border-destructive/50 rounded-lg border p-6">
<h3 className="text-lg font-semibold text-destructive">Danger Zone</h3>
<p className="text-muted-foreground mt-1 text-sm">
Permanently delete your account and all associated data. This action
cannot be undone.
</p>
<Button
variant="destructive"
className="mt-4"
onClick={() => setOpen(true)}
>
Delete Account
</Button>
</div>
<AlertDialog open={open} onOpenChange={(v) => {
if (!isDeleting) {
setOpen(v);
if (!v) setConfirmText("");
}
}}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<AlertTriangle className="size-5 text-destructive" />
Delete your account
</AlertDialogTitle>
<AlertDialogDescription asChild>
<div className="space-y-3">
<p>
This will permanently delete your account and all associated
data, including:
</p>
<ul className="list-disc space-y-1 pl-5">
<li>All saved themes</li>
<li>Community published themes</li>
<li>AI usage history</li>
<li>Active subscription (if any)</li>
</ul>
<p className="font-medium">This action cannot be undone.</p>
<div className="pt-1">
<label
htmlFor="confirm-delete"
className="text-sm font-medium"
>
Type <span className="font-mono font-bold">{CONFIRMATION_TEXT}</span> to
confirm
</label>
<Input
id="confirm-delete"
className="mt-1.5"
value={confirmText}
onChange={(e) => setConfirmText(e.target.value)}
placeholder={CONFIRMATION_TEXT}
disabled={isDeleting}
autoComplete="off"
/>
</div>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
<Button
variant="destructive"
onClick={handleDelete}
disabled={!isConfirmed || isDeleting}
>
{isDeleting && <Loader2 className="mr-2 size-4 animate-spin" />}
Delete Account
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}
+23
View File
@@ -0,0 +1,23 @@
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { SettingsHeader } from "../components/settings-header";
import { DeleteAccountSection } from "./components/delete-account-section";
export default async function AccountPage() {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) redirect("/editor/theme");
return (
<div>
<SettingsHeader
title="Account"
description="Manage your account settings"
/>
<DeleteAccountSection />
</div>
);
}
+3 -1
View File
@@ -3,7 +3,7 @@
import { Separator } from "@/components/ui/separator";
import { useSubscription } from "@/hooks/use-subscription";
import { cn } from "@/lib/utils";
import { ChartNoAxesCombined, CreditCard, ExternalLink, LucideIcon, Palette } from "lucide-react";
import { ChartNoAxesCombined, CreditCard, ExternalLink, LucideIcon, Palette, UserCog } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useMemo } from "react";
@@ -24,6 +24,8 @@ type NavItem =
const BASE_NAV_ITEMS: NavItem[] = [
{ type: "link", href: "/settings/themes", label: "Themes", icon: Palette },
{ type: "link", href: "/settings/usage", label: "AI Usage", icon: ChartNoAxesCombined },
{ type: "separator", id: "account-separator" },
{ type: "link", href: "/settings/account", label: "Account", icon: UserCog },
];
const getSubscriptionNavItems = (): NavItem[] => [