Skip to content

Commit c95164e

Browse files
committed
User Onboarding UI Revamp
1 parent a67e980 commit c95164e

File tree

38 files changed

+718
-850
lines changed

38 files changed

+718
-850
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"use server";
2+
3+
import { getRawAccount } from "../../app/account/settings/getAccount";
4+
5+
export async function getRawAccountAction() {
6+
return getRawAccount();
7+
}

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

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import "server-only";
2-
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
32
import { API_SERVER_URL } from "@/constants/env";
4-
import { cookies } from "next/headers";
53
import { getAuthToken } from "../../app/api/lib/getAuthToken";
64

75
export type Team = {
@@ -38,14 +36,9 @@ export async function getTeamBySlug(slug: string) {
3836
}
3937

4038
export async function getTeams() {
41-
const cookiesManager = await cookies();
42-
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
43-
const token = activeAccount
44-
? cookiesManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value
45-
: null;
46-
39+
const token = await getAuthToken();
4740
if (!token) {
48-
return [];
41+
return null;
4942
}
5043

5144
const teamsRes = await fetch(`${API_SERVER_URL}/v1/teams`, {
@@ -56,7 +49,7 @@ export async function getTeams() {
5649
if (teamsRes.ok) {
5750
return (await teamsRes.json())?.result as Team[];
5851
}
59-
return [];
52+
return null;
6053
}
6154

6255
type TeamNebulWaitList = {

apps/dashboard/src/@3rdweb-sdk/react/components/connect-wallet/index.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Spinner } from "@/components/ui/Spinner/Spinner";
44
import { Button } from "@/components/ui/button";
55
import { useThirdwebClient } from "@/constants/thirdweb.client";
66
import { useStore } from "@/lib/reactive";
7+
import { cn } from "@/lib/utils";
78
import { getSDKTheme } from "app/components/sdk-component-theme";
89
import { CustomChainRenderer } from "components/selects/CustomChainRenderer";
910
import { mapV4ChainToV5Chain } from "contexts/map-chains";
@@ -35,6 +36,7 @@ export const CustomConnectWallet = (props: {
3536
connectButtonClassName?: string;
3637
signInLinkButtonClassName?: string;
3738
detailsButtonClassName?: string;
39+
loadingButtonClassName?: string;
3840
chain?: Chain;
3941
}) => {
4042
const thirdwebClient = useThirdwebClient();
@@ -123,7 +125,12 @@ export const CustomConnectWallet = (props: {
123125
if (isPending) {
124126
return (
125127
<>
126-
<div className="flex h-[48px] w-[144px] items-center justify-center rounded-lg border border-border bg-muted">
128+
<div
129+
className={cn(
130+
"flex h-[48px] w-[144px] items-center justify-center rounded-lg border border-border bg-muted",
131+
props.loadingButtonClassName,
132+
)}
133+
>
127134
<Spinner className="size-4" />
128135
</div>
129136
</>

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { UserOpStats } from "@/api/analytics";
2+
import type { Team } from "@/api/team";
23
import {
34
type Query,
45
useMutation,
@@ -528,7 +529,7 @@ export function useConfirmEmail() {
528529
throw new Error(json.error.message);
529530
}
530531

531-
return json.data;
532+
return json.data as { team: Team; account: Account };
532533
},
533534
onSuccess: async () => {
534535
// invalidate related cache, since could be relinking account

apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/FaucetButton.tsx

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,17 @@ import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
1313
import { Turnstile } from "@marsidev/react-turnstile";
1414
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
1515
import type { CanClaimResponseType } from "app/api/testnet-faucet/can-claim/CanClaimResponseType";
16-
import { Onboarding } from "components/onboarding";
1716
import { mapV4ChainToV5Chain } from "contexts/map-chains";
1817
import { useTrack } from "hooks/analytics/useTrack";
19-
import { useState } from "react";
18+
import Link from "next/link";
19+
import { usePathname } from "next/navigation";
2020
import { useForm } from "react-hook-form";
2121
import { toast } from "sonner";
2222
import { toUnits } from "thirdweb";
2323
import type { ChainMetadata } from "thirdweb/chains";
2424
import { useActiveAccount, useWalletBalance } from "thirdweb/react";
2525
import { z } from "zod";
26+
import { isOnboardingComplete } from "../../../../../../login/isOnboardingRequired";
2627

2728
function formatTime(seconds: number) {
2829
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
@@ -52,6 +53,7 @@ export function FaucetButton({
5253
chain: ChainMetadata;
5354
amount: number;
5455
}) {
56+
const pathname = usePathname();
5557
const client = useThirdwebClient();
5658
const address = useActiveAccount()?.address;
5759
const chainId = chain.chainId;
@@ -118,7 +120,6 @@ export function FaucetButton({
118120

119121
const accountQuery = useAccount();
120122
const userQuery = useLoggedInUser();
121-
const [showOnboarding, setShowOnBoarding] = useState(false);
122123

123124
const canClaimFaucetQuery = useQuery({
124125
queryKey: ["testnet-faucet-can-claim", chainId],
@@ -145,7 +146,8 @@ export function FaucetButton({
145146
return (
146147
<CustomConnectWallet
147148
loginRequired={true}
148-
connectButtonClassName="!w-full !rounded !bg-primary !text-primary-foreground !px-4 !py-2 !text-sm"
149+
loadingButtonClassName="!w-full"
150+
signInLinkButtonClassName="!w-full !h-auto !rounded !bg-primary !text-primary-foreground !px-4 !py-2 !text-sm hover:!bg-primary/80"
149151
/>
150152
);
151153
}
@@ -201,23 +203,17 @@ export function FaucetButton({
201203
);
202204
}
203205

204-
// Email verification is required to claim from the faucet
205-
if (
206-
!accountQuery.data.emailConfirmedAt &&
207-
!accountQuery.data.unconfirmedEmail
208-
) {
206+
if (!isOnboardingComplete(accountQuery.data)) {
209207
return (
210-
<>
211-
<Button
212-
variant="outline"
213-
className="!opacity-100 w-full"
214-
onClick={() => setShowOnBoarding(true)}
208+
<Button asChild className="w-full">
209+
<Link
210+
href={
211+
pathname ? `/login?next=${encodeURIComponent(pathname)}` : "/login"
212+
}
215213
>
216214
Verify your Email
217-
</Button>
218-
{/* We will show the modal only if the user click on it, because this is a public page */}
219-
{showOnboarding && <Onboarding onOpenChange={setShowOnBoarding} />}
220-
</>
215+
</Link>
216+
</Button>
221217
);
222218
}
223219

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
import { getProjects } from "@/api/projects";
2-
import { getTeams } from "@/api/team";
2+
import { type Team, getTeams } from "@/api/team";
33
import { AppFooter } from "@/components/blocks/app-footer";
44
import type React from "react";
55
import { TabPathLinks } from "../../@/components/ui/tabs";
66
import { TWAutoConnect } from "../components/autoconnect";
7+
import { loginRedirect } from "../login/loginRedirect";
78
import { AccountHeader } from "./components/AccountHeader";
89

910
export default async function AccountLayout(props: {
1011
children: React.ReactNode;
1112
}) {
13+
const teams = await getTeams();
14+
if (!teams) {
15+
loginRedirect("/account");
16+
}
17+
1218
return (
1319
<div className="flex min-h-screen flex-col bg-background">
1420
<div className="flex grow flex-col">
15-
<HeaderAndNav />
21+
<HeaderAndNav teams={teams} />
1622
{props.children}
1723
</div>
1824
<TWAutoConnect />
@@ -21,11 +27,11 @@ export default async function AccountLayout(props: {
2127
);
2228
}
2329

24-
async function HeaderAndNav() {
25-
const teams = await getTeams();
26-
30+
async function HeaderAndNav(props: {
31+
teams: Team[];
32+
}) {
2733
const teamsAndProjects = await Promise.all(
28-
teams.map(async (team) => ({
34+
props.teams.map(async (team) => ({
2935
team,
3036
projects: await getProjects(team.slug),
3137
})),

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

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
import { getTeams } from "@/api/team";
22
import { getMembers } from "@/api/team-members";
33
import { getThirdwebClient } from "@/constants/thirdweb.server";
4-
import { redirect } from "next/navigation";
4+
import { loginRedirect } from "../login/loginRedirect";
55
import { AccountTeamsUI } from "./overview/AccountTeamsUI";
6-
import { getAccount } from "./settings/getAccount";
6+
import { getValidAccount } from "./settings/getAccount";
77

88
export default async function Page() {
9-
const account = await getAccount();
10-
11-
if (!account) {
12-
redirect("/login?next=/account");
13-
}
14-
9+
const account = await getValidAccount("/account");
1510
const teams = await getTeams();
11+
if (!teams) {
12+
loginRedirect("/account");
13+
}
1614

1715
const teamsWithRole = (
1816
await Promise.all(

apps/dashboard/src/app/account/settings/getAccount.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
import { API_SERVER_URL } from "@/constants/env";
22
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
33
import { getAuthToken } from "../../api/lib/getAuthToken";
4+
import { isOnboardingComplete } from "../../login/isOnboardingRequired";
5+
import { loginRedirect } from "../../login/loginRedirect";
46

5-
export async function getAccount() {
7+
/**
8+
* Just get the account object without enforcing onboarding.
9+
* In most cases - you should just be using `getValidAccount`
10+
*/
11+
export async function getRawAccount() {
612
const authToken = await getAuthToken();
713

14+
if (!authToken) {
15+
return undefined;
16+
}
17+
818
const res = await fetch(`${API_SERVER_URL}/v1/account/me`, {
919
method: "GET",
1020
headers: {
@@ -21,3 +31,18 @@ export async function getAccount() {
2131

2232
return json.data as Account;
2333
}
34+
35+
/**
36+
* If there's no account or account onboarding not complete, redirect to login page
37+
* @param pagePath - the path of the current page to redirect back to after login/onboarding
38+
*/
39+
export async function getValidAccount(pagePath: string) {
40+
const account = await getRawAccount();
41+
42+
// enforce login & onboarding
43+
if (!account || !isOnboardingComplete(account)) {
44+
loginRedirect(pagePath);
45+
}
46+
47+
return account;
48+
}

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import { getThirdwebClient } from "@/constants/thirdweb.server";
2-
import { redirect } from "next/navigation";
32
import { getAuthToken } from "../../api/lib/getAuthToken";
3+
import { loginRedirect } from "../../login/loginRedirect";
44
import { AccountSettingsPage } from "./AccountSettingsPage";
5-
import { getAccount } from "./getAccount";
5+
import { getValidAccount } from "./getAccount";
66

77
export default async function Page() {
8-
const account = await getAccount();
8+
const pagePath = "/account";
9+
const account = await getValidAccount(pagePath);
910
const token = await getAuthToken();
1011

11-
if (!account || !token) {
12-
redirect(`/login?next=${encodeURIComponent("/account")}`);
12+
if (!token) {
13+
loginRedirect(pagePath);
1314
}
1415

1516
return (

0 commit comments

Comments
 (0)