Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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