Skip to content

feat(billing): unified BillingCardComponent + annual toggle disabled state#4149

Merged
PierreBrisorgueil merged 5 commits into
masterfrom
feat/billing-unified-card-annual
May 20, 2026
Merged

feat(billing): unified BillingCardComponent + annual toggle disabled state#4149
PierreBrisorgueil merged 5 commits into
masterfrom
feat/billing-unified-card-annual

Conversation

@PierreBrisorgueil
Copy link
Copy Markdown
Collaborator

@PierreBrisorgueil PierreBrisorgueil commented May 12, 2026

Summary

  • What changed: Introduce unified BillingCardComponent (flat resolved-item schema, plans + packs), add disabled prop to BillingPricingToggleComponent, migrate devkit static-content plans to V4 schema (title/subtitle/highlight), delete legacy BillingPricingCardComponent.
  • Why: Trawl and other downstream projects ship usage-based pricing UX that needs (a) a card that accepts a fully-resolved item (so parent owns CTA / variant / disabled / loading) and (b) a toggle that disables on tabs where billing period doesn't apply (Extras). Savings info moves from a toggle caption to a chip on each card's price line.
  • Related issues: Closes feat(billing): unified BillingCardComponent + annual toggle disabled state #4148

Scope

  • Modules impacted: billing (components, view, static-content, tests)
  • Cross-module impact: none — billing module internals only.
  • Risk level: medium — schema migration of devkit plans static-content (downstream projects already use V4 schema; only the devkit dev server example is impacted).

Validation

  • npm run lint
  • npm run test:unit (1669/1669 passing across 101 files)
  • npm run build (CI will verify)
  • Manual checks: dev server renders V4 cards (titles, badge, highlight, annual chip, free+guest signup query preserved)

Guardrails check

  • No secrets or credentials introduced
  • No risky rename/move of core stack paths
  • Changes remain merge-friendly for downstream projects (Trawl already on V4 schema; other downstreams will pull this through /update-stack)
  • Tests added or updated when behavior changed (17 new BillingCardComponent tests, updated toggle + view tests for new schema/contract)

Optional: Infra/Stack alignment details

Before vs After (key changes only)

Area Before After Notes
Card component BillingPricingCardComponent (424 lines, internal price/CTA resolution, skeleton loader, tooltip on unavailable, dual feature/featureSections paths, equivalences mode) BillingCardComponent (165 lines, dumb item-driven, no internal logic) Parent owns all state via resolvedPlanItems
Toggle disabled prop absent; savings caption rendered below switch (maxAnnualSavingsPct prop) disabled prop added (wrapper opacity + v-switch passthrough); no caption (savings on card chip) maxAnnualSavingsPct prop dropped — public composable API kept for downstream
Annual savings UX Caption below toggle Tonal chip next to price on each card Per-plan chip is more discoverable + scales when discount % varies
View BillingPricingCardComponent with multi-prop pass-through BillingCardComponent with single :item + @cta-click resolvedPlanItems computed builds the item per plan
Devkit static-content plans name/tagline/highlighted + legacy features: [{ text, included }] + featureSections title/subtitle/highlight + V4 features: [{ icon, color, text }] + info line Matches Trawl downstream + new card contract
Free+guest CTA cta.to = '/signup' (string) cta.to = { path: '/signup', query: { redirect: '/pricing' } } v-btn :to accepts route-location objects; preserves redirect on signup
BillingCardComponent.onCtaClick n/a Skips emit when cta.disabled OR cta.to is set Prevents double-nav when router-link is bound
  • Upstream parity target: Trawl downstream already shipping this card; this PR aligns devkit
  • Automation / policy impact: none
  • Rollback plan: revert the 4 commits; downstream projects on V4 schema continue to work with the prior view (or pin pierreb-devkit/Vue@HEAD^)

