@lukoweb/apitogo-module-user-panel 0.2.22 → 0.2.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/account-paths.ts +42 -42
- package/src/components/AccountLayout.tsx +119 -119
- package/src/hooks/useAccountSettings.ts +93 -93
- package/src/index.tsx +14 -14
- package/src/pages/DefaultDashboardPage.tsx +126 -126
- package/src/pages/DefaultPlansPage.tsx +62 -62
- package/src/pages/DefaultProfilePage.tsx +79 -79
- package/src/pages/SettingsPage.tsx +279 -279
- package/src/renderModuleElement.tsx +18 -18
- package/src/userPanelModule.tsx +152 -152
- package/src/utils/dev-portal-api.ts +125 -125
|
@@ -1,279 +1,279 @@
|
|
|
1
|
-
import type { ResolvedProfileContent } from "@lukoweb/apitogo";
|
|
2
|
-
import { Head, Link } from "@lukoweb/apitogo/components";
|
|
3
|
-
import { useAuth } from "@lukoweb/apitogo/hooks";
|
|
4
|
-
import { Button } from "@lukoweb/apitogo/ui/Button.js";
|
|
5
|
-
import {
|
|
6
|
-
Card,
|
|
7
|
-
CardContent,
|
|
8
|
-
CardHeader,
|
|
9
|
-
CardTitle,
|
|
10
|
-
} from "@lukoweb/apitogo/ui/Card.js";
|
|
11
|
-
import { Input } from "@lukoweb/apitogo/ui/Input.js";
|
|
12
|
-
import { Label } from "@lukoweb/apitogo/ui/Label.js";
|
|
13
|
-
import { useEffect, useState } from "react";
|
|
14
|
-
import {
|
|
15
|
-
useAccountMe,
|
|
16
|
-
useChangeAccountPassword,
|
|
17
|
-
useDeleteAccount,
|
|
18
|
-
useUpdateAccountProfile,
|
|
19
|
-
} from "../hooks/useAccountSettings.js";
|
|
20
|
-
|
|
21
|
-
export type SettingsPageProps = {
|
|
22
|
-
content: ResolvedProfileContent;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export const SettingsPage = ({ content }: SettingsPageProps) => {
|
|
26
|
-
const auth = useAuth();
|
|
27
|
-
const { data: account, isLoading } = useAccountMe();
|
|
28
|
-
const updateProfile = useUpdateAccountProfile();
|
|
29
|
-
const changePassword = useChangeAccountPassword();
|
|
30
|
-
const deleteAccountMutation = useDeleteAccount();
|
|
31
|
-
|
|
32
|
-
const [displayName, setDisplayName] = useState("");
|
|
33
|
-
const [currentPassword, setCurrentPassword] = useState("");
|
|
34
|
-
const [newPassword, setNewPassword] = useState("");
|
|
35
|
-
const [deletePassword, setDeletePassword] = useState("");
|
|
36
|
-
const [deleteConfirm, setDeleteConfirm] = useState("");
|
|
37
|
-
const [profileMessage, setProfileMessage] = useState<string | null>(null);
|
|
38
|
-
const [passwordMessage, setPasswordMessage] = useState<string | null>(null);
|
|
39
|
-
const [deleteMessage, setDeleteMessage] = useState<string | null>(null);
|
|
40
|
-
|
|
41
|
-
const title = content.title ?? "Settings";
|
|
42
|
-
const hasNativePassword = account?.hasNativePassword === true;
|
|
43
|
-
const email = account?.email ?? auth.profile?.email ?? "";
|
|
44
|
-
|
|
45
|
-
useEffect(() => {
|
|
46
|
-
if (account?.displayName !== undefined) {
|
|
47
|
-
setDisplayName(account.displayName ?? "");
|
|
48
|
-
} else if (auth.profile?.name) {
|
|
49
|
-
setDisplayName(auth.profile.name);
|
|
50
|
-
}
|
|
51
|
-
}, [account?.displayName, auth.profile?.name]);
|
|
52
|
-
|
|
53
|
-
if (!auth.isAuthenticated) {
|
|
54
|
-
return (
|
|
55
|
-
<div className="mx-auto max-w-[760px] px-8 py-7">
|
|
56
|
-
<Head>
|
|
57
|
-
<title>{title}</title>
|
|
58
|
-
</Head>
|
|
59
|
-
<h1 className="mb-2 text-[1.6rem] font-bold tracking-[-0.01em]">
|
|
60
|
-
{title}
|
|
61
|
-
</h1>
|
|
62
|
-
<p className="text-muted-foreground">
|
|
63
|
-
{auth.isAuthEnabled
|
|
64
|
-
? "Sign in to manage your account settings."
|
|
65
|
-
: "Configure authentication to enable account settings."}
|
|
66
|
-
</p>
|
|
67
|
-
{auth.isAuthEnabled ? (
|
|
68
|
-
<Button asChild className="mt-4">
|
|
69
|
-
<Link to="/signin">Sign in</Link>
|
|
70
|
-
</Button>
|
|
71
|
-
) : null}
|
|
72
|
-
</div>
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return (
|
|
77
|
-
<div className="mx-auto max-w-[760px] px-8 py-7">
|
|
78
|
-
<Head>
|
|
79
|
-
<title>{title}</title>
|
|
80
|
-
</Head>
|
|
81
|
-
|
|
82
|
-
<h1 className="mb-6 text-[1.6rem] font-bold tracking-[-0.01em]">
|
|
83
|
-
{title}
|
|
84
|
-
</h1>
|
|
85
|
-
|
|
86
|
-
<div className="grid gap-5">
|
|
87
|
-
<Card>
|
|
88
|
-
<CardHeader>
|
|
89
|
-
<CardTitle>Profile</CardTitle>
|
|
90
|
-
</CardHeader>
|
|
91
|
-
<CardContent className="space-y-4">
|
|
92
|
-
<div className="grid gap-4 sm:grid-cols-2">
|
|
93
|
-
<div className="space-y-2">
|
|
94
|
-
<Label htmlFor="settings-display-name">Full name</Label>
|
|
95
|
-
<Input
|
|
96
|
-
id="settings-display-name"
|
|
97
|
-
value={displayName}
|
|
98
|
-
onChange={(event) => setDisplayName(event.target.value)}
|
|
99
|
-
disabled={isLoading || updateProfile.isPending}
|
|
100
|
-
/>
|
|
101
|
-
</div>
|
|
102
|
-
<div className="space-y-2">
|
|
103
|
-
<Label htmlFor="settings-email">Email</Label>
|
|
104
|
-
<Input
|
|
105
|
-
id="settings-email"
|
|
106
|
-
type="email"
|
|
107
|
-
value={email}
|
|
108
|
-
readOnly
|
|
109
|
-
disabled
|
|
110
|
-
/>
|
|
111
|
-
</div>
|
|
112
|
-
</div>
|
|
113
|
-
{profileMessage ? (
|
|
114
|
-
<p className="text-sm text-muted-foreground">{profileMessage}</p>
|
|
115
|
-
) : null}
|
|
116
|
-
<Button
|
|
117
|
-
size="sm"
|
|
118
|
-
disabled={isLoading || updateProfile.isPending}
|
|
119
|
-
onClick={() => {
|
|
120
|
-
setProfileMessage(null);
|
|
121
|
-
updateProfile.mutate(displayName, {
|
|
122
|
-
onSuccess: () => setProfileMessage("Profile saved."),
|
|
123
|
-
onError: (error) =>
|
|
124
|
-
setProfileMessage(
|
|
125
|
-
error instanceof Error
|
|
126
|
-
? error.message
|
|
127
|
-
: "Unable to save profile.",
|
|
128
|
-
),
|
|
129
|
-
});
|
|
130
|
-
}}
|
|
131
|
-
>
|
|
132
|
-
Save changes
|
|
133
|
-
</Button>
|
|
134
|
-
</CardContent>
|
|
135
|
-
</Card>
|
|
136
|
-
|
|
137
|
-
{hasNativePassword ? (
|
|
138
|
-
<Card>
|
|
139
|
-
<CardHeader>
|
|
140
|
-
<CardTitle>Password</CardTitle>
|
|
141
|
-
</CardHeader>
|
|
142
|
-
<CardContent className="space-y-4">
|
|
143
|
-
<div className="grid gap-4 sm:grid-cols-2">
|
|
144
|
-
<div className="space-y-2">
|
|
145
|
-
<Label htmlFor="settings-current-password">
|
|
146
|
-
Current password
|
|
147
|
-
</Label>
|
|
148
|
-
<Input
|
|
149
|
-
id="settings-current-password"
|
|
150
|
-
type="password"
|
|
151
|
-
value={currentPassword}
|
|
152
|
-
onChange={(event) => setCurrentPassword(event.target.value)}
|
|
153
|
-
autoComplete="current-password"
|
|
154
|
-
/>
|
|
155
|
-
</div>
|
|
156
|
-
<div className="space-y-2">
|
|
157
|
-
<Label htmlFor="settings-new-password">New password</Label>
|
|
158
|
-
<Input
|
|
159
|
-
id="settings-new-password"
|
|
160
|
-
type="password"
|
|
161
|
-
value={newPassword}
|
|
162
|
-
onChange={(event) => setNewPassword(event.target.value)}
|
|
163
|
-
autoComplete="new-password"
|
|
164
|
-
/>
|
|
165
|
-
</div>
|
|
166
|
-
</div>
|
|
167
|
-
{passwordMessage ? (
|
|
168
|
-
<p className="text-sm text-muted-foreground">
|
|
169
|
-
{passwordMessage}
|
|
170
|
-
</p>
|
|
171
|
-
) : null}
|
|
172
|
-
<Button
|
|
173
|
-
variant="outline"
|
|
174
|
-
size="sm"
|
|
175
|
-
disabled={changePassword.isPending}
|
|
176
|
-
onClick={() => {
|
|
177
|
-
setPasswordMessage(null);
|
|
178
|
-
changePassword.mutate(
|
|
179
|
-
{ currentPassword, newPassword },
|
|
180
|
-
{
|
|
181
|
-
onSuccess: () => {
|
|
182
|
-
setCurrentPassword("");
|
|
183
|
-
setNewPassword("");
|
|
184
|
-
setPasswordMessage("Password updated.");
|
|
185
|
-
},
|
|
186
|
-
onError: (error) =>
|
|
187
|
-
setPasswordMessage(
|
|
188
|
-
error instanceof Error
|
|
189
|
-
? error.message
|
|
190
|
-
: "Unable to update password.",
|
|
191
|
-
),
|
|
192
|
-
},
|
|
193
|
-
);
|
|
194
|
-
}}
|
|
195
|
-
>
|
|
196
|
-
Update password
|
|
197
|
-
</Button>
|
|
198
|
-
</CardContent>
|
|
199
|
-
</Card>
|
|
200
|
-
) : null}
|
|
201
|
-
|
|
202
|
-
<Card className="border-destructive/40">
|
|
203
|
-
<CardHeader>
|
|
204
|
-
<CardTitle className="text-destructive">Danger zone</CardTitle>
|
|
205
|
-
</CardHeader>
|
|
206
|
-
<CardContent className="space-y-4">
|
|
207
|
-
<p className="text-sm text-muted-foreground">
|
|
208
|
-
Deleting your account removes all keys, usage history, and billing
|
|
209
|
-
data. This cannot be undone.
|
|
210
|
-
</p>
|
|
211
|
-
{hasNativePassword ? (
|
|
212
|
-
<div className="max-w-sm space-y-2">
|
|
213
|
-
<Label htmlFor="settings-delete-password">
|
|
214
|
-
Confirm with password
|
|
215
|
-
</Label>
|
|
216
|
-
<Input
|
|
217
|
-
id="settings-delete-password"
|
|
218
|
-
type="password"
|
|
219
|
-
value={deletePassword}
|
|
220
|
-
onChange={(event) => setDeletePassword(event.target.value)}
|
|
221
|
-
autoComplete="current-password"
|
|
222
|
-
/>
|
|
223
|
-
</div>
|
|
224
|
-
) : (
|
|
225
|
-
<div className="max-w-sm space-y-2">
|
|
226
|
-
<Label htmlFor="settings-delete-confirm">
|
|
227
|
-
Type DELETE to confirm
|
|
228
|
-
</Label>
|
|
229
|
-
<Input
|
|
230
|
-
id="settings-delete-confirm"
|
|
231
|
-
value={deleteConfirm}
|
|
232
|
-
onChange={(event) => setDeleteConfirm(event.target.value)}
|
|
233
|
-
autoComplete="off"
|
|
234
|
-
/>
|
|
235
|
-
</div>
|
|
236
|
-
)}
|
|
237
|
-
{deleteMessage ? (
|
|
238
|
-
<p className="text-sm text-muted-foreground">{deleteMessage}</p>
|
|
239
|
-
) : null}
|
|
240
|
-
<Button
|
|
241
|
-
variant="outline"
|
|
242
|
-
size="sm"
|
|
243
|
-
className="border-destructive text-destructive hover:bg-destructive/10"
|
|
244
|
-
disabled={
|
|
245
|
-
deleteAccountMutation.isPending ||
|
|
246
|
-
(hasNativePassword
|
|
247
|
-
? !deletePassword
|
|
248
|
-
: deleteConfirm !== "DELETE")
|
|
249
|
-
}
|
|
250
|
-
onClick={() => {
|
|
251
|
-
if (
|
|
252
|
-
!window.confirm(
|
|
253
|
-
"Delete your account permanently? This cannot be undone.",
|
|
254
|
-
)
|
|
255
|
-
) {
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
setDeleteMessage(null);
|
|
259
|
-
deleteAccountMutation.mutate(
|
|
260
|
-
hasNativePassword ? deletePassword : undefined,
|
|
261
|
-
{
|
|
262
|
-
onError: (error) =>
|
|
263
|
-
setDeleteMessage(
|
|
264
|
-
error instanceof Error
|
|
265
|
-
? error.message
|
|
266
|
-
: "Unable to delete account.",
|
|
267
|
-
),
|
|
268
|
-
},
|
|
269
|
-
);
|
|
270
|
-
}}
|
|
271
|
-
>
|
|
272
|
-
Delete account
|
|
273
|
-
</Button>
|
|
274
|
-
</CardContent>
|
|
275
|
-
</Card>
|
|
276
|
-
</div>
|
|
277
|
-
</div>
|
|
278
|
-
);
|
|
279
|
-
};
|
|
1
|
+
import type { ResolvedProfileContent } from "@lukoweb/apitogo";
|
|
2
|
+
import { Head, Link } from "@lukoweb/apitogo/components";
|
|
3
|
+
import { useAuth } from "@lukoweb/apitogo/hooks";
|
|
4
|
+
import { Button } from "@lukoweb/apitogo/ui/Button.js";
|
|
5
|
+
import {
|
|
6
|
+
Card,
|
|
7
|
+
CardContent,
|
|
8
|
+
CardHeader,
|
|
9
|
+
CardTitle,
|
|
10
|
+
} from "@lukoweb/apitogo/ui/Card.js";
|
|
11
|
+
import { Input } from "@lukoweb/apitogo/ui/Input.js";
|
|
12
|
+
import { Label } from "@lukoweb/apitogo/ui/Label.js";
|
|
13
|
+
import { useEffect, useState } from "react";
|
|
14
|
+
import {
|
|
15
|
+
useAccountMe,
|
|
16
|
+
useChangeAccountPassword,
|
|
17
|
+
useDeleteAccount,
|
|
18
|
+
useUpdateAccountProfile,
|
|
19
|
+
} from "../hooks/useAccountSettings.js";
|
|
20
|
+
|
|
21
|
+
export type SettingsPageProps = {
|
|
22
|
+
content: ResolvedProfileContent;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const SettingsPage = ({ content }: SettingsPageProps) => {
|
|
26
|
+
const auth = useAuth();
|
|
27
|
+
const { data: account, isLoading } = useAccountMe();
|
|
28
|
+
const updateProfile = useUpdateAccountProfile();
|
|
29
|
+
const changePassword = useChangeAccountPassword();
|
|
30
|
+
const deleteAccountMutation = useDeleteAccount();
|
|
31
|
+
|
|
32
|
+
const [displayName, setDisplayName] = useState("");
|
|
33
|
+
const [currentPassword, setCurrentPassword] = useState("");
|
|
34
|
+
const [newPassword, setNewPassword] = useState("");
|
|
35
|
+
const [deletePassword, setDeletePassword] = useState("");
|
|
36
|
+
const [deleteConfirm, setDeleteConfirm] = useState("");
|
|
37
|
+
const [profileMessage, setProfileMessage] = useState<string | null>(null);
|
|
38
|
+
const [passwordMessage, setPasswordMessage] = useState<string | null>(null);
|
|
39
|
+
const [deleteMessage, setDeleteMessage] = useState<string | null>(null);
|
|
40
|
+
|
|
41
|
+
const title = content.title ?? "Settings";
|
|
42
|
+
const hasNativePassword = account?.hasNativePassword === true;
|
|
43
|
+
const email = account?.email ?? auth.profile?.email ?? "";
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (account?.displayName !== undefined) {
|
|
47
|
+
setDisplayName(account.displayName ?? "");
|
|
48
|
+
} else if (auth.profile?.name) {
|
|
49
|
+
setDisplayName(auth.profile.name);
|
|
50
|
+
}
|
|
51
|
+
}, [account?.displayName, auth.profile?.name]);
|
|
52
|
+
|
|
53
|
+
if (!auth.isAuthenticated) {
|
|
54
|
+
return (
|
|
55
|
+
<div className="mx-auto max-w-[760px] px-8 py-7">
|
|
56
|
+
<Head>
|
|
57
|
+
<title>{title}</title>
|
|
58
|
+
</Head>
|
|
59
|
+
<h1 className="mb-2 text-[1.6rem] font-bold tracking-[-0.01em]">
|
|
60
|
+
{title}
|
|
61
|
+
</h1>
|
|
62
|
+
<p className="text-muted-foreground">
|
|
63
|
+
{auth.isAuthEnabled
|
|
64
|
+
? "Sign in to manage your account settings."
|
|
65
|
+
: "Configure authentication to enable account settings."}
|
|
66
|
+
</p>
|
|
67
|
+
{auth.isAuthEnabled ? (
|
|
68
|
+
<Button asChild className="mt-4">
|
|
69
|
+
<Link to="/signin">Sign in</Link>
|
|
70
|
+
</Button>
|
|
71
|
+
) : null}
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div className="mx-auto max-w-[760px] px-8 py-7">
|
|
78
|
+
<Head>
|
|
79
|
+
<title>{title}</title>
|
|
80
|
+
</Head>
|
|
81
|
+
|
|
82
|
+
<h1 className="mb-6 text-[1.6rem] font-bold tracking-[-0.01em]">
|
|
83
|
+
{title}
|
|
84
|
+
</h1>
|
|
85
|
+
|
|
86
|
+
<div className="grid gap-5">
|
|
87
|
+
<Card>
|
|
88
|
+
<CardHeader>
|
|
89
|
+
<CardTitle>Profile</CardTitle>
|
|
90
|
+
</CardHeader>
|
|
91
|
+
<CardContent className="space-y-4">
|
|
92
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
93
|
+
<div className="space-y-2">
|
|
94
|
+
<Label htmlFor="settings-display-name">Full name</Label>
|
|
95
|
+
<Input
|
|
96
|
+
id="settings-display-name"
|
|
97
|
+
value={displayName}
|
|
98
|
+
onChange={(event) => setDisplayName(event.target.value)}
|
|
99
|
+
disabled={isLoading || updateProfile.isPending}
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
<div className="space-y-2">
|
|
103
|
+
<Label htmlFor="settings-email">Email</Label>
|
|
104
|
+
<Input
|
|
105
|
+
id="settings-email"
|
|
106
|
+
type="email"
|
|
107
|
+
value={email}
|
|
108
|
+
readOnly
|
|
109
|
+
disabled
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
{profileMessage ? (
|
|
114
|
+
<p className="text-sm text-muted-foreground">{profileMessage}</p>
|
|
115
|
+
) : null}
|
|
116
|
+
<Button
|
|
117
|
+
size="sm"
|
|
118
|
+
disabled={isLoading || updateProfile.isPending}
|
|
119
|
+
onClick={() => {
|
|
120
|
+
setProfileMessage(null);
|
|
121
|
+
updateProfile.mutate(displayName, {
|
|
122
|
+
onSuccess: () => setProfileMessage("Profile saved."),
|
|
123
|
+
onError: (error) =>
|
|
124
|
+
setProfileMessage(
|
|
125
|
+
error instanceof Error
|
|
126
|
+
? error.message
|
|
127
|
+
: "Unable to save profile.",
|
|
128
|
+
),
|
|
129
|
+
});
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
Save changes
|
|
133
|
+
</Button>
|
|
134
|
+
</CardContent>
|
|
135
|
+
</Card>
|
|
136
|
+
|
|
137
|
+
{hasNativePassword ? (
|
|
138
|
+
<Card>
|
|
139
|
+
<CardHeader>
|
|
140
|
+
<CardTitle>Password</CardTitle>
|
|
141
|
+
</CardHeader>
|
|
142
|
+
<CardContent className="space-y-4">
|
|
143
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
144
|
+
<div className="space-y-2">
|
|
145
|
+
<Label htmlFor="settings-current-password">
|
|
146
|
+
Current password
|
|
147
|
+
</Label>
|
|
148
|
+
<Input
|
|
149
|
+
id="settings-current-password"
|
|
150
|
+
type="password"
|
|
151
|
+
value={currentPassword}
|
|
152
|
+
onChange={(event) => setCurrentPassword(event.target.value)}
|
|
153
|
+
autoComplete="current-password"
|
|
154
|
+
/>
|
|
155
|
+
</div>
|
|
156
|
+
<div className="space-y-2">
|
|
157
|
+
<Label htmlFor="settings-new-password">New password</Label>
|
|
158
|
+
<Input
|
|
159
|
+
id="settings-new-password"
|
|
160
|
+
type="password"
|
|
161
|
+
value={newPassword}
|
|
162
|
+
onChange={(event) => setNewPassword(event.target.value)}
|
|
163
|
+
autoComplete="new-password"
|
|
164
|
+
/>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
{passwordMessage ? (
|
|
168
|
+
<p className="text-sm text-muted-foreground">
|
|
169
|
+
{passwordMessage}
|
|
170
|
+
</p>
|
|
171
|
+
) : null}
|
|
172
|
+
<Button
|
|
173
|
+
variant="outline"
|
|
174
|
+
size="sm"
|
|
175
|
+
disabled={changePassword.isPending}
|
|
176
|
+
onClick={() => {
|
|
177
|
+
setPasswordMessage(null);
|
|
178
|
+
changePassword.mutate(
|
|
179
|
+
{ currentPassword, newPassword },
|
|
180
|
+
{
|
|
181
|
+
onSuccess: () => {
|
|
182
|
+
setCurrentPassword("");
|
|
183
|
+
setNewPassword("");
|
|
184
|
+
setPasswordMessage("Password updated.");
|
|
185
|
+
},
|
|
186
|
+
onError: (error) =>
|
|
187
|
+
setPasswordMessage(
|
|
188
|
+
error instanceof Error
|
|
189
|
+
? error.message
|
|
190
|
+
: "Unable to update password.",
|
|
191
|
+
),
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
}}
|
|
195
|
+
>
|
|
196
|
+
Update password
|
|
197
|
+
</Button>
|
|
198
|
+
</CardContent>
|
|
199
|
+
</Card>
|
|
200
|
+
) : null}
|
|
201
|
+
|
|
202
|
+
<Card className="border-destructive/40">
|
|
203
|
+
<CardHeader>
|
|
204
|
+
<CardTitle className="text-destructive">Danger zone</CardTitle>
|
|
205
|
+
</CardHeader>
|
|
206
|
+
<CardContent className="space-y-4">
|
|
207
|
+
<p className="text-sm text-muted-foreground">
|
|
208
|
+
Deleting your account removes all keys, usage history, and billing
|
|
209
|
+
data. This cannot be undone.
|
|
210
|
+
</p>
|
|
211
|
+
{hasNativePassword ? (
|
|
212
|
+
<div className="max-w-sm space-y-2">
|
|
213
|
+
<Label htmlFor="settings-delete-password">
|
|
214
|
+
Confirm with password
|
|
215
|
+
</Label>
|
|
216
|
+
<Input
|
|
217
|
+
id="settings-delete-password"
|
|
218
|
+
type="password"
|
|
219
|
+
value={deletePassword}
|
|
220
|
+
onChange={(event) => setDeletePassword(event.target.value)}
|
|
221
|
+
autoComplete="current-password"
|
|
222
|
+
/>
|
|
223
|
+
</div>
|
|
224
|
+
) : (
|
|
225
|
+
<div className="max-w-sm space-y-2">
|
|
226
|
+
<Label htmlFor="settings-delete-confirm">
|
|
227
|
+
Type DELETE to confirm
|
|
228
|
+
</Label>
|
|
229
|
+
<Input
|
|
230
|
+
id="settings-delete-confirm"
|
|
231
|
+
value={deleteConfirm}
|
|
232
|
+
onChange={(event) => setDeleteConfirm(event.target.value)}
|
|
233
|
+
autoComplete="off"
|
|
234
|
+
/>
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
{deleteMessage ? (
|
|
238
|
+
<p className="text-sm text-muted-foreground">{deleteMessage}</p>
|
|
239
|
+
) : null}
|
|
240
|
+
<Button
|
|
241
|
+
variant="outline"
|
|
242
|
+
size="sm"
|
|
243
|
+
className="border-destructive text-destructive hover:bg-destructive/10"
|
|
244
|
+
disabled={
|
|
245
|
+
deleteAccountMutation.isPending ||
|
|
246
|
+
(hasNativePassword
|
|
247
|
+
? !deletePassword
|
|
248
|
+
: deleteConfirm !== "DELETE")
|
|
249
|
+
}
|
|
250
|
+
onClick={() => {
|
|
251
|
+
if (
|
|
252
|
+
!window.confirm(
|
|
253
|
+
"Delete your account permanently? This cannot be undone.",
|
|
254
|
+
)
|
|
255
|
+
) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
setDeleteMessage(null);
|
|
259
|
+
deleteAccountMutation.mutate(
|
|
260
|
+
hasNativePassword ? deletePassword : undefined,
|
|
261
|
+
{
|
|
262
|
+
onError: (error) =>
|
|
263
|
+
setDeleteMessage(
|
|
264
|
+
error instanceof Error
|
|
265
|
+
? error.message
|
|
266
|
+
: "Unable to delete account.",
|
|
267
|
+
),
|
|
268
|
+
},
|
|
269
|
+
);
|
|
270
|
+
}}
|
|
271
|
+
>
|
|
272
|
+
Delete account
|
|
273
|
+
</Button>
|
|
274
|
+
</CardContent>
|
|
275
|
+
</Card>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
};
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import type { ComponentType, ReactNode } from "react";
|
|
2
|
-
import { isValidElement } from "react";
|
|
3
|
-
|
|
4
|
-
export const renderModuleElement = (
|
|
5
|
-
component: ComponentType | ReactNode | undefined,
|
|
6
|
-
fallback: ReactNode,
|
|
7
|
-
) => {
|
|
8
|
-
if (!component) {
|
|
9
|
-
return fallback;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
if (isValidElement(component)) {
|
|
13
|
-
return component;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const Component = component as ComponentType;
|
|
17
|
-
return <Component />;
|
|
18
|
-
};
|
|
1
|
+
import type { ComponentType, ReactNode } from "react";
|
|
2
|
+
import { isValidElement } from "react";
|
|
3
|
+
|
|
4
|
+
export const renderModuleElement = (
|
|
5
|
+
component: ComponentType | ReactNode | undefined,
|
|
6
|
+
fallback: ReactNode,
|
|
7
|
+
) => {
|
|
8
|
+
if (!component) {
|
|
9
|
+
return fallback;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (isValidElement(component)) {
|
|
13
|
+
return component;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const Component = component as ComponentType;
|
|
17
|
+
return <Component />;
|
|
18
|
+
};
|