Skip to content

[Dashboard] Rename getMemberById to getMemberByAccountId and add onboarding banner #7334

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions apps/dashboard/src/@/api/team-members.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export async function getMembers(teamSlug: string) {
}

const teamsRes = await fetch(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamSlug}/members`,
new URL(`/v1/teams/${teamSlug}/members`, NEXT_PUBLIC_THIRDWEB_API_HOST),
{
headers: {
Authorization: `Bearer ${token}`,
Expand All @@ -48,15 +48,21 @@ export async function getMembers(teamSlug: string) {
return undefined;
}

export async function getMemberById(teamSlug: string, memberId: string) {
export async function getMemberByAccountId(
teamSlug: string,
accountId: string,
) {
const token = await getAuthToken();

if (!token) {
return undefined;
}

const res = await fetch(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamSlug}/members/${memberId}`,
new URL(
`/v1/teams/${teamSlug}/members/${accountId}`,
NEXT_PUBLIC_THIRDWEB_API_HOST,
),
{
headers: {
Authorization: `Bearer ${token}`,
Expand Down
22 changes: 22 additions & 0 deletions apps/dashboard/src/@/api/team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import { API_SERVER_SECRET } from "@/constants/server-envs";
import type { TeamResponse } from "@thirdweb-dev/service-utils";
import { cookies } from "next/headers";
import { getValidAccount } from "../../app/(app)/account/settings/getAccount";
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
import { LAST_USED_TEAM_ID } from "../../constants/cookies";
import { getMemberByAccountId } from "./team-members";

export type Team = TeamResponse & { stripeCustomerId: string | null };

Expand Down Expand Up @@ -103,3 +105,23 @@ export async function getLastVisitedTeam() {

return null;
}

export async function hasToCompleteTeamOnboarding(
team: Team,
pagePath: string,
) {
// if the team is already onboarded, we don't need to check anything else here
if (team.isOnboarded) {
return false;
}
const account = await getValidAccount(pagePath);
const teamMember = await getMemberByAccountId(team.slug, account.id);

// if the team member is not an owner (or we cannot find them), they cannot complete onboarding anyways
if (teamMember?.role !== "OWNER") {
return false;
}

// if we get here the team is not onboarded and the team member is an owner, so we need to show the onboarding page
return true;
}
4 changes: 2 additions & 2 deletions apps/dashboard/src/app/(app)/account/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getTeams } from "@/api/team";
import { getMemberById } from "@/api/team-members";
import { getMemberByAccountId } from "@/api/team-members";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { notFound } from "next/navigation";
import { getAuthToken } from "../api/lib/getAuthToken";
Expand All @@ -25,7 +25,7 @@ export default async function Page() {

const teamsWithRole = await Promise.all(
teams.map(async (team) => {
const member = await getMemberById(team.slug, account.id);
const member = await getMemberByAccountId(team.slug, account.id);

if (!member) {
notFound();
Expand Down
50 changes: 27 additions & 23 deletions apps/dashboard/src/app/(app)/components/TeamPlanBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Team } from "@/api/team";
import { Badge, type BadgeProps } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import { useTrack } from "hooks/analytics/useTrack";
import Link from "next/link";
import { useDashboardRouter } from "../../../@/lib/DashboardRouter";

const teamPlanToBadgeVariant: Record<
Team["billingPlan"],
Expand Down Expand Up @@ -38,33 +38,37 @@ export function TeamPlanBadge(props: {
className?: string;
postfix?: string;
}) {
const badge = (
const router = useDashboardRouter();
const track = useTrack();

function handleNavigateToBilling(e: React.MouseEvent | React.KeyboardEvent) {
if (props.plan !== "free") {
return;
}
e.stopPropagation();
e.preventDefault();
track({
category: "billing",
action: "show_plans",
label: "team_badge",
});
router.push(`/team/${props.teamSlug}/~/settings/billing?showPlans=true`);
}

return (
<Badge
variant={teamPlanToBadgeVariant[props.plan]}
className={cn("px-1.5 capitalize", props.className)}
role={props.plan === "free" ? "button" : undefined}
tabIndex={props.plan === "free" ? 0 : undefined}
onClick={handleNavigateToBilling}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
handleNavigateToBilling(e);
}
}}
>
{`${getTeamPlanBadgeLabel(props.plan)}${props.postfix || ""}`}
</Badge>
);

const track = useTrack();

if (props.plan === "free") {
return (
<Link
href={`/team/${props.teamSlug}/~/settings/billing?showPlans=true`}
onClick={() => {
track({
category: "billing",
action: "show_plans",
label: "team_badge",
});
}}
>
{badge}
</Link>
);
}

return badge;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { getProjects } from "@/api/projects";
import { getTeamBySlug, getTeams } from "@/api/team";
import { AppFooter } from "@/components/blocks/app-footer";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { differenceInDays } from "date-fns";
import { InfoIcon } from "lucide-react";
import { notFound } from "next/navigation";
import { getValidAccount } from "../../../account/settings/getAccount";
import {
Expand Down Expand Up @@ -32,6 +35,10 @@ export default async function Layout(props: {
notFound();
}

// show the banner only if the team was created more than 3 days ago
const shouldShowOnboardingBanner =
differenceInDays(new Date(), new Date(team.createdAt)) > 3;

// Note:
// Do not check that team is already onboarded or not and redirect away from /get-started pages
// because the team is marked as onboarded in the first step- instead of after completing all the steps
Expand Down Expand Up @@ -60,6 +67,21 @@ export default async function Layout(props: {
teamsAndProjects={teamsAndProjects}
/>
</div>
{shouldShowOnboardingBanner && (
<div className="container mt-10">
<Alert variant="info">
<InfoIcon className="size-5" />
<AlertTitle>Finish setting up your team</AlertTitle>
<AlertDescription>
Your team predates our latest onboarding flow, so a few steps
might still be pending.
<br />
Completing this updated guide takes less than a minute and ensures
everything is set up correctly.
</AlertDescription>
</Alert>
</div>
)}
{props.children}
<AppFooter />
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import type { Team } from "@/api/team";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";

export function isAccountOnboardingComplete(account: Account) {
// if email is confirmed, onboarding is considered complete
return !!account.emailConfirmedAt;
}

export function isTeamOnboardingComplete(team: Team) {
return team.isOnboarded;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getStripeBalance } from "@/actions/stripe-actions";
import { type Team, getTeamBySlug } from "@/api/team";
import { getMemberById } from "@/api/team-members";
import { getMemberByAccountId } from "@/api/team-members";
import { getTeamSubscriptions } from "@/api/team-subscription";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { redirect } from "next/navigation";
Expand Down Expand Up @@ -31,7 +31,7 @@ export default async function Page(props: {
const [team, authToken, teamMember] = await Promise.all([
getTeamBySlug(params.team_slug),
getAuthToken(),
getMemberById(params.team_slug, account.id),
getMemberByAccountId(params.team_slug, account.id),
]);

if (!team || !teamMember) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getTeamInvoices } from "@/actions/stripe-actions";
import { getTeamBySlug } from "@/api/team";
import { getMemberById } from "@/api/team-members";
import { getMemberByAccountId } from "@/api/team-members";
import { redirect } from "next/navigation";
import type { SearchParams } from "nuqs/server";
import { getValidAccount } from "../../../../../../account/settings/getAccount";
Expand All @@ -25,7 +25,7 @@ export default async function Page(props: {

const [team, teamMember] = await Promise.all([
getTeamBySlug(params.team_slug),
getMemberById(params.team_slug, account.id),
getMemberByAccountId(params.team_slug, account.id),
]);

if (!team) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getTeamBySlug } from "@/api/team";
import { getMemberById } from "@/api/team-members";
import { getMemberByAccountId } from "@/api/team-members";
import { checkDomainVerification } from "@/api/verified-domain";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { notFound } from "next/navigation";
Expand All @@ -19,7 +19,7 @@ export default async function Page(props: {

const [team, teamMember, token, initialVerification] = await Promise.all([
getTeamBySlug(params.team_slug),
getMemberById(params.team_slug, account.id),
getMemberByAccountId(params.team_slug, account.id),
getAuthToken(),
checkDomainVerification(params.team_slug),
]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getProject } from "@/api/projects";
import { getTeams } from "@/api/team";
import { getMemberById } from "@/api/team-members";
import { getMemberByAccountId } from "@/api/team-members";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { notFound, redirect } from "next/navigation";
import { getValidAccount } from "../../../../../account/settings/getAccount";
Expand Down Expand Up @@ -37,7 +37,7 @@ export default async function Page(props: {

const teamsWithRole = await Promise.all(
teams.map(async (team) => {
const member = await getMemberById(team.slug, account.id);
const member = await getMemberByAccountId(team.slug, account.id);

if (!member) {
notFound();
Expand Down
12 changes: 7 additions & 5 deletions apps/dashboard/src/app/(app)/team/[team_slug]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { getTeamBySlug } from "@/api/team";
import { getTeamBySlug, hasToCompleteTeamOnboarding } from "@/api/team";
import { PosthogIdentifierServer } from "components/wallets/PosthogIdentifierServer";
import { redirect } from "next/navigation";
import { Suspense } from "react";
import { getAuthToken } from "../../api/lib/getAuthToken";
import { EnsureValidConnectedWalletLoginServer } from "../../components/EnsureValidConnectedWalletLogin/EnsureValidConnectedWalletLoginServer";
import { isTeamOnboardingComplete } from "../../login/onboarding/isOnboardingRequired";
import { SaveLastVisitedTeamPage } from "../components/last-visited-page/SaveLastVisitedPage";
import {
PastDueBanner,
Expand All @@ -16,8 +15,11 @@ export default async function RootTeamLayout(props: {
params: Promise<{ team_slug: string }>;
}) {
const { team_slug } = await props.params;
const authToken = await getAuthToken();
const team = await getTeamBySlug(team_slug).catch(() => null);

const [authToken, team] = await Promise.all([
getAuthToken(),
getTeamBySlug(team_slug).catch(() => null),
]);

if (!team) {
redirect("/team");
Expand All @@ -27,7 +29,7 @@ export default async function RootTeamLayout(props: {
redirect("/login");
}

if (!isTeamOnboardingComplete(team)) {
if (await hasToCompleteTeamOnboarding(team, `/team/${team_slug}`)) {
redirect(`/get-started/team/${team.slug}`);
}

Expand Down
Loading