Notes for reviewers

  • Phase 0 critical-review gate (DeepSeek): 3 iterations. Iter 1 BLOCK (2 critical: V3 vs V4 schema mismatch on plans static-content). Iter 2 BLOCK (1 high: signup redirect query loss when cta.to is set as string). Iter 3 OK with nits. Final commit fixed inert density attr.
  • Security: no auth changes; CTA redirect preserves the same intent the legacy view had.
  • Mergeability: downstream projects already on V4 schema (Trawl) are unaffected; other downstream projects using the legacy name/tagline/highlighted schema in their own billing.static-content.js are NOT touched (this PR only updates the devkit defaults).
  • Follow-up tasks (optional): usePricing.maxAnnualSavingsPct is unused by the view but retained as a public composable API. A future cleanup could deprecate it if no downstream consumes it.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added optional savings chip to billing card pricing display
    • Plan cards now show operations/week metrics
  • Bug Fixes

    • Fixed pricing card CTA behavior when navigation is involved
    • Improved signup flow to preserve pricing page context
  • Improvements

    • Simplified monthly/annual pricing toggle interface
    • Streamlined plan display with refreshed layout and feature structure
    • Enhanced pricing calculations for better Stripe integration support

Review Change Stack

@PierreBrisorgueil PierreBrisorgueil added Feat billing Phase 3 — Stripe billing integration labels May 12, 2026
@PierreBrisorgueil PierreBrisorgueil self-assigned this May 12, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 12, 2026

Warning

Rate limit exceeded

@PierreBrisorgueil has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 27 minutes and 10 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 14f6dcf6-1f5c-481b-ad65-62bad6e3e4df

📥 Commits

Reviewing files that changed from the base of the PR and between f50061e and 189a66b.

📒 Files selected for processing (2)
  • src/modules/auth/tests/auth.signup.view.unit.tests.js
  • src/modules/auth/views/signup.view.vue

Walkthrough

This PR migrates the billing module's pricing UI and data to V4 design by removing the savings-on-toggle pattern (moving savings display to the card), simplifying the pricing toggle component, updating the static plan schema, and refactoring the pricing view to support both legacy and new plan data shapes while preserving Stripe price overrides.

Changes

Billing Pricing V4 Migration

Layer / File(s) Summary
Toggle component simplification
src/modules/billing/components/billing.pricingToggle.component.vue, src/modules/billing/tests/billing.pricingToggle.component.unit.tests.js, src/modules/billing/views/billing.pricing.view.vue
Removed maxAnnualSavingsPct prop from BillingPricingToggleComponent; updated component documentation and props to reflect a simplified Monthly/Annual toggle without savings caption; removed max-annual-savings-pct binding from both subscription and both-tabs toggle usage in the view; updated toggle tests to verify savings phrases and inline chips are not rendered.
Card component CTA routing guard
src/modules/billing/components/billing.card.component.vue, src/modules/billing/tests/billing.card.component.unit.tests.js
Added logic to prevent cta-click emission when cta.to is set, delegating navigation to router-link; updated item prop documentation to clarify cta.to may be present and supports string | RouteLocationRaw | null; added test coverage verifying non-emission when cta.to is populated.
Plan schema migration to V4 structure
src/modules/billing/config/billing.static-content.js, src/modules/billing/tests/billing.pricing.view.unit.tests.js
Migrated plan data from legacy structure (name/tagline/featureSections/equivalences) to V4 unified schema with title/subtitle, highlight/badge, simplified features using { icon, color, text }, and new per-plan info field; updated test plan fixtures to match the new schema shape.
View logic refactoring for new schema and components
src/modules/billing/views/billing.pricing.view.vue
Removed meterMode computed property; refactored resolvedPlanItems() to derive prices from either plan direct fields (monthlyPrice/annualPrice) or metadata shape (plan.meta.*), apply Stripe overrides when present, calculate annual savings from both shape variants, and return plan items with only _activePriceId internal field; changed free-guest CTA destination to route object preserving redirect=/pricing query parameter.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

  • pierreb-devkit/Vue#3728 — Introduces billing pricing module scaffolding and pricing UI that this PR refactors and updates.
  • pierreb-devkit/Vue#3726 — Introduces BillingPricingToggleComponent and BillingCardComponent that this PR updates for CTA routing and prop simplification.
  • pierreb-devkit/Vue#4106 — Both PRs adjust free/guest "sign up" CTA navigation and redirect parameter handling in the pricing view.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title "feat(billing): unified BillingCardComponent + annual toggle disabled state" accurately describes the main changes: introduction of a unified BillingCardComponent and addition of a disabled state to the annual toggle.
