From 899bb966fa5dac17ba6f721da06918a356476609 Mon Sep 17 00:00:00 2001 From: Luke Vella Date: Sat, 18 May 2024 15:02:51 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20option=20to=20delete=20accoun?= =?UTF-8?q?t=20(#1110)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/public/locales/en/app.json | 8 +- .../profile/delete-account-dialog.tsx | 125 ++++++++++++++++++ .../(admin)/settings/profile/profile-page.tsx | 29 +++- .../components/settings/profile-settings.tsx | 10 +- packages/backend/trpc/routers/user.ts | 41 ++++++ packages/ui/src/icon.tsx | 2 +- packages/ui/src/input.tsx | 2 +- 7 files changed, 207 insertions(+), 10 deletions(-) create mode 100644 apps/web/src/app/[locale]/(admin)/settings/profile/delete-account-dialog.tsx diff --git a/apps/web/public/locales/en/app.json b/apps/web/public/locales/en/app.json index d31396ae4..db12e6cd1 100644 --- a/apps/web/public/locales/en/app.json +++ b/apps/web/public/locales/en/app.json @@ -97,7 +97,6 @@ "addComment": "Add Comment", "profile": "Profile", "polls": "Polls", - "showMore": "Show more…", "timeZoneSelect__noOption": "No option found", "timeZoneSelect__inputPlaceholder": "Search…", "poweredByRallly": "Powered by {name}", @@ -246,5 +245,10 @@ "timeShownIn": "Times shown in {timeZone}", "pollStatusPausedDescription": "Votes cannot be submitted or edited at this time", "eventHostTitle": "Manage Access", - "eventHostDescription": "You are the creator of this poll" + "eventHostDescription": "You are the creator of this poll", + "deleteAccount": "Delete Account", + "deleteAccountDialogTitle": "Delete Account", + "deleteAccountDialogDescription": "Are you sure you want to delete your account?", + "deleteAccountInstruction": "Please confirm your email address to delete your account", + "emailMismatch": "Email does not match the account email" } diff --git a/apps/web/src/app/[locale]/(admin)/settings/profile/delete-account-dialog.tsx b/apps/web/src/app/[locale]/(admin)/settings/profile/delete-account-dialog.tsx new file mode 100644 index 000000000..2943524cd --- /dev/null +++ b/apps/web/src/app/[locale]/(admin)/settings/profile/delete-account-dialog.tsx @@ -0,0 +1,125 @@ +"use client"; +import { Button } from "@rallly/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogProps, + DialogTitle, +} from "@rallly/ui/dialog"; +import { Form, FormField, FormItem, FormMessage } from "@rallly/ui/form"; +import { Input } from "@rallly/ui/input"; +import { signOut } from "next-auth/react"; +import { useForm } from "react-hook-form"; + +import { useTranslation } from "@/app/i18n/client"; +import { Trans } from "@/components/trans"; +import { usePostHog } from "@/utils/posthog"; +import { trpc } from "@/utils/trpc/client"; + +export function DeleteAccountDialog({ + email, + children, + ...rest +}: DialogProps & { + email: string; +}) { + const form = useForm<{ email: string }>({ + defaultValues: { + email: "", + }, + }); + const { t } = useTranslation("app"); + const trpcUtils = trpc.useUtils(); + const posthog = usePostHog(); + const deleteAccount = trpc.user.delete.useMutation({ + onSuccess() { + posthog?.capture("delete account"); + trpcUtils.invalidate(); + signOut({ + callbackUrl: "/login", + }); + }, + }); + + return ( +
+ + {children} + + { + await deleteAccount.mutateAsync(); + })} + > + + + + + + + + +
+

+ +

