Skip to content

Commit 1cf99be

Browse files
committed
Add account settings page with Name and Email fields
1 parent e95ddcd commit 1cf99be

File tree

7 files changed

+372
-124
lines changed

7 files changed

+372
-124
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
throw new Error("No Auth token");
11+
}
12+
13+
const res = await fetch(`${API_SERVER_URL}/v1/account/confirmEmail`, {
14+
method: "PUT",
15+
headers: {
16+
"Content-Type": "application/json",
17+
Authorization: `Bearer ${token}`,
18+
},
19+
body: JSON.stringify({
20+
confirmationToken: otp,
21+
}),
22+
});
23+
24+
if (!res.ok) {
25+
const json = await res.json();
26+
27+
if (json.error) {
28+
throw new Error(json.error.message);
29+
}
30+
31+
throw new Error("Failed to confirm email");
32+
}
33+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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 updateAccount(values: {
7+
name?: string;
8+
email?: string;
9+
}) {
10+
const token = await getAuthToken();
11+
12+
if (!token) {
13+
throw new Error("No Auth token");
14+
}
15+
16+
const res = await fetch(`${API_SERVER_URL}/v1/account`, {
17+
method: "PUT",
18+
headers: {
19+
"Content-Type": "application/json",
20+
Authorization: `Bearer ${token}`,
21+
},
22+
body: JSON.stringify(values),
23+
});
24+
25+
if (!res.ok) {
26+
const json = await res.json();
27+
28+
if (json.error) {
29+
throw new Error(json.error.message);
30+
}
31+
32+
throw new Error("Failed to update account");
33+
}
34+
}

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: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,47 @@
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+
await confirmEmailWithOTP(otp);
32+
router.refresh();
33+
}}
34+
updateName={async (name) => {
35+
await updateAccount({ name });
36+
router.refresh();
37+
}}
38+
// yes, this is weird -
39+
// to send OTP to email, we use updateAccount
40+
sendEmail={async (email) => {
41+
await updateAccount({ email });
42+
}}
43+
/>
44+
</div>
3345
</div>
3446
);
3547
}

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-
email: "johndoe@gmail.com",
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+
email: "johndoe@gmail.com",
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)