fix(billing): surface meterError alert + remove dead showAlreadyActiveDialog - #4081
Conversation
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (4)
WalkthroughThe PR enhances the billing module with error handling visibility, extended subscription statuses, polling cleanup hardening, and Stripe URL validation. It adds shared ChangesBilling Module Enhancements
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Duplication | 5 |
AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Pull request overview
This PR improves the billing UX and security by (1) surfacing useMeter() refresh failures to the user via a warning alert, and (2) centralizing Stripe redirect URL validation (HTTPS + hostname allowlist) while removing dead 409-dialog code from the subscriptions component.
Changes:
- Added
validateStripeUrl()helper and replaced ad-hoc URL parsing in billing store + pricing view redirects. - Introduced shared
meterErrorinuseMeter()and exposed it to billing UI consumers; added a warningv-alertinBillingSubscriptionsComponent. - Removed dead
showAlreadyActiveDialog+ related state/template, and updated unit tests + i18n keys (including new status labels).
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/modules/billing/views/billing.pricing.view.vue | Uses validateStripeUrl() before redirecting to Stripe (checkout + portal URL from 409 payload). |
| src/modules/billing/stores/billing.store.js | Centralizes portal/checkout redirect validation through validateStripeUrl(). |
| src/modules/billing/lib/stripeRedirect.js | Adds the Stripe URL validation helper (HTTPS + host allowlist). |
| src/modules/billing/components/billing.subscriptions.component.vue | Surfaces meterError via a warning alert; removes dead already-active dialog; adds paused/unpaid status handling. |
| src/modules/billing/components/billing.usageBar.component.vue | Exposes meterError from useMeter() (UI handled by parent). |
| src/modules/billing/composables/billing.useMeter.js | Adds shared meterError ref and populates it on initial fetch / refresh failures. |
| src/modules/billing/lang/en.js | Adds i18n keys for meter refresh failure + new paused/unpaid status labels. |
| src/modules/billing/lang/fr.js | Adds i18n keys for meter refresh failure + new paused/unpaid status labels; fixes multiple French accent/wording strings. |
| src/modules/billing/tests/billing.stripeRedirect.unit.tests.js | Adds unit coverage for validateStripeUrl(). |
| src/modules/billing/tests/billing.store.unit.tests.js | Updates expected error messages to match validateStripeUrl() errors. |
| src/modules/billing/tests/billing.useMeter.unit.tests.js | Adds coverage for meterError behavior on initial fetch and polling failures. |
| src/modules/billing/tests/billing.subscriptions.component.unit.tests.js | Updates status chip expectations, replaces dead-code tests with meterError alert tests, adds poll abort coverage. |
| meterError.value = null; | ||
| void fetchMissingMeterData(billingStore).catch((err) => { | ||
| console.error('[billing.useMeter] initial fetch failed', err); | ||
| meterError.value = err; | ||
| }); |
| if (['active', 'trialing'].includes(this.subscriptionStatus)) return 'fa-solid fa-circle-check'; | ||
| if (this.subscriptionStatus === 'past_due') return 'fa-solid fa-triangle-exclamation'; | ||
| if (this.subscriptionStatus === 'paused') return 'fa-solid fa-pause-circle'; | ||
| if (this.subscriptionStatus === 'unpaid') return 'fa-solid fa-exclamation-triangle'; |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/modules/billing/components/billing.subscriptions.component.vue`:
- Around line 199-211: The meter error alert is showing for non-meter
deployments because meterError is set even when meterMode is false; in the
template change the v-alert to only render when both meterMode and meterError
are true (use v-if="meterMode && meterError"), and in setup prevent useMeter({
pollIntervalMs: 0 }) and the fetchMissingMeterData call from running unless
meterMode is truthy (wrap the useMeter invocation and any call to
fetchMissingMeterData in an if (meterMode) check) so meter-related logic only
runs for meter-enabled deployments.
- Around line 495-496: Update the Font Awesome icon names returned for
this.subscriptionStatus: replace 'fa-pause-circle' with the FA6+ canonical
'fa-circle-pause' for the 'paused' case, and replace 'fa-exclamation-triangle'
with 'fa-triangle-exclamation' for the 'unpaid' case so the computed/method that
maps this.subscriptionStatus to icon classes (the clauses checking
this.subscriptionStatus === 'paused' and === 'unpaid') uses the same FA6+ naming
as the other icons (e.g., 'fa-circle-check').
In `@src/modules/billing/composables/billing.useMeter.js`:
- Line 11: The module-level sharedMeterError (const sharedMeterError) is being
unconditionally reset on each useMeter() consumer mount which wipes errors for
other components; remove the unconditional clear of sharedMeterError/meterError
performed during useMeter() initialization (the pre-clear around where
meterError is assigned), rely on safeRefresh and the initial-fetch catch to
manage error lifecycle, and add a unit test in billing.useMeter.unit.tests.js
that mounts a second consumer after setting sharedMeterError to assert the error
is preserved across mounts.
In `@src/modules/billing/lib/stripeRedirect.js`:
- Around line 19-36: The validateStripeUrl function currently accepts URLs with
embedded credentials (e.g., parsed.username/parsed.password) which should be
rejected; update validateStripeUrl to throw an error when parsed.username or
parsed.password are non-empty (e.g., "Stripe URL must not contain credentials"),
keep existing checks for protocol and ALLOWED_HOSTS, and add a unit test in
billing.stripeRedirect.unit.tests.js that verifies URLs like
"https://attacker@checkout.stripe.com/..." are rejected.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 7f4b3892-533c-4574-bfcd-2d5a6325e764
📒 Files selected for processing (12)
src/modules/billing/components/billing.subscriptions.component.vuesrc/modules/billing/components/billing.usageBar.component.vuesrc/modules/billing/composables/billing.useMeter.jssrc/modules/billing/lang/en.jssrc/modules/billing/lang/fr.jssrc/modules/billing/lib/stripeRedirect.jssrc/modules/billing/stores/billing.store.jssrc/modules/billing/tests/billing.store.unit.tests.jssrc/modules/billing/tests/billing.stripeRedirect.unit.tests.jssrc/modules/billing/tests/billing.subscriptions.component.unit.tests.jssrc/modules/billing/tests/billing.useMeter.unit.tests.jssrc/modules/billing/views/billing.pricing.view.vue
| if (this.subscriptionStatus === 'paused') return 'fa-solid fa-pause-circle'; | ||
| if (this.subscriptionStatus === 'unpaid') return 'fa-solid fa-exclamation-triangle'; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Font Awesome 7 icon names fa-circle-pause fa-triangle-exclamation canonical class names
💡 Result:
In Font Awesome 7, the canonical CSS class names for the specified icons are fa-circle-pause and fa-triangle-exclamation. These are used with style prefixes like fa-solid (e.g., <i class="fas fa-circle-pause"></i>).[1][2][3][4][5][6] Font Awesome 7 is the current version as of 2026, with official pages at fontawesome.com/icons/circle-pause and fontawesome.com/icons/exclamation-triangle (noting the slug matches circle-pause, while display may vary).[7][8] These classes have been consistent from version 6 to 7, as confirmed by multiple technical references linking Font Awesome 6/7 CDNs and class usage.[2][5][6]
Citations:
- 1: https://fontawesome.com/icons/circle-pause
- 2: https://fontawesomeicons.com/circle-pause
- 3: https://fontawesome.com/v7/icons/triangle-exclamation?s=regular
- 4: https://fontawesome.com/icons/exclamation-triangle
- 5: https://www.angularjswiki.com/fontawesome/fa-circle-pause
- 6: https://www.angularjswiki.com/fontawesome/fa-triangle-exclamation
- 7: https://www.fontawesome.com
- 8: https://fontawesome.com/
Update icon names to Font Awesome 6+ canonical form.
Lines 495–496 use Font Awesome 5 names that are inconsistent with the rest of the file and incompatible with Font Awesome 7.2.0. The canonical FA 6+ names are fa-circle-pause and fa-triangle-exclamation:
| Line | Current | Expected |
|---|---|---|
| 495 | fa-pause-circle |
fa-circle-pause |
| 496 | fa-exclamation-triangle |
fa-triangle-exclamation |
This matches the naming used for other icons in the file (e.g., fa-circle-check, fa-triangle-exclamation, fa-circle-exclamation) and ensures rendering works in FA 7.
Proposed fix
- if (this.subscriptionStatus === 'paused') return 'fa-solid fa-pause-circle';
- if (this.subscriptionStatus === 'unpaid') return 'fa-solid fa-exclamation-triangle';
+ if (this.subscriptionStatus === 'paused') return 'fa-solid fa-circle-pause';
+ if (this.subscriptionStatus === 'unpaid') return 'fa-solid fa-triangle-exclamation';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (this.subscriptionStatus === 'paused') return 'fa-solid fa-pause-circle'; | |
| if (this.subscriptionStatus === 'unpaid') return 'fa-solid fa-exclamation-triangle'; | |
| if (this.subscriptionStatus === 'paused') return 'fa-solid fa-circle-pause'; | |
| if (this.subscriptionStatus === 'unpaid') return 'fa-solid fa-triangle-exclamation'; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/modules/billing/components/billing.subscriptions.component.vue` around
lines 495 - 496, Update the Font Awesome icon names returned for
this.subscriptionStatus: replace 'fa-pause-circle' with the FA6+ canonical
'fa-circle-pause' for the 'paused' case, and replace 'fa-exclamation-triangle'
with 'fa-triangle-exclamation' for the 'unpaid' case so the computed/method that
maps this.subscriptionStatus to icon classes (the clauses checking
this.subscriptionStatus === 'paused' and === 'unpaid') uses the same FA6+ naming
as the other icons (e.g., 'fa-circle-check').
| let consumerCount = 0; | ||
| // Shared error ref so the single interval callback can write into it | ||
| // and all consumers receive the same reactive signal. | ||
| const sharedMeterError = ref(null); |
There was a problem hiding this comment.
Shared meterError is wiped on every new consumer mount.
Because meterError aliases the module-scope sharedMeterError, the unconditional reset on Line 148 runs every time any component calls useMeter() in setup(). This PR wires meterError into both billing.subscriptions.component.vue and billing.usageBar.component.vue, and the usage bar can render inside the subscriptions view — when the second consumer mounts, it instantly clears any error the first consumer (or the polling loop) just surfaced, hiding the new warning v-alert until the next 30s poll cycle re-captures the failure. The same hazard applies to remounts during route changes.
safeRefresh already clears stale errors before each poll, and the initial-fetch catch on Line 149 only writes on failure, so the pre-clear here is redundant in addition to being racy.
🛠️ Proposed fix — drop the unconditional clear on every consumer mount
- meterError.value = null;
void fetchMissingMeterData(billingStore).catch((err) => {
console.error('[billing.useMeter] initial fetch failed', err);
meterError.value = err;
});Alternatively, gate the clear on first-consumer-only:
- meterError.value = null;
+ if (consumerCount === 1) meterError.value = null;Add a test in billing.useMeter.unit.tests.js that mounts a second consumer after meterError is set and asserts it is preserved.
Also applies to: 62-62, 148-152
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/modules/billing/composables/billing.useMeter.js` at line 11, The
module-level sharedMeterError (const sharedMeterError) is being unconditionally
reset on each useMeter() consumer mount which wipes errors for other components;
remove the unconditional clear of sharedMeterError/meterError performed during
useMeter() initialization (the pre-clear around where meterError is assigned),
rely on safeRefresh and the initial-fetch catch to manage error lifecycle, and
add a unit test in billing.useMeter.unit.tests.js that mounts a second consumer
after setting sharedMeterError to assert the error is preserved across mounts.
| export function validateStripeUrl(url) { | ||
| if (typeof url !== 'string' || !url) { | ||
| throw new Error('Stripe URL is empty'); | ||
| } | ||
| let parsed; | ||
| try { | ||
| parsed = new URL(url); // throws on relative URLs and invalid strings | ||
| } catch { | ||
| throw new Error('Stripe URL is invalid'); | ||
| } | ||
| if (parsed.protocol !== 'https:') { | ||
| throw new Error('Stripe URL must use HTTPS'); | ||
| } | ||
| if (!ALLOWED_HOSTS.includes(parsed.hostname)) { | ||
| throw new Error(`Stripe URL host not allowed: ${parsed.hostname}`); | ||
| } | ||
| return parsed.toString(); | ||
| } |
There was a problem hiding this comment.
Reject URLs containing embedded credentials.
validateStripeUrl does not inspect parsed.username / parsed.password. A URL like https://attacker@checkout.stripe.com/... parses with hostname checkout.stripe.com and currently passes validation. Stripe never returns redirect URLs with credentials; rejecting them tightens the helper to match its stated security purpose.
🛡️ Proposed hardening
if (!ALLOWED_HOSTS.includes(parsed.hostname)) {
throw new Error(`Stripe URL host not allowed: ${parsed.hostname}`);
}
+ if (parsed.username || parsed.password) {
+ throw new Error('Stripe URL must not contain credentials');
+ }
return parsed.toString();Add a matching test in billing.stripeRedirect.unit.tests.js.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/modules/billing/lib/stripeRedirect.js` around lines 19 - 36, The
validateStripeUrl function currently accepts URLs with embedded credentials
(e.g., parsed.username/parsed.password) which should be rejected; update
validateStripeUrl to throw an error when parsed.username or parsed.password are
non-empty (e.g., "Stripe URL must not contain credentials"), keep existing
checks for protocol and ALLOWED_HOSTS, and add a unit test in
billing.stripeRedirect.unit.tests.js that verifies URLs like
"https://attacker@checkout.stripe.com/..." are rejected.
208a0d0 to
cc629d3
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #4081 +/- ##
=======================================
Coverage 99.51% 99.51%
=======================================
Files 32 32
Lines 1036 1036
Branches 278 278
=======================================
Hits 1031 1031
Misses 5 5 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
…iptions - Surface meterError ref from useMeter via v-alert gated to meterMode template (no rendering in legacy mode) - a11y: aria-live=polite on the alert - i18n key billing.meter.error.refreshFailed (en.js only) - Remove dead showAlreadyActiveDialog method, dialog template, state, and Suite 9 tests from subscriptions component (real 409 flow lives in billing.pricing.view.vue)
cc629d3 to
4c580c9
Compare
… credentials (#4082) - subscriptionStatusIcon: fa-pause-circle -> fa-circle-pause, fa-exclamation-triangle -> fa-triangle-exclamation (FA6+ canonical, sibling-consistent) - useMeter: drop unconditional sharedMeterError reset on consumer mount; safeRefresh manages the lifecycle - stripeRedirect: reject URLs containing credentials (parsed.username/password) to prevent phishing display tricks CodeRabbit follow-up findings from PR #4081 review on stale commit.
# [2.1.0](v2.0.0...v2.1.0) (2026-06-01) ### Bug Fixes * **billing:** 4 pricing page bugs (signed-in features, hero bg, truncation, guest CTA) ([#4123](#4123)) ([bd399bd](bd399bd)) * **billing:** add aria-expanded + touch support to BillingComputeGauge components ([#4181](#4181)) ([7e7fa97](7e7fa97)) * **billing:** billing UX hardening — 5 reliability fixes ([#4079](#4079)) ([dd62b3d](dd62b3d)), closes [#4078](#4078) * **billing:** canonical FA6 icons + meterError lifecycle + reject URL credentials ([#4082](#4082)) ([be16045](be16045)), closes [#4081](#4081) * **billing:** clear intentId on Stripe cancel-redirect ([#4085](#4085)) ([11be9ab](11be9ab)) * **billing:** drop empty overage aria-live div + add fr overDetail key ([#4142](#4142)) ([10e5d81](10e5d81)) * **billing:** i18n migration + Intl.NumberFormat USD (audit Codex P1) ([#4069](#4069)) ([3d3a4f6](3d3a4f6)) * **billing:** mega vue hardening v3 — UX + design + a11y + V4 ([#4063](#4063)) ([802cbc4](802cbc4)) * **billing:** send {} body on portal POST to satisfy Zod PortalRequest schema ([#4137](#4137)) ([bf6b9c8](bf6b9c8)) * **billing:** send intentId UUID to close extras double-charge window ([#4084](#4084)) ([3e27c4c](3e27c4c)) * **billing:** subscription state safety + webhook lag polling (audit Codex P1) ([#4068](#4068)) ([328700b](328700b)) * **billing:** surface meterError + remove dead 409 dialog from subscriptions ([#4081](#4081)) ([61f5d53](61f5d53)) * **billing:** V5 polish — F5 polling recovery + visibility refetch + i18n residual + locale null-guard ([#4070](#4070)) ([4908adb](4908adb)) * **billing:** V6 polish — sessionStorage guard + locale BCP47 + i18n plural + NaN guard ([#4076](#4076)) ([2ad018c](2ad018c)) * **build:** drop ARG defaults for analytics_* + filter empty env vars ([#4112](#4112)) ([afd336e](afd336e)), closes [Vue#4110](https://github.com/Vue/issues/4110) [#4110](#4110) * **build:** restore Layer 5 empty-string env override (revert [#4112](#4112) part 2) ([c9aa9e6](c9aa9e6)), closes [Vue#4110](https://github.com/Vue/issues/4110) [comes-io/trawl_vue#880](https://github.com/comes-io/trawl_vue/issues/880) [Vue#4110](https://github.com/Vue/issues/4110) * **core:** pin CorePageHeader to content size in flex-column contexts ([#4210](#4210)) ([e182512](e182512)), closes [#4202](#4202) * **layout:** route /pricing outside app shell — no drawer offset on signed-in ([#4124](#4124)) ([d862fbb](d862fbb)) * **legal:** cookie banner UA-detection + appName fallback to app.title ([#4113](#4113)) ([04cc70a](04cc70a)), closes [trawl_vue#876](https://github.com/trawl_vue/issues/876) [#4109](#4109) * **legal:** cookie consent + footer polish (5 QA bugs) ([#4098](#4098)) ([a9bf0a3](a9bf0a3)) * **legal:** gate cookie banner on isMounted to prevent prerender hydration double-render ([#4109](#4109)) ([3198e48](3198e48)), closes [#app](https://github.com/pierreb-devkit/Vue/issues/app) * **router:** redirect authenticated users to config.sign.route instead of hardcoded '/' ([#4088](#4088)) ([ca9094e](ca9094e)), closes [#4083](#4083) * **tasks:** propagate store errors so views gate navigation on success ([#4221](#4221)) ([b029d39](b029d39)), closes [#4218](#4218) * **ui:** chrome convergence — restore section title + surface backgrounds + homogeneous gutter ([#4188](#4188)) ([1e5fdf1](1e5fdf1)) * **users:** re-apply route tab when serverConfig.billing arrives async ([#4138](#4138)) ([87cc7b2](87cc7b2)) * **users:** refetch billing subscription on auth state change ([#4125](#4125)) ([5a3425a](5a3425a)) ### Features * **admin:** invitations management tab ([#4217](#4217)) ([61ad86c](61ad86c)), closes [invitedBy/#actions](https://github.com/pierreb-devkit/Vue/issues/actions) * **analytics:** add identify and reset helpers, wire auth store ([#4104](#4104)) ([c4d8b87](c4d8b87)) * **auth:** invite-gated signup UI ([#4212](#4212)) ([d17a841](d17a841)) * **billing:** add 'Manage subscription' footer link in meterDrawer ([#4056](#4056)) ([#4057](#4057)) ([7d494af](7d494af)) * **billing:** align packs.component on BillingCardComponent (V4 unified schema) ([#4147](#4147)) ([7f5dd41](7f5dd41)) * **billing:** combined pool gauge + linear breakdown bars + alerts cleanup ([f5034b1](f5034b1)) * **billing:** config-driven static-content resolver (no file replace) ([e95d944](e95d944)) * **billing:** expose netRemainingRaw + overage in useMeter for negative quota display ([#4061](#4061)) ([111e64b](111e64b)), closes [#4060](#4060) * **billing:** meter gauges display % primary, compute units in overflow tooltip ([#4139](#4139)) ([50498ba](50498ba)) * **billing:** meter UX refonte — drop drawer + subscriptions tab ([#4059](#4059)) ([891b5ae](891b5ae)), closes [#subscriptions](https://github.com/pierreb-devkit/Vue/issues/subscriptions) [#1](#1) [#2](#2) [#3](#3) [#4](#4) * **billing:** post-grant upgrade prompt variant for depleted signupGrant ([#4128](#4128)) ([0ae8558](0ae8558)) * **billing:** pricing page redesign — multi-mode + auto-savings + sectioned features + FAQ ([#4102](#4102)) ([2c83ab6](2c83ab6)) * **billing:** pricingCard Free CTA — 'Sign up' for guests, route to /signup ([#4106](#4106)) ([cc92ac8](cc92ac8)) * **billing:** redesign Subscriptions view to match user-tabs aesthetic ([#4127](#4127)) ([15fd466](15fd466)) * **billing:** relocate billing under Organization settings ([#4175](#4175)) ([1f36137](1f36137)) * **billing:** sidenav compute gauge above sign-out row (meter mode) ([#4126](#4126)) ([fe1497f](fe1497f)) * **billing:** sidenav compute gauge redesign — button-shape above sign-out row ([#4140](#4140)) ([9d3a090](9d3a090)) * **billing:** sidenav compute gauge revamp — v-progress-circular + v-tooltip ([#4144](#4144)) ([5d60f1b](5d60f1b)), closes [#prepend](https://github.com/pierreb-devkit/Vue/issues/prepend) * **billing:** subscriptions view 2-col layout — drop dup bar, CTA to /pricing#units ([#4141](#4141)) ([b7ef516](b7ef516)), closes [pricing#units](https://github.com/pricing/issues/units) * **billing:** unified BillingCardComponent + annual toggle disabled state ([#4146](#4146)) ([474bb11](474bb11)) * **billing:** unified BillingCardComponent + annual toggle disabled state ([#4149](#4149)) ([d1403ff](d1403ff)) * **billing:** v4 hardening + Phase 3 polish — equivalences chips + UX gaps + a11y ([#4066](#4066)) ([dd40b2a](dd40b2a)) * **billing:** wire netRemainingRaw + overage into devkit components ([#4062](#4062)) ([61d6897](61d6897)), closes [#4061](#4061) * **core:** reusable PageTabs component + Account view refactor ([#4183](#4183)) ([dc2504a](dc2504a)) * **core:** unified logo+title lockup in header and sidenav ([#4086](#4086)) ([ec457db](ec457db)), closes [#4083](#4083) * **feature:** read ERRORS.md in Phase 0 before coding ([#4089](#4089)) ([ddf8f60](ddf8f60)) * **legal:** Add legal module + cookie consent (RGPD) ([#4097](#4097)) ([595e6c4](595e6c4)), closes [#3204116570](https://github.com/pierreb-devkit/Vue/issues/3204116570) [#3204116650](https://github.com/pierreb-devkit/Vue/issues/3204116650) [#1](#1) [#19](#19) [#18](#18) [#13](#13) [#11](#11) [#18](#18) [#3](#3) [#10](#10) [#12](#12) [#14](#14) [#15](#15) [#17](#17) [#20](#20) [#4](#4) [#5](#5) [#6](#6) [#7](#7) [#8](#8) [#9](#9) [#16](#16) [#3](#3) [#16](#16) [#22](#22) * **legal:** liquid glass cookie banner — Vuetify-only with friendlier copy ([#4115](#4115)) ([3d74789](3d74789)), closes [#4114](#4114) * **monitoring:** single-source PostHog Error Tracking (drop Sentry) ([#4118](#4118)) ([82dfca6](82dfca6)) * **organizations:** add Organization tab + rename General route to tab-addressable ([#4184](#4184)) ([fdfbf0b](fdfbf0b)) * **organizations:** soft suggestedJoin onboarding banner + recovery-screen copy ([#4176](#4176)) ([25a3ceb](25a3ceb)) * **seo:** enrich seoInjectPlugin — multi-schemas + rich SoftwareApplication + themeColor ([#4092](#4092)) ([#4111](#4111)) ([7f8bd09](7f8bd09)) * **skill/feature:** add Phase 0.0 issue claim-on-start ([#4117](#4117)) ([a582430](a582430)), closes [pierreb-projects/infra#28](https://github.com/pierreb-projects/infra/issues/28) * **skills/update-stack:** block on undeclared drift vs upstream ([#4228](#4228)) ([ab8ee68](ab8ee68)), closes [#4227](#4227) * **users:** subs full-width + delete account danger zone + halo full-bleed fix ([#4143](#4143)) ([981f073](981f073))
Summary
showAlreadyActiveDialogmethod,alreadyActiveDialog/alreadyActivePortalUrlstate, and the 409 dialog template block frombilling.subscriptions.component.vue. Confirmed zero production callers (the live 409 handling is self-contained inbilling.pricing.view.vue). Dropped Suite 9 from unit tests — those 5 tests called the method directly, bypassing the real flow (fake coverage). i18n keys retained as they are still used bybilling.pricing.view.vue.meterErrorfromuseMeter()into both consumers'setup()returns (billing.subscriptions.component.vueandbilling.usageBar.component.vue). Renders a closablev-alert[type=warning]above the meter section in the subscriptions component when polling fails. Addedbilling.meter.error.refreshFailedi18n keys (en + fr). Added new Suite 9 (3 tests) covering render when error is set, suppression when null, and dismiss by settingmeterError = null. usageBar exposesmeterErrorin its return but defers the UI to the parent subscriptions alert (no duplicate indicator — bar lives adjacent to subscriptions component).Test plan
billing.subscriptions.component.unit.tests.jspass (was 79, Suite 9 swapped: 5 dead removed, 3 real added)npm run buildcleannpm run lintcleanSummary by CodeRabbit
New Features
Bug Fixes
Localization