Skip to content

Make free plan badge clickable to upgrade #7134

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 1 commit into from
May 23, 2025
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
@@ -0,0 +1,75 @@
import type { Meta, StoryObj } from "@storybook/react";
import { ArrowRightIcon, RocketIcon, StarIcon } from "lucide-react";
import { BadgeContainer } from "../../../stories/utils";
import { UpsellBannerCard } from "./UpsellBannerCard";

function Story() {
return (
<div className="container flex max-w-4xl flex-col gap-8 py-10">
<BadgeContainer label="Green with icon (default)">
<UpsellBannerCard
title="Unlock more with thirdweb"
description="Upgrade to increase limits and access advanced features."
cta={{
text: "View plans",
icon: <ArrowRightIcon className="size-4" />,
link: "#",
}}
trackingCategory="storybook"
trackingLabel="green"
icon={<RocketIcon className="size-5" />}
accentColor="green"
/>
</BadgeContainer>

<BadgeContainer label="Blue accent">
<UpsellBannerCard
title="Need more storage?"
description="Add additional space to your account."
cta={{
text: "Upgrade storage",
icon: <ArrowRightIcon className="size-4" />,
link: "#",
}}
trackingCategory="storybook"
trackingLabel="blue"
icon={<StarIcon className="size-5" />}
accentColor="blue"
/>
</BadgeContainer>

<BadgeContainer label="Purple without leading icon">
<UpsellBannerCard
title="Join the beta"
description="Get early access to experimental features."
cta={{
text: "Request access",
icon: <ArrowRightIcon className="size-4" />,
link: "#",
}}
trackingCategory="storybook"
trackingLabel="purple"
accentColor="purple"
/>
</BadgeContainer>
</div>
);
}

const meta = {
title: "blocks/Banners/UpsellBannerCard",
component: Story,
parameters: {
nextjs: {
appDirectory: true,
},
},
} satisfies Meta<typeof Story>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Variants: Story = {
args: {},
};
119 changes: 119 additions & 0 deletions apps/dashboard/src/@/components/blocks/UpsellBannerCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"use client";

import { Button } from "@/components/ui/button";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { cn } from "@/lib/utils";
import type React from "react";

const ACCENT = {
green: {
border: "border-green-600 dark:border-green-700",
bgFrom: "from-green-50 dark:from-green-900/20",
blur: "bg-green-600",
title: "text-green-900 dark:text-green-200",
desc: "text-green-800 dark:text-green-300",
iconBg: "bg-green-600 text-white",
btn: "bg-green-600 text-white hover:bg-green-700",
},
blue: {
border: "border-blue-600 dark:border-blue-700",
bgFrom: "from-blue-50 dark:from-blue-900/20",
blur: "bg-blue-600",
title: "text-blue-900 dark:text-blue-200",
desc: "text-blue-800 dark:text-blue-300",
iconBg: "bg-blue-600 text-white",
btn: "bg-blue-600 text-white hover:bg-blue-700",
},
purple: {
border: "border-purple-600 dark:border-purple-700",
bgFrom: "from-purple-50 dark:from-purple-900/20",
blur: "bg-purple-600",
title: "text-purple-900 dark:text-purple-200",
desc: "text-purple-800 dark:text-purple-300",
iconBg: "bg-purple-600 text-white",
btn: "bg-purple-600 text-white hover:bg-purple-700",
},
} as const;

type UpsellBannerCardProps = {
title: React.ReactNode;
description: React.ReactNode;
cta: {
text: React.ReactNode;
icon?: React.ReactNode;
link: string;
};
trackingCategory: string;
trackingLabel: string;
accentColor?: keyof typeof ACCENT;
icon?: React.ReactNode;
};

