From cde2dd941f0cfb17c55c9c3736757e7ffddc670b Mon Sep 17 00:00:00 2001 From: jnsdls Date: Fri, 23 May 2025 04:21:27 +0000 Subject: [PATCH] Make free plan badge clickable to upgrade (#7134) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 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. ## 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. --- ## 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 `` with `` 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}` --- .../blocks/UpsellBannerCard.stories.tsx | 75 +++++++++++ .../@/components/blocks/UpsellBannerCard.tsx | 119 ++++++++++++++++++ .../(app)/account/overview/AccountTeamsUI.tsx | 2 +- .../app/(app)/components/TeamPlanBadge.tsx | 28 ++++- .../_components/BillingAlertBannersUI.tsx | 14 ++- .../_components/FreePlanUpsellBannerUI.tsx | 33 +++++ .../(app)/team/[team_slug]/(team)/page.tsx | 18 ++- .../[team_slug]/(team)/~/usage/rpc/page.tsx | 2 +- .../src/app/(app)/team/[team_slug]/layout.tsx | 34 +++-- .../components/TeamHeader/TeamHeaderUI.tsx | 2 +- .../components/TeamHeader/TeamSelectionUI.tsx | 5 +- .../app/(app)/team/~/[[...paths]]/page.tsx | 5 +- .../onboarding/ApplyForOpCreditsModal.tsx | 1 + .../src/components/onboarding/PlanCard.tsx | 2 +- .../settings/Account/Billing/GatedSwitch.tsx | 6 +- 15 files changed, 318 insertions(+), 28 deletions(-) create mode 100644 apps/dashboard/src/@/components/blocks/UpsellBannerCard.stories.tsx create mode 100644 apps/dashboard/src/@/components/blocks/UpsellBannerCard.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/FreePlanUpsellBannerUI.tsx 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 && ( - + )}