diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 60b385403..3471f4f88 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -10,7 +10,13 @@ "ghcr.io/devcontainers/features/node:1": {} }, "onCreateCommand": "./.devcontainer/on-create.sh", - "forwardPorts": [3000, 54320, 9000, 2500, 1100], + "forwardPorts": [ + 3000, + 54320, + 9000, + 2500, + 1100 + ], "customizations": { "vscode": { "extensions": [ @@ -25,8 +31,8 @@ "GitHub.copilot", "GitHub.vscode-pull-request-github", "Prisma.prisma", - "VisualStudioExptTeam.vscodeintellicode", + "VisualStudioExptTeam.vscodeintellicode" ] } } -} +} \ No newline at end of file diff --git a/apps/web/src/components/forms/profile.tsx b/apps/web/src/components/forms/profile.tsx index 78da0f636..b36df4e7f 100644 --- a/apps/web/src/components/forms/profile.tsx +++ b/apps/web/src/components/forms/profile.tsx @@ -3,6 +3,7 @@ import { useRouter } from 'next/navigation'; import { zodResolver } from '@hookform/resolvers/zod'; +import { signOut } from 'next-auth/react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; @@ -65,6 +66,7 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => { const isSubmitting = form.formState.isSubmitting; const { mutateAsync: updateProfile } = trpc.profile.updateProfile.useMutation(); + const { mutateAsync: deleteAccount } = trpc.profile.deleteAccount.useMutation(); const onFormSubmit = async ({ name, signature }: TProfileFormSchema) => { try { @@ -98,6 +100,39 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => { } }; + const onDeleteAccount = async () => { + try { + await deleteAccount(); + + await signOut({ callbackUrl: '/' }); + + toast({ + title: 'Account deleted', + description: 'Your account has been deleted successfully.', + duration: 5000, + }); + + // logout after deleting account + + router.push('/'); + } catch (err) { + if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') { + toast({ + title: 'An error occurred', + description: err.message, + variant: 'destructive', + }); + } else { + toast({ + title: 'An unknown error occurred', + variant: 'destructive', + description: + 'We encountered an unknown error while attempting to delete your account. Please try again later.', + }); + } + } + }; + return (
{ all of your documents, along with all of your completed documents, signatures, and all other resources belonging to your Account. - + + + This action is not reversible. Please be certain. + + Cancel - + Delete Account @@ -189,12 +231,6 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => { ); }; -export function AlertDestructive() { - return ( - - - This action is not reversible. Please be certain. - - - ); -} +// Cal.com Delete User TRPC = https://github.com/calcom/cal.com/blob/main/packages/trpc/server/routers/loggedInViewer/deleteMe.handler.ts#L11 +// https://github.com/calcom/cal.com/blob/main/packages/features/users/lib/userDeletionService.ts#L7 +// delete stripe: https://github.com/calcom/cal.com/blob/main/packages/app-store/stripepayment/lib/customer.ts#L72 diff --git a/packages/ee/server-only/stripe/delete-customer.ts b/packages/ee/server-only/stripe/delete-customer.ts new file mode 100644 index 000000000..16120de68 --- /dev/null +++ b/packages/ee/server-only/stripe/delete-customer.ts @@ -0,0 +1,10 @@ +import { stripe } from '@documenso/lib/server-only/stripe'; +import type { User } from '@documenso/prisma/client'; + +export const deleteStripeCustomer = async (user: User) => { + if (!user.customerId) { + return null; + } + + return await stripe.customers.del(user.customerId); +}; diff --git a/packages/trpc/react/index.tsx b/packages/trpc/react/index.tsx index 85161d0e8..ce80ba267 100644 --- a/packages/trpc/react/index.tsx +++ b/packages/trpc/react/index.tsx @@ -9,7 +9,7 @@ import SuperJSON from 'superjson'; import { getBaseUrl } from '@documenso/lib/universal/get-base-url'; -import { AppRouter } from '../server/router'; +import type { AppRouter } from '../server/router'; export const trpc = createTRPCReact({ unstable_overrides: { diff --git a/packages/trpc/server/auth-router/schema.ts b/packages/trpc/server/auth-router/schema.ts index cc969c679..f342c25fb 100644 --- a/packages/trpc/server/auth-router/schema.ts +++ b/packages/trpc/server/auth-router/schema.ts @@ -10,3 +10,9 @@ export const ZSignUpMutationSchema = z.object({ export type TSignUpMutationSchema = z.infer; export const ZVerifyPasswordMutationSchema = ZSignUpMutationSchema.pick({ password: true }); + +export const ZDeleteAccountMutationSchema = z.object({ + email: z.string().email(), +}); + +export type TDeleteAccountMutationSchema = z.infer; diff --git a/packages/trpc/server/profile-router/router.ts b/packages/trpc/server/profile-router/router.ts index 4dcf4ca93..cf5fdbf94 100644 --- a/packages/trpc/server/profile-router/router.ts +++ b/packages/trpc/server/profile-router/router.ts @@ -1,5 +1,7 @@ import { TRPCError } from '@trpc/server'; +import { deleteStripeCustomer } from '@documenso/ee/server-only/stripe/delete-customer'; +import { deleteUser } from '@documenso/lib/server-only/user/delete-user'; import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password'; import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id'; import { resetPassword } from '@documenso/lib/server-only/user/reset-password'; @@ -133,4 +135,27 @@ export const profileRouter = router({ }); } }), + + deleteAccount: authenticatedProcedure.mutation(async ({ ctx }) => { + try { + const user = ctx.user; + + const deletedUser = await deleteStripeCustomer(user); + + console.log(deletedUser); + + return await deleteUser(user); + } catch (err) { + let message = 'We were unable to delete your account. Please try again.'; + + if (err instanceof Error) { + message = err.message; + } + + throw new TRPCError({ + code: 'BAD_REQUEST', + message, + }); + } + }), });