feat(billing): meter UX refonte — drop drawer + subscriptions tab#4059
Conversation
Refonte of the meter UX so the compute bar is no longer mounted globally
and lives inside the user account "Subscriptions" tab. All meter-specific
UI is gated by `serverConfig.billing.meterMode === true`.
D1 — Drop drawer
- Delete `billing.meterDrawer.component.vue` and its test.
- `billing.usageBar.component.vue` is now informational only: drop
`@open-drawer` emit, click handler, role=button, cursor:pointer.
D2 — Extract subscriptions component
- New `billing.subscriptions.component.vue` containing plan card,
meter widget (gated), extras balance, ledger, Stripe portal button.
- Replaces the standalone billing view UI.
D3 — Subscriptions tab in account
- Add 3rd tab in `users/views/user.view.vue`, visible to org
owners/admins on billing-enabled servers (meterMode OR active sub).
- Tab content renders `<BillingSubscriptionsComponent />`.
- Auto-select the tab when ?tab=subscriptions / #subscriptions is on
the route (used by the /billing redirect).
D4 — Pricing tabs glass + packs cards
- New `billing.packs.component.vue` rendering `config.billing.packs[]`
as clickable purchase cards (calls `createExtrasCheckout(packId)`).
- In meter mode, wrap pricing in glass tabs (Plans | Units) using the
existing `home.tabs.component.vue`. Legacy mode unchanged.
D5 — Redirect /billing
- `/billing` route is now a redirect to `/users?tab=subscriptions`.
- `billing.billing.view.vue` is deleted (superseded by the component).
D6 — i18n
- Add `billing.subscriptions.*`, `billing.pricing.tabs.*`,
`billing.packs.*` keys (en + fr).
- New `users/lang/{en,fr}.js` with `users.tabs.{profile,organizations,subscriptions}`.
D7 — Meter threshold alerts in devkit app.vue
- Port the 80% / 100% snackbar logic from trawl_vue into devkit
`app/app.vue`, gated on `isLoggedIn && meterMode`. The bar/drawer
are NOT mounted globally — only the alerts.
Tests
- 1278 / 1278 passed (lint + vitest), coverage thresholds met.
- New `billing.subscriptions.component.unit.tests.js` covers meter +
legacy mode rendering, ledger pagination, manage-subscription flow.
- `user.view.unit.tests.js` rewritten for showSubscriptionsTab + tab
routing from query/hash.
- `app.router.unit.tests.js` updated for the /billing → /users
redirect.
- `billing.e2e.tests.js` updated to assert /users?tab=subscriptions
landing instead of the retired Billing heading.
BREAKING UI: drawer removed, /billing redirected, the page-level
Billing view is gone. Downstream projects must remove any direct global
mount of `BillingUsageBarComponent` / `BillingMeterDrawerComponent`
from their `app.vue` (handled by /update-stack).
|
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 (9)
WalkthroughThis PR consolidates billing UI from a dedicated page into the user profile as a "Subscriptions" tab. The Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 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. Review rate limit: 0/1 reviews remaining, refill in 43 minutes and 27 seconds.Comment |
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| ErrorProne | 10 high |
🟢 Metrics 117 complexity · 18 duplication
Metric Results Complexity 117 Duplication 18
AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.
TIP This summary will be updated as you push new changes.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #4059 +/- ##
=======================================
Coverage 99.51% 99.51%
=======================================
Files 31 31
Lines 1034 1034
Branches 278 278
=======================================
Hits 1029 1029
Misses 5 5 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull Request Overview
This PR is currently not up to standards. While the structural migration of the billing UX is largely complete, there are critical architectural and safety issues that prevent merging. Most notably, a development-specific configuration is imported directly into a production module, and the core app.vue logic for billing alerts contains both a potential race condition and an inefficient Pinia usage pattern that has significantly increased cyclomatic complexity. Furthermore, although the PR intent explicitly lists i18n support, multiple components remain dependent on hardcoded English strings. There is also a notable absence of automated tests for the new threshold alert logic and pricing tab functionality.
About this PR
- Systemic issue: New UI components and alert logic are using hardcoded strings despite the PR's stated objective to support i18n and the presence of new keys in 'en.js' and 'fr.js'.
Test suggestions
- Verify /billing redirects to /users?tab=subscriptions for authenticated users
- Verify the Subscriptions tab in User Account is only visible to owners or admins
- Verify compute threshold alerts (80%/100%) fire once per week key
- Verify tab switching logic in Pricing view between Plans and Units
- Verify the usage bar is no longer interactive in meter mode
Prompt proposal for missing tests
Consider implementing these tests if applicable:
1. Verify compute threshold alerts (80%/100%) fire once per week key
2. Verify tab switching logic in Pricing view between Plans and Units
Low confidence findings
- The 'meterProgress' watch in 'app.vue' may fire duplicate alerts if the 'meterWeekKey' is initially null ('unknown') and subsequently updates to a valid week key after the first alert fires. Consider adding a check to ensure both values are resolved before triggering the snackbar.
TIP Improve review quality by adding custom instructions
TIP How was this review? Give us feedback
There was a problem hiding this comment.
Pull request overview
Refactors the billing “meter” UX by removing the global drawer/page flow and relocating subscription + meter UI into the user account “Subscriptions” tab, while keeping /billing working via redirect and adding meter-related UI guards.
Changes:
- Moves billing/subscription UX into
UserViewvia a newBillingSubscriptionsComponent, gated by billing config + org role. - Updates pricing to add “Plans | Units” glass tabs in meter mode and introduces a new packs purchase grid component.
- Removes the legacy
/billingstandalone view and meter drawer, updating unit/e2e/router tests accordingly.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/modules/users/views/user.view.vue | Adds “Subscriptions” tab + route-driven tab selection and mounts BillingSubscriptionsComponent |
| src/modules/users/tests/user.view.unit.tests.js | Updates/extends tests for tab visibility and query/hash tab routing |
| src/modules/users/lang/en.js | Adds users tab i18n key map (EN) |
| src/modules/users/lang/fr.js | Adds users tab i18n key map (FR) |
| src/modules/billing/views/billing.pricing.view.vue | Adds glass tabs in meter mode and renders BillingPacksComponent as “Units” tab |
| src/modules/billing/views/billing.billing.view.vue | Deletes standalone Billing view |
| src/modules/billing/router/billing.router.js | Changes /billing route to redirect to /users?tab=subscriptions |
| src/modules/billing/components/billing.usageBar.component.vue | Makes meter usage bar informational-only (drops click/keyboard emit) |
| src/modules/billing/tests/billing.usageBar.component.unit.tests.js | Updates usage bar tests to match informational behavior |
| src/modules/billing/components/billing.subscriptions.component.vue | New consolidated subscriptions panel with meter + extras ledger in meter mode |
| src/modules/billing/tests/billing.subscriptions.component.unit.tests.js | Adds unit tests for BillingSubscriptionsComponent behaviors |
| src/modules/billing/components/billing.packs.component.vue | Adds purchasable packs grid that calls createExtrasCheckout |
| src/modules/billing/lang/en.js | Adds i18n keys for subscriptions/pricing tabs/packs (EN) |
| src/modules/billing/lang/fr.js | Adds i18n keys for subscriptions/pricing tabs/packs (FR) |
| src/modules/billing/components/billing.meterDrawer.component.vue | Deletes meter drawer component |
| src/modules/billing/tests/billing.meterDrawer.component.unit.tests.js | Deletes meter drawer unit tests |
| src/modules/billing/tests/billing.billing.view.unit.tests.js | Deletes Billing view unit tests |
| src/modules/billing/tests/billing.compute.integration.unit.tests.js | Deletes billing compute integration unit tests |
| src/modules/billing/tests/billing.e2e.tests.js | Updates e2e expectations for /billing redirect behavior |
| src/modules/app/tests/app.router.unit.tests.js | Updates router tests for /billing redirect |
| src/modules/app/app.vue | Adds global meter threshold snackbars gated by login + meterMode |
…setup, dedup ledger call - Rename billing.development.config.js → billing.static-content.js (canonical source); keep old file as backward-compat re-export shim for downstream projects - Move useAuthStore/useBillingStore init from computed into setup() in app.vue (fixes Pinia store initialised on every computed evaluation — HIGH) - Align meterAlert texts with i18n key values in billing.alerts.threshold80/100 - Add i18n key comments to hardcoded tab labels in billing.pricing.view.vue - Remove redundant fetchExtrasLedger() in mounted() — already handled by immediate watcher in setup() (billing.subscriptions.component.vue)
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/modules/users/views/user.view.vue (1)
254-279:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRe-run the route-tab selection after organizations finish loading.
mounted()can execute beforefetchOrganizations()resolves, so/users?tab=subscriptionsinitially fails theshowSubscriptionsTabcheck and falls back toprofile. Because only route changes callapplyTabFromRoute(), the tab never self-corrects once org roles arrive.💡 Minimal fix
async created() { try { await this.organizationsStore.fetchOrganizations(); + this.applyTabFromRoute(); } catch { // interceptor handles snackbar } },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/modules/users/views/user.view.vue` around lines 254 - 279, The tab selection can run before organizations finish loading, so after successfully awaiting this.organizationsStore.fetchOrganizations() in the created() lifecycle hook call this.applyTabFromRoute() to re-run the route-tab selection (so showSubscriptionsTab has up-to-date org roles); keep the existing mounted() call or remove it if redundant, and preserve the existing catch behavior for the fetchOrganizations() call.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/modules/app/app.vue`:
- Around line 101-106: The dedupe Set meterAlertedKeys is currently keyed only
by `${weekKey}:${level}`, causing alerts to be suppressed across organizations;
update all places that build or check the dedupe key (where
meterAlertedKeys.add(...) and meterAlertedKeys.has(...) are used) to include the
active organization id (e.g., `${activeOrganizationId}:${weekKey}:${level}`),
update the JSDoc/type comment for meterAlertedKeys to reflect the new format,
and ensure any helper that constructs the key (or create a small helper like
getMeterAlertKey(orgId, weekKey, level)) is used consistently wherever the
dedupe check or insertion occurs.
In `@src/modules/billing/components/billing.packs.component.vue`:
- Around line 124-133: The onBuy method currently calls
billingStore.createExtrasCheckout(pack.packId) directly and must reuse the same
auth/org guard used by onSelectPlan: before setting this.purchasingId or calling
createExtrasCheckout, check the user's auth and current organization (the same
condition/redirect logic used in onSelectPlan) and if unauthenticated or missing
org, trigger the sign-in/org-selection redirect flow instead of initiating the
extras checkout; only proceed to call createExtrasCheckout when the guard
passes, and ensure to reference onBuy, onSelectPlan, createExtrasCheckout,
billingStore and purchasingId when making the change.
In `@src/modules/billing/components/billing.subscriptions.component.vue`:
- Around line 194-204: The watch on meterMode with { immediate: true } already
triggers billingStore.fetchExtrasLedger({ page: 1, limit: 20 }) when meterMode
is active, so remove the duplicate fetch call from mounted() (and the similar
duplicate at the other occurrence around the code referenced as lines 262-264)
to avoid sending the first ledger page twice; keep the watch as-is (or
alternatively remove the immediate flag and call fetch from mounted only) but do
not call billingStore.fetchExtrasLedger for page 1 in both places.
- Around line 251-265: The mounted() hook must detect the legacy packPurchased
query flag and surface a success state: read this.$route.query.packPurchased (or
equivalent router query) in mounted(), and if present (truthy), call a new
handler (e.g., handlePackPurchased() or
billingStore.processPackPurchaseSuccess()) or set a component flag (e.g.,
packPurchasedSuccess) that triggers the existing success UI/modal and refreshes
billing state (ensure you still call billingStore.fetchSubscription()); also
clear or replace the query param so repeated mounts don't re-show the message.
Ensure the references are to mounted(), billingStore.createExtrasCheckout(), and
billingStore.fetchSubscription() so the behavior connects to the checkout flow.
In `@src/modules/billing/tests/billing.e2e.tests.js`:
- Around line 140-145: Tests reverted to using waitForURL/toHaveURL reintroduced
a flakey race with auth-store rehydration; restore the intentional fixed delay
before asserting redirects. In the test named "redirects unauthenticated user to
signin" (and the analogous test around lines 184-189), reinsert a
page.waitForTimeout(2000) immediately after page.goto('/billing') and before the
URL assertion, so the SPA redirect has time to settle, then perform the
expect(page.url()).toContain('/signin') (and the other redirect assertions) as
before.
In `@src/modules/users/views/user.view.vue`:
- Around line 241-252: The showSubscriptionsTab() guard in the component is too
strict: instead of requiring meterMode || this.isPlanActive, change it so that
if billing is enabled (this.authStore.serverConfig?.billing?.enabled === true)
and the user has owner/admin role (this.hasOwnerOrAdminRole) the tab is shown;
i.e. remove the final meterMode || this.isPlanActive check in
showSubscriptionsTab() so BillingSubscriptionsComponent can render its free-plan
state (subscription may be null).
---
Outside diff comments:
In `@src/modules/users/views/user.view.vue`:
- Around line 254-279: The tab selection can run before organizations finish
loading, so after successfully awaiting
this.organizationsStore.fetchOrganizations() in the created() lifecycle hook
call this.applyTabFromRoute() to re-run the route-tab selection (so
showSubscriptionsTab has up-to-date org roles); keep the existing mounted() call
or remove it if redundant, and preserve the existing catch behavior for the
fetchOrganizations() call.
🪄 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: fcd8a749-cbee-4a2b-879d-68e1e347e072
📒 Files selected for processing (21)
src/modules/app/app.vuesrc/modules/app/tests/app.router.unit.tests.jssrc/modules/billing/components/billing.meterDrawer.component.vuesrc/modules/billing/components/billing.packs.component.vuesrc/modules/billing/components/billing.subscriptions.component.vuesrc/modules/billing/components/billing.usageBar.component.vuesrc/modules/billing/lang/en.jssrc/modules/billing/lang/fr.jssrc/modules/billing/router/billing.router.jssrc/modules/billing/tests/billing.billing.view.unit.tests.jssrc/modules/billing/tests/billing.compute.integration.unit.tests.jssrc/modules/billing/tests/billing.e2e.tests.jssrc/modules/billing/tests/billing.meterDrawer.component.unit.tests.jssrc/modules/billing/tests/billing.subscriptions.component.unit.tests.jssrc/modules/billing/tests/billing.usageBar.component.unit.tests.jssrc/modules/billing/views/billing.billing.view.vuesrc/modules/billing/views/billing.pricing.view.vuesrc/modules/users/lang/en.jssrc/modules/users/lang/fr.jssrc/modules/users/tests/user.view.unit.tests.jssrc/modules/users/views/user.view.vue
💤 Files with no reviewable changes (5)
- src/modules/billing/tests/billing.billing.view.unit.tests.js
- src/modules/billing/tests/billing.meterDrawer.component.unit.tests.js
- src/modules/billing/tests/billing.compute.integration.unit.tests.js
- src/modules/billing/components/billing.meterDrawer.component.vue
- src/modules/billing/views/billing.billing.view.vue
…t, tests - user.view.vue: add watcher on showSubscriptionsTab to re-apply route tab when it becomes true; fixes async race where applyTabFromRoute() runs before fetchOrganizations() resolves (Copilot #1) - billing.packs.component.vue: remove card-level @click + cursor:pointer — the <v-btn> inside is already keyboard accessible; avoids inaccessible clickable area for keyboard/screen-reader users (Copilot #2) - billing.packs.component.vue: update component header comment to accurately describe the real data source (store first, static fallback) (Copilot #3) - billing.packs.component.unit.tests.js: add focused unit test suite covering empty state, pack card rendering, happy-path purchase, failure banner, and loading/disabled state (Copilot #4)
…uard, tab gate, pack UX
- app.vue: include activeOrgId in meterAlertedKeys dedupe key
(${orgId}:${weekKey}:${level}) so switching organizations does not suppress
alerts for the new org within the same billing week (CR major)
- billing.packs.component.vue: add auth/org guard in onBuy() — mirrors
onSelectPlan() pattern: guests → /signin, no-org → /organization-required;
update tests to cover logged-in (happy/error/loading) and guest guard paths
- billing.subscriptions.component.vue: detect ?packPurchased=1 redirect param
in mounted() and show success banner; clear the query param to prevent
re-display on refresh (CR minor)
- user.view.vue: remove meterMode || isPlanActive restriction from
showSubscriptionsTab — gate is now billingEnabled && hasOwnerOrAdminRole
only; BillingSubscriptionsComponent already handles the free-plan state (CR major)
- user.view.unit.tests.js: update test expectation to match new correct behavior
Summary
Refonte the meter UX so the compute bar is no longer mounted globally and lives inside the user account "Subscriptions" tab. All meter-specific UI is gated by
serverConfig.billing.meterMode === true. Bundled in ONE PR to minimise downstream/update-stackruns.Changes (D1…D7)
billing.meterDrawer.component.vue;billing.usageBar.component.vuenow informational only (no@open-drawer, no role=button, no cursor:pointer).billing.subscriptions.component.vue(plan card + meter widget + extras + ledger + Stripe portal).users/views/user.view.vuegets a 3rd tab visible to org owners/admins on billing-enabled servers; auto-switches when route has?tab=subscriptionsor#subscriptions.billing.packs.component.vue(purchase cards mappingconfig.billing.packs[]), and the pricing view wraps content in glass tabs (Plans | Units) when meter mode is on. Legacy mode unchanged./billingnow redirects to/users?tab=subscriptions. The standalonebilling.billing.view.vueis deleted.billing.subscriptions.*,billing.pricing.tabs.*,billing.packs.*(en + fr) and newusers/lang/{en,fr}.jswithusers.tabs.{profile,organizations,subscriptions}.trawl_vue/src/modules/app/app.vue, gated onisLoggedIn && meterMode. The bar/drawer are NOT mounted globally — only the alerts.BREAKING UI
/billingis no longer a stand-alone page; it redirects.BillingUsageBarComponent/BillingMeterDrawerComponentfrom theirapp.vue(handled by/update-stack).Test plan
npm run lint— cleannpm run test:unit— 1278 / 1278 passed, no failures/update-stackpropagation: trawl_vue landing on/users?tab=subscriptionsafter hitting/billing, glass tabs on pricing, no global meter bar.Summary by CodeRabbit
Release Notes
New Features
Changes
Documentation