Skip to content

[Dashboard] Add plan selection step to team onboarding flow #7324

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
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default async function Page(props: {
});

return (
<TeamOnboardingLayout currentStep={2}>
<TeamOnboardingLayout currentStep={3}>
<InviteTeamMembers team={team} client={client} />
</TeamOnboardingLayout>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
"use client";

import type { Team } from "@/api/team";
import { PricingCard } from "@/components/blocks/pricing-card";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { useTrack } from "hooks/analytics/useTrack";
import Link from "next/link";
import { pollWithTimeout } from "utils/pollWithTimeout";
import { useStripeRedirectEvent } from "../../../../../(stripe)/stripe-redirect/stripeRedirectChannel";

export function PlanSelector(props: {
team: Team;
getTeam: () => Promise<Team>;
}) {
const trackEvent = useTrack();
const router = useDashboardRouter();

useStripeRedirectEvent(async () => {
// poll until the team has a non-free billing plan with a timeout of 5 seconds
await pollWithTimeout({
shouldStop: async () => {
const team = await props.getTeam();
const isNonFreePlan = team.billingPlan !== "free";

if (isNonFreePlan) {
trackEvent({
category: "teamOnboarding",
action: "upgradePlan",
label: "success",
plan: team.billingPlan,
});
router.replace(`/get-started/team/${props.team.slug}/add-members`);
}

return isNonFreePlan;
},
timeoutMs: 20_000,
});
});

const starterPlan = (
<PricingCard
billingPlan="starter"
billingStatus={props.team.billingStatus}
teamSlug={props.team.slug}
cta={{
label: "Get Started",
type: "checkout",
onClick() {
trackEvent({
category: "teamOnboarding",
action: "selectPlan",
label: "attempt",
plan: "starter",
});
},
}}
getTeam={props.getTeam}
teamId={props.team.id}
/>
);

const growthPlan = (
<PricingCard
billingPlan="growth"
billingStatus={props.team.billingStatus}
teamSlug={props.team.slug}
cta={{
label: "Get Started",
type: "checkout",
onClick() {
trackEvent({
category: "teamOnboarding",
action: "selectPlan",
label: "attempt",
plan: "growth",
});
},
}}
highlighted
getTeam={props.getTeam}
teamId={props.team.id}
/>
);

const scalePlan = (
<PricingCard
billingPlan="scale"
billingStatus={props.team.billingStatus}
teamSlug={props.team.slug}
cta={{
label: "Get started",
type: "checkout",
onClick() {
trackEvent({
category: "teamOnboarding",
action: "selectPlan",
label: "attempt",
plan: "scale",
});
},
}}
getTeam={props.getTeam}
teamId={props.team.id}
/>
);

const proPlan = (
<PricingCard
billingPlan="pro"
billingStatus={props.team.billingStatus}
teamSlug={props.team.slug}
cta={{
label: "Get started",
type: "checkout",
onClick() {
trackEvent({
category: "teamOnboarding",
action: "selectPlan",
label: "attempt",
plan: "pro",
});
},
}}
getTeam={props.getTeam}
teamId={props.team.id}
/>
);

return (
<div className="grid grid-cols-1 gap-6 md:grid-cols-4">
{starterPlan}
{growthPlan}
{scalePlan}
{proPlan}
<div className="col-span-1 flex flex-col gap-2 md:col-span-4">
<div className="relative">
<Separator className="my-4" orientation="horizontal" />
<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">
or
</div>
</div>
<Button
variant="link"
className="self-center text-muted-foreground"
asChild
onClick={() => {
trackEvent({
category: "teamOnboarding",
action: "selectPlan",
label: "skip",
});
}}
>
<Link
replace
href={`/get-started/team/${props.team.slug}/add-members`}
>
Skip picking a plan for now and upgrade later
</Link>
</Button>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { type Team, getTeamBySlug } from "@/api/team";
import { notFound } from "next/navigation";
import { getAuthToken } from "../../../../api/lib/getAuthToken";
import { TeamOnboardingLayout } from "../../../../login/onboarding/onboarding-layout";
import { PlanSelector } from "./_components/plan-selector";

export default async function Page(props: {
params: Promise<{ team_slug: string }>;
}) {
const params = await props.params;
const [team, authToken] = await Promise.all([
getTeamBySlug(params.team_slug),
getAuthToken(),
]);

if (!team || !authToken) {
notFound();
}

// const client = getClientThirdwebClient({
// jwt: authToken,
// teamId: team.id,
// });

async function getTeam() {
"use server";
const resolvedTeam = await getTeamBySlug(params.team_slug);
if (!resolvedTeam) {
return team as Team;
}
return resolvedTeam;
}

return (
<TeamOnboardingLayout currentStep={2}>
<PlanSelector team={team} getTeam={getTeam} />
</TeamOnboardingLayout>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
BoxIcon,
LogOutIcon,
MailIcon,
RocketIcon,
UserIcon,
UsersIcon,
} from "lucide-react";
Expand Down Expand Up @@ -76,17 +77,23 @@ const teamOnboardingSteps: OnboardingStep[] = [
description: "Provide team details",
number: 1,
},
{
icon: RocketIcon,
title: "Plan Selection",
description: "Choose a plan that fits your needs",
number: 2,
},
{
icon: UsersIcon,
title: "Team Members",
description: "Invite members to your team",
number: 2,
number: 3,
},
];

export function TeamOnboardingLayout(props: {
children: React.ReactNode;
currentStep: 1 | 2;
currentStep: 1 | 2 | 3;
}) {
return (
<OnboardingLayout
Expand Down Expand Up @@ -116,11 +123,9 @@ function OnboardingLayout(props: {
{props.cta}
</div>
</div>
<div className="container flex grow flex-col gap-10 xl:flex-row">
<div className="container flex grow flex-col gap-8 xl:flex-row xl:gap-10">
{/* Left */}
<div className="flex w-full max-w-[850px] flex-col py-8">
{props.children}
</div>
<div className="flex w-full flex-col py-8">{props.children}</div>

{/* Right */}
<div className="hidden shrink-0 grow flex-col xl:flex">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function TeamInfoForm(props: {
teamSlug={props.teamSlug}
client={props.client}
onComplete={(updatedTeam) => {
router.replace(`/get-started/team/${updatedTeam.slug}/add-members`);
router.replace(`/get-started/team/${updatedTeam.slug}/select-plan`);
}}
updateTeam={async (data) => {
const teamValue: Partial<Team> = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ function BillingInfo({
<span className="text-muted-foreground text-sm">
-{" "}
<Link
className="hover:underline"
className="underline decoration-dotted underline-offset-2 hover:decoration-solid"
href={`/team/${teamSlug}/~/usage`}
>
View Breakdown
Expand Down Expand Up @@ -332,15 +332,11 @@ function SubscriptionOverview(props: {
props.title
))}
<p className="text-muted-foreground text-sm">
{format(
new Date(props.subscription.currentPeriodStart),
"MMMM dd yyyy",
)}{" "}
-{" "}
Your next billing period begins{" "}
{format(
new Date(props.subscription.currentPeriodEnd),
"MMMM dd yyyy",
)}{" "}
"MMMM dd, yyyy",
)}
</p>
</div>

Expand Down
Loading