Skip to content

Commit 3842b0c

Browse files
committed
[TOOL-3446] Dashboard: Revamp account onboarding, Add Team onboarding
1 parent 3f6d525 commit 3842b0c

File tree

46 files changed

+2277
-1067
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+2277
-1067
lines changed

.changeset/chilly-trams-wash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@thirdweb-dev/service-utils": patch
3+
---
4+
5+
Update `TeamResponse` type

apps/dashboard/.storybook/preview.tsx

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Inter as interFont } from "next/font/google";
77
// biome-ignore lint/style/useImportType: <explanation>
88
import React from "react";
99
import { useEffect } from "react";
10+
import { Toaster } from "sonner";
1011
import { Button } from "../src/@/components/ui/button";
1112

1213
const queryClient = new QueryClient();
@@ -16,8 +17,30 @@ const fontSans = interFont({
1617
variable: "--font-sans",
1718
});
1819

20+
const customViewports = {
21+
xs: {
22+
// Regular sized phones (iphone 15 / 15 pro)
23+
name: "iPhone",
24+
styles: {
25+
width: "390px",
26+
height: "844px",
27+
},
28+
},
29+
sm: {
30+
// Larger phones (iphone 15 plus / 15 pro max)
31+
name: "iPhone Plus",
32+
styles: {
33+
width: "430px",
34+
height: "932px",
35+
},
36+
},
37+
};
38+
1939
const preview: Preview = {
2040
parameters: {
41+
viewport: {
42+
viewports: customViewports,
43+
},
2144
controls: {
2245
matchers: {
2346
color: /(background|color)$/i,
@@ -57,13 +80,13 @@ function StoryLayout(props: {
5780

5881
return (
5982
<QueryClientProvider client={queryClient}>
60-
<div className="flex min-h-screen min-w-0 flex-col bg-background text-foreground">
83+
<div className="flex min-h-dvh min-w-0 flex-col bg-background text-foreground">
6184
<div className="flex justify-end gap-2 border-b p-4">
6285
<Button
6386
onClick={() => setTheme("dark")}
6487
size="sm"
6588
variant={theme === "dark" ? "default" : "outline"}
66-
className="h-auto w-auto rounded-full p-2"
89+
className="h-auto w-auto shrink-0 rounded-full p-2"
6790
>
6891
<MoonIcon className="size-4" />
6992
</Button>
@@ -72,14 +95,20 @@ function StoryLayout(props: {
7295
onClick={() => setTheme("light")}
7396
size="sm"
7497
variant={theme === "light" ? "default" : "outline"}
75-
className="h-auto w-auto rounded-full p-2"
98+
className="h-auto w-auto shrink-0 rounded-full p-2"
7699
>
77100
<SunIcon className="size-4" />
78101
</Button>
79102
</div>
80103

81104
<div className="flex min-w-0 grow flex-col">{props.children}</div>
105+
<ToasterSetup />
82106
</div>
83107
</QueryClientProvider>
84108
);
85109
}
110+
111+
function ToasterSetup() {
112+
const { theme } = useTheme();
113+
return <Toaster richColors theme={theme === "light" ? "light" : "dark"} />;
114+
}

apps/dashboard/src/@/components/blocks/pricing-card.tsx

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
"use client";
12
import type { Team } from "@/api/team";
23
import { Badge } from "@/components/ui/badge";
34
import { Button } from "@/components/ui/button";
@@ -6,10 +7,12 @@ import { TrackedLinkTW } from "@/components/ui/tracked-link";
67
import { cn } from "@/lib/utils";
78
import { CheckIcon, CircleAlertIcon, CircleDollarSignIcon } from "lucide-react";
89
import type React from "react";
10+
import { useState } from "react";
911
import { TEAM_PLANS } from "utils/pricing";
1012
import { remainingDays } from "../../../utils/date-utils";
1113
import type { RedirectBillingCheckoutAction } from "../../actions/billing";
1214
import { CheckoutButton } from "../billing";
15+
import { Spinner } from "../ui/Spinner/Spinner";
1316

1417
type ButtonProps = React.ComponentProps<typeof Button>;
1518

@@ -31,7 +34,6 @@ type PricingCardProps = {
3134
ctaHint?: string;
3235
highlighted?: boolean;
3336
current?: boolean;
34-
canTrialGrowth?: boolean;
3537
activeTrialEndsAt?: string;
3638
redirectPath: string;
3739
redirectToCheckout: RedirectBillingCheckoutAction;
@@ -43,11 +45,11 @@ export const PricingCard: React.FC<PricingCardProps> = ({
4345
cta,
4446
highlighted = false,
4547
current = false,
46-
canTrialGrowth = false,
4748
activeTrialEndsAt,
4849
redirectPath,
4950
redirectToCheckout,
5051
}) => {
52+
const [isRouteLoading, setIsRouteLoading] = useState(false);
5153
const plan = TEAM_PLANS[billingPlan];
5254
const isCustomPrice = typeof plan.price === "string";
5355

@@ -88,18 +90,7 @@ export const PricingCard: React.FC<PricingCardProps> = ({
8890
<div className="flex flex-col gap-0.5">
8991
<div className="flex items-center gap-2">
9092
<span className="font-semibold text-3xl text-foreground tracking-tight">
91-
{isCustomPrice ? (
92-
plan.price
93-
) : canTrialGrowth ? (
94-
<>
95-
<span className="text-muted-foreground line-through">
96-
${plan.price}
97-
</span>{" "}
98-
$0
99-
</>
100-
) : (
101-
`$${plan.price}`
102-
)}
93+
${plan.price}
10394
</span>
10495

10596
{!isCustomPrice && (
@@ -154,7 +145,10 @@ export const PricingCard: React.FC<PricingCardProps> = ({
154145
sku={billingPlan === "starter" ? "plan:starter" : "plan:growth"}
155146
redirectPath={redirectPath}
156147
redirectToCheckout={redirectToCheckout}
148+
className="gap-2"
149+
onClick={() => setIsRouteLoading(true)}
157150
>
151+
{isRouteLoading && <Spinner className="size-4" />}
158152
{cta.title}
159153
</CheckoutButton>
160154
) : (

apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts

Lines changed: 72 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,6 @@ export type Account = {
4444
// TODO - add image URL
4545
};
4646

47-
interface UpdateAccountInput {
48-
name?: string;
49-
email?: string;
50-
linkWallet?: boolean;
51-
subscribeToUpdates?: boolean;
52-
onboardSkipped?: boolean;
53-
}
54-
5547
interface UpdateAccountNotificationsInput {
5648
billing: "email" | "none";
5749
updates: "email" | "none";
@@ -140,45 +132,40 @@ export function useAccountCredits() {
140132
});
141133
}
142134

143-
export function useUpdateAccount() {
144-
const queryClient = useQueryClient();
145-
const address = useActiveAccount()?.address;
146-
147-
return useMutation({
148-
mutationFn: async (input: UpdateAccountInput) => {
149-
type Result = {
150-
data: object;
151-
error?: { message: string };
152-
};
135+
export type UpdateAccountParams = {
136+
name?: string;
137+
email?: string;
138+
linkWallet?: boolean;
139+
subscribeToUpdates?: boolean;
140+
onboardSkipped?: boolean;
141+
};
153142

154-
const res = await apiServerProxy<Result>({
155-
pathname: "/v1/account",
156-
method: "PUT",
157-
headers: {
158-
"Content-Type": "application/json",
159-
},
160-
body: JSON.stringify(input),
161-
});
143+
export async function updateAccountClient(input: UpdateAccountParams) {
144+
type Result = {
145+
data: object;
146+
error?: { message: string };
147+
};
162148

163-
if (!res.ok) {
164-
throw new Error(res.error);
165-
}
149+
const res = await apiServerProxy<Result>({
150+
pathname: "/v1/account",
151+
method: "PUT",
152+
headers: {
153+
"Content-Type": "application/json",
154+
},
155+
body: JSON.stringify(input),
156+
});
166157

167-
const json = res.data;
158+
if (!res.ok) {
159+
throw new Error(res.error);
160+
}
168161

169-
if (json.error) {
170-
throw new Error(json.error.message);
171-
}
162+
const json = res.data;
172163

173-
return json.data;
174-
},
164+
if (json.error) {
165+
throw new Error(json.error.message);
166+
}
175167

176-
onSuccess: () => {
177-
return queryClient.invalidateQueries({
178-
queryKey: accountKeys.me(address || ""),
179-
});
180-
},
181-
});
168+
return json.data;
182169
}
183170

184171
export function useUpdateNotifications() {
@@ -221,77 +208,61 @@ export function useUpdateNotifications() {
221208
});
222209
}
223210

224-
export function useConfirmEmail() {
225-
return useMutation({
226-
mutationFn: async (input: ConfirmEmailInput) => {
227-
type Result = {
228-
error?: { message: string };
229-
data: { team: Team; account: Account };
230-
};
231-
232-
const res = await apiServerProxy<Result>({
233-
pathname: "/v1/account/confirmEmail",
234-
method: "PUT",
235-
headers: {
236-
"Content-Type": "application/json",
237-
},
238-
body: JSON.stringify(input),
239-
});
240-
241-
if (!res.ok) {
242-
throw new Error(res.error);
243-
}
244-
245-
const json = res.data;
246-
247-
if (json.error) {
248-
throw new Error(json.error.message);
249-
}
211+
export const verifyEmailClient = async (input: ConfirmEmailInput) => {
212+
type Result = {
213+
error?: { message: string };
214+
data: { team: Team; account: Account };
215+
};
250216

251-
return json.data;
217+
const res = await apiServerProxy<Result>({
218+
pathname: "/v1/account/confirmEmail",
219+
method: "PUT",
220+
headers: {
221+
"Content-Type": "application/json",
252222
},
223+
body: JSON.stringify(input),
253224
});
254-
}
255225

256-
export function useResendEmailConfirmation() {
257-
const address = useActiveAccount()?.address;
258-
const queryClient = useQueryClient();
226+
if (!res.ok) {
227+
throw new Error(res.error);
228+
}
259229

260-
return useMutation({
261-
mutationFn: async () => {
262-
type Result = {
263-
error?: { message: string };
264-
data: object;
265-
};
230+
const json = res.data;
266231

267-
const res = await apiServerProxy<Result>({
268-
pathname: "/v1/account/resendEmailConfirmation",
269-
method: "POST",
270-
headers: {
271-
"Content-Type": "application/json",
272-
},
273-
body: JSON.stringify({}),
274-
});
275-
276-
if (!res.ok) {
277-
throw new Error(res.error);
278-
}
232+
if (json.error) {
233+
throw new Error(json.error.message);
234+
}
279235

280-
const json = res.data;
236+
return json.data;
237+
};
281238

282-
if (json.error) {
283-
throw new Error(json.error.message);
284-
}
239+
export const resendEmailClient = async () => {
240+
type Result = {
241+
error?: { message: string };
242+
data: object;
243+
};
285244

286-
return json.data;
287-
},
288-
onSuccess: () => {
289-
return queryClient.invalidateQueries({
290-
queryKey: accountKeys.me(address || ""),
291-
});
245+
const res = await apiServerProxy<Result>({
246+
pathname: "/v1/account/resendEmailConfirmation",
247+
method: "POST",
248+
headers: {
249+
"Content-Type": "application/json",
292250
},
251+
body: JSON.stringify({}),
293252
});
294-
}
253+
254+
if (!res.ok) {
255+
throw new Error(res.error);
256+
}
257+
258+
const json = res.data;
259+
260+
if (json.error) {
261+
throw new Error(json.error.message);
262+
}
263+
264+
return json.data;
265+
};
295266

296267
export async function createProjectClient(
297268
teamId: string,

apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/server/routelist-card.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export async function RouteListCard({
7474
<CardHeader className="flex flex-row items-center justify-between p-4">
7575
<div className="flex flex-row items-center gap-2">
7676
{resolvedOriginTokenIconUri ? (
77+
// eslint-disable-next-line @next/next/no-img-element
7778
<img
7879
src={resolvedOriginTokenIconUri}
7980
alt={originTokenAddress}
@@ -83,6 +84,7 @@ export async function RouteListCard({
8384
<div className="size-8 rounded-full bg-white/10" />
8485
)}
8586
{resolvedDestinationTokenIconUri ? (
87+
// eslint-disable-next-line @next/next/no-img-element
8688
<img
8789
src={resolvedDestinationTokenIconUri}
8890
alt={destinationTokenAddress}

apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/server/routelist-row.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export async function RouteListRow({
7676
<div className="flex items-center gap-1">
7777
{resolvedOriginTokenIconUri ? (
7878
// For now we're using a normal img tag because the domain for these images is unknown
79+
// eslint-disable-next-line @next/next/no-img-element
7980
<img
8081
src={resolvedOriginTokenIconUri}
8182
alt={originTokenAddress}
@@ -110,6 +111,7 @@ export async function RouteListRow({
110111
<div className="flex flex-row items-center gap-4">
111112
<div className="flex items-center gap-1">
112113
{resolvedDestinationTokenIconUri ? (
114+
// eslint-disable-next-line @next/next/no-img-element
113115
<img
114116
src={resolvedDestinationTokenIconUri}
115117
alt={destinationTokenAddress}

0 commit comments

Comments
 (0)