Skip to content

Commit cc6c585

Browse files
fix(billing): preserve signup redirect query + drop dead view code
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.
1 parent 53ec981 commit cc6c585

3 files changed

Lines changed: 26 additions & 12 deletions

File tree

src/modules/billing/components/billing.card.component.vue

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,33 @@
1010
USAGE (pack — from billing.packs.component.vue):
1111
<BillingCardComponent :item="resolvedPackItem" @cta-click="onCtaClick" />
1212
13-
ITEM SCHEMA:
13+
ITEM SCHEMA (fully resolved — parent transforms static-content plan/pack into this):
1414
id : string
1515
title : string — large card heading
1616
subtitle : string — 1-liner below title
17-
price : { amount: string, period: string|null }
17+
price : { amount: string, period: string|null, chip?: { text: string, color?: string } }
1818
e.g. { amount: 'FREE', period: null }
19-
e.g. { amount: '$39', period: '/month' }
19+
e.g. { amount: '$39', period: '/month', chip: { text: 'Save 17%', color: 'success' } }
2020
cta : {
21-
label : string,
21+
label : string, — resolved label (parent picks "Sign up" / "Current Plan" / etc.)
2222
variant : 'elevated'|'outlined'|'flat'|'tonal',
2323
color : string|null,
2424
disabled : boolean,
2525
loading : boolean,
26-
to : string|null, — router-link target (free plan signup)
26+
to : string|RouteLocationRaw|null, — router-link target (v-btn :to passes through). Object form preserves query params.
2727
}
28+
Note: static-content plans carry `cta` as a plain string (the label).
29+
The parent (e.g. billing.pricing.view.vue resolvedPlanItems) expands it
30+
into this object — the name collision between the two layers is intentional
31+
but worth flagging for downstream overrides.
2832
info : string|null — ops-eval line, shown between CTA and features
2933
features : [{ icon: string, color: string, text: string }]
3034
badge : string|null — e.g. 'MOST POPULAR'
3135
highlight : boolean — elevated card variant
3236
3337
EVENTS:
34-
- cta-click ({ id }): emitted on CTA click (parent handles routing/checkout)
38+
- cta-click ({ id }): emitted on CTA click. Skipped when cta.disabled OR cta.to is set
39+
(router-link owns navigation in the latter case — see onCtaClick).
3540
-->
3641
<template>
3742
<v-card

src/modules/billing/tests/billing.pricing.view.unit.tests.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ vi.mock('../../auth/stores/auth.store', () => ({
3030

3131
const pricingState = vi.hoisted(() => ({
3232
mode: 'subscription',
33-
plans: [{ id: 'free', name: 'Free' }, { id: 'pro', name: 'Pro' }],
33+
// V4 schema — title/subtitle/highlight (matches static-content & BillingCardComponent contract).
34+
plans: [
35+
{ id: 'free', title: 'Free', subtitle: 'For starters', highlight: false, badge: null, cta: 'Get started', features: [] },
36+
{ id: 'pro', title: 'Pro', subtitle: 'For pros', highlight: false, badge: null, cta: 'Upgrade', features: [] },
37+
],
3438
packs: [],
3539
faqs: { title: '', subtitle: null, content: [] },
3640
tabs: {},
@@ -284,7 +288,11 @@ describe('BillingPricingView — mode-aware layout', () => {
284288
pricingState.plans = plans;
285289
pricingState.hasPlans = plans.length > 0;
286290
} else {
287-
pricingState.plans = [{ id: 'free', name: 'Free' }, { id: 'pro', name: 'Pro' }];
291+
// V4 schema — title/subtitle/highlight (matches static-content & BillingCardComponent contract).
292+
pricingState.plans = [
293+
{ id: 'free', title: 'Free', subtitle: 'For starters', highlight: false, badge: null, cta: 'Get started', features: [] },
294+
{ id: 'pro', title: 'Pro', subtitle: 'For pros', highlight: false, badge: null, cta: 'Upgrade', features: [] },
295+
];
288296
pricingState.hasPlans = true;
289297
}
290298
pricingState.packs = pricingMode === 'packs' ? [{ id: 'p1', name: 'Pack 500' }] : [];

src/modules/billing/views/billing.pricing.view.vue

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -207,9 +207,6 @@ export default {
207207
if (!this.authStore.isLoggedIn) return null;
208208
return this.billingStore.subscription?.plan ?? 'free';
209209
},
210-
meterMode() {
211-
return this.authStore.serverConfig?.billing?.meterMode === true;
212-
},
213210
hasPaidPlans() {
214211
return this.plans.some((p) => p.id !== 'free');
215212
},
@@ -296,7 +293,11 @@ export default {
296293
ctaVariant = 'outlined';
297294
ctaColor = null;
298295
ctaDisabled = false;
299-
ctaTo = '/signup';
296+
// Preserve the `redirect` query param so the user lands back on the pricing
297+
// page after signup. v-btn's :to accepts the same shape as $router.push().
298+
// The card skips its cta-click emit when cta.to is set (router-link owns
299+
// the navigation), so the view's onCtaClick fallback is intentionally bypassed.
300+
ctaTo = { path: '/signup', query: { redirect: '/pricing' } };
300301
} else {
301302
ctaLabel = plan.cta;
302303
ctaVariant = plan.highlight ? 'flat' : 'outlined';

0 commit comments

Comments
 (0)