DeleteAccountButton.tsx 6.5 KB

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