Skip to content

Commit 38f1821

Browse files
authored
[Dashboard] Add plan selection step to team onboarding flow (#7324)
1 parent 8489844 commit 38f1821

File tree

6 files changed

+223
-16
lines changed

6 files changed

+223
-16
lines changed

apps/dashboard/src/app/(app)/get-started/team/[team_slug]/add-members/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export default async function Page(props: {
2424
});
2525

2626
return (
27-
<TeamOnboardingLayout currentStep={2}>
27+
<TeamOnboardingLayout currentStep={3}>
2828
<InviteTeamMembers team={team} client={client} />
2929
</TeamOnboardingLayout>
3030
);
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
"use client";
2+
3+
import type { Team } from "@/api/team";
4+
import { PricingCard } from "@/components/blocks/pricing-card";
5+
import { Button } from "@/components/ui/button";
6+
import { Separator } from "@/components/ui/separator";
7+
import { useDashboardRouter } from "@/lib/DashboardRouter";
8+
import { useTrack } from "hooks/analytics/useTrack";
9+
import Link from "next/link";
10+
import { pollWithTimeout } from "utils/pollWithTimeout";
11+
import { useStripeRedirectEvent } from "../../../../../(stripe)/stripe-redirect/stripeRedirectChannel";
12+
13+
export function PlanSelector(props: {
14+
team: Team;
15+
getTeam: () => Promise<Team>;
16+
}) {
17+
const trackEvent = useTrack();
18+
const router = useDashboardRouter();
19+
20+
useStripeRedirectEvent(async () => {
21+
// poll until the team has a non-free billing plan with a timeout of 5 seconds
22+
await pollWithTimeout({
23+
shouldStop: async () => {
24+
const team = await props.getTeam();
25+
const isNonFreePlan = team.billingPlan !== "free";
26+
27+
if (isNonFreePlan) {
28+
trackEvent({
29+
category: "teamOnboarding",
30+
action: "upgradePlan",
31+
label: "success",
32+
plan: team.billingPlan,
33+
});
34+
router.replace(`/get-started/team/${props.team.slug}/add-members`);
35+
}
36+
37+
return isNonFreePlan;
38+
},
39+
timeoutMs: 20_000,
40+
});
41+
});
42+
43+
const starterPlan = (
44+
<PricingCard
45+
billingPlan="starter"
46+
billingStatus={props.team.billingStatus}
47+
teamSlug={props.team.slug}
48+
cta={{
49+
label: "Get Started",
50+
type: "checkout",
51+
onClick() {
52+
trackEvent({
53+
category: "teamOnboarding",
54+
action: "selectPlan",
55+
label: "attempt",
56+
plan: "starter",
57+
});
58+
},
59+
}}
60+
getTeam={props.getTeam}
61+
teamId={props.team.id}
62+
/>
63+
);
64+
65+
const growthPlan = (
66+
<PricingCard
67+
billingPlan="growth"
68+
billingStatus={props.team.billingStatus}
69+
teamSlug={props.team.slug}
70+
cta={{
71+
label: "Get Started",
72+
type: "checkout",
73+
onClick() {
74+
trackEvent({
75+
category: "teamOnboarding",
76+
action: "selectPlan",
77+
label: "attempt",
78+
plan: "growth",
79+
});
80+
},
81+
}}
82+
highlighted
83+
getTeam={props.getTeam}
84+
teamId={props.team.id}
85+
/>
86+
);
87+
88+
const scalePlan = (
89+
<PricingCard
90+
billingPlan="scale"
91+
billingStatus={props.team.billingStatus}
92+
teamSlug={props.team.slug}
93+
cta={{
94+
label: "Get started",
95+
type: "checkout",
96+
onClick() {
97+
trackEvent({
98+
category: "teamOnboarding",
99+
action: "selectPlan",
100+
label: "attempt",
101+
plan: "scale",
102+
});
103+
},
104+
}}
105+
getTeam={props.getTeam}
106+
teamId={props.team.id}
107+
/>
108+
);
109+
110+
const proPlan = (
111+
<PricingCard
112+
billingPlan="pro"
113+
billingStatus={props.team.billingStatus}
114+
teamSlug={props.team.slug}
115+
cta={{
116+
label: "Get started",
117+
type: "checkout",
118+
onClick() {
119+
trackEvent({
120+
category: "teamOnboarding",
121+
action: "selectPlan",
122+
label: "attempt",
123+
plan: "pro",
124+
});
125+
},
126+
}}
127+
getTeam={props.getTeam}
128+
teamId={props.team.id}
129+
/>
130+
);
131+
132+
return (
133+
<div className="grid grid-cols-1 gap-6 md:grid-cols-4">
134+
{starterPlan}
135+
{growthPlan}
136+
{scalePlan}
137+
{proPlan}
138+
<div className="col-span-1 flex flex-col gap-2 md:col-span-4">
139+
<div className="relative">
140+
<Separator className="my-4" orientation="horizontal" />
141+
<div className="-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-1/2 bg-background px-1 text-muted-foreground text-sm">
142+
or
143+
</div>
144+
</div>
145+
<Button
146+
variant="link"
147+
className="self-center text-muted-foreground"
148+
asChild
149+
onClick={() => {
150+
trackEvent({
151+
category: "teamOnboarding",
152+
action: "selectPlan",
153+
label: "skip",
154+
});
155+
}}
156+
>
157+
<Link
158+
replace
159+
href={`/get-started/team/${props.team.slug}/add-members`}
160+
>
161+
Skip picking a plan for now and upgrade later
162+
</Link>
163+
</Button>
164+
</div>
165+
</div>
166+
);
167+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { type Team, getTeamBySlug } from "@/api/team";
2+
import { notFound } from "next/navigation";
3+
import { getAuthToken } from "../../../../api/lib/getAuthToken";
4+
import { TeamOnboardingLayout } from "../../../../login/onboarding/onboarding-layout";
5+
import { PlanSelector } from "./_components/plan-selector";
6+
7+
export default async function Page(props: {
8+
params: Promise<{ team_slug: string }>;
9+
}) {
10+
const params = await props.params;
11+
const [team, authToken] = await Promise.all([
12+
getTeamBySlug(params.team_slug),
13+
getAuthToken(),
14+
]);
15+
16+
if (!team || !authToken) {
17+
notFound();
18+
}
19+
20+
// const client = getClientThirdwebClient({
21+
// jwt: authToken,
22+
// teamId: team.id,
23+
// });
24+
25+
async function getTeam() {
26+
"use server";
27+
const resolvedTeam = await getTeamBySlug(params.team_slug);
28+
if (!resolvedTeam) {
29+
return team as Team;
30+
}
31+
return resolvedTeam;
32+
}
33+
34+
return (
35+
<TeamOnboardingLayout currentStep={2}>
36+
<PlanSelector team={team} getTeam={getTeam} />
37+
</TeamOnboardingLayout>
38+
);
39+
}

apps/dashboard/src/app/(app)/login/onboarding/onboarding-layout.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
BoxIcon,
77
LogOutIcon,
88
MailIcon,
9+
RocketIcon,
910
UserIcon,
1011
UsersIcon,
1112
} from "lucide-react";
@@ -76,17 +77,23 @@ const teamOnboardingSteps: OnboardingStep[] = [
7677
description: "Provide team details",
7778
number: 1,
7879
},
80+
{
81+
icon: RocketIcon,
82+
title: "Plan Selection",
83+
description: "Choose a plan that fits your needs",
84+
number: 2,
85+
},
7986
{
8087
icon: UsersIcon,
8188
title: "Team Members",
8289
description: "Invite members to your team",
83-
number: 2,
90+
number: 3,
8491
},
8592
];
8693

