Skip to content

BillingPage: 4-tier grid + Annual-default + 2 months free + Most Popular badge + collapse promo (per pricing research)#52

Merged
mastermanas805 merged 1 commit into
mainfrom
feat/billing-page-redesign-2months-free-fresh
May 13, 2026
Merged

BillingPage: 4-tier grid + Annual-default + 2 months free + Most Popular badge + collapse promo (per pricing research)#52
mastermanas805 merged 1 commit into
mainfrom
feat/billing-page-redesign-2months-free-fresh

Conversation

@mastermanas805
Copy link
Copy Markdown
Member

Summary

Transforms BillingPage from a single current-plan card with a lone Upgrade
button into a research-backed 4-tier conversion surface. Implements the
three top-impact changes from PRICING-BEST-PRACTICES-2026-05-13.md:

  • All 4 tiers side-by-side (Free / Hobby / Pro / Team) in a responsive
    grid (4 col → 2x2 at ≤1100px → 1 col at ≤640px). Pro is highlighted with
    the "Most Popular" badge + thicker border + raised box-shadow. Team is
    the anchor (visible price, de-emphasised colour, "Contact sales" CTA).
  • Annual is the default frequency on first render. Toggle copy reads
    Annual — 2 months free. Annual prices show the monthly-equivalent
    ($7.50/mo, billed yearly) with absolute-dollar savings subtext ($90/yr · save $18).
  • Most Popular badge on Pro only. Tier-aware, price-anchored CTAs:
    Get Pro — $40.83/mo (annual) / Get Pro — $49/mo (monthly), and the
    current tier shows a Your plan pill instead of a CTA.

Promo UI: removed entirely (not collapsed)

Razorpay's hosted checkout has its own Have a code? affordance, so the
in-product promo input was both redundant and Baymard-failing (the empty
field triggers "coupon hunting" abandonment to Google). The previous P3
toggle/input/applied chip is gone; a small mono-styled line under the grid
notes Promo codes apply at checkout (Razorpay). so users know where their
code goes. validatePromotion is no longer called from this surface — the
api function stays exported for any future surface that wants it.

New components

  • src/components/TierCard.tsx — single tier card with badge, raised
    state, "Your plan" pill, and an optional ctaSlot (lets the Pro card
    compose with the existing UpgradeButton for P1's A/B variant without
    forking the grid).
  • src/components/PricingGrid.tsx — 4-column responsive grid with its own
    frequency toggle. Designed for shared use by BillingPage and a future
    marketing-page refresh; tier definitions live in one place so the
    in-product and public surfaces stay byte-identical.

Test plan

  • All 4 tier cards render (data-tier="free|hobby|pro|team")
  • Annual is default on first mount (no stored value)
  • Stored monthly preference rehydrates correctly
  • "2 months free" copy renders inside the Annual toggle position
  • Monthly-equivalent price + savings subtext renders in Annual mode only
  • Most Popular badge renders only on the Pro card
  • Current tier shows "Your plan" pill, every other tier shows a CTA
  • Tier-aware CTA copy (Get Pro — $40.83/mo, Start Hobby — $7.50/mo, etc.)
  • createCheckout strict 2-arg signature (no opts third arg)
  • Promo UI testids assert-absent across the page
  • validatePromotion is never called
  • Pre-existing tests preserved (skeleton, error state, no self-serve
    cancel, no fabricated card expiry, Usage panel from
    fetchBillingUsage, listResources never called for usage)
  • npm test: 17 files, 389 passed, 3 skipped (baseline 382 + 7 net new = 389)
  • npm run build: clean. BillingPage chunk 17.57 kB / 5.49 kB gzip
    (down from baseline 18.73 kB / 5.79 kB gzip — net shrink because
    removed promo logic outweighs the new components)
  • Playwright: dashboard E2E (upgrade-journey.spec.ts) — the
    Upgrade to Pro text-matcher selector may need an update to target
    data-testid="upgrade-button" (or the new Get Pro — $X/mo label).
    Flagging for review.

🤖 Generated with Claude Code

…lar badge + collapse promo

Replace the single-plan upgrade card with a 4-tier side-by-side grid
(Free / Hobby / Pro / Team) per pricing research. The user's current
tier shows a "Your plan" pill; every other tier shows a tier-aware,
price-anchored CTA ("Get Pro — \$40.83/mo" in Annual mode, "Get Pro —
\$49/mo" in Monthly mode, etc.).

Anchoring + defaults:
- Pro card rendered with the "Most Popular" badge + raised border +
  thicker box-shadow. Anchoring research: +25-60% mid-tier conversion,
  +158% lift from "Most Popular" badge in cited studies.
- Frequency toggle defaults to Annual on first render (Athenic
  framing: +342% annual signups, +62% LTV). Stored monthly preference
  is still respected on subsequent loads.
- Annual position copy reads "Annual - 2 months free" inline with the
  toggle (absolute-dollar savings beat percentages at low price
  points: "save \$18", "save \$98").
- Annual price shown as monthly-equivalent ("\$7.50/mo, billed yearly")
  with a savings subtext line per card ("\$90/yr · save \$18").
- Team tier is visible-but-de-emphasised (anchor role): dimmed price
  color, "Contact sales" CTA (team tier still comingSoon).

Promo input removed:
The previous in-product promo toggle + input + applied chip has been
removed entirely. Razorpay's hosted checkout already has its own
"Have a code?" affordance, so duplicating it on BillingPage was both
Baymard-failing ("coupon hunting" abandonment to Google) and
redundant. A small mono-styled line under the grid reminds users:
"Promo codes apply at checkout (Razorpay)." validatePromotion is no
longer called from this surface.

Componentry:
- New: src/components/TierCard.tsx -- single tier column with badge,
  raised state, "Your plan" pill, and optional CTA slot (lets the
  Pro card compose with the existing UpgradeButton for P1's A/B
  variant without forking the grid).
- New: src/components/PricingGrid.tsx -- 4-column responsive grid
  (collapses to 2x2 at <=1100px, single-stack at <=640px) with its
  own frequency toggle. Shared between BillingPage and (eventually)
  the public /pricing marketing page so the in-product and public
  surfaces stay byte-identical.
- BillingPage owns auth/checkout state; PricingGrid is pure
  presentation + selection callbacks.

Tests (50 -> 57 in BillingPage.test.tsx):
- All 4 tier cards render with correct data-tier attributes
- Annual is default on first mount; rehydrates stored monthly
- "2 months free" copy renders inside the Annual toggle position
- Monthly-equivalent price + savings subtext renders in Annual mode
- "Most Popular" badge renders only on Pro
- Current tier shows "Your plan" pill; other tiers show CTA
- Tier-aware CTA copy ("Get Pro -- \$40.83/mo" etc.)
- createCheckout signature: strict 2-arg shape, no opts third arg
- Promo UI testids assert-absent across the page; validatePromotion
  is never called
- Pre-existing tests preserved (skeleton, error state, no self-serve
  cancel, no fabricated card expiry, Usage panel from
  fetchBillingUsage, listResources never called for usage)

Bundle:
- BillingPage chunk: 18.73 kB -> 17.57 kB (-1.16 kB), gzip 5.79 ->
  5.49 kB (-0.30 kB). Net shrink because removed promo logic
  outweighs the new components.

npm test: 17 files, 389 passed, 3 skipped (baseline 382 passed).
npm run build: clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mastermanas805 mastermanas805 merged commit d0a209d into main May 13, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant