Skip to content

Commit 73bcbe9

Browse files
committed
[TOOL-3969] Dashboard: Update Plans, Add cancel plan and related changes
1 parent 1dae74b commit 73bcbe9

File tree

23 files changed

+1299
-687
lines changed

23 files changed

+1299
-687
lines changed

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

Lines changed: 65 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,36 @@ 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+
variant?: "outline" | "default";
21+
onClick?: () => void;
22+
} & (
23+
| {
24+
type: "link";
25+
href: string;
26+
}
27+
| {
28+
type: "checkout";
29+
}
30+
);
1931

2032
type PricingCardProps = {
2133
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-
};
34+
billingPlan: keyof typeof TEAM_PLANS;
35+
cta?: PricingCardCta;
3336
ctaHint?: string;
3437
highlighted?: boolean;
3538
current?: boolean;
@@ -49,13 +52,25 @@ export const PricingCard: React.FC<PricingCardProps> = ({
4952
const plan = TEAM_PLANS[billingPlan];
5053
const isCustomPrice = typeof plan.price === "string";
5154

55+
const trackEvent = useTrack();
5256
const remainingTrialDays =
5357
(activeTrialEndsAt ? remainingDays(activeTrialEndsAt) : 0) || 0;
5458

59+
const handleCTAClick = () => {
60+
cta?.onClick?.();
61+
trackEvent({
62+
category: "account",
63+
label: `${billingPlan}Plan`,
64+
action: "click",
65+
});
66+
};
67+
68+
const ctaVariant = cta?.variant || "outline";
69+
5570
return (
5671
<div
5772
className={cn(
58-
"z-10 flex w-full flex-col gap-6 rounded-xl border border-border bg-card p-4 md:p-6",
73+
"z-10 flex w-full flex-col gap-4 rounded-xl border border-border bg-card p-4",
5974
current && "border-blue-500",
6075
highlighted && "border-active-border",
6176
)}
@@ -71,44 +86,27 @@ export const PricingCard: React.FC<PricingCardProps> = ({
7186
<div className="flex flex-col gap-5">
7287
{/* Title + Desc */}
7388
<div>
74-
<div className="mb-2 flex flex-row items-center gap-2">
89+
<div className="mb-1 flex flex-row items-center gap-3">
7590
<h3 className="font-semibold text-2xl capitalize tracking-tight">
7691
{plan.title}
7792
</h3>
7893
{current && <Badge className="capitalize">Current plan</Badge>}
7994
</div>
80-
<p className="max-w-[320px] text-muted-foreground">
95+
<p className="max-w-[320px] text-muted-foreground text-sm">
8196
{plan.description}
8297
</p>
8398
</div>
8499

85100
{/* Price */}
86101
<div className="flex flex-col gap-0.5">
87102
<div className="flex items-center gap-2">
88-
<span className="font-semibold text-3xl text-foreground tracking-tight">
103+
<span className="font-semibold text-2xl text-foreground tracking-tight">
89104
${plan.price}
90105
</span>
91106

92107
{!isCustomPrice && (
93108
<span className="text-muted-foreground">/ month</span>
94109
)}
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-
)}
112110
</div>
113111

114112
{remainingTrialDays > 0 && (
@@ -124,7 +122,7 @@ export const PricingCard: React.FC<PricingCardProps> = ({
124122

125123
<div className="flex grow flex-col items-start gap-2 text-foreground">
126124
{plan.subTitle && (
127-
<p className="font-medium text-foreground">{plan.subTitle}</p>
125+
<p className="font-medium text-foreground text-sm">{plan.subTitle}</p>
128126
)}
129127

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

135133
{cta && (
136134
<div className="flex flex-col gap-3">
137-
{billingPlan !== "pro" ? (
135+
{billingPlanToSkuMap[billingPlan] && cta.type === "checkout" && (
138136
<CheckoutButton
139137
buttonProps={{
140-
variant: cta.variant || "outline",
141-
className: "gap-2",
142-
onClick: cta.onClick,
138+
variant: ctaVariant,
139+
className: cn(ctaVariant === "outline" && "bg-background"),
140+
onClick: handleCTAClick,
143141
}}
144142
teamSlug={teamSlug}
145-
sku={billingPlan === "starter" ? "plan:starter" : "plan:growth"}
143+
sku={billingPlanToSkuMap[billingPlan]}
146144
getBillingCheckoutUrl={getBillingCheckoutUrl}
147145
>
148146
{cta.title}
149147
</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-
>
148+
)}
149+
150+
{cta.type === "link" && (
151+
<Button
152+
className={cn(ctaVariant === "outline" && "bg-background")}
153+
variant={ctaVariant}
154+
asChild
155+
>
156+
<Link href={cta.href} target="_blank" onClick={handleCTAClick}>
158157
{cta.title}
159-
</TrackedLinkTW>
158+
</Link>
160159
</Button>
161160
)}
162161

@@ -171,6 +170,19 @@ export const PricingCard: React.FC<PricingCardProps> = ({
171170
);
172171
};
173172

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

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
// keep in line with product SKUs in the backend
22
export type ProductSKU =
3-
| "plan:starter"
3+
| "plan:starter" // this will be removed later
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)