Skip to content

Commit b4258ae

Browse files
committed
[TOOL-3446] Dashboard: Revamp account onboarding, Add Team onboarding
1 parent 158a08a commit b4258ae

File tree

69 files changed

+2493
-1326
lines changed

Some content is hidden

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

69 files changed

+2493
-1326
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/@/actions/billing.ts

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,19 @@
22

33
import "server-only";
44
import { API_SERVER_URL } from "@/constants/env";
5-
import { redirect } from "next/navigation";
65
import { getAuthToken } from "../../app/api/lib/getAuthToken";
76
import type { ProductSKU } from "../lib/billing";
87

9-
export type RedirectCheckoutOptions = {
8+
export type GetBillingCheckoutUrlOptions = {
109
teamSlug: string;
1110
sku: ProductSKU;
1211
redirectUrl: string;
1312
metadata?: Record<string, string>;
1413
};
1514

16-
export async function redirectToCheckout(
17-
options: RedirectCheckoutOptions,
18-
): Promise<{ status: number }> {
15+
export async function getBillingCheckoutURL(
16+
options: GetBillingCheckoutUrlOptions,
17+
): Promise<{ status: number; url?: string }> {
1918
if (!options.teamSlug) {
2019
return {
2120
status: 400,
@@ -49,27 +48,30 @@ export async function redirectToCheckout(
4948
status: res.status,
5049
};
5150
}
51+
5252
const json = await res.json();
5353
if (!json.result) {
5454
return {
5555
status: 500,
5656
};
5757
}
5858

59-
// redirect to the stripe checkout session
60-
redirect(json.result);
59+
return {
60+
status: 200,
61+
url: json.result as string,
62+
};
6163
}
6264

63-
export type RedirectBillingCheckoutAction = typeof redirectToCheckout;
65+
export type GetBillingCheckoutUrlAction = typeof getBillingCheckoutURL;
6466

65-
export type BillingPortalOptions = {
67+
export type GetBillingPortalUrlOptions = {
6668
teamSlug: string | undefined;
6769
redirectUrl: string;
6870
};
6971

70-
export async function redirectToBillingPortal(
71-
options: BillingPortalOptions,
72-
): Promise<{ status: number }> {
72+
export async function getBillingPortalURL(
73+
options: GetBillingPortalUrlOptions,
74+
): Promise<{ status: number; url?: string }> {
7375
if (!options.teamSlug) {
7476
return {
7577
status: 400,
@@ -110,8 +112,10 @@ export async function redirectToBillingPortal(
110112
};
111113
}
112114

113-
// redirect to the stripe billing portal
114-
redirect(json.result);
115+
return {
116+
status: 200,
117+
url: json.result as string,
118+
};
115119
}
116120

117-
export type BillingBillingPortalAction = typeof redirectToBillingPortal;
121+
export type GetBillingPortalUrlAction = typeof getBillingPortalURL;

apps/dashboard/src/@/components/TextDivider.tsx

Lines changed: 0 additions & 19 deletions
This file was deleted.

apps/dashboard/src/@/components/billing.tsx

Lines changed: 79 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,132 @@
11
"use client";
22

3+
import { useMutation } from "@tanstack/react-query";
4+
import { toast } from "sonner";
35
import type {
4-
BillingBillingPortalAction,
5-
BillingPortalOptions,
6-
RedirectBillingCheckoutAction,
7-
RedirectCheckoutOptions,
6+
GetBillingCheckoutUrlAction,
7+
GetBillingCheckoutUrlOptions,
8+
GetBillingPortalUrlAction,
9+
GetBillingPortalUrlOptions,
810
} from "../actions/billing";
11+
import { cn } from "../lib/utils";
12+
import { Spinner } from "./ui/Spinner/Spinner";
913
import { Button, type ButtonProps } from "./ui/button";
1014

11-
type CheckoutButtonProps = Omit<RedirectCheckoutOptions, "redirectUrl"> &
15+
type CheckoutButtonProps = Omit<GetBillingCheckoutUrlOptions, "redirectUrl"> &
1216
ButtonProps & {
13-
redirectPath: string;
14-
redirectToCheckout: RedirectBillingCheckoutAction;
17+
getBillingCheckoutUrl: GetBillingCheckoutUrlAction;
1518
};
1619

1720
export function CheckoutButton({
1821
onClick,
1922
teamSlug,
2023
sku,
2124
metadata,
22-
redirectPath,
2325
children,
24-
redirectToCheckout,
26+
getBillingCheckoutUrl,
2527
...restProps
2628
}: CheckoutButtonProps) {
29+
const redirectMutation = useMutation({
30+
mutationFn: async () => {
31+
return getBillingCheckoutUrl({
32+
teamSlug,
33+
sku,
34+
metadata,
35+
redirectUrl: getAbsoluteUrl("/stripe-redirect"),
36+
});
37+
},
38+
});
39+
2740
return (
2841
<Button
2942
{...restProps}
43+
className={cn(restProps.className, "gap-2")}
44+
disabled={redirectMutation.isPending || restProps.disabled}
3045
onClick={async (e) => {
3146
onClick?.(e);
32-
await redirectToCheckout({
33-
teamSlug,
34-
sku,
35-
metadata,
36-
redirectUrl: getRedirectUrl(redirectPath),
47+
redirectMutation.mutate(undefined, {
48+
onSuccess: (res) => {
49+
if (!res.url) {
50+
toast.error("Failed to open checkout page");
51+
return;
52+
}
53+
54+
const tab = window.open(res.url, "_blank");
55+
56+
if (!tab) {
57+
toast.error("Failed to open checkout page");
58+
return;
59+
}
60+
},
61+
onError: () => {
62+
toast.error("Failed to open checkout page");
63+
},
3764
});
3865
}}
3966
>
67+
{redirectMutation.isPending && <Spinner className="size-4" />}
4068
{children}
4169
</Button>
4270
);
4371
}
4472

45-
type BillingPortalButtonProps = Omit<BillingPortalOptions, "redirectUrl"> &
73+
type BillingPortalButtonProps = Omit<
74+
GetBillingPortalUrlOptions,
75+
"redirectUrl"
76+
> &
4677
ButtonProps & {
47-
redirectPath: string;
48-
redirectToBillingPortal: BillingBillingPortalAction;
78+
getBillingPortalUrl: GetBillingPortalUrlAction;
4979
};
5080

5181
export function BillingPortalButton({
5282
onClick,
5383
teamSlug,
54-
redirectPath,
5584
children,
56-
redirectToBillingPortal,
57-
...restProps
85+
getBillingPortalUrl,
86+
...buttonProps
5887
}: BillingPortalButtonProps) {
88+
const redirectMutation = useMutation({
89+
mutationFn: async () => {
90+
return getBillingPortalUrl({
91+
teamSlug,
92+
redirectUrl: getAbsoluteUrl("/stripe-redirect"),
93+
});
94+
},
95+
});
96+
5997
return (
6098
<Button
61-
{...restProps}
99+
{...buttonProps}
100+
className={cn(buttonProps.className, "gap-2")}
101+
disabled={redirectMutation.isPending || buttonProps.disabled}
62102
onClick={async (e) => {
63103
onClick?.(e);
64-
await redirectToBillingPortal({
65-
teamSlug,
66-
redirectUrl: getRedirectUrl(redirectPath),
104+
redirectMutation.mutate(undefined, {
105+
onSuccess(res) {
106+
if (!res.url) {
107+
toast.error("Failed to open billing portal");
108+
return;
109+
}
110+
111+
const tab = window.open(res.url, "_blank");
112+
if (!tab) {
113+
toast.error("Failed to open billing portal");
114+
return;
115+
}
116+
},
117+
onError: () => {
118+
toast.error("Failed to open billing portal");
119+
},
67120
});
68121
}}
69122
>
123+
{redirectMutation.isPending && <Spinner className="size-4" />}
70124
{children}
71125
</Button>
72126
);
73127
}
74128

75-
function getRedirectUrl(path: string) {
129+
function getAbsoluteUrl(path: string) {
76130
const url = new URL(window.location.origin);
77131
url.pathname = path;
78132
return url.toString();

0 commit comments

Comments
 (0)