Skip to content

Commit

Permalink
[Dashboard] User Onboarding Revamp (#5471)
Browse files Browse the repository at this point in the history
## 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 -->
  • Loading branch information
MananTank committed Nov 21, 2024
1 parent ecaa304 commit 776f5aa
Show file tree
Hide file tree
Showing 38 changed files with 718 additions and 848 deletions.
7 changes: 7 additions & 0 deletions apps/dashboard/src/@/actions/getAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"use server";

import { getRawAccount } from "../../app/account/settings/getAccount";

export async function getRawAccountAction() {
return getRawAccount();
}
13 changes: 3 additions & 10 deletions apps/dashboard/src/@/api/team.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import "server-only";
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import { API_SERVER_URL } from "@/constants/env";
import { cookies } from "next/headers";
import { getAuthToken } from "../../app/api/lib/getAuthToken";

export type Team = {
Expand Down Expand Up @@ -38,14 +36,9 @@ export async function getTeamBySlug(slug: string) {
}

export async function getTeams() {
const cookiesManager = await cookies();
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
const token = activeAccount
? cookiesManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value
: null;

const token = await getAuthToken();
if (!token) {
return [];
return null;
}

const teamsRes = await fetch(`${API_SERVER_URL}/v1/teams`, {
Expand All @@ -56,7 +49,7 @@ export async function getTeams() {
if (teamsRes.ok) {
return (await teamsRes.json())?.result as Team[];
}
return [];
return null;
}

type TeamNebulWaitList = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { useStore } from "@/lib/reactive";
import { cn } from "@/lib/utils";
import { getSDKTheme } from "app/components/sdk-component-theme";
import { CustomChainRenderer } from "components/selects/CustomChainRenderer";
import { mapV4ChainToV5Chain } from "contexts/map-chains";
Expand Down Expand Up @@ -35,6 +36,7 @@ export const CustomConnectWallet = (props: {
connectButtonClassName?: string;
signInLinkButtonClassName?: string;
detailsButtonClassName?: string;
loadingButtonClassName?: string;
chain?: Chain;
}) => {
const thirdwebClient = useThirdwebClient();
Expand Down Expand Up @@ -123,7 +125,12 @@ export const CustomConnectWallet = (props: {
if (isPending) {
return (
<>
<div className="flex h-[48px] w-[144px] items-center justify-center rounded-lg border border-border bg-muted">
<div
className={cn(
"flex h-[48px] w-[144px] items-center justify-center rounded-lg border border-border bg-muted",
props.loadingButtonClassName,
)}
>
<Spinner className="size-4" />
</div>
</>
Expand Down
3 changes: 2 additions & 1 deletion apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { UserOpStats } from "@/api/analytics";
import type { Team } from "@/api/team";
import {
type Query,
useMutation,
Expand Down Expand Up @@ -495,7 +496,7 @@ export function useConfirmEmail() {
throw new Error(json.error.message);
}

return json.data;
return json.data as { team: Team; account: Account };
},
onSuccess: async () => {
// invalidate related cache, since could be relinking account
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,17 @@ import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
import { Turnstile } from "@marsidev/react-turnstile";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { CanClaimResponseType } from "app/api/testnet-faucet/can-claim/CanClaimResponseType";
import { Onboarding } from "components/onboarding";
import { mapV4ChainToV5Chain } from "contexts/map-chains";
import { useTrack } from "hooks/analytics/useTrack";
import { useState } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { toUnits } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { useActiveAccount, useWalletBalance } from "thirdweb/react";
import { z } from "zod";
import { isOnboardingComplete } from "../../../../../../login/isOnboardingRequired";

function formatTime(seconds: number) {
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
Expand Down Expand Up @@ -52,6 +53,7 @@ export function FaucetButton({
chain: ChainMetadata;
amount: number;
}) {
const pathname = usePathname();
const client = useThirdwebClient();
const address = useActiveAccount()?.address;
const chainId = chain.chainId;
Expand Down Expand Up @@ -118,7 +120,6 @@ export function FaucetButton({

const accountQuery = useAccount();
const userQuery = useLoggedInUser();
const [showOnboarding, setShowOnBoarding] = useState(false);

const canClaimFaucetQuery = useQuery({
queryKey: ["testnet-faucet-can-claim", chainId],
Expand All @@ -145,7 +146,8 @@ export function FaucetButton({
return (
<CustomConnectWallet
loginRequired={true}
connectButtonClassName="!w-full !rounded !bg-primary !text-primary-foreground !px-4 !py-2 !text-sm"
loadingButtonClassName="!w-full"
signInLinkButtonClassName="!w-full !h-auto !rounded !bg-primary !text-primary-foreground !px-4 !py-2 !text-sm hover:!bg-primary/80"
/>
);
}
Expand Down Expand Up @@ -201,23 +203,17 @@ export function FaucetButton({
);
}

// Email verification is required to claim from the faucet
if (
!accountQuery.data.emailConfirmedAt &&
!accountQuery.data.unconfirmedEmail
) {
if (!isOnboardingComplete(accountQuery.data)) {
return (
<>
<Button
variant="outline"
className="!opacity-100 w-full"
onClick={() => setShowOnBoarding(true)}
<Button asChild className="w-full">
<Link
href={
pathname ? `/login?next=${encodeURIComponent(pathname)}` : "/login"
}
>
Verify your Email
</Button>
{/* We will show the modal only if the user click on it, because this is a public page */}
{showOnboarding && <Onboarding onOpenChange={setShowOnBoarding} />}
</>
</Link>
</Button>
);
}

Expand Down
18 changes: 12 additions & 6 deletions apps/dashboard/src/app/account/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import { getProjects } from "@/api/projects";
import { getTeams } from "@/api/team";
import { type Team, getTeams } from "@/api/team";
import { AppFooter } from "@/components/blocks/app-footer";
import type React from "react";
import { TabPathLinks } from "../../@/components/ui/tabs";
import { TWAutoConnect } from "../components/autoconnect";
import { loginRedirect } from "../login/loginRedirect";
import { AccountHeader } from "./components/AccountHeader";

export default async function AccountLayout(props: {
children: React.ReactNode;
}) {
const teams = await getTeams();
if (!teams) {
loginRedirect("/account");
}

return (
<div className="flex min-h-screen flex-col bg-background">
<div className="flex grow flex-col">
<HeaderAndNav />
<HeaderAndNav teams={teams} />
{props.children}
</div>
<TWAutoConnect />
Expand All @@ -21,11 +27,11 @@ export default async function AccountLayout(props: {
);
}

async function HeaderAndNav() {
const teams = await getTeams();

async function HeaderAndNav(props: {
teams: Team[];
}) {
const teamsAndProjects = await Promise.all(
teams.map(async (team) => ({
props.teams.map(async (team) => ({
team,
projects: await getProjects(team.slug),
})),
Expand Down
14 changes: 6 additions & 8 deletions apps/dashboard/src/app/account/page.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import { getTeams } from "@/api/team";
import { getMembers } from "@/api/team-members";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { redirect } from "next/navigation";
import { loginRedirect } from "../login/loginRedirect";
import { AccountTeamsUI } from "./overview/AccountTeamsUI";
import { getAccount } from "./settings/getAccount";
import { getValidAccount } from "./settings/getAccount";

export default async function Page() {
const account = await getAccount();

if (!account) {
redirect("/login?next=/account");
}

const account = await getValidAccount("/account");
const teams = await getTeams();
if (!teams) {
loginRedirect("/account");
}

const teamsWithRole = (
await Promise.all(
Expand Down
27 changes: 26 additions & 1 deletion apps/dashboard/src/app/account/settings/getAccount.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import { API_SERVER_URL } from "@/constants/env";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import { getAuthToken } from "../../api/lib/getAuthToken";
import { isOnboardingComplete } from "../../login/isOnboardingRequired";
import { loginRedirect } from "../../login/loginRedirect";

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

if (!authToken) {
return undefined;
}

const res = await fetch(`${API_SERVER_URL}/v1/account/me`, {
method: "GET",
headers: {
Expand All @@ -21,3 +31,18 @@ export async function getAccount() {

return json.data as Account;
}

/**
* If there's no account or account onboarding not complete, redirect to login page
* @param pagePath - the path of the current page to redirect back to after login/onboarding
*/
export async function getValidAccount(pagePath: string) {
const account = await getRawAccount();

// enforce login & onboarding
if (!account || !isOnboardingComplete(account)) {
loginRedirect(pagePath);
}

return account;
}
11 changes: 6 additions & 5 deletions apps/dashboard/src/app/account/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { redirect } from "next/navigation";
import { getAuthToken } from "../../api/lib/getAuthToken";
import { loginRedirect } from "../../login/loginRedirect";
import { AccountSettingsPage } from "./AccountSettingsPage";
import { getAccount } from "./getAccount";
import { getValidAccount } from "./getAccount";

export default async function Page() {
const account = await getAccount();
const pagePath = "/account";
const account = await getValidAccount(pagePath);
const token = await getAuthToken();

if (!account || !token) {
redirect(`/login?next=${encodeURIComponent("/account")}`);
if (!token) {
loginRedirect(pagePath);
}

return (
Expand Down
Loading

0 comments on commit 776f5aa

Please sign in to comment.