Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add account deletion flow #1990

Merged
merged 16 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 133 additions & 32 deletions components/organisms/UserSettingsPage/user-settings-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { useFetchUser } from "lib/hooks/useFetchUser";
import { getInterestOptions } from "lib/utils/getInterestOptions";
import { useToast } from "lib/hooks/useToast";
import { validateTwitterUsername } from "lib/utils/validate-twitter-username";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "components/molecules/Dialog/dialog";
import CouponForm from "./coupon-form";

interface userSettingsPageProps {
Expand All @@ -32,7 +33,67 @@ type EmailPreferenceType = {
display_email?: boolean;
receive_collaboration?: boolean;
};

interface DeleteAccountModalProps {
open: boolean;
setOpen: (open: boolean) => void;
onDelete: () => void;
}

const DeleteAccountModal = ({ open, setOpen, onDelete }: DeleteAccountModalProps) => {
const [confirmText, setConfirmText] = useState("");
const disabled = confirmText !== "DELETE";

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="p-4">
<DialogHeader>
<DialogTitle className="text-left">Delete Account</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-4">
<Text>Are you sure you want to delete your account?</Text>
<Text>
Type <span className="font-bold text-light-red-10">DELETE</span> in all caps to confirm
</Text>
<TextInput
onChange={(e) => {
setConfirmText(e.target.value);
}}
/>
<div className="flex gap-4">
<Button
type="submit"
rel="noopener noreferrer"
target="_blank"
className="w-max border-dark-red-8 bg-dark-red-8 text-white hover:border-dark-red-7 hover:bg-dark-red-7"
variant="primary"
onClick={() => {
if (!disabled) {
onDelete();
}
}}
disabled={disabled}
>
Delete
</Button>
<Button
variant="default"
onClick={() => {
setOpen(false);
}}
>
Cancel
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
};

const UserSettingsPage = ({ user }: userSettingsPageProps) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const deleteFormRef = useRef<HTMLFormElement>(null);
const { data: insightsUser, mutate } = useFetchUser(user?.user_metadata.user_name, {
revalidateOnFocus: false,
});
Expand Down Expand Up @@ -389,44 +450,84 @@ const UserSettingsPage = ({ user }: userSettingsPageProps) => {
</Button>
</div>
{userInfo && (
<div>
{!hasReports && !coupon ? (
<div className="flex flex-col order-first gap-6 md:order-last">
<div className="flex flex-col gap-3">
<label className="text-2xl font-normal text-light-slate-11">Upgrade Access</label>
<div className="w-full sm:max-w-80">
<Text>Upgrade to a subscription to gain access to generate custom reports!</Text>
</div>
</div>
<StripeCheckoutButton variant="primary" />

{!coupon && <CouponForm refreshUser={mutate} />}
</div>
) : (
<div>
<>
<div>
{!hasReports && !coupon ? (
<div className="flex flex-col order-first gap-6 md:order-last">
<div className="flex flex-col gap-3">
<label className="text-2xl font-normal text-light-slate-11">Manage Subscriptions</label>
<div className="w-full md:w-96">
<Text>
You are currently subscribed to the Pro plan and currently have access to all premium
features.
</Text>
<label className="text-2xl font-normal text-light-slate-11">Upgrade Access</label>
<div className="w-full sm:max-w-80">
<Text>Upgrade to a subscription to gain access to generate custom reports!</Text>
</div>
</div>
<Button
rel="noopener noreferrer"
target="_blank"
href={process.env.NEXT_PUBLIC_STRIPE_SUB_CANCEL_URL}
className="w-max"
variant="primary"
>
Cancel Subscription
</Button>
<StripeCheckoutButton variant="primary" />

{!coupon && <CouponForm refreshUser={mutate} />}
</div>
) : (
<div>
<div className="flex flex-col order-first gap-6 md:order-last">
<div className="flex flex-col gap-3">
<label className="text-2xl font-normal text-light-slate-11">Manage Subscriptions</label>
<div className="w-full md:w-96">
<Text>
You are currently subscribed to the Pro plan and currently have access to all premium
features.
</Text>
</div>
</div>
<Button
rel="noopener noreferrer"
target="_blank"
href={process.env.NEXT_PUBLIC_STRIPE_SUB_CANCEL_URL}
className="w-max"
variant="primary"
>
Cancel Subscription
</Button>
</div>
</div>
)}
</div>
<form
name="delete-account"
action="/api/delete-account"
method="POST"
className="flex flex-col order-first gap-6 md:order-last"
ref={deleteFormRef}
onSubmit={(e) => {
setIsModalOpen(true);
e.preventDefault();
}}
>
<div className="flex flex-col gap-3">
<label className="text-2xl font-normal text-light-slate-11">Delete Account</label>
<div className="w-full md:w-96">
<Text>
Please note that account deletion is irreversible. Proceed only if you are certain about this
action.
</Text>
</div>
</div>
)}
</div>
<Button
type="submit"
rel="noopener noreferrer"
target="_blank"
className="w-max border-dark-red-8 bg-dark-red-8 text-white hover:border-dark-red-7 hover:bg-dark-red-7"
variant="primary"
>
Delete Account
</Button>
<DeleteAccountModal
open={isModalOpen}
setOpen={setIsModalOpen}
onDelete={() => {
setIsModalOpen(false);
deleteFormRef.current?.submit();
}}
/>
</form>
</>
)}
</div>
</div>
Expand Down
9 changes: 9 additions & 0 deletions middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const pathsToMatch = [
"/feed/",
"/user/notifications",
"/user/settings",
"/account-deleted"
];

export async function middleware(req: NextRequest) {
Expand All @@ -28,6 +29,14 @@ export async function middleware(req: NextRequest) {
data: { session },
} = await supabase.auth.getSession();

if (session?.user && req.nextUrl.pathname === "/account-deleted") {
nickytonline marked this conversation as resolved.
Show resolved Hide resolved
// Delete the account from Supabase and log the user out.
await supabase.auth.admin.deleteUser(session.user.id);
nickytonline marked this conversation as resolved.
Show resolved Hide resolved
await supabase.auth.signOut();

return res;
}

// Check auth condition
if (session?.user || req.nextUrl.searchParams.has("login")) {
// Authentication successful, forward request to protected route.
Expand Down
17 changes: 17 additions & 0 deletions pages/account-deleted.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import TopNav from "components/organisms/TopNav/top-nav";

const AccountDeletedPage = () => {
return (
<div className="min-h-screen flex flex-col">
<TopNav />
<main className="page-container flex flex-col m-4 pt-28 items-center">
<div className="info-container container w-full">
<h1 className="text-2xl">Account Deleted</h1>
<p>Your account has been deleted.</p>
nickytonline marked this conversation as resolved.
Show resolved Hide resolved
</div>
</main>
</div>
);
};

export default AccountDeletedPage;
45 changes: 45 additions & 0 deletions pages/api/delete-account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { NextApiRequest, NextApiResponse } from "next";
import { createPagesServerClient } from "@supabase/auth-helpers-nextjs";
import { fetchApiData } from "helpers/fetchApiData";

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
res.status(405).json({
message: "Method not allowed",
});
}

const supabaseServerClient = createPagesServerClient({
req,
res,
});

try {
const {
data: { session },
} = await supabaseServerClient.auth.getSession();

if (session) {
const { error } = await fetchApiData({
path: "/profile",
method: "DELETE",
bearerToken: session.access_token,
pathValidator: () => true,
});

if (error) {
throw error;
}
} else {
res.status(401).json({
message: "Unauthorized",
});
}

res.redirect("/account-deleted");
} catch (error) {
res.status(500).json({
message: "Internal server error",
});
}
}
Loading