Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: delete account and all data #48

Merged
merged 2 commits into from
Sep 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/components/ProfileDeleteButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"use client"

import * as React from "react"
import { ProfileDeleteDialog } from "./ProfileDeleteDialog"
import { Button } from "./ui/button"

export default function ProfileDeleteButton() {
const [showDeleteDialog, setShowDeleteDialog] = React.useState(false)
return (
<>
<ProfileDeleteDialog
open={showDeleteDialog}
onOpenChange={setShowDeleteDialog}
showTrigger={false}
onSuccess={() => setShowDeleteDialog(false)}
/>
<Button
onClick={() => setShowDeleteDialog(true)}
size={"sm"}
variant={"destructive"}
>
Delete
</Button>
</>
)
}
18 changes: 18 additions & 0 deletions src/components/ProfileDeleteCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use client"

import ProfileDeleteButton from "./ProfileDeleteButton"
import { Card, CardContent, CardFooter, CardHeader } from "./ui/card"

export function ProfileDeleteCard() {
return (
<Card>
<CardHeader className="text-red-400">Delete account</CardHeader>
<CardContent className="flex flex-col gap-4">
Once you delete your account, there is no going back. Please be certain.
</CardContent>
<CardFooter>
<ProfileDeleteButton />
</CardFooter>
</Card>
)
}
144 changes: 144 additions & 0 deletions src/components/ProfileDeleteDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
"use client"

import { Button } from "@/components/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"
import { useMediaQuery } from "@/hooks/useMediaQuery"
import { deleteOwnAccount } from "@/services/settings/api"
import { TrashIcon } from "@radix-ui/react-icons"
import { useRouter } from "next/navigation"
import * as React from "react"
import { toast } from "sonner"
import { Icons } from "./icons"

interface DeleteProfileDialogProps
extends React.ComponentPropsWithoutRef<typeof Dialog> {
showTrigger?: boolean
onSuccess?: () => void
}

export function ProfileDeleteDialog({
showTrigger = true,
onSuccess,
...props
}: DeleteProfileDialogProps) {
const router = useRouter()
const [isDeletePending, startDeleteTransition] = React.useTransition()
const isDesktop = useMediaQuery("(min-width: 640px)")
const DRAWER_TITLE = "Are you absolutely sure?"
const DRAWER_DESCRIPTION =
"This action cannot be undone. It will permanently delete your account from your Myntenance. This action will not affect any of your GitHub repositories."

function onDelete() {
startDeleteTransition(async () => {
try {
await deleteOwnAccount()
router.push("/")
onSuccess?.()
toast.success(`Your account was successfully deleted!`)
} catch (error: unknown) {
const message =
error instanceof Error
? error.message
: "An error occurred while deleting your account."
toast.error(message)
}
})
}

if (isDesktop) {
return (
<Dialog {...props}>
{showTrigger ? (
<DialogTrigger asChild>
<Button variant="outline" size="sm">
<TrashIcon className="mr-2 size-4" aria-hidden="true" />
Delete
</Button>
</DialogTrigger>
) : null}
<DialogContent>
<DialogHeader>
<DialogTitle>{DRAWER_TITLE}</DialogTitle>
<DialogDescription>{DRAWER_DESCRIPTION}</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2 sm:space-x-0">
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button
aria-label="Delete your account"
variant="destructive"
onClick={onDelete}
disabled={isDeletePending}
>
{isDeletePending && (
<Icons.spinner
className="mr-2 size-4 animate-spin"
aria-hidden="true"
/>
)}
Delete
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

return (
<Drawer {...props}>
{showTrigger ? (
<DrawerTrigger asChild>
<Button variant="outline" size="sm">
<TrashIcon className="mr-2 size-4" aria-hidden="true" />
Delete
</Button>
</DrawerTrigger>
) : null}
<DrawerContent>
<DrawerHeader>
<DrawerTitle>{DRAWER_TITLE}</DrawerTitle>
<DrawerDescription>{DRAWER_DESCRIPTION}</DrawerDescription>
</DrawerHeader>
<DrawerFooter className="gap-2 sm:space-x-0">
<DrawerClose asChild>
<Button variant="outline">Cancel</Button>
</DrawerClose>
<Button
aria-label="Delete selected repository"
variant="destructive"
onClick={onDelete}
disabled={isDeletePending}
>
{isDeletePending && (
<Icons.spinner
className="mr-2 size-4 animate-spin"
aria-hidden="true"
/>
)}
Delete
</Button>
</DrawerFooter>
</DrawerContent>
</Drawer>
)
}
111 changes: 58 additions & 53 deletions src/components/ProfileSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getOwnSettings, updateOwnSettings } from "@/services/settings/api"
import { useForm } from "@tanstack/react-form"
import { use, useTransition } from "react"
import { toast } from "sonner"
import { ProfileDeleteCard } from "./ProfileDeleteCard"
import { Button } from "./ui/button"
import { Card, CardContent, CardFooter, CardHeader } from "./ui/card"
import { Checkbox } from "./ui/checkbox"
Expand Down Expand Up @@ -48,58 +49,62 @@ export function ProfileSettings({ settingsPromise, profilePromise }: Props) {
})

