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.team.name}

- +

{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 && ( - + )}