Skip to content

Commit

Permalink
feat: add delete account section
Browse files Browse the repository at this point in the history
  • Loading branch information
mickasmt committed Jun 21, 2024
1 parent c2b685d commit 9cbb432
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 9 deletions.
1 change: 0 additions & 1 deletion actions/open-customer-portal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { redirect } from "next/navigation";
import { auth } from "@/auth";

import { stripe } from "@/lib/stripe";
import { getUserSubscriptionPlan } from "@/lib/subscription";
import { absoluteUrl } from "@/lib/utils";

export type responseAction = {
Expand Down
11 changes: 6 additions & 5 deletions app/(dashboard)/dashboard/settings/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CardSkeleton } from "@/components/shared/card-skeleton"
import { DashboardHeader } from "@/components/dashboard/header"
import { DashboardShell } from "@/components/dashboard/shell"
import { DashboardHeader } from "@/components/dashboard/header";
import { DashboardShell } from "@/components/dashboard/shell";
import { CardSkeleton } from "@/components/shared/card-skeleton";

export default function DashboardSettingsLoading() {
return (
Expand All @@ -9,9 +9,10 @@ export default function DashboardSettingsLoading() {
heading="Settings"
text="Manage account and website settings."
/>
<div className="grid gap-10">
<div className="grid gap-6">
<CardSkeleton />
<CardSkeleton />
</div>
</DashboardShell>
)
);
}
4 changes: 3 additions & 1 deletion app/(dashboard)/dashboard/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { redirect } from "next/navigation";

import { getCurrentUser } from "@/lib/session";
import { constructMetadata } from "@/lib/utils";
import { DeleteAccountSection } from "@/components/dashboard/delete-account";
import { DashboardHeader } from "@/components/dashboard/header";
import { DashboardShell } from "@/components/dashboard/shell";
import { UserNameForm } from "@/components/forms/user-name-form";
Expand All @@ -24,8 +25,9 @@ export default async function SettingsPage() {
heading="Settings"
text="Manage account and website settings."
/>
<div className="grid gap-10">
<div className="grid gap-6">
<UserNameForm user={{ id: user.id, name: user.name || "" }} />
<DeleteAccountSection />
</div>
</DashboardShell>
);
Expand Down
26 changes: 26 additions & 0 deletions app/api/user/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { auth } from "@/auth";

import { prisma } from "@/lib/db";

export const DELETE = auth(async (req) => {
if (!req.auth) {
return new Response("Not authenticated", { status: 401 });
}

const currentUser = req.auth.user;
if (!currentUser) {
return new Response("Invalid user", { status: 401 });
}

try {
await prisma.user.delete({
where: {
id: currentUser.id,
},
});
} catch (error) {
return new Response("Internal server error", { status: 500 });
}

return new Response("User deleted successfully!", { status: 200 });
});
42 changes: 42 additions & 0 deletions components/dashboard/delete-account.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"use client";

import { useDeleteAccountModal } from "@/components/modals/delete-account-modal";
import { Button } from "@/components/ui/button";
import {
Card,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { siteConfig } from "@/config/site";

export function DeleteAccountSection() {
const { setShowDeleteAccountModal, DeleteAccountModal } =
useDeleteAccountModal();

return (
<>
<DeleteAccountModal />
<Card className="border border-red-600">
<CardHeader className="space-y-2">
<CardTitle>Delete Account</CardTitle>
<CardDescription className="text-pretty text-[15px] lg:text-balance">
Permanently delete your {siteConfig.name} account and your
subscription. This action cannot be undone - please proceed with
caution.
</CardDescription>
</CardHeader>
<CardFooter className="mt-2 flex justify-end border-t border-red-600 bg-red-500/5 py-2">
<Button
type="submit"
variant="destructive"
onClick={() => setShowDeleteAccountModal(true)}
>
<span>Delete Account</span>
</Button>
</CardFooter>
</Card>
</>
);
}
4 changes: 2 additions & 2 deletions components/forms/user-name-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ export function UserNameForm({ user }: UserNameFormProps) {
return (
<form onSubmit={onSubmit}>
<Card>
<CardHeader>
<CardHeader className="space-y-2">
<CardTitle>Your Name</CardTitle>
<CardDescription>
<CardDescription className="text-[15px]">
Please enter your full name or a display name you are comfortable
with.
</CardDescription>
Expand Down
135 changes: 135 additions & 0 deletions components/modals/delete-account-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import {
Dispatch,
SetStateAction,
useCallback,
useMemo,
useState,
} from "react";
import { signOut, useSession } from "next-auth/react";
import { toast } from "sonner";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Modal } from "@/components/ui/modal";
import { UserAvatar } from "@/components/shared/user-avatar";

function DeleteAccountModal({
showDeleteAccountModal,
setShowDeleteAccountModal,
}: {
showDeleteAccountModal: boolean;
setShowDeleteAccountModal: Dispatch<SetStateAction<boolean>>;
}) {
const { data: session } = useSession();
const [deleting, setDeleting] = useState(false);

async function deleteAccount() {
setDeleting(true);
await fetch(`/api/user`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}).then(async (res) => {
if (res.status === 200) {
// delay to allow for the route change to complete
await new Promise((resolve) =>
setTimeout(() => {
signOut({
callbackUrl: `${window.location.origin}/`,
});
resolve(null);
}, 500),
);
} else {
setDeleting(false);
const error = await res.text();
throw error;
}
});
}

return (
<Modal
showModal={showDeleteAccountModal}
setShowModal={setShowDeleteAccountModal}
className="gap-0"
>
<div className="flex flex-col items-center justify-center space-y-3 border-b p-4 pt-8 sm:px-16">
<UserAvatar
user={{
name: session?.user?.name || null,
image: session?.user?.image || null,
}}
/>
<h3 className="text-lg font-semibold">Delete Account</h3>
<p className="text-center text-sm text-muted-foreground">
<b>Warning:</b> This will permanently delete your account and your
active subscription!
</p>

{/* TODO: Use getUserSubscriptionPlan(session.user.id) to display the user's subscription if he have a paid plan */}
</div>

<form
onSubmit={async (e) => {
e.preventDefault();
toast.promise(deleteAccount(), {
loading: "Deleting account...",
success: "Account deleted successfully!",
error: (err) => err,
});
}}
className="flex flex-col space-y-6 bg-accent px-4 py-8 text-left sm:px-16"
>
<div>
<label htmlFor="verification" className="block text-sm">
To verify, type{" "}
<span className="font-semibold text-black dark:text-white">
confirm delete account
</span>{" "}
below
</label>
<Input
type="text"
name="verification"
id="verification"
pattern="confirm delete account"
required
autoFocus={false}
autoComplete="off"
className="mt-1 w-full border bg-background"
/>
</div>

<Button
variant={deleting ? "disable" : "destructive"}
disabled={deleting}
>
Confirm delete account
</Button>
</form>
</Modal>
);
}

export function useDeleteAccountModal() {
const [showDeleteAccountModal, setShowDeleteAccountModal] = useState(false);

const DeleteAccountModalCallback = useCallback(() => {
return (
<DeleteAccountModal
showDeleteAccountModal={showDeleteAccountModal}
setShowDeleteAccountModal={setShowDeleteAccountModal}
/>
);
}, [showDeleteAccountModal, setShowDeleteAccountModal]);

return useMemo(
() => ({
setShowDeleteAccountModal,
DeleteAccountModal: DeleteAccountModalCallback,
}),
[setShowDeleteAccountModal, DeleteAccountModalCallback],
);
}

0 comments on commit 9cbb432

Please sign in to comment.