Skip to content

Commit 776f5aa

Browse files
committed
[Dashboard] User Onboarding Revamp (#5471)
## Problem solved Short description of the bug fixed or feature added <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on improving the onboarding process and account management in the application. It introduces better handling of team retrieval, login redirection, and email confirmation, while also refining the UI components related to these features. ### Detailed summary - Added optional chaining for `firstTeam` retrieval. - Introduced `loginRedirect` function for consistent login handling. - Created `getValidAccount` to enforce login and onboarding checks. - Improved `Onboarding` components for better user experience. - Enhanced email confirmation flow with clearer messaging. - Updated various components to use new account and team management functions. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent ecaa304 commit 776f5aa

File tree

38 files changed

+718
-848
lines changed

38 files changed

+718
-848
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,
@@ -495,7 +496,7 @@ export function useConfirmEmail() {
495496
throw new Error(json.error.message);
496497
}
497498

498-
return json.data;
499+
return json.data as { team: Team; account: Account };
499500
},
500501
onSuccess: async () => {
501502
// 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)