diff --git a/apps/dashboard/src/@/components/blocks/UpsellBannerCard.stories.tsx b/apps/dashboard/src/@/components/blocks/UpsellBannerCard.stories.tsx
new file mode 100644
index 00000000000..3355e4c2e08
--- /dev/null
+++ b/apps/dashboard/src/@/components/blocks/UpsellBannerCard.stories.tsx
@@ -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 (
+
+
+ ,
+ link: "#",
+ }}
+ trackingCategory="storybook"
+ trackingLabel="green"
+ icon={}
+ accentColor="green"
+ />
+
+
+
+ ,
+ link: "#",
+ }}
+ trackingCategory="storybook"
+ trackingLabel="blue"
+ icon={}
+ accentColor="blue"
+ />
+
+
+
+ ,
+ link: "#",
+ }}
+ trackingCategory="storybook"
+ trackingLabel="purple"
+ accentColor="purple"
+ />
+
+
+ );
+}
+
+const meta = {
+ title: "blocks/Banners/UpsellBannerCard",
+ component: Story,
+ parameters: {
+ nextjs: {
+ appDirectory: true,
+ },
+ },
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Variants: Story = {
+ args: {},
+};
diff --git a/apps/dashboard/src/@/components/blocks/UpsellBannerCard.tsx b/apps/dashboard/src/@/components/blocks/UpsellBannerCard.tsx
new file mode 100644
index 00000000000..04f86fbb030
--- /dev/null
+++ b/apps/dashboard/src/@/components/blocks/UpsellBannerCard.tsx
@@ -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 (
+
+ {/* Decorative blur */}
+
+
+
+
+ {props.icon ? (
+
+ {props.icon}
+
+ ) : null}
+
+
+
+ {props.title}
+
+
+ {props.description}
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/account/overview/AccountTeamsUI.tsx b/apps/dashboard/src/app/(app)/account/overview/AccountTeamsUI.tsx
index 11c7c50b852..fc1b6cc3d2c 100644
--- a/apps/dashboard/src/app/(app)/account/overview/AccountTeamsUI.tsx
+++ b/apps/dashboard/src/app/(app)/account/overview/AccountTeamsUI.tsx
@@ -116,7 +116,7 @@ function TeamRow(props: {
{props.role.toLowerCase()}
diff --git a/apps/dashboard/src/app/(app)/components/TeamPlanBadge.tsx b/apps/dashboard/src/app/(app)/components/TeamPlanBadge.tsx
index 2bd74d420e9..a18315e9d96 100644
--- a/apps/dashboard/src/app/(app)/components/TeamPlanBadge.tsx
+++ b/apps/dashboard/src/app/(app)/components/TeamPlanBadge.tsx
@@ -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"],
@@ -31,11 +35,12 @@ export function getTeamPlanBadgeLabel(plan: Team["billingPlan"]) {
}
export function TeamPlanBadge(props: {
+ teamSlug: string;
plan: Team["billingPlan"];
className?: string;
postfix?: string;
}) {
- return (
+ const badge = (
);
+
+ const track = useTrack();
+
+ if (props.plan === "free") {
+ return (
+ {
+ track({
+ category: "billing",
+ action: "show_plans",
+ label: "team_badge",
+ });
+ }}
+ >
+ {badge}
+
+ );
+ }
+
+ return badge;
}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/BillingAlertBannersUI.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/BillingAlertBannersUI.tsx
index 148de2c96a1..08bfd10311d 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/BillingAlertBannersUI.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/BillingAlertBannersUI.tsx
@@ -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";
@@ -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",
)}
>
-
+
{props.ctaLabel}
-
+
);
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/FreePlanUpsellBannerUI.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/FreePlanUpsellBannerUI.tsx
new file mode 100644
index 00000000000..857bd8e7036
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/FreePlanUpsellBannerUI.tsx
@@ -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 (
+ ,
+ link: `/team/${props.teamSlug}/~/settings/billing?showPlans=true&highlight=${
+ props.highlightPlan || "growth"
+ }`,
+ }}
+ trackingCategory="billingBanner"
+ trackingLabel="freePlan_viewPlans"
+ icon={}
+ accentColor="green"
+ />
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/page.tsx
index d69e5ed050e..1d7afe40978 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/page.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/page.tsx
@@ -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,
@@ -55,11 +56,18 @@ export default async function Page(props: {
{/* left */}
-
+ {team.billingPlan === "free" ? (
+
+ ) : (
+
+ )}
{currentRateLimit.toLocaleString()} RPS
-
+
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/layout.tsx
index ed5f8e79cd5..fba9572f5d5 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/layout.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/layout.tsx
@@ -1,8 +1,8 @@
import { getTeamBySlug } from "@/api/team";
import { Button } from "@/components/ui/button";
+import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { PosthogIdentifierServer } from "components/wallets/PosthogIdentifierServer";
import { ArrowRightIcon } from "lucide-react";
-import Link from "next/link";
import { redirect } from "next/navigation";
import { Suspense } from "react";
import { getAuthToken } from "../../api/lib/getAuthToken";
@@ -37,17 +37,25 @@ export default async function RootTeamLayout(props: {
return (
- {team.billingPlan === "starter_legacy" && (
-
- )}
+ {(() => {
+ // Show only one banner at a time following priority:
+ // 1. Service cut off (invalid payment)
+ // 2. Past due invoices
+ // 3. Starter legacy plan discontinued notice
+ if (team.billingStatus === "invalidPayment") {
+ return
;
+ }
- {team.billingStatus === "pastDue" && (
-
- )}
+ if (team.billingStatus === "pastDue") {
+ return
;
+ }
- {team.billingStatus === "invalidPayment" && (
-
- )}
+ if (team.billingPlan === "starter_legacy") {
+ return
;
+ }
+
+ return null;
+ })()}
{props.children}
@@ -80,12 +88,14 @@ function StarterLegacyDiscontinuedBanner(props: {
size="sm"
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"
>
-
Select a new plan
-
+
diff --git a/apps/dashboard/src/app/(app)/team/components/TeamHeader/TeamHeaderUI.tsx b/apps/dashboard/src/app/(app)/team/components/TeamHeader/TeamHeaderUI.tsx
index e7d5a32db63..c132292ac9e 100644
--- a/apps/dashboard/src/app/(app)/team/components/TeamHeader/TeamHeaderUI.tsx
+++ b/apps/dashboard/src/app/(app)/team/components/TeamHeader/TeamHeaderUI.tsx
@@ -66,7 +66,7 @@ export function TeamHeaderDesktopUI(props: TeamHeaderCompProps) {
/>
{currentTeam.name}
-
+
-
+
diff --git a/apps/dashboard/src/app/(app)/team/~/[[...paths]]/page.tsx b/apps/dashboard/src/app/(app)/team/~/[[...paths]]/page.tsx
index 3e2f0aa57dc..a3119dbb2b0 100644
--- a/apps/dashboard/src/app/(app)/team/~/[[...paths]]/page.tsx
+++ b/apps/dashboard/src/app/(app)/team/~/[[...paths]]/page.tsx
@@ -101,7 +101,10 @@ export default async function Page(props: {
>
{team.name}
-
+
);
diff --git a/apps/dashboard/src/components/onboarding/ApplyForOpCreditsModal.tsx b/apps/dashboard/src/components/onboarding/ApplyForOpCreditsModal.tsx
index da295878cb1..782fafae1a3 100644
--- a/apps/dashboard/src/components/onboarding/ApplyForOpCreditsModal.tsx
+++ b/apps/dashboard/src/components/onboarding/ApplyForOpCreditsModal.tsx
@@ -113,6 +113,7 @@ export function ApplyForOpCredits(props: {
diff --git a/apps/dashboard/src/components/onboarding/PlanCard.tsx b/apps/dashboard/src/components/onboarding/PlanCard.tsx
index 144a066355f..0cdaceb4bf5 100644
--- a/apps/dashboard/src/components/onboarding/PlanCard.tsx
+++ b/apps/dashboard/src/components/onboarding/PlanCard.tsx
@@ -15,7 +15,7 @@ export function PlanCard({ creditsRecord, teamSlug }: PlanCardProps) {
{creditsRecord.upTo || "Up To"} {creditsRecord.credits} Gas Credits
-
+
diff --git a/apps/dashboard/src/components/settings/Account/Billing/GatedSwitch.tsx b/apps/dashboard/src/components/settings/Account/Billing/GatedSwitch.tsx
index fbecb1f40d6..9a64ca1babc 100644
--- a/apps/dashboard/src/components/settings/Account/Billing/GatedSwitch.tsx
+++ b/apps/dashboard/src/components/settings/Account/Billing/GatedSwitch.tsx
@@ -63,7 +63,11 @@ export const GatedSwitch: React.FC
= (
>
{isUpgradeRequired && (
-
+
)}