DeleteAccountButton.tsx203 lines · main
1import { zodResolver } from '@hookform/resolvers/zod'
2import { SupportCategories } from '@supabase/shared-types/out/constants'
3import { LOCAL_STORAGE_KEYS } from 'common'
4import { useEffect, useState } from 'react'
5import { useForm } from 'react-hook-form'
6import { toast } from 'sonner'
7import {
8 Button,
9 Dialog,
10 DialogContent,
11 DialogDescription,
12 DialogFooter,
13 DialogHeader,
14 DialogSection,
15 DialogTitle,
16 DialogTrigger,
17 Form,
18 FormControl,
19 FormField,
20 FormItem,
21 FormLabel,
22 Input,
23 Separator,
24} from 'ui'
25import * as z from 'zod'
26
27import { NO_PROJECT_MARKER } from '@/components/interfaces/Support/SupportForm.utils'
28import { useSendSupportTicketMutation } from '@/data/feedback/support-ticket-send'
29import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
30import { useProfile } from '@/lib/profile'
31
32const setDeletionRequestFlag = () => {
33 const expiryDate = new Date()
34 expiryDate.setDate(expiryDate.getDate() + 30)
35 localStorage.setItem(LOCAL_STORAGE_KEYS.ACCOUNT_DELETION_REQUEST, expiryDate.toString())
36}
37
38const hasActiveDeletionRequest = () => {
39 const expiryDateStr = localStorage.getItem(LOCAL_STORAGE_KEYS.ACCOUNT_DELETION_REQUEST)
40 if (!expiryDateStr) return false
41
42 const expiryDate = new Date(expiryDateStr)
43 const now = new Date()
44
45 if (now > expiryDate) {
46 localStorage.removeItem(LOCAL_STORAGE_KEYS.ACCOUNT_DELETION_REQUEST)
47 return false
48 }
49
50 return true
51}
52
53export const DeleteAccountButton = () => {
54 const { profile } = useProfile()
55 const [isOpen, setIsOpen] = useState(false)
56 const { data: organizations, isSuccess } = useOrganizationsQuery()
57
58 const accountEmail = profile?.primary_email
59 const FormSchema = z.object({ account: z.string() })
60 const form = useForm<z.infer<typeof FormSchema>>({
61 mode: 'onBlur',
62 reValidateMode: 'onBlur',
63 resolver: zodResolver(FormSchema as any),
64 defaultValues: { account: '' },
65 })
66 const { account } = form.watch()
67
68 const { mutate: submitSupportTicket, isPending } = useSendSupportTicketMutation({
69 onSuccess: () => {
70 setIsOpen(false)
71 setDeletionRequestFlag()
72 toast.success(
73 'Successfully submitted account deletion request - we will reach out to you via email once the request is completed!',
74 { duration: 8000 }
75 )
76 },
77 onError: (error) => {
78 toast.error(`Failed to submit account deletion request: ${error}`)
79 },
80 })
81
82 const onConfirmDelete = async () => {
83 if (!accountEmail) return console.error('Account information is required')
84
85 if (hasActiveDeletionRequest()) {
86 return toast.error('You have already submitted a deletion request within the last 30 days.')
87 }
88
89 const payload = {
90 subject: 'Account Deletion Request',
91 message: 'I want to delete my account.',
92 category: SupportCategories.ACCOUNT_DELETION,
93 severity: 'Low',
94 allowSupportAccess: false,
95 verified: true,
96 projectRef: NO_PROJECT_MARKER,
97 }
98
99 submitSupportTicket(payload)
100 }
101
102 useEffect(() => {
103 if (isOpen && form !== undefined) form.reset({ account: '' })
104 }, [form, isOpen])
105
106 return (
107 <Dialog open={isOpen} onOpenChange={setIsOpen}>
108 <DialogTrigger asChild>
109 <Button type="danger" loading={!accountEmail}>
110 Request to delete account
111 </Button>
112 </DialogTrigger>
113 <DialogContent className="w-[500px]!">
114 <DialogHeader>
115 {(organizations ?? []).length > 0 ? (
116 <>
117 <DialogTitle>Leave all organizations before requesting account deletion</DialogTitle>
118 <DialogDescription>
119 This will allow us to process your account deletion request faster
120 </DialogDescription>
121 </>
122 ) : (
123 <>
124 <DialogTitle>Are you sure you want to delete your account?</DialogTitle>
125 <DialogDescription>
126 Deleting your account is permanent and{' '}
127 <span className="text-foreground">cannot</span> be undone
128 </DialogDescription>
129 </>
130 )}
131 </DialogHeader>
132
133 <Separator />
134
135 {isSuccess && (
136 <>
137 {organizations.length > 0 ? (
138 <>
139 <DialogSection>
140 <span className="text-sm text-foreground flex flex-col gap-y-2">
141 Before submitting an account deletion request, please ensure that your account
142 is not part of any organization. This can be done by leaving or deleting the
143 organizations that you are a part of.
144 </span>
145 </DialogSection>
146 <DialogFooter>
147 <Button block type="primary" size="medium" onClick={() => setIsOpen(false)}>
148 Understood
149 </Button>
150 </DialogFooter>
151 </>
152 ) : (
153 <Form {...form}>
154 <form
155 id="account-deletion-request"
156 onSubmit={form.handleSubmit(() => onConfirmDelete())}
157 >
158 <DialogSection>
159 <FormField
160 name="account"
161 control={form.control}
162 render={({ field }) => (
163 <FormItem>
164 <FormLabel>
165 Please type{' '}
166 <span className="font-bold">{profile?.primary_email ?? ''}</span> to
167 confirm
168 </FormLabel>
169 <FormControl>
170 <Input
171 autoFocus
172 {...field}
173 autoComplete="off"
174 disabled={isPending}
175 placeholder="Enter the account above"
176 />
177 </FormControl>
178 </FormItem>
179 )}
180 />
181 </DialogSection>
182
183 <DialogFooter>
184 <Button
185 block
186 size="small"
187 type="danger"
188 htmlType="submit"
189 loading={isPending}
190 disabled={account !== accountEmail || isPending}
191 >
192 Submit request for account deletion
193 </Button>
194 </DialogFooter>
195 </form>
196 </Form>
197 )}
198 </>
199 )}
200 </DialogContent>
201 </Dialog>
202 )
203}