Skip to content

Commit a67e980

Browse files
committed
[Dashboard] Feature: Add account settings page with Name and Email fields (#5461)
## Problem solved Short description of the bug fixed or feature added <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on enhancing the account settings functionality, including email confirmation and account updates, while improving error handling and user interactions. ### Detailed summary - Added a new route for `/account/settings`. - Improved onboarding logic for email confirmation. - Updated waitlist joining logic to handle responses better. - Enhanced `joinTeamWaitlist` and `updateAccount` actions with better error handling. - Implemented email confirmation with OTP in `confirmEmailWithOTP`. - Improved `AccountSettingsPage` to include email update functionalities. - Added UI components for email verification and account updates. - Refined error handling in various components for better user feedback. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 4c8333d commit a67e980

File tree

9 files changed

+405
-129
lines changed

9 files changed

+405
-129
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"use server";
2+
3+
import { getAuthToken } from "../../app/api/lib/getAuthToken";
4+
import { API_SERVER_URL } from "../constants/env";
5+
6+
export async function confirmEmailWithOTP(otp: string) {
7+
const token = await getAuthToken();
8+
9+
if (!token) {
10+
return {
11+
errorMessage: "You are not authorized to perform this action",
12+
};
13+
}
14+
15+
const res = await fetch(`${API_SERVER_URL}/v1/account/confirmEmail`, {
16+
method: "PUT",
17+
headers: {
18+
"Content-Type": "application/json",
19+
Authorization: `Bearer ${token}`,
20+
},
21+
body: JSON.stringify({
22+
confirmationToken: otp,
23+
}),
24+
});
25+
26+
if (!res.ok) {
27+
const json = await res.json();
28+
29+
if (json.error) {
30+
return {
31+
errorMessage: json.error.message,
32+
};
33+
}
34+
35+
return {
36+
errorMessage: "Failed to confirm email",
37+
};
38+
}
39+
}

apps/dashboard/src/@/actions/joinWaitlist.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ export async function joinTeamWaitlist(options: {
1212
const token = await getAuthToken();
1313

1414
if (!token) {
15-
throw new Error("No Auth token");
15+
return {
16+
errorMessage: "You are not authorized to perform this action",
17+
};
1618
}
1719

1820
const res = await fetch(`${API_SERVER_URL}/v1/teams/${teamSlug}/waitlist`, {
@@ -27,8 +29,12 @@ export async function joinTeamWaitlist(options: {
2729
});
2830

2931
if (!res.ok) {
30-
throw new Error("Failed to join waitlist");
32+
return {
33+
errorMessage: "Failed to join waitlist",
34+
};
3135
}
3236

33-
return true;
37+
return {
38+
success: true,
39+
};
3440
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"use server";
2+
import { getAuthToken } from "../../app/api/lib/getAuthToken";
3+
import { API_SERVER_URL } from "../constants/env";
4+
5+
export async function updateAccount(values: {
6+
name?: string;
7+
email?: string;
8+
}) {
9+
const token = await getAuthToken();
10+
11+
if (!token) {
12+
throw new Error("No Auth token");
13+
}
14+
15+
const res = await fetch(`${API_SERVER_URL}/v1/account`, {
16+
method: "PUT",
17+
headers: {
18+
"Content-Type": "application/json",
19+
Authorization: `Bearer ${token}`,
20+
},
21+
body: JSON.stringify(values),
22+
});
23+
24+
if (!res.ok) {
25+
const json = await res.json();
26+
27+
if (json.error) {
28+
return {
29+
errorMessage: json.error.message,
30+
};
31+
}
32+
33+
return {
34+
errorMessage: "Failed To Update Account",
35+
};
36+
}
37+
}

apps/dashboard/src/app/account/layout.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,12 @@ async function HeaderAndNav() {
4747
name: "Contracts",
4848
exactMatch: true,
4949
},
50+
{
51+
path: "/account/settings",
52+
name: "Settings",
53+
},
5054
// TODO - enable these links after they are functional
5155
// {
52-
// path: "/account/settings",
53-
// name: "Settings",
54-
// },
55-
// {
5656
// path: "/account/wallets",
5757
// name: "Wallets",
5858
// },
Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,56 @@
11
"use client";
22

3+
import { confirmEmailWithOTP } from "@/actions/confirmEmail";
4+
import { updateAccount } from "@/actions/updateAccount";
5+
import { useDashboardRouter } from "@/lib/DashboardRouter";
36
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
47
import type { ThirdwebClient } from "thirdweb";
5-
import { upload } from "thirdweb/storage";
68
import { AccountSettingsPageUI } from "./AccountSettingsPageUI";
79

810
export function AccountSettingsPage(props: {
911
account: Account;
1012
client: ThirdwebClient;
1113
}) {
14+
const router = useDashboardRouter();
1215
return (
1316
<div>
14-
<AccountSettingsPageUI
15-
account={props.account}
16-
updateAccountImage={async (file) => {
17-
if (file) {
18-
// upload to IPFS
19-
const ipfsUri = await upload({
20-
client: props.client,
21-
files: [file],
22-
});
23-
24-
// TODO - Implement updating the account image with uri
25-
console.log(ipfsUri);
26-
} else {
27-
// TODO - Implement deleting the account image
28-
}
29-
30-
throw new Error("Not implemented");
31-
}}
32-
/>
17+
<div className="border-border border-b py-10">
18+
<div className="container max-w-[950px]">
19+
<h1 className="font-semibold text-3xl tracking-tight">
20+
Account Settings
21+
</h1>
22+
</div>
23+
</div>
24+
<div className="container max-w-[950px] grow pt-8 pb-20">
25+
<AccountSettingsPageUI
26+
// TODO - remove hide props these when these fields are functional
27+
hideAvatar
28+
hideDeleteAccount
29+
account={props.account}
30+
updateEmailWithOTP={async (otp) => {
31+
const res = await confirmEmailWithOTP(otp);
32+
if (res?.errorMessage) {
33+
throw new Error(res.errorMessage);
34+
}
35+
router.refresh();
36+
}}
37+
updateName={async (name) => {
38+
const res = await updateAccount({ name });
39+
if (res?.errorMessage) {
40+
throw new Error(res.errorMessage);
41+
}
42+
router.refresh();
43+
}}
44+
// yes, this is weird -
45+
// to send OTP to email, we use updateAccount
46+
sendEmail={async (email) => {
47+
const res = await updateAccount({ email });
48+
if (res?.errorMessage) {
49+
throw new Error(res.errorMessage);
50+
}
51+
}}
52+
/>
53+
</div>
3354
</div>
3455
);
3556
}

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

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox";
12
import type { Meta, StoryObj } from "@storybook/react";
3+
import { useState } from "react";
24
import { Toaster } from "sonner";
3-
import { ThirdwebProvider } from "thirdweb/react";
45
import { mobileViewport } from "../../../stories/utils";
56
import { AccountSettingsPageUI } from "./AccountSettingsPageUI";
67

@@ -33,20 +34,63 @@ export const Mobile: Story = {
3334
};
3435

3536
function Variants() {
37+
const [isVerifiedEmail, setIsVerifiedEmail] = useState(true);
38+
const [sendEmailFails, setSendEmailFails] = useState(false);
39+
const [emailConfirmationFails, setEmailConfirmationFails] = useState(false);
40+
3641
return (
37-
<ThirdwebProvider>
38-
<div className="container mx-auto flex w-full max-w-[1132px] flex-col gap-10 py-10">
39-
<AccountSettingsPageUI
40-
account={{
41-
name: "John Doe",
42-
43-
}}
44-
updateAccountImage={async () => {
45-
await new Promise((resolve) => setTimeout(resolve, 1000));
46-
}}
47-
/>
42+
<div className="container flex max-w-[1132px] flex-col gap-10 py-10">
43+
<div className="flex flex-col gap-2">
44+
<CheckboxWithLabel>
45+
<Checkbox
46+
checked={isVerifiedEmail}
47+
onCheckedChange={(v) => setIsVerifiedEmail(!!v)}
48+
/>
49+
is Verified Email
50+
</CheckboxWithLabel>
51+
52+
<CheckboxWithLabel>
53+
<Checkbox
54+
checked={sendEmailFails}
55+
onCheckedChange={(v) => setSendEmailFails(!!v)}
56+
/>
57+
Sending Email Fails
58+
</CheckboxWithLabel>
59+
60+
<CheckboxWithLabel>
61+
<Checkbox
62+
checked={emailConfirmationFails}
63+
onCheckedChange={(v) => setEmailConfirmationFails(!!v)}
64+
/>
65+
Email Confirmation Fails
66+
</CheckboxWithLabel>
4867
</div>
68+
69+
<AccountSettingsPageUI
70+
account={{
71+
name: "John Doe",
72+
73+
emailConfirmedAt: isVerifiedEmail
74+
? new Date().toISOString()
75+
: undefined,
76+
}}
77+
updateEmailWithOTP={async () => {
78+
await new Promise((resolve) => setTimeout(resolve, 1000));
79+
if (emailConfirmationFails) {
80+
throw new Error("Invalid OTP");
81+
}
82+
}}
83+
updateName={async () => {
84+
await new Promise((resolve) => setTimeout(resolve, 1000));
85+
}}
86+
sendEmail={async () => {
87+
await new Promise((resolve) => setTimeout(resolve, 1000));
88+
if (sendEmailFails) {
89+
throw new Error("Email already exists");
90+
}
91+
}}
92+
/>
4993
<Toaster richColors />
50-
</ThirdwebProvider>
94+
</div>
5195
);
5296
}

0 commit comments

Comments
 (0)