Skip to content

Commit e450d32

Browse files
committed
[TOOL-3969] Dashboard: Update Plans, Add cancel plan, and other related UI updates (#6674)
<!-- start pr-codex --> ## PR-Codex overview This PR focuses on enhancing the billing and subscription management system by adding new team plans, updating existing components, and improving the user interface for better user experience. ### Detailed summary - Added new plans: `accelerate` and `scale`. - Updated `TeamPlan` type to include new plans. - Modified `PlanInfoCard` to support new plans and improved UI. - Refactored billing components to use new plan data. - Enhanced `InviteSection` with new team plan options. - Updated storybook files for new billing plans. - Improved `CancelPlanModal` for better user feedback. - Added helper functions for plan CTA logic. > The following files were skipped due to too many changes: `apps/dashboard/src/components/onboarding/ApplyForOpCreditsModal.tsx` > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 838f8fa commit e450d32

File tree

22 files changed

+1361
-686
lines changed

22 files changed

+1361
-686
lines changed

apps/dashboard/src/@/components/blocks/pricing-card.tsx

Lines changed: 62 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,35 @@ import type { Team } from "@/api/team";
33
import { Badge } from "@/components/ui/badge";
44
import { Button } from "@/components/ui/button";
55
import { ToolTipLabel } from "@/components/ui/tooltip";
6-
import { TrackedLinkTW } from "@/components/ui/tracked-link";
76
import { cn } from "@/lib/utils";
8-
import { CheckIcon, CircleAlertIcon, CircleDollarSignIcon } from "lucide-react";
7+
import { CheckIcon, CircleDollarSignIcon } from "lucide-react";
8+
import Link from "next/link";
99
import type React from "react";
1010
import { TEAM_PLANS } from "utils/pricing";
11+
import { useTrack } from "../../../hooks/analytics/useTrack";
1112
import { remainingDays } from "../../../utils/date-utils";
1213
import type { GetBillingCheckoutUrlAction } from "../../actions/billing";
14+
import type { ProductSKU } from "../../lib/billing";
1315
import { CheckoutButton } from "../billing";
1416

15-
type ButtonProps = React.ComponentProps<typeof Button>;
16-
17-
const PRO_CONTACT_US_URL =
18-
"https://meetings.hubspot.com/sales-thirdweb/thirdweb-pro";
17+
type PricingCardCta = {
18+
hint?: string;
19+
title: string;
20+
onClick?: () => void;
21+
} & (
22+
| {
23+
type: "link";
24+
href: string;
25+
}
26+
| {
27+
type: "checkout";
28+
}
29+
);
1930

2031
type PricingCardProps = {
2132
teamSlug: string;
22-
billingPlan: Exclude<Team["billingPlan"], "free">;
23-
cta?: {
24-
hint?: string;
25-
title: string;
26-
tracking: {
27-
category: string;
28-
label?: string;
29-
};
30-
variant?: ButtonProps["variant"];
31-
onClick?: () => void;
32-
};
33+
billingPlan: keyof typeof TEAM_PLANS;
34+
cta?: PricingCardCta;
3335
ctaHint?: string;
3436
highlighted?: boolean;
3537
current?: boolean;
@@ -49,13 +51,23 @@ export const PricingCard: React.FC<PricingCardProps> = ({
4951
const plan = TEAM_PLANS[billingPlan];
5052
const isCustomPrice = typeof plan.price === "string";
5153

54+
const trackEvent = useTrack();
5255
const remainingTrialDays =
5356
(activeTrialEndsAt ? remainingDays(activeTrialEndsAt) : 0) || 0;
5457

58+
const handleCTAClick = () => {
59+
cta?.onClick?.();
60+
trackEvent({
61+
category: "account",
62+
label: `${billingPlan}Plan`,
63+
action: "click",
64+
});
65+
};
66+
5567
return (
5668
<div
5769
className={cn(
58-
"z-10 flex w-full flex-col gap-6 rounded-xl border border-border bg-card p-4 md:p-6",
70+
"z-10 flex w-full flex-col gap-4 rounded-xl border border-border bg-card p-4",
5971
current && "border-blue-500",
6072
highlighted && "border-active-border",
6173
)}
@@ -71,44 +83,27 @@ export const PricingCard: React.FC<PricingCardProps> = ({
7183
<div className="flex flex-col gap-5">
7284
{/* Title + Desc */}
7385
<div>
74-
<div className="mb-2 flex flex-row items-center gap-2">
86+
<div className="mb-1 flex flex-row items-center gap-3">
7587
<h3 className="font-semibold text-2xl capitalize tracking-tight">
7688
{plan.title}
7789
</h3>
7890
{current && <Badge className="capitalize">Current plan</Badge>}
7991
</div>
80-
<p className="max-w-[320px] text-muted-foreground">
92+
<p className="max-w-[320px] text-muted-foreground text-sm">
8193
{plan.description}
8294
</p>
8395
</div>
8496

8597
{/* Price */}
8698
<div className="flex flex-col gap-0.5">
8799
<div className="flex items-center gap-2">
88-
<span className="font-semibold text-3xl text-foreground tracking-tight">
100+
<span className="font-semibold text-2xl text-foreground tracking-tight">
89101
${plan.price}
90102
</span>
91103

92104
{!isCustomPrice && (
93105
<span className="text-muted-foreground">/ month</span>
94106
)}
95-
96-
{billingPlan === "starter" && (
97-
<ToolTipLabel
98-
contentClassName="max-w-[320px]"
99-
label="We will place a temporary hold of $25 to verify your card, this will be immediately released back to you after verification."
100-
>
101-
<Button
102-
asChild
103-
variant="ghost"
104-
className="h-auto w-auto p-1 text-muted-foreground hover:text-foreground"
105-
>
106-
<div>
107-
<CircleAlertIcon className="size-5 shrink-0" />
108-
</div>
109-
</Button>
110-
</ToolTipLabel>
111-
)}
112107
</div>
113108

114109
{remainingTrialDays > 0 && (
@@ -124,7 +119,7 @@ export const PricingCard: React.FC<PricingCardProps> = ({
124119

125120
<div className="flex grow flex-col items-start gap-2 text-foreground">
126121
{plan.subTitle && (
127-
<p className="font-medium text-foreground">{plan.subTitle}</p>
122+
<p className="font-medium text-foreground text-sm">{plan.subTitle}</p>
128123
)}
129124

130125
{plan.features.map((f) => (
@@ -134,29 +129,30 @@ export const PricingCard: React.FC<PricingCardProps> = ({
134129

135130
{cta && (
136131
<div className="flex flex-col gap-3">
137-
{billingPlan !== "pro" ? (
132+
{billingPlanToSkuMap[billingPlan] && cta.type === "checkout" && (
138133
<CheckoutButton
139134
buttonProps={{
140-
variant: cta.variant || "outline",
141-
className: "gap-2",
142-
onClick: cta.onClick,
135+
variant: highlighted ? "default" : "outline",
136+
className: highlighted ? undefined : "bg-background",
137+
onClick: handleCTAClick,
143138
}}
144139
teamSlug={teamSlug}
145-
sku={billingPlan === "starter" ? "plan:starter" : "plan:growth"}
140+
sku={billingPlanToSkuMap[billingPlan]}
146141
getBillingCheckoutUrl={getBillingCheckoutUrl}
147142
>
148143
{cta.title}
149144
</CheckoutButton>
150-
) : (
151-
<Button variant={cta.variant || "outline"} asChild>
152-
<TrackedLinkTW
153-
href={PRO_CONTACT_US_URL}
154-
label={cta.tracking?.label}
155-
category={cta.tracking?.category}
156-
target="_blank"
157-
>
145+
)}
146+
147+
{cta.type === "link" && (
148+
<Button
149+
variant={highlighted ? "default" : "outline"}
150+
className={highlighted ? undefined : "bg-background"}
151+
asChild
152+
>
153+
<Link href={cta.href} target="_blank" onClick={handleCTAClick}>
158154
{cta.title}
159-
</TrackedLinkTW>
155+
</Link>
160156
</Button>
161157
)}
162158

@@ -171,6 +167,19 @@ export const PricingCard: React.FC<PricingCardProps> = ({
171167
);
172168
};
173169

170+
const billingPlanToSkuMap: Record<Team["billingPlan"], ProductSKU | undefined> =
171+
{
172+
starter: "plan:starter",
173+
growth: "plan:growth",
174+
accelerate: "plan:accelerate",
175+
scale: "plan:scale",
176+
// we can't render checkout buttons for these plans:
177+
pro: undefined,
178+
free: undefined,
179+
growth_legacy: undefined,
180+
starter_legacy: undefined,
181+
};
182+
174183
type FeatureItemProps = {
175184
text: string | string[];
176185
};

apps/dashboard/src/@/lib/billing.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ export type ProductSKU =
33
| "plan:starter"
44
| "plan:growth"
55
| "plan:custom"
6+
| "plan:accelerate"
7+
| "plan:scale"
68
| "product:ecosystem_wallets"
79
| "product:engine_standard"
810
| "product:engine_premium"
Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,45 @@
11
import type { Team } from "@/api/team";
2-
import { Badge } from "@/components/ui/badge";
2+
import { Badge, type BadgeProps } from "@/components/ui/badge";
33
import { cn } from "@/lib/utils";
44

5+
const teamPlanToBadgeVariant: Record<
6+
Team["billingPlan"],
7+
BadgeProps["variant"]
8+
> = {
9+
// gray
10+
free: "secondary",
11+
starter: "secondary",
12+
// yellow
13+
starter_legacy: "warning",
14+
growth_legacy: "warning",
15+
// green
16+
accelerate: "success",
17+
growth: "success",
18+
scale: "success",
19+
// blue
20+
pro: "default",
21+
};
22+
23+
function getTeamPlanBadgeLabel(plan: Team["billingPlan"]) {
24+
if (plan === "growth_legacy") {
25+
return "Growth - Legacy";
26+
}
27+
if (plan === "starter_legacy") {
28+
return "Starter - Legacy";
29+
}
30+
return plan;
31+
}
32+
533
export function TeamPlanBadge(props: {
634
plan: Team["billingPlan"];
735
className?: string;
836
}) {
937
return (
1038
<Badge
11-
variant={
12-
props.plan === "free" || props.plan === "starter"
13-
? "secondary"
14-
: props.plan === "growth"
15-
? "success"
16-
: "default"
17-
}
39+
variant={teamPlanToBadgeVariant[props.plan]}
1840
className={cn("px-1.5 capitalize", props.className)}
1941
>
20-
{props.plan}
42+
{getTeamPlanBadgeLabel(props.plan)}
2143
</Badge>
2244
);
2345
}

apps/dashboard/src/app/login/onboarding/team-onboarding/InviteTeamMembers.stories.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Meta, StoryObj } from "@storybook/react";
2+
import type { Team } from "../../../../@/api/team";
23
import { teamStub } from "../../../../stories/stubs";
34
import { storybookLog } from "../../../../stories/utils";
45
import { TeamOnboardingLayout } from "../onboarding-layout";
@@ -36,8 +37,26 @@ export const GrowthPlan: Story = {
3637
},
3738
};
3839

40+
export const AcceleratePlan: Story = {
41+
args: {
42+
plan: "accelerate",
43+
},
44+
};
45+
46+
export const ScalePlan: Story = {
47+
args: {
48+
plan: "scale",
49+
},
50+
};
51+
52+
export const ProPlan: Story = {
53+
args: {
54+
plan: "pro",
55+
},
56+
};
57+
3958
function Story(props: {
40-
plan: "free" | "growth" | "starter";
59+
plan: Team["billingPlan"];
4160
}) {
4261
return (
4362
<TeamOnboardingLayout currentStep={2}>

0 commit comments

Comments
 (0)