Skip to content
177 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

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

This file's history is long; its oldest lines are credited to the oldest commit read.