8794
export function TeamOnboardingLayout(props: {
8895
children: React.ReactNode;
89-
currentStep: 1 | 2;
96+
currentStep: 1 | 2 | 3;
9097
}) {
9198
return (
9299
<OnboardingLayout
@@ -116,11 +123,9 @@ function OnboardingLayout(props: {
116123
{props.cta}
117124
</div>
118125
</div>
119-
<div className="container flex grow flex-col gap-10 xl:flex-row">
126+
<div className="container flex grow flex-col gap-8 xl:flex-row xl:gap-10">
120127
{/* Left */}
121-
<div className="flex w-full max-w-[850px] flex-col py-8">
122-
{props.children}
123-
</div>
128+
<div className="flex w-full flex-col py-8">{props.children}</div>
124129

125130
{/* Right */}
126131
<div className="hidden shrink-0 grow flex-col xl:flex">

apps/dashboard/src/app/(app)/login/onboarding/team-onboarding/team-onboarding.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function TeamInfoForm(props: {
4040
teamSlug={props.teamSlug}
4141
client={props.client}
4242
onComplete={(updatedTeam) => {
43-
router.replace(`/get-started/team/${updatedTeam.slug}/add-members`);
43+
router.replace(`/get-started/team/${updatedTeam.slug}/select-plan`);
4444
}}
4545
updateTeam={async (data) => {
4646
const teamValue: Partial<Team> = {

apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/PlanInfoCard.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ function BillingInfo({
300300
<span className="text-muted-foreground text-sm">
301301
-{" "}
302302
<Link
303-
className="hover:underline"
303+
className="underline decoration-dotted underline-offset-2 hover:decoration-solid"
304304
href={`/team/${teamSlug}/~/usage`}
305305
>
306306
View Breakdown
@@ -332,15 +332,11 @@ function SubscriptionOverview(props: {
332332
props.title
333333
))}
334334
<p className="text-muted-foreground text-sm">
335-
{format(
336-
new Date(props.subscription.currentPeriodStart),
337-
"MMMM dd yyyy",
338-
)}{" "}
339-
-{" "}
335+
Your next billing period begins{" "}
340336
{format(
341337
new Date(props.subscription.currentPeriodEnd),
342-
"MMMM dd yyyy",
343-
)}{" "}
338+
"MMMM dd, yyyy",
339+
)}
344340
</p>
345341
</div>
346342

0 commit comments

Comments
 (0)