Skip to content

Commit 853ed47

Browse files
committed
[TOOL-3525] Dashboard: Add delete account in account settings page (#6339)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on enhancing the management of teams and accounts in the application. It introduces a new method to fetch the default team and updates various components to utilize this information, improving user experience and functionality. ### Detailed summary - Added `getDefaultTeam` function to fetch the default team using an API call. - Updated `TeamRootPage` to use `getDefaultTeam` instead of fetching all teams. - Modified middleware to redirect using the default team. - Refactored account settings page to include `defaultTeamSlug` and `defaultTeamName`. - Enhanced `DangerSettingCard` to manage confirmation dialog state. - Updated `AccountSettingsPageUI` to handle account deletion with relevant alerts for errors. - Added UI components for handling account deletion status responses in the storybook. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent f576581 commit 853ed47

File tree

8 files changed

+262
-49
lines changed

8 files changed

+262
-49
lines changed

apps/dashboard/src/@/api/team.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,23 @@ export async function getTeams() {
5757
return null;
5858
}
5959

60+
export async function getDefaultTeam() {
61+
const token = await getAuthToken();
62+
if (!token) {
63+
return null;
64+
}
65+
66+
const res = await fetch(`${API_SERVER_URL}/v1/teams/~`, {
67+
headers: {
68+
Authorization: `Bearer ${token}`,
69+
},
70+
});
71+
if (res.ok) {
72+
return (await res.json())?.result as Team;
73+
}
74+
return null;
75+
}
76+
6077
type TeamNebulaWaitList = {
6178
onWaitlist: boolean;
6279
createdAt: null | string;

apps/dashboard/src/@/components/blocks/DangerSettingCard.tsx

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import {
1010
DialogTitle,
1111
DialogTrigger,
1212
} from "@/components/ui/dialog";
13+
import { useState } from "react";
1314
import { cn } from "../../lib/utils";
15+
import { DynamicHeight } from "../ui/DynamicHeight";
1416

1517
export function DangerSettingCard(props: {
1618
title: string;
@@ -24,9 +26,14 @@ export function DangerSettingCard(props: {
2426
confirmationDialog: {
2527
title: string;
2628
description: React.ReactNode;
29+
children?: React.ReactNode;
30+
onClose?: () => void;
2731
};
2832
children?: React.ReactNode;
2933
}) {
34+
const [isConfirmationDialogOpen, setIsConfirmationDialogOpen] =
35+
useState(false);
36+
3037
return (
3138
<div
3239
className={cn(
@@ -50,7 +57,15 @@ export function DangerSettingCard(props: {
5057
props.footerClassName,
5158
)}
5259
>
53-
<Dialog>
60+
<Dialog
61+
open={isConfirmationDialogOpen}
62+
onOpenChange={(v) => {
63+
setIsConfirmationDialogOpen(v);
64+
if (!v) {
65+
props.confirmationDialog.onClose?.();
66+
}
67+
}}
68+
>
5469
<DialogTrigger asChild>
5570
<Button
5671
variant="destructive"
@@ -66,17 +81,20 @@ export function DangerSettingCard(props: {
6681
className="z-[10001] overflow-hidden p-0"
6782
dialogOverlayClassName="z-[10000]"
6883
>
69-
<div className="p-6">
70-
<DialogHeader className="pr-10">
71-
<DialogTitle className="leading-snug">
72-
{props.confirmationDialog.title}
73-
</DialogTitle>
84+
<DynamicHeight>
85+
<div className="p-6">
86+
<DialogHeader className="pr-10">
87+
<DialogTitle className="leading-snug">
88+
{props.confirmationDialog.title}
89+
</DialogTitle>
7490

75-
<DialogDescription>
76-
{props.confirmationDialog.description}
77-
</DialogDescription>
78-
</DialogHeader>
79-
</div>
91+
<DialogDescription>
92+
{props.confirmationDialog.description}
93+
</DialogDescription>
94+
</DialogHeader>
95+
{props.confirmationDialog.children}
96+
</div>
97+
</DynamicHeight>
8098

8199
<div className="flex justify-end gap-4 border-t bg-card p-6 lg:gap-2">
82100
<DialogClose asChild>

apps/dashboard/src/app/account/settings/AccountSettingsPage.tsx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
11
"use client";
22

3+
import { redirectToBillingPortal } from "@/actions/billing";
34
import { confirmEmailWithOTP } from "@/actions/confirmEmail";
5+
import { apiServerProxy } from "@/actions/proxies";
46
import { updateAccount } from "@/actions/updateAccount";
57
import { useDashboardRouter } from "@/lib/DashboardRouter";
68
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
79
import type { ThirdwebClient } from "thirdweb";
10+
import { useActiveWallet, useDisconnect } from "thirdweb/react";
811
import { upload } from "thirdweb/storage";
12+
import { doLogout } from "../../login/auth-actions";
913
import { AccountSettingsPageUI } from "./AccountSettingsPageUI";
1014

1115
export function AccountSettingsPage(props: {
1216
account: Account;
1317
client: ThirdwebClient;
18+
defaultTeamSlug: string;
19+
defaultTeamName: string;
1420
}) {
1521
const router = useDashboardRouter();
22+
const activeWallet = useActiveWallet();
23+
const { disconnect } = useDisconnect();
1624
return (
1725
<div>
1826
<header className="border-border border-b py-10">
@@ -25,8 +33,32 @@ export function AccountSettingsPage(props: {
2533

2634
<div className="container max-w-[950px] grow pt-8 pb-20">
2735
<AccountSettingsPageUI
28-
hideDeleteAccount
2936
client={props.client}
37+
defaultTeamSlug={props.defaultTeamSlug}
38+
defaultTeamName={props.defaultTeamName}
39+
onAccountDeleted={async () => {
40+
await doLogout();
41+
if (activeWallet) {
42+
disconnect(activeWallet);
43+
}
44+
router.replace("/login");
45+
}}
46+
deleteAccount={async () => {
47+
try {
48+
const res = await apiServerProxy({
49+
method: "DELETE",
50+
pathname: "/v1/account",
51+
});
52+
53+
return {
54+
status: res.status,
55+
};
56+
} catch (error) {
57+
console.error(error);
58+
return { status: 500 };
59+
}
60+
}}
61+
redirectToBillingPortal={redirectToBillingPortal}
3062
updateAccountAvatar={async (file) => {
3163
let uri: string | undefined = undefined;
3264

apps/dashboard/src/app/account/settings/AccountSettingsPageUI.stories.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox";
2+
import { Label } from "@/components/ui/label";
3+
import {
4+
Select,
5+
SelectContent,
6+
SelectItem,
7+
SelectTrigger,
8+
SelectValue,
9+
} from "@/components/ui/select";
210
import { getThirdwebClient } from "@/constants/thirdweb.server";
311
import type { Meta, StoryObj } from "@storybook/react";
412
import { useState } from "react";
@@ -40,6 +48,13 @@ function Variants() {
4048
const [isVerifiedEmail, setIsVerifiedEmail] = useState(true);
4149
const [sendEmailFails, setSendEmailFails] = useState(false);
4250
const [emailConfirmationFails, setEmailConfirmationFails] = useState(false);
51+
const [deleteAccountStatusResponse, setDeleteAccountStatusResponse] =
52+
useState<400 | 402 | 500 | 200>(200);
53+
54+
const deleteAccountStub = async () => {
55+
await new Promise((resolve) => setTimeout(resolve, 1000));
56+
return { status: deleteAccountStatusResponse };
57+
};
4358

4459
return (
4560
<div className="container flex max-w-[1132px] flex-col gap-10 py-10">
@@ -67,9 +82,37 @@ function Variants() {
6782
/>
6883
Email Confirmation Fails
6984
</CheckboxWithLabel>
85+
86+
<div className="mt-3 flex flex-col gap-2">
87+
<Label>Delete Account Response</Label>
88+
<Select
89+
value={String(deleteAccountStatusResponse)}
90+
onValueChange={(value) =>
91+
setDeleteAccountStatusResponse(
92+
Number(value) as 400 | 402 | 500 | 200,
93+
)
94+
}
95+
>
96+
<SelectTrigger className="min-w-[320px]">
97+
<SelectValue placeholder="Select status" />
98+
</SelectTrigger>
99+
<SelectContent>
100+
<SelectItem value="200">200 - Success</SelectItem>
101+
<SelectItem value="400">400 - Active Subscriptions</SelectItem>
102+
<SelectItem value="402">402 - Unpaid Invoices</SelectItem>
103+
<SelectItem value="500">500 - Unknown Error</SelectItem>
104+
</SelectContent>
105+
</Select>
106+
</div>
70107
</div>
71108

72109
<AccountSettingsPageUI
110+
defaultTeamSlug="foo"
111+
defaultTeamName="Foo"
112+
redirectToBillingPortal={async () => {
113+
await new Promise((resolve) => setTimeout(resolve, 1000));
114+
return { status: 200 };
115+
}}
73116
account={{
74117
name: "John Doe",
75118
email: "johndoe@gmail.com",
@@ -96,6 +139,10 @@ function Variants() {
96139
throw new Error("Email already exists");
97140
}
98141
}}
142+
deleteAccount={deleteAccountStub}
143+
onAccountDeleted={() => {
144+
console.log("Account deleted");
145+
}}
99146
/>
100147
<Toaster richColors />
101148
</div>

0 commit comments

Comments
 (0)