diff --git a/apps/dashboard/src/@/api/team-members.ts b/apps/dashboard/src/@/api/team-members.ts
index f76f5422fe8..518152bf1e0 100644
--- a/apps/dashboard/src/@/api/team-members.ts
+++ b/apps/dashboard/src/@/api/team-members.ts
@@ -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}`,
@@ -48,7 +48,10 @@ 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) {
@@ -56,7 +59,10 @@ export async function getMemberById(teamSlug: string, memberId: string) {
}
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}`,
diff --git a/apps/dashboard/src/@/api/team.ts b/apps/dashboard/src/@/api/team.ts
index 5656535e520..46ab70029cb 100644
--- a/apps/dashboard/src/@/api/team.ts
+++ b/apps/dashboard/src/@/api/team.ts
@@ -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 };
@@ -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;
+}
diff --git a/apps/dashboard/src/app/(app)/account/page.tsx b/apps/dashboard/src/app/(app)/account/page.tsx
index 10c75fe26e4..dcf91adaa6c 100644
--- a/apps/dashboard/src/app/(app)/account/page.tsx
+++ b/apps/dashboard/src/app/(app)/account/page.tsx
@@ -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";
@@ -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();
diff --git a/apps/dashboard/src/app/(app)/components/TeamPlanBadge.tsx b/apps/dashboard/src/app/(app)/components/TeamPlanBadge.tsx
index 454c0e99ef1..ae1f9b14030 100644
--- a/apps/dashboard/src/app/(app)/components/TeamPlanBadge.tsx
+++ b/apps/dashboard/src/app/(app)/components/TeamPlanBadge.tsx
@@ -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"],
@@ -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 (
{
+ if (e.key === "Enter" || e.key === " ") {
+ handleNavigateToBilling(e);
+ }
+ }}
>
{`${getTeamPlanBadgeLabel(props.plan)}${props.postfix || ""}`}
);
-
- const track = useTrack();
-
- if (props.plan === "free") {
- return (
- {
- track({
- category: "billing",
- action: "show_plans",
- label: "team_badge",
- });
- }}
- >
- {badge}
-
- );
- }
-
- return badge;
}
diff --git a/apps/dashboard/src/app/(app)/get-started/team/[team_slug]/layout.tsx b/apps/dashboard/src/app/(app)/get-started/team/[team_slug]/layout.tsx
index f1570b96004..0dcd2af9874 100644
--- a/apps/dashboard/src/app/(app)/get-started/team/[team_slug]/layout.tsx
+++ b/apps/dashboard/src/app/(app)/get-started/team/[team_slug]/layout.tsx
@@ -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 {
@@ -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
@@ -60,6 +67,21 @@ export default async function Layout(props: {
teamsAndProjects={teamsAndProjects}
/>
+ {shouldShowOnboardingBanner && (
+
+
+
+ Finish setting up your team
+
+ Your team predates our latest onboarding flow, so a few steps
+ might still be pending.
+
+ Completing this updated guide takes less than a minute and ensures
+ everything is set up correctly.
+
+
+
+ )}
{props.children}
diff --git a/apps/dashboard/src/app/(app)/login/onboarding/isOnboardingRequired.ts b/apps/dashboard/src/app/(app)/login/onboarding/isOnboardingRequired.ts
index 03bcbcf6c1a..c3f800fed07 100644
--- a/apps/dashboard/src/app/(app)/login/onboarding/isOnboardingRequired.ts
+++ b/apps/dashboard/src/app/(app)/login/onboarding/isOnboardingRequired.ts
@@ -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;
-}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/page.tsx
index b3771da3b2c..ea5579bcb4d 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/page.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/page.tsx
@@ -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";
@@ -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) {
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/invoices/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/invoices/page.tsx
index e4f9ecb86e5..72dc4b7de90 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/invoices/page.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/invoices/page.tsx
@@ -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";
@@ -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) {
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/page.tsx
index c95a35f8030..524353ae17e 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/page.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/page.tsx
@@ -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";
@@ -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),
]);
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/page.tsx
index 8e3b569c987..97b2a9701fa 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/page.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/page.tsx
@@ -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";
@@ -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();
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/layout.tsx
index 00c81d3b704..03597e31b1f 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/layout.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/layout.tsx
@@ -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,
@@ -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");
@@ -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}`);
}