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}`); }