+ { + if (value !== email) { + return t("emailMismatch", { + defaultValue: "Email does not match the account email", + }); + } + return true; + }, + }} + render={({ field }) => ( + + + + + )} + /> +
+ + + + + + + +
+
+ + ); +} diff --git a/apps/web/src/app/[locale]/(admin)/settings/profile/profile-page.tsx b/apps/web/src/app/[locale]/(admin)/settings/profile/profile-page.tsx index 75fe08af8..7bc142299 100644 --- a/apps/web/src/app/[locale]/(admin)/settings/profile/profile-page.tsx +++ b/apps/web/src/app/[locale]/(admin)/settings/profile/profile-page.tsx @@ -1,12 +1,16 @@ "use client"; import { Alert, AlertDescription, AlertTitle } from "@rallly/ui/alert"; +import { Button } from "@rallly/ui/button"; +import { DialogTrigger } from "@rallly/ui/dialog"; +import { Icon } from "@rallly/ui/icon"; import { Input } from "@rallly/ui/input"; import { Label } from "@rallly/ui/label"; -import { InfoIcon, LogOutIcon, UserXIcon } from "lucide-react"; +import { InfoIcon, LogOutIcon, TrashIcon, UserXIcon } from "lucide-react"; import Head from "next/head"; import Link from "next/link"; import { useTranslation } from "next-i18next"; +import { DeleteAccountDialog } from "@/app/[locale]/(admin)/settings/profile/delete-account-dialog"; import { LogoutButton } from "@/app/components/logout-button"; import { ProfileSettings } from "@/components/settings/profile-settings"; import { @@ -91,6 +95,29 @@ export const ProfilePage = () => { + {user.email ? ( + <> +
+ + + + + + + + + ) : null} )} diff --git a/apps/web/src/components/settings/profile-settings.tsx b/apps/web/src/components/settings/profile-settings.tsx index 13470252d..428603b0d 100644 --- a/apps/web/src/components/settings/profile-settings.tsx +++ b/apps/web/src/components/settings/profile-settings.tsx @@ -1,3 +1,4 @@ +import { Button } from "@rallly/ui/button"; import { Form, FormControl, @@ -8,7 +9,6 @@ import { import { Input } from "@rallly/ui/input"; import { useForm } from "react-hook-form"; -import { LegacyButton } from "@/components/button"; import { Trans } from "@/components/trans"; import { UserAvatar } from "@/components/user"; import { useUser } from "@/components/user-provider"; @@ -72,14 +72,14 @@ export const ProfileSettings = () => { )} />
- - +
diff --git a/packages/backend/trpc/routers/user.ts b/packages/backend/trpc/routers/user.ts index edf7b219d..240faf208 100644 --- a/packages/backend/trpc/routers/user.ts +++ b/packages/backend/trpc/routers/user.ts @@ -38,6 +38,47 @@ export const user = router({ }, }); }), + delete: privateProcedure.mutation(async ({ ctx }) => { + await prisma.$transaction(async (tx) => { + const polls = await tx.poll.findMany({ + select: { id: true }, + where: { userId: ctx.user.id }, + }); + const pollIds = polls.map((poll) => poll.id); + await tx.comment.deleteMany({ + where: { pollId: { in: pollIds } }, + }); + await tx.option.deleteMany({ + where: { pollId: { in: pollIds } }, + }); + await tx.participant.deleteMany({ + where: { OR: [{ pollId: { in: pollIds } }, { userId: ctx.user.id }] }, + }); + await tx.watcher.deleteMany({ + where: { OR: [{ pollId: { in: pollIds } }, { userId: ctx.user.id }] }, + }); + await tx.vote.deleteMany({ + where: { pollId: { in: pollIds } }, + }); + await tx.event.deleteMany({ + where: { userId: ctx.user.id }, + }); + await tx.poll.deleteMany({ + where: { userId: ctx.user.id }, + }); + await tx.account.deleteMany({ + where: { userId: ctx.user.id }, + }); + await tx.userPaymentData.deleteMany({ + where: { userId: ctx.user.id }, + }); + await tx.user.delete({ + where: { + id: ctx.user.id, + }, + }); + }); + }), subscription: possiblyPublicProcedure.query( async ({ ctx }): Promise<{ legacy?: boolean; active: boolean }> => { if (ctx.user.isGuest) { diff --git a/packages/ui/src/icon.tsx b/packages/ui/src/icon.tsx index 84b93eef4..17d97789e 100644 --- a/packages/ui/src/icon.tsx +++ b/packages/ui/src/icon.tsx @@ -33,7 +33,7 @@ export function Icon({ children, size, variant }: IconProps) { {children} diff --git a/packages/ui/src/input.tsx b/packages/ui/src/input.tsx index 11871c6cb..6c9148047 100644 --- a/packages/ui/src/input.tsx +++ b/packages/ui/src/input.tsx @@ -13,7 +13,7 @@ export type InputProps = Omit< const inputVariants = cva( cn( - "focus-visible:border-primary-400 focus-visible:ring-offset-1 focus-visible:outline-none focus-visible:ring-primary-200 focus-visible:ring-1", + "w-full focus-visible:border-primary-400 focus-visible:ring-offset-1 focus-visible:outline-none focus-visible:ring-primary-200 focus-visible:ring-1", "border-input placeholder:text-muted-foreground h-9 rounded border bg-gray-50 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50", ), {