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 ? (
People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how.76 <Link to={`/${workspace.slug}/-/members`} className="text-accent underline-offset-4 hover:underline">
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)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.