return (
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<Card>
<CardHeader>Public Profile</CardHeader>
<CardContent className="flex flex-col gap-4">
<form.Field name="fullName">
{(field) => (
<div>
<Label htmlFor={field.name}>Full Name</Label>
<Input
id={field.name}
type="text"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
</div>
)}
</form.Field>
<form.Field name="showPublicActivity">
{(field) => (
<div className="flex items-center gap-2">
<Checkbox
id={field.name}
checked={field.state.value}
onCheckedChange={(checked) =>
field.handleChange(checked === true)
}
/>
<Label htmlFor={field.name}>Show Activity</Label>
</div>
)}
</form.Field>
</CardContent>
<CardFooter>
<form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
>
{([canSubmit, isSubmitting]) => {
const isSubmitPending = isUpdatePending || isSubmitting
return (
<Button disabled={!canSubmit || isSubmitPending}>
{isSubmitPending ? "Saving..." : "Save"}
</Button>
)
}}
</form.Subscribe>
</CardFooter>
</Card>
</form>
<>
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<Card>
<CardHeader>Public Profile</CardHeader>
<CardContent className="flex flex-col gap-4">
<form.Field name="fullName">
{(field) => (
<div>
<Label htmlFor={field.name}>Full Name</Label>
<Input
id={field.name}
type="text"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
</div>
)}
</form.Field>
<form.Field name="showPublicActivity">
{(field) => (
<div className="flex items-center gap-2">
<Checkbox
id={field.name}
checked={field.state.value}
onCheckedChange={(checked) =>
field.handleChange(checked === true)
}
/>
<Label htmlFor={field.name}>Show Activity</Label>
</div>
)}
</form.Field>
</CardContent>
<CardFooter>
<form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
>
{([canSubmit, isSubmitting]) => {
const isSubmitPending = isUpdatePending || isSubmitting
return (
<Button disabled={!canSubmit || isSubmitPending}>
{isSubmitPending ? "Saving..." : "Save"}
</Button>
)
}}
</form.Subscribe>
</CardFooter>
</Card>
</form>

<ProfileDeleteCard />
</>
)
}
16 changes: 16 additions & 0 deletions src/services/settings/api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"use server"
import { createClient } from "@/lib/supabase/server"
import { createAdminClient } from "@/lib/supabase/serverAdmin"
import { ProfileUpdate, SettingsUpdate } from "@/lib/supabase/types"
import { getCurrentUserId } from "@/lib/supabase/utils"
import { revalidatePath } from "next/cache"

export async function getOwnSettings() {
const supabase = createClient()
Expand Down Expand Up @@ -38,3 +40,17 @@ export async function updateOwnSettings(

return { error: profileError }
}

export async function deleteOwnAccount() {
const supabase = createClient()
const userId = await getCurrentUserId(supabase)

if (!userId) {
throw new Error("User not found")
}

await createAdminClient().auth.admin.deleteUser(userId)
await supabase.auth.signOut()

revalidatePath("/")
}