feat: role-aware billing status banner for team workspaces (FE-1246)#13641
Conversation
🎭 Playwright: ✅ 1712 passed, 0 failed · 2 flaky📊 Browser Reports
🎨 Storybook: ✅ Built — View Storybook📦 Bundle: 7.86 MB gzip 🔴 +2.1 kBDetailsSummary
Category Glance App Entry Points — 53.2 kB (baseline 53.2 kB) • ⚪ 0 BMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 1.27 MB (baseline 1.27 MB) • ⚪ 0 BGraph editor runtime, canvas, workflow orchestration
Status: 1 added / 1 removed Views & Navigation — 110 kB (baseline 110 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Status: 11 added / 11 removed / 3 unchanged Panels & Settings — 553 kB (baseline 553 kB) • 🟢 -30 BConfiguration panels, inspectors, and settings screens
Status: 12 added / 12 removed / 15 unchanged User & Accounts — 29.1 kB (baseline 29.1 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Status: 7 added / 7 removed / 3 unchanged Editors & Dialogs — 117 kB (baseline 117 kB) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
Status: 4 added / 4 removed / 1 unchanged UI Components — 57.5 kB (baseline 57.5 kB) • ⚪ 0 BReusable component library chunks
Status: 5 added / 5 removed / 8 unchanged Data & Services — 275 kB (baseline 275 kB) • ⚪ 0 BStores, services, APIs, and repositories
Status: 13 added / 13 removed / 3 unchanged Utilities & Hooks — 3.42 MB (baseline 3.42 MB) • 🔴 +3.01 kBHelpers, composables, and utility bundles
Status: 19 added / 19 removed / 17 unchanged Vendor & Third-Party — 15.3 MB (baseline 15.3 MB) • ⚪ 0 BExternal libraries and shared vendor chunks Status: 16 unchanged Other — 11.9 MB (baseline 11.9 MB) • 🔴 +8.97 kBBundles that do not match a named category
Status: 75 added / 75 removed / 88 unchanged ⚡ Performance Report
Show regressions
All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-07-16T18:26:35.312Z",
"gitSha": "c861e3d2fe317e14240349d6fed4a8253e313354",
"branch": "jaewon/fe-1246-team-credit-exhaustion-blocked-state-ux-role-aware-cta",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2017.15200000001,
"styleRecalcs": 9,
"styleRecalcDurationMs": 9.337,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 400.25399999999996,
"heapDeltaBytes": -4892328,
"heapUsedBytes": 47982536,
"domNodes": -290,
"jsHeapTotalBytes": 16379904,
"scriptDurationMs": 17.05,
"eventListeners": -199,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-idle",
"durationMs": 2104.4019999999364,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.315999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 422.991,
"heapDeltaBytes": -6559224,
"heapUsedBytes": 63478640,
"domNodes": -277,
"jsHeapTotalBytes": 18751488,
"scriptDurationMs": 18.572000000000003,
"eventListeners": -199,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1924.1690000000062,
"styleRecalcs": 77,
"styleRecalcDurationMs": 42.300999999999995,
"layouts": 12,
"layoutDurationMs": 3.7740000000000005,
"taskDurationMs": 894.876,
"heapDeltaBytes": -5662472,
"heapUsedBytes": 53386140,
"domNodes": 59,
"jsHeapTotalBytes": 24903680,
"scriptDurationMs": 139.303,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1882.0890000000645,
"styleRecalcs": 76,
"styleRecalcDurationMs": 42.925,
"layouts": 12,
"layoutDurationMs": 3.883,
"taskDurationMs": 852.187,
"heapDeltaBytes": -12155460,
"heapUsedBytes": 57836320,
"domNodes": -237,
"jsHeapTotalBytes": 18751488,
"scriptDurationMs": 125.76599999999999,
"eventListeners": -199,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1738.2390000000214,
"styleRecalcs": 33,
"styleRecalcDurationMs": 18.713,
"layouts": 6,
"layoutDurationMs": 0.7699999999999998,
"taskDurationMs": 351.84900000000005,
"heapDeltaBytes": 6817716,
"heapUsedBytes": 68782168,
"domNodes": 78,
"jsHeapTotalBytes": 24117248,
"scriptDurationMs": 23.929,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1741.2269999999808,
"styleRecalcs": 31,
"styleRecalcDurationMs": 18.642,
"layouts": 6,
"layoutDurationMs": 0.745,
"taskDurationMs": 345.58200000000005,
"heapDeltaBytes": 5185796,
"heapUsedBytes": 57362288,
"domNodes": -241,
"jsHeapTotalBytes": 1437696,
"scriptDurationMs": 18.928,
"eventListeners": -186,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "dom-widget-clipping",
"durationMs": 657.400999999993,
"styleRecalcs": 12,
"styleRecalcDurationMs": 10.306999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 430.298,
"heapDeltaBytes": -22546564,
"heapUsedBytes": 47627296,
"domNodes": -274,
"jsHeapTotalBytes": 6692864,
"scriptDurationMs": 62.137,
"eventListeners": -201,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "dom-widget-clipping",
"durationMs": 596.9860000000153,
"styleRecalcs": 11,
"styleRecalcDurationMs": 8.459999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 356.931,
"heapDeltaBytes": 7850432,
"heapUsedBytes": 67738060,
"domNodes": 18,
"jsHeapTotalBytes": 19922944,
"scriptDurationMs": 59.876,
"eventListeners": 0,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-idle",
"durationMs": 2026.1629999999968,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.8919999999999995,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 553.604,
"heapDeltaBytes": -8814176,
"heapUsedBytes": 59863836,
"domNodes": -273,
"jsHeapTotalBytes": -1855488,
"scriptDurationMs": 93.59500000000001,
"eventListeners": -199,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-idle",
"durationMs": 2014.6899999999732,
"styleRecalcs": 6,
"styleRecalcDurationMs": 5.311,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 492.753,
"heapDeltaBytes": -1056592,
"heapUsedBytes": 60735572,
"domNodes": -277,
"jsHeapTotalBytes": -1101824,
"scriptDurationMs": 84.64899999999999,
"eventListeners": -197,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-pan",
"durationMs": 2159.208000000035,
"styleRecalcs": 68,
"styleRecalcDurationMs": 19.979,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1130.152,
"heapDeltaBytes": 10421484,
"heapUsedBytes": 71482792,
"domNodes": -266,
"jsHeapTotalBytes": 5160960,
"scriptDurationMs": 392.396,
"eventListeners": -128,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2095.17900000003,
"styleRecalcs": 69,
"styleRecalcDurationMs": 18.006000000000004,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1049.231,
"heapDeltaBytes": 29712328,
"heapUsedBytes": 83085820,
"domNodes": -268,
"jsHeapTotalBytes": 204800,
"scriptDurationMs": 385.97400000000005,
"eventListeners": -197,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-zoom",
"durationMs": 3111.2249999999904,
"styleRecalcs": 63,
"styleRecalcDurationMs": 16.496999999999996,
"layouts": 60,
"layoutDurationMs": 7.5600000000000005,
"taskDurationMs": 1309.371,
"heapDeltaBytes": 12983040,
"heapUsedBytes": 67306572,
"domNodes": -281,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 483.272,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3125.904999999989,
"styleRecalcs": 63,
"styleRecalcDurationMs": 15.901999999999996,
"layouts": 60,
"layoutDurationMs": 7.521999999999999,
"taskDurationMs": 1317.5789999999997,
"heapDeltaBytes": 15048348,
"heapUsedBytes": 69780476,
"domNodes": 6,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 492.40799999999996,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "minimap-idle",
"durationMs": 2026.3229999999908,
"styleRecalcs": 5,
"styleRecalcDurationMs": 4.717000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 530.04,
"heapDeltaBytes": -3143420,
"heapUsedBytes": 60523772,
"domNodes": -278,
"jsHeapTotalBytes": -1626112,
"scriptDurationMs": 93.10600000000002,
"eventListeners": -197,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2025.859999999966,
"styleRecalcs": 7,
"styleRecalcDurationMs": 7.960999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 556.666,
"heapDeltaBytes": -16325788,
"heapUsedBytes": 60045428,
"domNodes": -274,
"jsHeapTotalBytes": 3473408,
"scriptDurationMs": 101.721,
"eventListeners": -197,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 549.3930000000091,
"styleRecalcs": 46,
"styleRecalcDurationMs": 10.819,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 373.14399999999995,
"heapDeltaBytes": 7846076,
"heapUsedBytes": 67188452,
"domNodes": 18,
"jsHeapTotalBytes": 18087936,
"scriptDurationMs": 124.019,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 563.5670000000346,
"styleRecalcs": 46,
"styleRecalcDurationMs": 11.316999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 359.758,
"heapDeltaBytes": 6972800,
"heapUsedBytes": 69630484,
"domNodes": 18,
"jsHeapTotalBytes": 21245952,
"scriptDurationMs": 121.957,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-idle",
"durationMs": 2045.299,
"styleRecalcs": 11,
"styleRecalcDurationMs": 10.187,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 416.085,
"heapDeltaBytes": -7294868,
"heapUsedBytes": 62857220,
"domNodes": -273,
"jsHeapTotalBytes": 19275776,
"scriptDurationMs": 15.115,
"eventListeners": -199,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-idle",
"durationMs": 2039.103999999952,
"styleRecalcs": 9,
"styleRecalcDurationMs": 9.476,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 421.73799999999994,
"heapDeltaBytes": -1879352,
"heapUsedBytes": 50589356,
"domNodes": -292,
"jsHeapTotalBytes": 16642048,
"scriptDurationMs": 18.492,
"eventListeners": -199,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1727.1809999999732,
"styleRecalcs": 75,
"styleRecalcDurationMs": 38.997,
"layouts": 16,
"layoutDurationMs": 4.175,
"taskDurationMs": 689.286,
"heapDeltaBytes": -9590668,
"heapUsedBytes": 49540596,
"domNodes": 62,
"jsHeapTotalBytes": 25165824,
"scriptDurationMs": 93.418,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1707.4219999999514,
"styleRecalcs": 76,
"styleRecalcDurationMs": 37.352999999999994,
"layouts": 16,
"layoutDurationMs": 4.13,
"taskDurationMs": 716.275,
"heapDeltaBytes": -9626732,
"heapUsedBytes": 49564464,
"domNodes": 62,
"jsHeapTotalBytes": 25952256,
"scriptDurationMs": 94.25999999999999,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-transition-enter",
"durationMs": 1082.6039999999466,
"styleRecalcs": 19,
"styleRecalcDurationMs": 33.385999999999996,
"layouts": 15,
"layoutDurationMs": 16.596,
"taskDurationMs": 902.8800000000001,
"heapDeltaBytes": 20814836,
"heapUsedBytes": 93793664,
"domNodes": 12277,
"jsHeapTotalBytes": 14626816,
"scriptDurationMs": 32.81399999999999,
"eventListeners": 862,
"totalBlockingTimeMs": 162,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "viewport-pan-sweep",
"durationMs": 8184.425000000033,
"styleRecalcs": 250,
"styleRecalcDurationMs": 53.44900000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3912.1470000000004,
"heapDeltaBytes": 816668,
"heapUsedBytes": 69063480,
"domNodes": -272,
"jsHeapTotalBytes": 970752,
"scriptDurationMs": 1272.528,
"eventListeners": -183,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8147.74900000009,
"styleRecalcs": 250,
"styleRecalcDurationMs": 52.873999999999995,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3759.8250000000003,
"heapDeltaBytes": 7521128,
"heapUsedBytes": 69088836,
"domNodes": -274,
"jsHeapTotalBytes": 1200128,
"scriptDurationMs": 1228.537,
"eventListeners": -183,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 12488.72199999994,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12470.693000000001,
"heapDeltaBytes": -50300028,
"heapUsedBytes": 166238244,
"domNodes": -3300,
"jsHeapTotalBytes": 7077888,
"scriptDurationMs": 581.3539999999999,
"eventListeners": -16371,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.223333333333358,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 12603.27499999994,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12583.402,
"heapDeltaBytes": -36947124,
"heapUsedBytes": 164127024,
"domNodes": -3300,
"jsHeapTotalBytes": 757760,
"scriptDurationMs": 610.754,
"eventListeners": -16378,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.223333333333358,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-pan",
"durationMs": 15360.319000000003,
"styleRecalcs": 78,
"styleRecalcDurationMs": 19.064999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 15334.896,
"heapDeltaBytes": -20793712,
"heapUsedBytes": 158357988,
"domNodes": -3291,
"jsHeapTotalBytes": 16515072,
"scriptDurationMs": 891.2220000000001,
"eventListeners": -16364,
"totalBlockingTimeMs": 8,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 15495.034000000032,
"styleRecalcs": 76,
"styleRecalcDurationMs": 19.63599999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 15470.053,
"heapDeltaBytes": -11911192,
"heapUsedBytes": 180944364,
"domNodes": -3302,
"jsHeapTotalBytes": 21118976,
"scriptDurationMs": 802.2710000000001,
"eventListeners": -16370,
"totalBlockingTimeMs": 57,
"frameDurationMs": 17.220000000000073,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "workflow-execution",
"durationMs": 472.6590000000215,
"styleRecalcs": 13,
"styleRecalcDurationMs": 17.679000000000002,
"layouts": 2,
"layoutDurationMs": 0.41100000000000014,
"taskDurationMs": 168.64700000000002,
"heapDeltaBytes": -22561848,
"heapUsedBytes": 46781228,
"domNodes": -194,
"jsHeapTotalBytes": 4595712,
"scriptDurationMs": 10.718000000000002,
"eventListeners": -134,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "workflow-execution",
"durationMs": 501.7250000000786,
"styleRecalcs": 13,
"styleRecalcDurationMs": 23.006,
"layouts": 3,
"layoutDurationMs": 0.9789999999999999,
"taskDurationMs": 206.74399999999997,
"heapDeltaBytes": -22836540,
"heapUsedBytes": 46616712,
"domNodes": -203,
"jsHeapTotalBytes": 5644288,
"scriptDurationMs": 12.578999999999999,
"eventListeners": -132,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
}
]
} |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds cloud billing-status banners, credit-error classification, workspace member dialogs, permission-aware top-up routing, reactive billing composables, localized copy, tests, and Storybook scenarios. ChangesCredit exhaustion experience
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant BillingContext
participant useBillingBanner
participant BillingStatusBanner
participant dialogService
BillingContext->>useBillingBanner: provide billing status and subscription state
useBillingBanner-->>BillingStatusBanner: return banner kind
BillingStatusBanner->>dialogService: request add credits
dialogService-->>BillingStatusBanner: open purchase or member dialog
Possibly related PRs
Suggested labels: Suggested reviewers: Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (2 warnings, 1 inconclusive)
✅ Passed checks (3 passed)
✨ 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 |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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/platform/cloud/subscription/composables/useAccountPreconditionDialog.ts`:
- Around line 32-44: Move the useBillingContext() call out of the credits branch
in open and invoke it synchronously at the root of useAccountPreconditionDialog
during setup. Reuse the returned fetchStatus and fetchBalance methods in the
existing credits handling, preserving the best-effort Promise.allSettled refresh
and top-up dialog behavior.
In `@src/platform/workspace/components/CreditsExhaustedBanner.vue`:
- Around line 10-14: Update the text color utilities in the
CreditsExhaustedBanner template: replace text-base-foreground,
text-muted-foreground, and text-warning-background with their corresponding
arbitrary-value CSS token utilities using --base-foreground, --muted-foreground,
and --warning-background. Preserve the existing layout and icon styling.
🪄 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 Plus
Run ID: 0cce2f9e-f5ab-4f36-8179-0adc0cb17da1
📒 Files selected for processing (17)
src/locales/en/main.jsonsrc/platform/cloud/subscription/composables/useAccountPreconditionDialog.test.tssrc/platform/cloud/subscription/composables/useAccountPreconditionDialog.tssrc/platform/cloud/subscription/composables/useSubscriptionDialog.test.tssrc/platform/cloud/subscription/composables/useSubscriptionDialog.tssrc/platform/errorCatalog/accountPreconditionRouting.test.tssrc/platform/errorCatalog/runtimeErrorMatcher.tssrc/platform/workspace/components/CreditsExhaustedBanner.test.tssrc/platform/workspace/components/CreditsExhaustedBanner.vuesrc/platform/workspace/components/InsufficientCreditsMemberDialog.vuesrc/platform/workspace/composables/useCreditExhaustion.tssrc/services/dialogService.topUpCredits.test.tssrc/services/dialogService.tssrc/views/GraphView.test.tssrc/views/GraphView.vuesrc/views/LinearView.test.tssrc/views/LinearView.vue
Codecov Report❌ Patch coverage is @@ Coverage Diff @@
## main #13641 +/- ##
==========================================
- Coverage 77.96% 77.78% -0.18%
==========================================
Files 1667 1677 +10
Lines 95411 97028 +1617
Branches 33434 34584 +1150
==========================================
+ Hits 74383 75472 +1089
- Misses 20362 20870 +508
- Partials 666 686 +20
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 36 files with indirect coverage changes 🚀 New features to boost your workflow:
|
6ff79af to
49c4cd7
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/platform/workspace/composables/useBillingBanner.ts`:
- Around line 87-95: Add test coverage for the funded-reset watcher in the
billing banner composable: update the existing BillingStatusBanner test flow to
dismiss the out-of-credits banner, change the workspace subscription state to
funded, then verify dismissal resets and a subsequent exhaustion displays the
banner again. Keep deriveBillingBanner.test.ts focused on the pure function and
exercise the reactive watch through the banner/composable test.
🪄 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 Plus
Run ID: 62819318-8292-4aa1-9ba2-ed8eca4c3b8e
📒 Files selected for processing (19)
src/locales/en/main.jsonsrc/platform/cloud/subscription/composables/useAccountPreconditionDialog.test.tssrc/platform/cloud/subscription/composables/useAccountPreconditionDialog.tssrc/platform/cloud/subscription/composables/useSubscriptionDialog.test.tssrc/platform/cloud/subscription/composables/useSubscriptionDialog.tssrc/platform/errorCatalog/accountPreconditionRouting.test.tssrc/platform/errorCatalog/runtimeErrorMatcher.tssrc/platform/workspace/api/workspaceApi.tssrc/platform/workspace/components/BillingStatusBanner.test.tssrc/platform/workspace/components/BillingStatusBanner.vuesrc/platform/workspace/components/InsufficientCreditsMemberDialog.vuesrc/platform/workspace/composables/deriveBillingBanner.test.tssrc/platform/workspace/composables/useBillingBanner.tssrc/services/dialogService.topUpCredits.test.tssrc/services/dialogService.tssrc/views/GraphView.test.tssrc/views/GraphView.vuesrc/views/LinearView.test.tssrc/views/LinearView.vue
Surface a single priority-ordered billing banner on the graph and linear shells (FE-1246 + FE-968 banner states): - BillingStatusBanner renders one of paused > payment_failed > out-of-credits > ending, gated to team workspaces. Owner variants get an action (Update payment / Add credits / Reactivate); members get read-only copy for paused and out-of-credits, and fall through owner-only states. - Pure deriveBillingBanner() encodes the priority + role gating; a thin useBillingbanner() shared composable feeds it from useBillingContext, and owns the session dismiss for the out-of-credits banner. - out-of-credits also routes the submit-time rejection to the credits account-precondition modal (team 429 message + personal insufficient_credits type), with team members getting a read-only notice instead of the top-up dialog. - paused is an inert stub until the backend projects a 'paused' subscription status; every other state renders from today's billing fields.
49c4cd7 to
596c71a
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
src/platform/cloud/subscription/composables/useAccountPreconditionDialog.ts (1)
14-44: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winCall composables synchronously in setup.
useBillingContext()is a Vue composable and should be called synchronously at the root ofuseAccountPreconditionDialog(during the component'ssetupphase), rather than asynchronously inside theopenmethod. Calling composables inside event handlers or delayed callbacks can fail if they rely ongetCurrentInstance()orinject()and haven't been initialized in an active effect scope.🐛 Proposed fix
export function useAccountPreconditionDialog() { const dialogService = useDialogService() + const { fetchStatus, fetchBalance } = useBillingContext() function open( precondition: AccountPrecondition, context: AccountPreconditionContext = {} ): void { switch (precondition) { case 'sign_in': void dialogService.showApiNodesSignInDialog( context.nodeType ? [context.nodeType] : [] ) return case 'subscription': void dialogService.showSubscriptionRequiredDialog({ reason: 'subscription_required' }) return case 'credits': { // The server just declared the balance exhausted; there is no push or // polling for billing state, so refresh it here to converge // hasFunds-keyed surfaces such as the credits-exhausted banner. The // refresh is best-effort: allSettled keeps a flaky billing API from // surfacing as unhandled rejections. - const { fetchStatus, fetchBalance } = useBillingContext() void Promise.allSettled([fetchStatus(), fetchBalance()]) void dialogService.showTopUpCreditsDialog({ isInsufficientCredits: true }) return } } }🤖 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/platform/cloud/subscription/composables/useAccountPreconditionDialog.ts` around lines 14 - 44, Move the useBillingContext() call from the credits branch of open into the root of useAccountPreconditionDialog, alongside useDialogService(), and retain the returned fetchStatus and fetchBalance functions for use in the credits case. Keep the existing best-effort Promise.allSettled refresh and dialog behavior unchanged.
🤖 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/platform/workspace/components/InsufficientCreditsMemberDialog.vue`:
- Around line 9-25: Replace the Tailwind foreground alias classes in the title,
close button, and description within the dialog template: change
text-base-foreground to text-(--base-foreground) and text-muted-foreground to
text-(--muted-foreground), preserving the existing styling and behavior.
In `@src/platform/workspace/composables/useBillingBanner.ts`:
- Around line 69-104: Scope the out-of-credits dismissed state in
useBillingBannerInternal to the current workspace, clearing it when the active
workspace changes so dismissal cannot carry across workspaces while the shared
composable remains mounted. Preserve the existing simultaneous-mount behavior.
Add a workspace-switch test in
src/platform/workspace/components/BillingStatusBanner.test.ts lines 190-203
covering dismissal, switching workspaces, and re-showing the banner.
---
Duplicate comments:
In `@src/platform/cloud/subscription/composables/useAccountPreconditionDialog.ts`:
- Around line 14-44: Move the useBillingContext() call from the credits branch
of open into the root of useAccountPreconditionDialog, alongside
useDialogService(), and retain the returned fetchStatus and fetchBalance
functions for use in the credits case. Keep the existing best-effort
Promise.allSettled refresh and dialog behavior unchanged.
🪄 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 Plus
Run ID: 28309e64-c48d-4915-a3ce-6a7e92e62a3a
📒 Files selected for processing (20)
src/locales/en/main.jsonsrc/platform/cloud/subscription/composables/useAccountPreconditionDialog.test.tssrc/platform/cloud/subscription/composables/useAccountPreconditionDialog.tssrc/platform/cloud/subscription/composables/useSubscriptionDialog.test.tssrc/platform/cloud/subscription/composables/useSubscriptionDialog.tssrc/platform/errorCatalog/accountPreconditionRouting.test.tssrc/platform/errorCatalog/runtimeErrorMatcher.tssrc/platform/workspace/api/workspaceApi.tssrc/platform/workspace/components/BillingStatusBanner.test.tssrc/platform/workspace/components/BillingStatusBanner.vuesrc/platform/workspace/components/InsufficientCreditsMemberDialog.vuesrc/platform/workspace/composables/deriveBillingBanner.test.tssrc/platform/workspace/composables/useBillingBanner.test.tssrc/platform/workspace/composables/useBillingBanner.tssrc/services/dialogService.topUpCredits.test.tssrc/services/dialogService.tssrc/views/GraphView.test.tssrc/views/GraphView.vuesrc/views/LinearView.test.tssrc/views/LinearView.vue
Investigating the cloud backend showed the paused signal belongs on the payment-lifecycle axis, not the subscription-activity one: a Stripe-paused subscription row stays `active`, and the pause is carried by billing_status (CONVENTIONS §4 T3). Cloud PR #5075 adds the `paused` value there. Two consequences the previous shape got wrong: - The check has to sit above the isActiveSubscription gate. The backend folds billing_status into is_active, so a paused workspace always reports is_active=false and a check below the gate would be unreachable. - The paused tests inherited isActiveSubscription: true from the funded fixture — a pairing the backend can never emit, so they passed without exercising the gate at all. They now pin is_active=false. subscription_status is no longer read by the banner, so it drops out of BillingBannerInputs and the mocks that fed it. Adds Storybook stories covering all six role/state variants. They drive the real deriveBillingBanner through a stubbed billing context, so a story can only show a state the backend can actually produce.
There was a problem hiding this comment.
♻️ Duplicate comments (2)
src/platform/workspace/composables/useBillingBanner.ts (2)
64-96: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winScope the dismissal state to a workspace, not the shared composable.
Because
createSharedComposableonly drops thisrefafter the last subscriber unmounts, a dismissal can carry into another active workspace while the banner stays mounted. If a user switches from one out-of-credits workspace to another out-of-credits workspace, the watcher won't fire and the banner will incorrectly remain dismissed.Please scope the out-of-credits dismissed state to the current workspace, clearing it when the active workspace changes so dismissal cannot carry across workspaces. Preserve the existing simultaneous-mount behavior, and add a workspace-switch test in
BillingStatusBanner.test.tscovering dismissal, switching workspaces, and re-showing the banner.🤖 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/platform/workspace/composables/useBillingBanner.ts` around lines 64 - 96, The dismissed state in the shared billing composable must be reset when the active workspace changes, rather than persisting across workspaces. Update the workspace-aware logic around dismissed, hasExhaustedFunds, and the existing workspace identity source to watch workspace changes and clear dismissal while preserving shared simultaneous-mount behavior. Add a BillingStatusBanner.test.ts case that dismisses the banner, switches to another out-of-credits workspace, and verifies it reappears.
81-89: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winMissing test coverage for the funded-reset watcher.
The
watch(hasExhaustedFunds, ...)reset behavior described in the comment isn't exercised by any provided test. As per path instructions for.test.tsfiles, you must ensure that missing tests for any new decision logic are covered.Please update the existing
BillingStatusBanner.test.tsflow to dismiss the out-of-credits banner, change the workspace subscription state to funded, and then verify dismissal resets and a subsequent exhaustion displays the banner again. KeepderiveBillingBanner.test.tsfocused on the pure function and exercise the reactive watch through the banner/composable test.🤖 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/platform/workspace/composables/useBillingBanner.ts` around lines 81 - 89, Extend the existing BillingStatusBanner.test.ts flow to cover the hasExhaustedFunds watcher: dismiss the out-of-credits banner, update the workspace subscription to funded, assert dismissal resets, then transition back to exhausted and verify the banner appears again. Keep deriveBillingBanner.test.ts limited to pure-function behavior and exercise this reactive reset through the banner/composable test.Source: Path instructions
🤖 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.
Duplicate comments:
In `@src/platform/workspace/composables/useBillingBanner.ts`:
- Around line 64-96: The dismissed state in the shared billing composable must
be reset when the active workspace changes, rather than persisting across
workspaces. Update the workspace-aware logic around dismissed,
hasExhaustedFunds, and the existing workspace identity source to watch workspace
changes and clear dismissal while preserving shared simultaneous-mount behavior.
Add a BillingStatusBanner.test.ts case that dismisses the banner, switches to
another out-of-credits workspace, and verifies it reappears.
- Around line 81-89: Extend the existing BillingStatusBanner.test.ts flow to
cover the hasExhaustedFunds watcher: dismiss the out-of-credits banner, update
the workspace subscription to funded, assert dismissal resets, then transition
back to exhausted and verify the banner appears again. Keep
deriveBillingBanner.test.ts limited to pure-function behavior and exercise this
reactive reset through the banner/composable test.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 3eaf819f-1240-4509-b3a9-8f162c2524f0
⛔ Files ignored due to path filters (6)
.github/pr-assets/banner-ending-owner.pngis excluded by!**/*.png.github/pr-assets/banner-out-of-credits-member.pngis excluded by!**/*.png.github/pr-assets/banner-out-of-credits-owner.pngis excluded by!**/*.png.github/pr-assets/banner-paused-member.pngis excluded by!**/*.png.github/pr-assets/banner-paused-owner.pngis excluded by!**/*.png.github/pr-assets/banner-payment-declined.pngis excluded by!**/*.png
📒 Files selected for processing (9)
.storybook/main.tssrc/platform/workspace/api/workspaceApi.tssrc/platform/workspace/components/BillingStatusBanner.stories.tssrc/platform/workspace/components/BillingStatusBanner.test.tssrc/platform/workspace/composables/deriveBillingBanner.test.tssrc/platform/workspace/composables/useBillingBanner.test.tssrc/platform/workspace/composables/useBillingBanner.tssrc/storybook/mocks/useBillingContext.tssrc/storybook/mocks/useWorkspaceUI.ts
💤 Files with no reviewable changes (1)
- src/platform/workspace/composables/useBillingBanner.test.ts
The prototype on comfydesigner/team-workspaces-v1 mounts BillingStatusBanner only inside the settings panels (MembersPanelContent, PlanCreditsPanelContent, PartnerNodesPanelContent) and puts no billing banner in GraphView or LinearView. Mounting it in the app shell was unfounded — match the prototype and Figma. Ours takes a single slot in WorkspacePanelContent between the tab list and the tab body, which is what "one banner above every settings tab" means for our two-tab container, rather than duplicating it into each panel. Drops the app-shell wrapper padding: spacing now comes from the mount site, and the @container stays on the banner so it isn't coupled to a parent's classes. The GraphView/LinearView placement tests go with the mounts. WorkspacePanelContent gains one pinning the banner above the tab body.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/platform/workspace/components/dialogs/settings/BillingStatusBanner.vue (1)
13-16: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winUse the information icon for the muted state.
The PR description specifies that the "Plan ends" state (which corresponds to
banner.muted) should display an "information icon (ⓘ)". However,icon-[lucide--circle-alert]renders an exclamation mark. Consider changing this toicon-[lucide--info](or your project's equivalent info icon) to match the intended design.🎨 Proposed fix
- ? 'icon-[lucide--circle-alert] text-muted-foreground' + ? 'icon-[lucide--info] text-muted-foreground'🤖 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/platform/workspace/components/dialogs/settings/BillingStatusBanner.vue` around lines 13 - 16, Update the muted-state icon selection in the banner class binding to use the project’s information icon, such as icon-[lucide--info], instead of the alert-circle icon; keep the existing warning icon for non-muted banners unchanged.
🤖 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/platform/workspace/components/dialogs/settings/WorkspacePanelContent.test.ts`:
- Around line 116-121: Rename the local variable planPanel to planTabTrigger in
the test, including its declaration and the compareDocumentPosition assertion,
while leaving the selected element and assertion behavior unchanged.
---
Outside diff comments:
In `@src/platform/workspace/components/dialogs/settings/BillingStatusBanner.vue`:
- Around line 13-16: Update the muted-state icon selection in the banner class
binding to use the project’s information icon, such as icon-[lucide--info],
instead of the alert-circle icon; keep the existing warning icon for non-muted
banners unchanged.
🪄 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 Plus
Run ID: ff30f8d3-89f2-4124-9240-f642efbc56d6
⛔ Files ignored due to path filters (6)
.github/pr-assets/banner-ending-owner.pngis excluded by!**/*.png.github/pr-assets/banner-out-of-credits-member.pngis excluded by!**/*.png.github/pr-assets/banner-out-of-credits-owner.pngis excluded by!**/*.png.github/pr-assets/banner-paused-member.pngis excluded by!**/*.png.github/pr-assets/banner-paused-owner.pngis excluded by!**/*.png.github/pr-assets/banner-payment-declined.pngis excluded by!**/*.png
📒 Files selected for processing (5)
src/platform/workspace/components/dialogs/settings/BillingStatusBanner.stories.tssrc/platform/workspace/components/dialogs/settings/BillingStatusBanner.test.tssrc/platform/workspace/components/dialogs/settings/BillingStatusBanner.vuesrc/platform/workspace/components/dialogs/settings/WorkspacePanelContent.test.tssrc/platform/workspace/components/dialogs/settings/WorkspacePanelContent.vue
The banner claimed to target team-plan users but tested workspaceType === 'team'. Those are different things, and the gap is real in both directions: - A team workspace can sit on a retired seat-based plan (tier STANDARD/CREATOR/ PRO), which the type gate admits. - Once consolidated billing lands, a personal workspace can hold a team plan (cloud#5010). Today the backend still blocks that pending BE-1526, so the type gate is only accidentally right, and only for now. Gate on plan identity instead. useBillingContext gains isTeamPlan: a credit stop marks the per-credit Team plan, a `team-` slug the retired seat-based ones. Deliberately not gated on isActiveSubscription, unlike isLegacyTeamPlan — the spend gate folds billing_status into is_active, so a paused or payment-failed team plan reports is_active=false and would otherwise stop reading as a team plan exactly when the banner is needed. Not gated on subscription_tier either: that would drop every legacy team subscriber, and the FE cannot even express 'TEAM' (tierPricing.ts resolves SubscriptionTier from the registry spec for what is an ingest field — a separate bug). Also fixes payment_failed, which was unreachable. It denies spend, so it always arrives with is_active=false, and the check sat below the is_active gate — the same trap already documented for paused two lines above it. Every team in Stripe dunning saw no banner. Hoisted alongside paused. Its tests passed only because they spread the funded fixture (isActiveSubscription: true) onto payment_failed, a pairing the backend never emits. They now pin is_active=false. Both fixes are mutation-tested: reverting either kills tests. Members no longer fall through from payment_failed to out-of-credits — with the real pairing that path was never reachable, and DES-380 says members never see the payment banner.
Replaces consolidated_billing_enabled as the flag that decides whether a personal workspace uses workspace-scoped billing. Team workspaces are unaffected — they are always workspace-scoped. This matters for the banner: isTeamPlan requires the workspace billing rail, so a personal workspace holding a team plan only surfaces billing state once its routing flag is on. Leaving routing on consolidated_billing_enabled while the subscribe path unblocks under billing_control_enabled would let the two diverge — a personal workspace could buy a team plan and then be routed to legacy, which has no billing_status at all, blanking the banner. consolidated_billing_enabled had exactly one consumer (this routing check), so it is replaced rather than left alongside. Cloud registers the new flag in Comfy-Org/cloud#5091. Renames the whole chain: remote config field, cached session ref, ServerFeatureFlag member, and the flags accessor.
The six PNGs were committed only to embed in the PR description. The Storybook stories they were captured from are the reproducible artifact and stay.
An audit of this branch's comments against the code found three assertions that are not true of the system: - "a team workspace can sit on a personal-tier legacy plan today" — it cannot. Plan rows are exclusive by construction (repository.go: AllowsTeam && !AllowsPersonal), and subscribe/preview/list all reject the cross-type case with INCOMPATIBLE_PLAN. The BE-1526 clause alone justifies the plan gate, so the false half is dropped rather than reworded. - "Shared across every banner mount (graph + linear)" — those mounts were deleted earlier on this branch (03ac671) and the parenthetical was left behind. createSharedComposable is still right, but for a different reason: the settings panel unmounts when the dialog closes. - The dismiss test's name repeated the same graph/linear claim. It now says what the test actually checks.
The flag rename missed browser_tests, so the spec kept booting with consolidated_billing_enabled. That no longer routes personal workspaces to the unified pricing table, and the deep-link assertions timed out waiting for a heading that never rendered. The spec types BOOT_FEATURES as `satisfies RemoteConfig`, which would have caught this — but pnpm typecheck only covers the root tsconfig, whose include list omits browser_tests. Verified with vue-tsc against browser_tests/tsconfig.json.
Put the whole FE-1246 banner behind the flag so a PostHog rollback hides it for every workspace, not just personal ones routed off the flag.
|
@coderabbitai approve |
✅ Action performedComments resolved and changes approved. |
Adds the single billing-status banner slot for team plans, rendered in priority order — paused > payment declined > out of credits > ending — with role-aware CTAs. At most one state shows at a time.
Placement matches the prototype (
comfydesigner/team-workspaces-v1) and Figma: the banner lives in the workspace settings panel, in one slot between the tab list and the tab body, so it shows on every tab. Our container has two tabs, so one mount covers both rather than duplicating the banner into each panel as the prototype does.Derivation lives in a pure
deriveBillingBanner(); the component only renders. State comes frombilling-statusfields, never from error strings (per Luke's user story).Gated on the plan, not the workspace type
useBillingContextgainsisTeamPlan: a credit stop marks the per-credit Team plan, ateam-slug the retired seat-based ones. Workspace type is the wrong question in both directions:Two deliberate non-choices:
isActiveSubscription, unlike the existingisLegacyTeamPlan. The spend gate foldsbilling_statusintois_active, so a paused or payment-failed team plan reportsis_active=false— it must still read as a team plan exactly when the banner is needed.subscription_tier === 'TEAM', which would silently drop every legacy team subscriber. The FE cannot express'TEAM'anyway:tierPricing.tsresolvesSubscriptionTierfrom the registry spec for what is an ingest field. That's a real bug, but orthogonal — filing separately.Fixes payment_failed, which was dead code
payment_faileddenies spend, so it always arrives withis_active=false— and the check sat below theis_activegate, two lines under a comment documenting that exact trap forpaused. Every team in Stripe dunning saw no banner. Now hoisted alongsidepaused.Its tests passed only because they spread the
fundedfixture (isActiveSubscription: true) ontopayment_failed— a pairing the backend never emits. They now pinis_active=false. Both this and theisTeamPlandecoupling are mutation-tested: reverting either kills tests.Members no longer fall through from
payment_failedto out-of-credits — with the real pairing that path was unreachable, and DES-380 says members never see the payment banner.Renders
The six role/state variants render from the Storybook stories at
Platform/Workspace/BillingStatusBanner. Each story drives the realderiveBillingBannerthrough a stubbed billing context, so a story can only show a state the backend can actually produce — not a hand-set banner kind.Run them with
DISTRIBUTION=cloud pnpm storybook— the banner is cloud-only andisCloudis compile-time, so a plainpnpm storybookrenders every story empty.Icon language follows the prototype's severity rule: amber triangle-alert for every action-needed state (paused, payment declined, out of credits), muted circle-alert reserved for the informational "plan ends" notice.
Feature flag
Personal-workspace routing now keys off
billing_control_enabled, replacingconsolidated_billing_enabled(which had exactly one consumer — this routing check — so it is replaced rather than left alongside). Cloud registers the new flag in Comfy-Org/cloud#5091.This coupling is load-bearing for the banner.
isTeamPlanrequires the workspace billing rail, so a personal workspace holding a team plan only surfaces billing state once its routing flag is on. Had routing stayed onconsolidated_billing_enabledwhile the subscribe path unblocks underbilling_control_enabled, the two could diverge: a personal workspace could buy a team plan and then be routed to legacy, which carries nobilling_statusat all, blanking the banner.Merge order: cloud#5091 must merge and
billing_control_enabledmust exist in PostHog before this ships, otherwise the flag resolves false for everyone and every personal workspace routes to legacy. Both flags default false, so the floor is today's behaviour — but any users already rolled out onconsolidated_billing_enabledwould revert to legacy billing untilbilling_control_enabledis rolled out to them. Worth confirming the current rollout state before merge.Backend status
billing_status: 'paused'merged in cloud #5075 but is not emitted yet: the lifecycle handler that writes it is unreachable in prod (ingest's forward allowlist omitscustomer.subscription.*; billing-api is ClusterIP with no ingress). That's BE-1530's unfinished half, tracked backend-side. The paused banner stays inert until then.The other three states render from fields the API already returns:
billing_status=payment_failed+renewal_date,has_funds, andcancel_at.How has this been tested?
useBillingContext.test.ts—isTeamPlanacross per-credit, legacy, paused, payment-failed, and team-workspace-on-personal-planderiveBillingBanner.test.ts— priority order, role gating, and the realisticis_active=falsepairing for both paused and payment_failedBillingStatusBanner.test.ts— per-variant copy and actions, date interpolation + no-date fallback, cross-mount dismiss, ending read-only for a non-original owneruseBillingBanner.test.ts— dismiss resets after a top-up so a later exhaustion re-showsWorkspacePanelContent.test.ts— the banner takes one slot above the tab bodyKnown gaps
isTeamPlanexcludes them by design, matching FE-1246's team scope and the!isInPersonalWorkspaceprecedent inuseSubscriptionDialog. Their copy would need personal variants (outOfCredits.bodysays "Your team…") and a free-tier decision. Belongs with the consolidated-billing rollout.actionsslot used only by its Invoices view. We have no such view, so it's omitted until needed.screenshots