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.

166 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> <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} days, and support can restore it until
96 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 your account; after that it is gone for
113 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 that your addresses, keys, profile and
132 settings are removed for good.
133 </li>
134 <li>
135 What you wrote stays where it is: work items, pull requests, comments and reviews show as <span className="font-mono text-fg">ghost</span> once
136 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 <ConfirmName confirm={username} id="confirm-account" value={confirm} onChange={(event) => setConfirm(event.target.value)} />
143 {hasPassword ? (
144 <FormField>
145 <FieldLabel htmlFor="confirm-password">Your password</FieldLabel>
146 <TextInput id="confirm-password" name="password" type="password" autoComplete="current-password" />
147 <FieldDescription>Not needed if you signed in within the last 10 minutes.</FieldDescription>
148 </FormField>
149 ) : (
150 <p className="text-sm text-muted">
151 Your account signs in with GitHub only: sign out, sign in with GitHub again, and delete it within 10 minutes.
152 </p>
153 )}
154 <ErrorText>{error}</ErrorText>
155 <AlertDialogFooter>
156 <AlertDialogCancel type="button">Cancel</AlertDialogCancel>
157 <Button type="submit" variant="destructive" disabled={!confirmsUsername(username, confirm) || deleting}>
158 {deleting ? "Deleting…" : "Delete account"}
159 </Button>
160 </AlertDialogFooter>
161 </Form>
162 </AlertDialogContent>
163 </AlertDialog>
164 </DangerAction>
165 );
166}