Skip to content

Commit cde2dd9

Browse files
committed
Make free plan badge clickable to upgrade (#7134)
# Add Upgrade Paths for Free Plan Teams This PR enhances the user experience for teams on the free plan by adding clear upgrade paths throughout the dashboard: - Makes the free plan badge clickable, linking directly to the billing page with pricing modal open - Adds analytics tracking to billing-related links for better conversion insights - Implements a new `FreePlanUpsellBannerUI` component on the team dashboard page - Ensures only one billing alert banner shows at a time (following priority order) - Updates all `TeamPlanBadge` components to include the team slug parameter - Improves billing alert banners with proper analytics tracking These changes aim to increase conversions from free to paid plans by providing contextual upgrade opportunities throughout the product. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a promotional upsell banner for teams on the free plan, encouraging upgrades with a direct link to view available plans. - Added a customizable promotional banner component with multiple accent color themes for enhanced marketing displays. - **Enhancements** - Team plan badges now provide direct links to billing settings for teams on the free plan, with analytics tracking for user interactions. - Improved analytics tracking on billing-related banners and links throughout the dashboard. - **Refactor** - Updated multiple components to pass team identifiers to plan badges for more contextual display and interaction. - Streamlined the display logic for billing-related banners to prioritize service status and plan type. <!-- end of auto-generated comment: release notes by coderabbit.ai --> <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on enhancing the `TeamPlanBadge` component by adding a `teamSlug` prop across various files and implementing a new `FreePlanUpsellBannerUI` component to encourage free plan users to upgrade. It also updates links to use `TrackedLinkTW` for better tracking. ### Detailed summary - Added `teamSlug` prop to `TeamPlanBadge` in multiple components. - Introduced `FreePlanUpsellBannerUI` to promote upgrades for free plan users. - Replaced `<Link>` with `<TrackedLinkTW>` for better tracking in several instances. - Updated conditional rendering logic for billing alerts in the team layout. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 7130f9a commit cde2dd9

File tree

15 files changed

+318
-28
lines changed

15 files changed

+318
-28
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { ArrowRightIcon, RocketIcon, StarIcon } from "lucide-react";
3+
import { BadgeContainer } from "../../../stories/utils";
4+
import { UpsellBannerCard } from "./UpsellBannerCard";
5+
6+
function Story() {
7+
return (
8+
<div className="container flex max-w-4xl flex-col gap-8 py-10">
9+
<BadgeContainer label="Green with icon (default)">
10+
<UpsellBannerCard
11+
title="Unlock more with thirdweb"
12+
description="Upgrade to increase limits and access advanced features."
13+
cta={{
14+
text: "View plans",
15+
icon: <ArrowRightIcon className="size-4" />,
16+
link: "#",
17+
}}
18+
trackingCategory="storybook"
19+
trackingLabel="green"
20+
icon={<RocketIcon className="size-5" />}
21+
accentColor="green"
22+
/>
23+
</BadgeContainer>
24+
25+
<BadgeContainer label="Blue accent">
26+
<UpsellBannerCard
27+
title="Need more storage?"
28+
description="Add additional space to your account."
29+
cta={{
30+
text: "Upgrade storage",
31+
icon: <ArrowRightIcon className="size-4" />,
32+
link: "#",
33+
}}
34+
trackingCategory="storybook"
35+
trackingLabel="blue"
36+
icon={<StarIcon className="size-5" />}
37+
accentColor="blue"
38+
/>
39+
</BadgeContainer>
40+
41+
<BadgeContainer label="Purple without leading icon">
42+
<UpsellBannerCard
43+
title="Join the beta"
44+
description="Get early access to experimental features."
45+
cta={{
46+
text: "Request access",
47+
icon: <ArrowRightIcon className="size-4" />,
48+
link: "#",
49+
}}
50+
trackingCategory="storybook"
51+
trackingLabel="purple"
52+
accentColor="purple"
53+
/>
54+
</BadgeContainer>
55+
</div>
56+
);
57+
}
58+
59+
const meta = {
60+
title: "blocks/Banners/UpsellBannerCard",
61+
component: Story,
62+
parameters: {
63+
nextjs: {
64+
appDirectory: true,
65+
},
66+
},
67+
} satisfies Meta<typeof Story>;
68+
69+
export default meta;
70+
71+
type Story = StoryObj<typeof meta>;
72+
73+
export const Variants: Story = {
74+
args: {},
75+
};
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"use client";
2+
3+
import { Button } from "@/components/ui/button";
4+
import { TrackedLinkTW } from "@/components/ui/tracked-link";
5+
import { cn } from "@/lib/utils";
6+
import type React from "react";
7+
8+
const ACCENT = {
9+
green: {
10+
border: "border-green-600 dark:border-green-700",
11+
bgFrom: "from-green-50 dark:from-green-900/20",
12+
blur: "bg-green-600",
13+
title: "text-green-900 dark:text-green-200",
14+
desc: "text-green-800 dark:text-green-300",
15+
iconBg: "bg-green-600 text-white",
16+
btn: "bg-green-600 text-white hover:bg-green-700",
17+
},
18+
blue: {
19+
border: "border-blue-600 dark:border-blue-700",
20+
bgFrom: "from-blue-50 dark:from-blue-900/20",
21+
blur: "bg-blue-600",
22+
title: "text-blue-900 dark:text-blue-200",
23+
desc: "text-blue-800 dark:text-blue-300",
24+
iconBg: "bg-blue-600 text-white",
25+
btn: "bg-blue-600 text-white hover:bg-blue-700",
26+
},
27+
purple: {
28+
border: "border-purple-600 dark:border-purple-700",
29+
bgFrom: "from-purple-50 dark:from-purple-900/20",
30+
blur: "bg-purple-600",
31+
title: "text-purple-900 dark:text-purple-200",
32+
desc: "text-purple-800 dark:text-purple-300",
33+
iconBg: "bg-purple-600 text-white",
34+
btn: "bg-purple-600 text-white hover:bg-purple-700",
35+
},
36+
} as const;
37+
38+
type UpsellBannerCardProps = {
39+
title: React.ReactNode;
40+
description: React.ReactNode;
41+
cta: {
42+
text: React.ReactNode;
43+
icon?: React.ReactNode;
44+
link: string;
45+
};
46+
trackingCategory: string;
47+
trackingLabel: string;
48+
accentColor?: keyof typeof ACCENT;
49+
icon?: React.ReactNode;
50+
};
51+
52+
export function UpsellBannerCard(props: UpsellBannerCardProps) {
53+
const color = ACCENT[props.accentColor || "green"];
54+
55+
return (
56+
<div
57+
className={cn(
58+
"relative overflow-hidden rounded-lg border bg-gradient-to-r p-5",
59+
color.border,
60+
color.bgFrom,
61+
)}
62+
>
63+
{/* Decorative blur */}
64+
<div
65+
className={cn(
66+
"-right-10 -top-10 pointer-events-none absolute size-28 rounded-full opacity-20 blur-2xl",
67+
color.blur,
68+
)}
69+
/>
70+
71+
<div className="relative flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
72+
<div className="flex items-start gap-3">
73+
{props.icon ? (
74+
<div
75+
className={cn(
76+
"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-full",
77+
color.iconBg,
78+
)}
79+
>
80+
{props.icon}
81+
</div>
82+
) : null}
83+
84+
<div>
85+
<h3
86+
className={cn(
87+
"font-semibold text-lg tracking-tight",
88+
color.title,
89+
)}
90+
>
91+
{props.title}
92+
</h3>
93+
<p className={cn("mt-0.5 text-sm", color.desc)}>
94+
{props.description}
95+
</p>
96+
</div>
97+
</div>
98+
99+
<Button
100+
asChild
101+
size="sm"
102+
className={cn(
103+
"mt-2 gap-2 hover:translate-y-0 hover:shadow-inner sm:mt-0",
104+
color.btn,
105+
)}
106+
>
107+
<TrackedLinkTW
108+
href={props.cta.link}
109+
category={props.trackingCategory}
110+
label={props.trackingLabel}
111+
>
112+
{props.cta.text}
113+
{props.cta.icon && <span className="ml-2">{props.cta.icon}</span>}
114+
</TrackedLinkTW>
115+
</Button>
116+
</div>
117+
</div>
118+
);
119+
}

apps/dashboard/src/app/(app)/account/overview/AccountTeamsUI.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ function TeamRow(props: {
116116
<div>
117117
<div className="flex items-center gap-3">
118118
<p className="font-semibold text-sm">{props.team.name}</p>
119-
<TeamPlanBadge plan={plan} />
119+
<TeamPlanBadge plan={plan} teamSlug={props.team.slug} />
120120
</div>
121121
<p className="text-muted-foreground text-sm capitalize">
122122
{props.role.toLowerCase()}

apps/dashboard/src/app/(app)/components/TeamPlanBadge.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
"use client";
2+
13
import type { Team } from "@/api/team";
24
import { Badge, type BadgeProps } from "@/components/ui/badge";
35
import { cn } from "@/lib/utils";
6+
import Link from "next/link";
7+
import { useTrack } from "../../../hooks/analytics/useTrack";
48

59
const teamPlanToBadgeVariant: Record<
610
Team["billingPlan"],
@@ -31,16 +35,38 @@ export function getTeamPlanBadgeLabel(plan: Team["billingPlan"]) {
3135
}
3236

3337
export function TeamPlanBadge(props: {
38+
teamSlug: string;
3439
plan: Team["billingPlan"];
3540
className?: string;
3641
postfix?: string;
3742
}) {
38-
return (
43+
const badge = (
3944
<Badge
4045
variant={teamPlanToBadgeVariant[props.plan]}
4146
className={cn("px-1.5 capitalize", props.className)}
4247
>
4348
{`${getTeamPlanBadgeLabel(props.plan)}${props.postfix || ""}`}
4449
</Badge>
4550
);
51+
52+
const track = useTrack();
53+
54+
if (props.plan === "free") {
55+
return (
56+
<Link
57+
href={`/team/${props.teamSlug}/~/settings/billing?showPlans=true`}
58+
onClick={() => {
59+
track({
60+
category: "billing",
61+
action: "show_plans",
62+
label: "team_badge",
63+
});
64+
}}
65+
>
66+
{badge}
67+
</Link>
68+
);
69+
}
70+
71+
return badge;
4672
}

apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/BillingAlertBannersUI.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
import { Spinner } from "@/components/ui/Spinner/Spinner";
44
import { Button } from "@/components/ui/button";
5+
import { TrackedLinkTW } from "@/components/ui/tracked-link";
56
import { useDashboardRouter } from "@/lib/DashboardRouter";
67
import { cn } from "@/lib/utils";
7-
import Link from "next/link";
88
import { useTransition } from "react";
99
import { useStripeRedirectEvent } from "../../../../(stripe)/stripe-redirect/stripeRedirectChannel";
1010

@@ -54,9 +54,17 @@ function BillingAlertBanner(props: {
5454
"border border-red-600 bg-red-100 text-red-800 hover:bg-red-200 dark:border-red-700 dark:bg-red-900 dark:text-red-100 dark:hover:bg-red-800",
5555
)}
5656
>
57-
<Link href={`/team/${props.teamSlug}/~/settings/invoices`}>
57+
<TrackedLinkTW
58+
href={`/team/${props.teamSlug}/~/settings/invoices`}
59+
category="billingBanner"
60+
label={
61+
props.variant === "warning"
62+
? "pastDue_viewInvoices"
63+
: "serviceCutoff_payNow"
64+
}
65+
>
5866
{props.ctaLabel}
59-
</Link>
67+
</TrackedLinkTW>
6068
</Button>
6169
</div>
6270
);
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"use client";
2+
3+
import type { Team } from "@/api/team";
4+
import { UpsellBannerCard } from "@/components/blocks/UpsellBannerCard";
5+
import { ArrowRightIcon, RocketIcon } from "lucide-react";
6+
7+
/**
8+
* Banner shown to teams on the free plan encouraging them to upgrade.
9+
* It links to the team's billing settings page and automatically opens
10+
* the pricing modal via the `showPlans=true` query param.
11+
*/
12+
export function FreePlanUpsellBannerUI(props: {
13+
teamSlug: string;
14+
highlightPlan: Team["billingPlan"];
15+
}) {
16+
return (
17+
<UpsellBannerCard
18+
title="Unlock more with thirdweb"
19+
description="Upgrade to increase limits and access advanced features."
20+
cta={{
21+
text: "View plans",
22+
icon: <ArrowRightIcon className="size-4" />,
23+
link: `/team/${props.teamSlug}/~/settings/billing?showPlans=true&highlight=${
24+
props.highlightPlan || "growth"
25+
}`,
26+
}}
27+
trackingCategory="billingBanner"
28+
trackingLabel="freePlan_viewPlans"
29+
icon={<RocketIcon className="size-5" />}
30+
accentColor="green"
31+
/>
32+
);
33+
}

apps/dashboard/src/app/(app)/team/[team_slug]/(team)/page.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { redirect } from "next/navigation";
88
import { getAuthToken } from "../../../api/lib/getAuthToken";
99
import { loginRedirect } from "../../../login/loginRedirect";
1010
import { Changelog } from "./_components/Changelog";
11+
import { FreePlanUpsellBannerUI } from "./_components/FreePlanUpsellBannerUI";
1112
import { InviteTeamMembersButton } from "./_components/invite-team-members-button";
1213
import {
1314
type ProjectWithAnalytics,
@@ -55,11 +56,18 @@ export default async function Page(props: {
5556
<div className="container flex grow flex-col gap-4 lg:flex-row">
5657
{/* left */}
5758
<div className="flex grow flex-col gap-6 pt-8 lg:pb-20">
58-
<DismissibleAlert
59-
title="Looking for Engines?"
60-
description="Engines, contracts, project settings, and more are now managed within projects. Open or create a project to access them."
61-
localStorageId={`${team.id}-engines-alert`}
62-
/>
59+
{team.billingPlan === "free" ? (
60+
<FreePlanUpsellBannerUI
61+
teamSlug={team.slug}
62+
highlightPlan="growth"
63+
/>
64+
) : (
65+
<DismissibleAlert
66+
title="Looking for Engines?"
67+
description="Engines, contracts, project settings, and more are now managed within projects. Open or create a project to access them."
68+
localStorageId={`${team.id}-engines-alert`}
69+
/>
70+
)}
6371

6472
<TeamProjectsPage
6573
projects={projectsWithTotalWallets}

apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/usage/rpc/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export default async function RPCUsage(props: {
8686
<div className="font-bold text-2xl capitalize">
8787
{currentRateLimit.toLocaleString()} RPS
8888
</div>
89-
<TeamPlanBadge plan={currentPlan} />
89+
<TeamPlanBadge plan={currentPlan} teamSlug={team.slug} />
9090
</div>
9191
</CardContent>
9292
</Card>

0 commit comments

Comments
 (0)