Description check ✅ Passed The PR description comprehensively covers all required sections: detailed summary with what/why, scope with modules and risk level, completed validation checks, guardrails compliance, before/after table, and thorough notes for reviewers.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/billing-unified-card-annual

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

… card edge cases

Phase 0 critical-review BLOCK findings — iter 1:

- [critical] Migrate devkit static-content plans to V4 schema (title/subtitle/highlight)
  to match the new BillingCardComponent contract. Drop legacy
  name/tagline/highlighted + featureSections + included:false features +
  equivalences. Cards now render with proper titles and elevated variant for the
  highlighted plan on the devkit dev server.
- [high] BillingCardComponent: skip cta-click emit when cta.to is set. v-btn's
  :to binds router-link which navigates natively; emitting also triggered a
  duplicate $router.push in the parent's @cta-click handler with a divergent
  URL (free+guest plan dropped the redirect query param).
- [medium] Drop dead _equivalences computation in resolvedPlanItems — the new
  BillingCardComponent does not consume equivalences. Trawl downstream already
  removed them too.
- [low] Drop dead maxAnnualSavingsPct prop on BillingPricingToggleComponent —
  caption was removed in V4 (savings live on card price.chip). Also drop the
  prop binding in billing.pricing.view.vue.
- Test updates: add cta.to → no-emit test; drop maxAnnualSavingsPct usage in
  toggle tests (prop removed); simplify i18n mock (savingsActive key unused).

All tests green (1669/1669, 101 files). Lint clean.
Phase 0 critical-review BLOCK findings — iter 2:

- [high] Free+guest CTA `cta.to` was '/signup' (string), losing the redirect=/pricing
  query param. Card skips emit when cta.to is set (intentional double-nav fix), so
  the view's onCtaClick fallback that pushed /signup?redirect=/pricing was dead.
  Fix: pass cta.to as { path: '/signup', query: { redirect: '/pricing' } } — v-btn :to
  accepts the same shape as $router.push().
- [medium] Drop dead `meterMode` computed from the view (only consumed by the
  removed _equivalences logic).
- [low] Update billing.pricing.view.unit.tests.js mock fixtures to V4 schema
  (title/subtitle/highlight) — masked schema-compliance gaps.
- [low] Clarify BillingCardComponent ITEM SCHEMA doc: cta is a string in
  static-content plans but expanded into an object by resolvedPlanItems; cta-click
  emit guard now documented (disabled OR to=set).

Note: usePricing.maxAnnualSavingsPct stays — it's a public composable API still
tested by billing.usePricing.unit.tests.js. Downstream may consume it.

All tests green (1669/1669). Lint clean.
Phase 0 nit — BillingPricingToggleComponent has no density prop; the binding
falls through as inert HTML attribute on the root div with no effect.
Remove the binding (Vuetify density is not surfaced by this wrapper).
@PierreBrisorgueil PierreBrisorgueil force-pushed the feat/billing-unified-card-annual branch from 65a5e1d to b20588a Compare May 20, 2026 10:05
… tests)

Replace this.$t('billing.pricingCard.saveAnnual') with Save ${pct}% template literal
to match master convention and avoid $t not a function failures in unit tests.
@codecov
Copy link
Copy Markdown

codecov Bot commented May 20, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.55%. Comparing base (25a3ceb) to head (189a66b).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #4149   +/-   ##
=======================================
  Coverage   99.55%   99.55%           
=======================================
  Files          31       31           
  Lines        1136     1136           
  Branches      328      328           
=======================================
  Hits         1131     1131           
  Misses          5        5           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@PierreBrisorgueil PierreBrisorgueil marked this pull request as ready for review May 20, 2026 10:17
Copilot AI review requested due to automatic review settings May 20, 2026 10:17
@PierreBrisorgueil
Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the billing pricing UI to use a unified, item-driven BillingCardComponent for plans/packs and adds a disabled state to the annual/monthly pricing toggle, while migrating the devkit billing static-content plans to the V4 schema (title/subtitle/highlight) and removing legacy toggle savings caption behavior.

