BillingPage: 4-tier grid + Annual-default + 2 months free + Most Popular badge + collapse promo (per pricing research)#52
Merged
mastermanas805 merged 1 commit intoMay 13, 2026
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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: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 — 2 months free. Annual prices show the monthly-equivalent(
$7.50/mo, billed yearly) with absolute-dollar savings subtext ($90/yr · save $18).Get Pro — $40.83/mo(annual) /Get Pro — $49/mo(monthly), and thecurrent tier shows a
Your planpill instead of a CTA.Promo UI: removed entirely (not collapsed)
Razorpay's hosted checkout has its own
Have a code?affordance, so thein-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 theircode goes.
validatePromotionis no longer called from this surface — theapi function stays exported for any future surface that wants it.
New components
src/components/TierCard.tsx— single tier card with badge, raisedstate, "Your plan" pill, and an optional
ctaSlot(lets the Pro cardcompose with the existing
UpgradeButtonfor P1's A/B variant withoutforking the grid).
src/components/PricingGrid.tsx— 4-column responsive grid with its ownfrequency 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
data-tier="free|hobby|pro|team")Get Pro — $40.83/mo,Start Hobby — $7.50/mo, etc.)createCheckoutstrict 2-arg signature (nooptsthird arg)validatePromotionis never calledcancel, no fabricated card expiry, Usage panel from
fetchBillingUsage,listResourcesnever 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)
upgrade-journey.spec.ts) — theUpgrade to Protext-matcher selector may need an update to targetdata-testid="upgrade-button"(or the newGet Pro — $X/molabel).Flagging for review.
🤖 Generated with Claude Code