Skip to content

Commit 0d54958

Browse files
committed
Make free plan badge clickable to upgrade
1 parent 23029db commit 0d54958

File tree

14 files changed

+238
-28
lines changed

14 files changed

+238
-28
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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+
},
17+
blue: {
18+
border: "border-blue-600 dark:border-blue-700",
19+
bgFrom: "from-blue-50 dark:from-blue-900/20",
20+
blur: "bg-blue-600",
21+
title: "text-blue-900 dark:text-blue-200",
22+
desc: "text-blue-800 dark:text-blue-300",
23+
iconBg: "bg-blue-600 text-white",
24+
},
25+
purple: {
26+
border: "border-purple-600 dark:border-purple-700",
27+
bgFrom: "from-purple-50 dark:from-purple-900/20",
28+
blur: "bg-purple-600",
29+
title: "text-purple-900 dark:text-purple-200",
30+
desc: "text-purple-800 dark:text-purple-300",
31+
iconBg: "bg-purple-600 text-white",
32+
},
33+
} as const;
34+
35+
export interface UpsellBannerCardProps {
36+
title: React.ReactNode;
37+
description: React.ReactNode;
38+
cta: {
39+
text: React.ReactNode;
40+
icon?: React.ReactNode;
41+
link: string;
42+
};
43+
trackingCategory: string;
44+
trackingLabel: string;
45+
accentColor?: keyof typeof ACCENT;
46+
icon?: React.ReactNode;
47+
}
48+
49+
export function UpsellBannerCard(props: UpsellBannerCardProps) {
50+
const color = ACCENT[props.accentColor || "green"];
51+
52+
return (
53+
<div
54+
className={cn(
55+
"relative overflow-hidden rounded-lg border bg-gradient-to-r p-5",
56+
color.border,
57+
color.bgFrom,
58+
)}
59+
>
60+
{/* Decorative blur */}
61+
<div
62+
className={cn(
63+
"-right-10 -top-10 pointer-events-none absolute size-28 rounded-full opacity-20 blur-2xl",
64+
color.blur,
65+
)}
66+
/>
67+
68+
<div className="relative flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
69+
<div className="flex items-start gap-3">
70+
{props.icon ? (
71+
<div
72+
className={cn(
73+
"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-full",
74+
color.iconBg,
75+
)}
76+
>
77+
{props.icon}
78+
</div>
79+
) : null}
80+
81+
<div>
82+
<h3
83+
className={cn(
84+
"font-semibold text-lg tracking-tight",
85+
color.title,
86+
)}
87+
>
88+
{props.title}
89+
</h3>
90+
<p className={cn("mt-0.5 text-sm", color.desc)}>
91+
{props.description}
92+
</p>
93+
</div>
94+
</div>
95+
96+
<Button
97+
asChild
98+
variant="upsell"
99+
size="sm"
100+
className="mt-2 gap-2 hover:translate-y-0 hover:shadow-inner sm:mt-0"
101+
>
102+
<TrackedLinkTW
103+
href={props.cta.link}
104+
category={props.trackingCategory}
105+
label={props.trackingLabel}
106+
>
107+
{props.cta.text}
108+
{props.cta.icon && <span className="ml-2">{props.cta.icon}</span>}
109+
</TrackedLinkTW>
110+
</Button>
111+
</div>
112+
</div>
113+
);
114+
}

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>

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

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { getTeamBySlug } from "@/api/team";
22
import { Button } from "@/components/ui/button";
3+
import { TrackedLinkTW } from "@/components/ui/tracked-link";
34
import { PosthogIdentifierServer } from "components/wallets/PosthogIdentifierServer";
45
import { ArrowRightIcon } from "lucide-react";
5-
import Link from "next/link";
66
import { redirect } from "next/navigation";
77
import { Suspense } from "react";
88
import { EnsureValidConnectedWalletLoginServer } from "../../components/EnsureValidConnectedWalletLogin/EnsureValidConnectedWalletLoginServer";
@@ -31,17 +31,25 @@ export default async function RootTeamLayout(props: {
3131
return (
3232
<div className="flex min-h-dvh flex-col">
3333
<div className="flex grow flex-col">
34-
{team.billingPlan === "starter_legacy" && (
35-
<StarterLegacyDiscontinuedBanner teamSlug={team_slug} />
36-
)}
34+
{(() => {
35+
// Show only one banner at a time following priority:
36+
// 1. Service cut off (invalid payment)
37+
// 2. Past due invoices
38+
// 3. Starter legacy plan discontinued notice
39+
if (team.billingStatus === "invalidPayment") {
40+
return <ServiceCutOffBanner teamSlug={team_slug} />;
41+
}
3742

38-
{team.billingStatus === "pastDue" && (
39-
<PastDueBanner teamSlug={team_slug} />
40-
)}
43+
if (team.billingStatus === "pastDue") {
44+
return <PastDueBanner teamSlug={team_slug} />;
45+
}
4146

42-
{team.billingStatus === "invalidPayment" && (
43-
<ServiceCutOffBanner teamSlug={team_slug} />
44-
)}
47+
if (team.billingPlan === "starter_legacy") {
48+
return <StarterLegacyDiscontinuedBanner teamSlug={team_slug} />;
49+
}
50+
51+
return null;
52+
})()}
4553

4654
{props.children}
4755
</div>
@@ -74,12 +82,14 @@ function StarterLegacyDiscontinuedBanner(props: {
7482
size="sm"
7583
className="mt-3 gap-2 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"
7684
>
77-
<Link
85+
<TrackedLinkTW
7886
href={`/team/${props.teamSlug}/~/settings/billing?showPlans=true`}
87+
category="billingBanner"
88+
label="starterLegacy_selectPlan"
7989
>
8090
Select a new plan
8191
<ArrowRightIcon className="size-4" />
82-
</Link>
92+
</TrackedLinkTW>
8393
</Button>
8494
</div>
8595
</div>

apps/dashboard/src/app/(app)/team/components/TeamHeader/TeamHeaderUI.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export function TeamHeaderDesktopUI(props: TeamHeaderCompProps) {
6666
/>
6767
<span> {currentTeam.name} </span>
6868
<TeamVerifiedIcon domain={currentTeam.verifiedDomain} />
69-
<TeamPlanBadge plan={teamPlan} />
69+
<TeamPlanBadge plan={teamPlan} teamSlug={currentTeam.slug} />
7070
</Link>
7171

7272
<TeamAndProjectSelectorPopoverButton

apps/dashboard/src/app/(app)/team/components/TeamHeader/TeamSelectionUI.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,10 @@ export function TeamSelectionUI(props: {
115115
<TeamVerifiedIcon domain={team.verifiedDomain} />
116116
</div>
117117

118-
<TeamPlanBadge plan={team.billingPlan} />
118+
<TeamPlanBadge
119+
plan={team.billingPlan}
120+
teamSlug={team.slug}
121+
/>
119122
</Link>
120123
</Button>
121124
</li>

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,10 @@ export default async function Page(props: {
101101
>
102102
{team.name}
103103
</Link>
104-
<TeamPlanBadge plan={team.billingPlan} />
104+
<TeamPlanBadge
105+
plan={team.billingPlan}
106+
teamSlug={team.slug}
107+
/>
105108
<ChevronRightIcon className="ml-auto size-4 text-muted-foreground group-hover:text-foreground" />
106109
</div>
107110
);

apps/dashboard/src/components/onboarding/ApplyForOpCreditsModal.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export function ApplyForOpCredits(props: {
113113
<TeamPlanBadge
114114
className="absolute top-5 right-6"
115115
plan={creditsRecord.plan}
116+
teamSlug={team.slug}
116117
/>
117118
</div>
118119

apps/dashboard/src/components/onboarding/PlanCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export function PlanCard({ creditsRecord, teamSlug }: PlanCardProps) {
1515
<h2 className="font-semibold text-foreground text-lg tracking-tight">
1616
{creditsRecord.upTo || "Up To"} {creditsRecord.credits} Gas Credits
1717
</h2>
18-
<TeamPlanBadge plan={creditsRecord.plan} />
18+
<TeamPlanBadge plan={creditsRecord.plan} teamSlug={teamSlug} />
1919
</div>
2020

2121
<div className="flex flex-col gap-2 px-6 py-4">

0 commit comments

Comments
 (0)