feat: delete user from db and unsubscribe from stripe
This commit is contained in:
@@ -10,7 +10,13 @@
|
|||||||
"ghcr.io/devcontainers/features/node:1": {}
|
"ghcr.io/devcontainers/features/node:1": {}
|
||||||
},
|
},
|
||||||
"onCreateCommand": "./.devcontainer/on-create.sh",
|
"onCreateCommand": "./.devcontainer/on-create.sh",
|
||||||
"forwardPorts": [3000, 54320, 9000, 2500, 1100],
|
"forwardPorts": [
|
||||||
|
3000,
|
||||||
|
54320,
|
||||||
|
9000,
|
||||||
|
2500,
|
||||||
|
1100
|
||||||
|
],
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
@@ -25,7 +31,7 @@
|
|||||||
"GitHub.copilot",
|
"GitHub.copilot",
|
||||||
"GitHub.vscode-pull-request-github",
|
"GitHub.vscode-pull-request-github",
|
||||||
"Prisma.prisma",
|
"Prisma.prisma",
|
||||||
"VisualStudioExptTeam.vscodeintellicode",
|
"VisualStudioExptTeam.vscodeintellicode"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { signOut } from 'next-auth/react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
@@ -65,6 +66,7 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
|
|||||||
const isSubmitting = form.formState.isSubmitting;
|
const isSubmitting = form.formState.isSubmitting;
|
||||||
|
|
||||||
const { mutateAsync: updateProfile } = trpc.profile.updateProfile.useMutation();
|
const { mutateAsync: updateProfile } = trpc.profile.updateProfile.useMutation();
|
||||||
|
const { mutateAsync: deleteAccount } = trpc.profile.deleteAccount.useMutation();
|
||||||
|
|
||||||
const onFormSubmit = async ({ name, signature }: TProfileFormSchema) => {
|
const onFormSubmit = async ({ name, signature }: TProfileFormSchema) => {
|
||||||
try {
|
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 (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
@@ -171,12 +206,19 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
|
|||||||
<span className="font-semibold">all of your documents</span>, along with all of
|
<span className="font-semibold">all of your documents</span>, along with all of
|
||||||
your completed documents, signatures, and all other resources belonging to your
|
your completed documents, signatures, and all other resources belonging to your
|
||||||
Account.
|
Account.
|
||||||
<AlertDestructive />
|
<Alert variant="destructive" className="mt-5">
|
||||||
|
<AlertDescription className="selection:bg-red-100">
|
||||||
|
This action is not reversible. Please be certain.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
<AlertDialogAction
|
||||||
|
onClick={onDeleteAccount}
|
||||||
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
|
>
|
||||||
Delete Account
|
Delete Account
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
@@ -189,12 +231,6 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AlertDestructive() {
|
// Cal.com Delete User TRPC = https://github.com/calcom/cal.com/blob/main/packages/trpc/server/routers/loggedInViewer/deleteMe.handler.ts#L11
|
||||||
return (
|
// https://github.com/calcom/cal.com/blob/main/packages/features/users/lib/userDeletionService.ts#L7
|
||||||
<Alert variant="destructive" className="mt-5">
|
// delete stripe: https://github.com/calcom/cal.com/blob/main/packages/app-store/stripepayment/lib/customer.ts#L72
|
||||||
<AlertDescription className="selection:bg-red-100">
|
|
||||||
This action is not reversible. Please be certain.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
10
packages/ee/server-only/stripe/delete-customer.ts
Normal file
10
packages/ee/server-only/stripe/delete-customer.ts
Normal file
@@ -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);
|
||||||
|
};
|
||||||
@@ -9,7 +9,7 @@ import SuperJSON from 'superjson';
|
|||||||
|
|
||||||
import { getBaseUrl } from '@documenso/lib/universal/get-base-url';
|
import { getBaseUrl } from '@documenso/lib/universal/get-base-url';
|
||||||
|
|
||||||
import { AppRouter } from '../server/router';
|
import type { AppRouter } from '../server/router';
|
||||||
|
|
||||||
export const trpc = createTRPCReact<AppRouter>({
|
export const trpc = createTRPCReact<AppRouter>({
|
||||||
unstable_overrides: {
|
unstable_overrides: {
|
||||||
|
|||||||
@@ -10,3 +10,9 @@ export const ZSignUpMutationSchema = z.object({
|
|||||||
export type TSignUpMutationSchema = z.infer<typeof ZSignUpMutationSchema>;
|
export type TSignUpMutationSchema = z.infer<typeof ZSignUpMutationSchema>;
|
||||||
|
|
||||||
export const ZVerifyPasswordMutationSchema = ZSignUpMutationSchema.pick({ password: true });
|
export const ZVerifyPasswordMutationSchema = ZSignUpMutationSchema.pick({ password: true });
|
||||||
|
|
||||||
|
export const ZDeleteAccountMutationSchema = z.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TDeleteAccountMutationSchema = z.infer<typeof ZDeleteAccountMutationSchema>;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { TRPCError } from '@trpc/server';
|
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 { forgotPassword } from '@documenso/lib/server-only/user/forgot-password';
|
||||||
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
|
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
|
||||||
import { resetPassword } from '@documenso/lib/server-only/user/reset-password';
|
import { resetPassword } from '@documenso/lib/server-only/user/reset-password';
|
||||||
@@ -127,6 +129,29 @@ export const profileRouter = router({
|
|||||||
message = err.message;
|
message = err.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
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({
|
throw new TRPCError({
|
||||||
code: 'BAD_REQUEST',
|
code: 'BAD_REQUEST',
|
||||||
message,
|
message,
|
||||||
|
|||||||
Reference in New Issue
Block a user