Changes:

  • Replace the pricing view’s card rendering with BillingCardComponent using a fully-resolved item schema (including annual savings as price.chip).
  • Add disabled prop support to BillingPricingToggleComponent and remove legacy “savings caption” rendering/tests.
  • Migrate devkit billing.static-content.js plans to the V4 schema and update unit tests to match.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/modules/billing/views/billing.pricing.view.vue Builds resolved card items (price display + annual savings chip) and wires toggle disabled state in both-tabs mode.
src/modules/billing/components/billing.pricingToggle.component.vue Removes savings caption API; adds disabled prop passthrough + UI dimming.
src/modules/billing/components/billing.card.component.vue Prevents cta-click emit when cta.to is set to avoid double navigation.
src/modules/billing/config/billing.static-content.js Migrates devkit plan copy to V4 (title/subtitle/highlight/info/features).
src/modules/billing/tests/billing.pricingToggle.component.unit.tests.js Updates assertions to reflect caption-free toggle + new disabled behavior.
src/modules/billing/tests/billing.pricing.view.unit.tests.js Updates mocked plan shape to V4 fields used by the view/card contract.
src/modules/billing/tests/billing.card.component.unit.tests.js Adds coverage for “no emit when cta.to is set” behavior.

Comment on lines +295 to +299
// Preserve the `redirect` query param so the user lands back on the pricing
// page after signup. v-btn's :to accepts the same shape as $router.push().
// The card skips its cta-click emit when cta.to is set (router-link owns
// the navigation), so the view's onCtaClick fallback is intentionally bypassed.
ctaTo = { path: '/signup', query: { redirect: '/pricing' } };
Addresses Copilot review on #4149. The pricing CTA already set
?redirect=/pricing on /signup (Phase 0 fix cc6c585), but signup.view
silently dropped it — only signin.view:157 honored redirect. The
redirect param was therefore dead code from the signup CTA's POV.

Adds a pushAfterAuth() helper mirroring signin's pattern:
- typeof redirect === 'string' && redirect.startsWith('/') → push redirect
- otherwise → push config.sign.route
- typeof guard prevents arrays (?redirect=/a&redirect=/b); startsWith('/')
  guard prevents open-redirect to external URLs.

5 call sites swapped to pushAfterAuth():
- watch.auth (orgs disabled branch)
- created() (already logged in + has org)
- validate() success, orgs enabled, no setup needed
- validate() success, orgs disabled
- proceedToApp() (end of org flow)

4 new unit tests (redirect honored / fallback / open-redirect guarded /
proceedToApp redirect). Total signup tests 25 → 29.
@PierreBrisorgueil
Copy link
Copy Markdown
Collaborator Author

@copilot — good catch. Fixed in latest commit: signup.view now honors $route.query.redirect via a new pushAfterAuth() helper that mirrors signin.view:157's pattern (typeof === 'string' && startsWith('/') open-redirect guard + fallback to config.sign.route). All 5 $router.push(this.config.sign.route) call sites in signup.view route through it now. 4 new unit tests cover honored/fallback/external-URL-rejected/proceedToApp paths. npm run test:unit → 1852/1852 pass, lint clean.

Copy link
Copy Markdown

Copilot AI commented May 20, 2026

@copilot — good catch. Fixed in latest commit: signup.view now honors $route.query.redirect via a new pushAfterAuth() helper that mirrors signin.view:157's pattern (`typeof === 'string' && start...

Verified in commit 189a66b. pushAfterAuth() mirrors signin.view's guard exactly (typeof === 'string' && startsWith('/')) and all 5 call sites route through it. The 4 new tests cover honored/fallback/external-URL-rejected/proceedToApp paths — looks correct.

@PierreBrisorgueil PierreBrisorgueil merged commit d1403ff into master May 20, 2026
8 checks passed
@PierreBrisorgueil PierreBrisorgueil deleted the feat/billing-unified-card-annual branch May 20, 2026 10:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

billing Phase 3 — Stripe billing integration Feat

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(billing): unified BillingCardComponent + annual toggle disabled state

3 participants