Skip to content
181 linesCodeBlameRaw
1import { useState } from "react";
2import { Form, Link, useNavigation } from "react-router";
3
4import { ACCOUNT_RESTORE_DAYS, type AccountDeletion } from "@g1t/contracts";
5
6import { DangerAction } from "./danger-zone";
7import { ErrorText } from "./ui";
8import {
9 AlertDialog,
10 AlertDialogCancel,
11 AlertDialogContent,
12 AlertDialogDescription,
13 AlertDialogFooter,
14 AlertDialogHeader,
15 AlertDialogTitle,
16} from "./ui/alert-dialog";
17import { Button } from "./ui/button";
18import { Card } from "./ui/card";
19import { FieldDescription, FieldLabel, Field as FormField } from "./ui/field";
20import { Input as TextInput } from "./ui/input";
21import { accountDeletionRefusal, confirmsUsername, whatAccountDeletionTakes } from "../lib/account-deletion";
22
23/**
24 * Deleting your account, in one step, typed out to confirm, with your
25 * password unless you signed in within the last few minutes. Kept for
26 * `ACCOUNT_RESTORE_DAYS`, when support can restore it. Workspaces you own
27 * alone are listed instead of the button until they have another owner or
28 * are deleted.
29 */
30export function DeleteAccountAction({
31 username,
32 deletion,
33 hasPassword,
34 error,
35 defaultOpen = false,
36}: {
37 username: string;
38 deletion: AccountDeletion | null;
39 hasPassword: boolean;
40 error?: string;
41 /** Open on first render: for previews of the dialog. */
42 defaultOpen?: boolean;
43}) {
44 const [open, setOpen] = useState(Boolean(error) || defaultOpen);
45 const [confirm, setConfirm] = useState("");
46 const navigation = useNavigation();
47 const deleting = navigation.state !== "idle" && navigation.formData?.get("intent") === "delete-account";
48 const refusal = accountDeletionRefusal(deletion);
49 const goes = deletion ? whatAccountDeletionTakes(deletion) : [];
50 if (deletion?.protected) {
51 return (
52 <DangerAction title="Delete your account" action={null}>
53 <span role="status">{refusal}</span>
54 </DangerAction>
55 );
56 }
57 return (
58 <DangerAction
59 title="Delete your account"
60 action={
61 <Button type="button" variant="destructive" disabled={Boolean(refusal)} onClick={() => setOpen(true)}>
62 Delete account
63 </Button>
64 }
65 >
66 {refusal ? (
67 <>
68 <span role="status">{refusal}</span>
69 <ul className="mt-3 space-y-2">
70 {deletion?.sole_owner_of.map((workspace) => (
71 <Card asChild key={workspace.slug} radius="lg" className="px-3 py-2">
72 <li>
73 <p className="text-fg">
74 <span className="font-medium">{workspace.name}</span>{" "}
75 <span className="font-mono text-xs text-faint">{workspace.slug}</span>
76 </p>
77 <p className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs">
78 {workspace.members > 1 ? (
79 <Link to={`/${workspace.slug}/-/members`} className="text-accent underline-offset-4 hover:underline">
80 Make someone else an owner
81 </Link>
82 ) : null}
83 <Link to={`/${workspace.slug}/-/settings`} className="text-accent underline-offset-4 hover:underline">
84 Delete the workspace
85 </Link>
86 </p>
87 {workspace.billing ? <p className="mt-1 text-xs text-warn">{workspace.billing}</p> : null}
88 </li>
89 </Card>
90 ))}
91 </ul>
92 </>
93 ) : (
94 <>
95 You sign out everywhere and leave every workspace at once. Your account is kept for {ACCOUNT_RESTORE_DAYS}{" "}
96 days, and support can restore it until then.
97 </>
98 )}
99 <AlertDialog
100 open={open}
101 onOpenChange={(next) => {
102 setOpen(next);
103 setConfirm("");
104 }}
105 >
106 <AlertDialogContent>
107 <Form method="post" className="grid gap-4">
108 <input type="hidden" name="intent" value="delete-account" />
109 <AlertDialogHeader>
110 <AlertDialogTitle>Delete your account?</AlertDialogTitle>
111 <AlertDialogDescription>
112 Everything you sign in with stops working now. For {ACCOUNT_RESTORE_DAYS} days, support can restore
113 your account; after that it is gone for good.
114 </AlertDialogDescription>
115 </AlertDialogHeader>
116 {goes.length > 0 ? (
117 <div className="grid gap-1.5">
118 <p className="text-sm font-medium">What goes with it</p>
119 <ul className="list-disc space-y-1 pl-5 text-sm text-muted">
120 {goes.map((line) => (
121 <li key={line}>{line}</li>
122 ))}
123 </ul>
124 </div>
125 ) : null}
126 <ul className="list-disc space-y-1.5 pl-5 text-sm text-muted">
127 <li>You are signed out everywhere. Your access tokens, SSH keys, deploy keys you added and applications stop working.</li>
128 <li>You leave every workspace, team and repository. Workspaces you share keep everything in them.</li>
129 <li>Your profile is no longer found, and nobody can add you to anything.</li>
130 <li>
131 To get it back within {ACCOUNT_RESTORE_DAYS} days, write to support from one of its addresses. After
132 that your addresses, keys, profile and settings are removed for good.
133 </li>
134 <li>
135 What you wrote stays where it is: issues, pull requests, comments and reviews show as{" "}
136 <span className="font-mono text-fg">ghost</span> once it is removed.
137 </li>
138 <li>
139 The username <span className="font-mono text-fg">{username}</span> is never given to anyone else.
140 </li>
141 </ul>
142 <FormField>
143 <FieldLabel htmlFor="confirm-account">
144 Type <span className="font-mono text-fg">{username}</span> to confirm
145 </FieldLabel>
146 <TextInput
147 id="confirm-account"
148 name="confirm"
149 value={confirm}
150 spellCheck={false}
151 autoCapitalize="off"
152 autoComplete="off"
153 onChange={(event) => setConfirm(event.target.value)}
154 className="font-mono"
155 />
156 </FormField>
157 {hasPassword ? (
158 <FormField>
159 <FieldLabel htmlFor="confirm-password">Your password</FieldLabel>
160 <TextInput id="confirm-password" name="password" type="password" autoComplete="current-password" />
161 <FieldDescription>Not needed if you signed in within the last 10 minutes.</FieldDescription>
162 </FormField>
163 ) : (
164 <p className="text-sm text-muted">
165 Your account signs in with GitHub only: sign out, sign in with GitHub again, and delete it within 10
166 minutes.
167 </p>
168 )}
169 <ErrorText>{error}</ErrorText>
170 <AlertDialogFooter>
171 <AlertDialogCancel type="button">Cancel</AlertDialogCancel>
172 <Button type="submit" variant="destructive" disabled={!confirmsUsername(username, confirm) || deleting}>
173 {deleting ? "Deleting…" : "Delete account"}
174 </Button>
175 </AlertDialogFooter>
176 </Form>
177 </AlertDialogContent>
178 </AlertDialog>
179 </DangerAction>
180 );
181}