export function UpsellBannerCard(props: UpsellBannerCardProps) {
const color = ACCENT[props.accentColor || "green"];

return (
<div
className={cn(
"relative overflow-hidden rounded-lg border bg-gradient-to-r p-5",
color.border,
color.bgFrom,
)}
>
{/* Decorative blur */}
<div
className={cn(
"-right-10 -top-10 pointer-events-none absolute size-28 rounded-full opacity-20 blur-2xl",
color.blur,
)}
/>

<div className="relative flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-start gap-3">
{props.icon ? (
<div
className={cn(
"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-full",
color.iconBg,
)}
>
{props.icon}
</div>
) : null}

<div>
<h3
className={cn(
"font-semibold text-lg tracking-tight",
color.title,
)}
>
{props.title}
</h3>
<p className={cn("mt-0.5 text-sm", color.desc)}>
{props.description}
</p>
</div>
</div>

<Button
asChild
size="sm"
className={cn(
"mt-2 gap-2 hover:translate-y-0 hover:shadow-inner sm:mt-0",
color.btn,
)}
>
<TrackedLinkTW
href={props.cta.link}
category={props.trackingCategory}
label={props.trackingLabel}
>
{props.cta.text}
{props.cta.icon && <span className="ml-2">{props.cta.icon}</span>}
</TrackedLinkTW>
</Button>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ function TeamRow(props: {
<div>
<div className="flex items-center gap-3">
<p className="font-semibold text-sm">{props.team.name}</p>
<TeamPlanBadge plan={plan} />
<TeamPlanBadge plan={plan} teamSlug={props.team.slug} />
</div>
<p className="text-muted-foreground text-sm capitalize">
{props.role.toLowerCase()}
Expand Down
28 changes: 27 additions & 1 deletion apps/dashboard/src/app/(app)/components/TeamPlanBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
"use client";

import type { Team } from "@/api/team";
import { Badge, type BadgeProps } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import Link from "next/link";
import { useTrack } from "../../../hooks/analytics/useTrack";

const teamPlanToBadgeVariant: Record<
Team["billingPlan"],
Expand Down Expand Up @@ -31,16 +35,38 @@ export function getTeamPlanBadgeLabel(plan: Team["billingPlan"]) {
}

export function TeamPlanBadge(props: {
teamSlug: string;
plan: Team["billingPlan"];
className?: string;
postfix?: string;
}) {
return (
const badge = (
<Badge
variant={teamPlanToBadgeVariant[props.plan]}
className={cn("px-1.5 capitalize", props.className)}
>
{`${getTeamPlanBadgeLabel(props.plan)}${props.postfix || ""}`}
</Badge>
);

const track = useTrack();

if (props.plan === "free") {
return (
<Link
href={`/team/${props.teamSlug}/~/settings/billing?showPlans=true`}
onClick={() => {
track({
category: "billing",
action: "show_plans",
label: "team_badge",
});
}}
>
{badge}
</Link>
);
}

return badge;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { cn } from "@/lib/utils";
import Link from "next/link";
import { useTransition } from "react";
import { useStripeRedirectEvent } from "../../../../(stripe)/stripe-redirect/stripeRedirectChannel";

Expand Down Expand Up @@ -54,9 +54,17 @@ function BillingAlertBanner(props: {
"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",
)}
>
<Link href={`/team/${props.teamSlug}/~/settings/invoices`}>
<TrackedLinkTW
href={`/team/${props.teamSlug}/~/settings/invoices`}
category="billingBanner"
label={
props.variant === "warning"
? "pastDue_viewInvoices"
: "serviceCutoff_payNow"
}
>
{props.ctaLabel}
</Link>
</TrackedLinkTW>
</Button>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"use client";

import type { Team } from "@/api/team";
import { UpsellBannerCard } from "@/components/blocks/UpsellBannerCard";
import { ArrowRightIcon, RocketIcon } from "lucide-react";

/**
* Banner shown to teams on the free plan encouraging them to upgrade.
* It links to the team's billing settings page and automatically opens
* the pricing modal via the `showPlans=true` query param.
*/
export function FreePlanUpsellBannerUI(props: {
teamSlug: string;
highlightPlan: Team["billingPlan"];
}) {
return (
<UpsellBannerCard
title="Unlock more with thirdweb"
description="Upgrade to increase limits and access advanced features."
cta={{
text: "View plans",
icon: <ArrowRightIcon className="size-4" />,
link: `/team/${props.teamSlug}/~/settings/billing?showPlans=true&highlight=${
props.highlightPlan || "growth"
}`,
}}
trackingCategory="billingBanner"
trackingLabel="freePlan_viewPlans"
icon={<RocketIcon className="size-5" />}
accentColor="green"
/>
);
}
18 changes: 13 additions & 5 deletions apps/dashboard/src/app/(app)/team/[team_slug]/(team)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { redirect } from "next/navigation";
import { getAuthToken } from "../../../api/lib/getAuthToken";
import { loginRedirect } from "../../../login/loginRedirect";
import { Changelog } from "./_components/Changelog";
import { FreePlanUpsellBannerUI } from "./_components/FreePlanUpsellBannerUI";
import { InviteTeamMembersButton } from "./_components/invite-team-members-button";
import {
type ProjectWithAnalytics,
Expand Down Expand Up @@ -55,11 +56,18 @@ export default async function Page(props: {
<div className="container flex grow flex-col gap-4 lg:flex-row">
{/* left */}
<div className="flex grow flex-col gap-6 pt-8 lg:pb-20">
<DismissibleAlert
title="Looking for Engines?"
description="Engines, contracts, project settings, and more are now managed within projects. Open or create a project to access them."
localStorageId={`${team.id}-engines-alert`}
/>
{team.billingPlan === "free" ? (
<FreePlanUpsellBannerUI
teamSlug={team.slug}
highlightPlan="growth"
/>
) : (
<DismissibleAlert
title="Looking for Engines?"
description="Engines, contracts, project settings, and more are now managed within projects. Open or create a project to access them."
localStorageId={`${team.id}-engines-alert`}
/>
)}

<TeamProjectsPage
projects={projectsWithTotalWallets}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export default async function RPCUsage(props: {
<div className="font-bold text-2xl capitalize">
{currentRateLimit.toLocaleString()} RPS
</div>
<TeamPlanBadge plan={currentPlan} />
<TeamPlanBadge plan={currentPlan} teamSlug={team.slug} />
</div>
</CardContent>
</Card>
Expand Down
Loading
Loading