fix(billing): resume a pending top-up as a top-up, not a subscription - #14715
Conversation
Billing status can hand back an in-flight operation so a client that lost its local reference — a reload, a different device — can resume it. The recovery path assumed that operation was always a subscription. A top-up resumed under subscription semantics takes the wrong branch throughout: isSettingUp replaces the subscription panel with a "setting up your subscription" spinner, the top-up dialog's own recovery prompt never matches, and completion runs reconcileSubscriptionSuccess() instead of refreshing the credit balance. Use the operation type the API now reports. Absent, it falls back to 'subscription', so this is inert against a backend that does not send it.
📄 Knowledge reviewDosu skipped reviewing this PR because your organization has used its |
🎨 Storybook: ✅ Built — View Storybook🎭 Playwright: ✅ 1786 passed, 0 failed · 2 flaky📊 Browser Reports
📦 Bundle: 8.25 MB gzip 🟢 -97 BDetailsSummary
Category Glance App Entry Points — 3.64 kB (baseline 3.64 kB) • ⚪ 0 BMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 1.35 MB (baseline 1.35 MB) • ⚪ 0 BGraph editor runtime, canvas, workflow orchestration
Status: 1 added / 1 removed / 1 unchanged Views & Navigation — 112 kB (baseline 112 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Status: 13 added / 13 removed / 4 unchanged Panels & Settings — 551 kB (baseline 551 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
Status: 11 added / 11 removed / 15 unchanged User & Accounts — 27 kB (baseline 27 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Status: 6 added / 6 removed / 4 unchanged Editors & Dialogs — 124 kB (baseline 124 kB) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
Status: 6 added / 6 removed / 1 unchanged UI Components — 70.8 kB (baseline 70.8 kB) • ⚪ 0 BReusable component library chunks
Status: 6 added / 6 removed / 9 unchanged Data & Services — 3.47 MB (baseline 3.47 MB) • 🔴 +55 BStores, services, APIs, and repositories
Status: 14 added / 14 removed / 3 unchanged Utilities & Hooks — 386 kB (baseline 386 kB) • ⚪ 0 BHelpers, composables, and utility bundles
Status: 16 added / 16 removed / 20 unchanged Vendor & Third-Party — 15.7 MB (baseline 15.7 MB) • ⚪ 0 BExternal libraries and shared vendor chunks Status: 16 unchanged Other — 12.8 MB (baseline 12.8 MB) • ⚪ 0 BBundles that do not match a named category
Status: 70 added / 70 removed / 211 unchanged ⚡ Performance Report
Show regressions
All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-08-04T22:32:11.353Z",
"gitSha": "89d887c9a7425b7c7152acee66305a18681f63f3",
"branch": "wei/topup-3ds-recovery-type",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2130.8609999999817,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.007999999999997,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 528.5609999999999,
"heapDeltaBytes": 5656488,
"heapUsedBytes": 69798816,
"domNodes": 18,
"jsHeapTotalBytes": 24641536,
"scriptDurationMs": 21.921,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-idle",
"durationMs": 2016.3009999999986,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.325,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 489.67800000000005,
"heapDeltaBytes": 5499524,
"heapUsedBytes": 69430640,
"domNodes": 18,
"jsHeapTotalBytes": 24903680,
"scriptDurationMs": 20.333,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 2052.030000000002,
"styleRecalcs": 76,
"styleRecalcDurationMs": 41.775000000000006,
"layouts": 12,
"layoutDurationMs": 3.8499999999999996,
"taskDurationMs": 1050.298,
"heapDeltaBytes": -13625992,
"heapUsedBytes": 50695208,
"domNodes": -282,
"jsHeapTotalBytes": 24096768,
"scriptDurationMs": 138.95100000000002,
"eventListeners": -153,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1890.337000000045,
"styleRecalcs": 74,
"styleRecalcDurationMs": 37.857,
"layouts": 12,
"layoutDurationMs": 3.6879999999999993,
"taskDurationMs": 906.905,
"heapDeltaBytes": -14023156,
"heapUsedBytes": 50087648,
"domNodes": -282,
"jsHeapTotalBytes": 23310336,
"scriptDurationMs": 126.29399999999998,
"eventListeners": -153,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1755.4489999999987,
"styleRecalcs": 32,
"styleRecalcDurationMs": 18.189,
"layouts": 6,
"layoutDurationMs": 0.6329999999999999,
"taskDurationMs": 416.199,
"heapDeltaBytes": 8588652,
"heapUsedBytes": 72773092,
"domNodes": 77,
"jsHeapTotalBytes": 24903680,
"scriptDurationMs": 24.644,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1735.0760000000491,
"styleRecalcs": 31,
"styleRecalcDurationMs": 18.267,
"layouts": 6,
"layoutDurationMs": 0.5349999999999999,
"taskDurationMs": 402.77399999999994,
"heapDeltaBytes": 8719772,
"heapUsedBytes": 72732896,
"domNodes": 76,
"jsHeapTotalBytes": 24641536,
"scriptDurationMs": 23.430000000000003,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "dom-widget-clipping",
"durationMs": 602.0359999999982,
"styleRecalcs": 10,
"styleRecalcDurationMs": 6.734,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 377.768,
"heapDeltaBytes": -11012924,
"heapUsedBytes": 52971304,
"domNodes": 16,
"jsHeapTotalBytes": 24379392,
"scriptDurationMs": 63.605,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "dom-widget-clipping",
"durationMs": 622.8509999999687,
"styleRecalcs": 11,
"styleRecalcDurationMs": 7.57,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 400.85499999999996,
"heapDeltaBytes": -11037132,
"heapUsedBytes": 52993308,
"domNodes": 18,
"jsHeapTotalBytes": 25690112,
"scriptDurationMs": 61.305,
"eventListeners": 0,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-idle",
"durationMs": 2028.4530000000132,
"styleRecalcs": 9,
"styleRecalcDurationMs": 9.03,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 668.968,
"heapDeltaBytes": 6917316,
"heapUsedBytes": 66160928,
"domNodes": -282,
"jsHeapTotalBytes": 3514368,
"scriptDurationMs": 104.337,
"eventListeners": -147,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-idle",
"durationMs": 2043.303999999921,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.908000000000003,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 720.8639999999999,
"heapDeltaBytes": 6768804,
"heapUsedBytes": 66077396,
"domNodes": -283,
"jsHeapTotalBytes": 2990080,
"scriptDurationMs": 115.80199999999999,
"eventListeners": -177,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-pan",
"durationMs": 2199.6950000000197,
"styleRecalcs": 68,
"styleRecalcDurationMs": 13.111,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1323.0469999999998,
"heapDeltaBytes": -6220872,
"heapUsedBytes": 55216884,
"domNodes": -283,
"jsHeapTotalBytes": 3514368,
"scriptDurationMs": 434.786,
"eventListeners": -179,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2165.3099999999768,
"styleRecalcs": 68,
"styleRecalcDurationMs": 13.222000000000005,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1296.5579999999998,
"heapDeltaBytes": 4619336,
"heapUsedBytes": 65304932,
"domNodes": -284,
"jsHeapTotalBytes": 3706880,
"scriptDurationMs": 432.37,
"eventListeners": -149,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-zoom",
"durationMs": 3228.1399999999962,
"styleRecalcs": 64,
"styleRecalcDurationMs": 13.939999999999998,
"layouts": 60,
"layoutDurationMs": 7.2909999999999995,
"taskDurationMs": 1552.168,
"heapDeltaBytes": -918052,
"heapUsedBytes": 62181620,
"domNodes": -285,
"jsHeapTotalBytes": 5349376,
"scriptDurationMs": 537.4749999999999,
"eventListeners": -183,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3296.7080000000806,
"styleRecalcs": 65,
"styleRecalcDurationMs": 15.042,
"layouts": 60,
"layoutDurationMs": 7.626,
"taskDurationMs": 1603.6979999999999,
"heapDeltaBytes": 2347640,
"heapUsedBytes": 64347296,
"domNodes": -287,
"jsHeapTotalBytes": 6922240,
"scriptDurationMs": 555.867,
"eventListeners": -153,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "minimap-idle",
"durationMs": 2043.655000000001,
"styleRecalcs": 8,
"styleRecalcDurationMs": 8.333999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 727.379,
"heapDeltaBytes": 6932796,
"heapUsedBytes": 67426348,
"domNodes": -283,
"jsHeapTotalBytes": 4562944,
"scriptDurationMs": 119.7,
"eventListeners": -179,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2012.2180000000753,
"styleRecalcs": 7,
"styleRecalcDurationMs": 6.8999999999999995,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 771.9610000000001,
"heapDeltaBytes": 6291236,
"heapUsedBytes": 66637692,
"domNodes": -285,
"jsHeapTotalBytes": 3514368,
"scriptDurationMs": 123.579,
"eventListeners": -147,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.660000000000007,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 624.5609999999715,
"styleRecalcs": 46,
"styleRecalcDurationMs": 10.229000000000003,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 430.348,
"heapDeltaBytes": -10695476,
"heapUsedBytes": 53340684,
"domNodes": 18,
"jsHeapTotalBytes": 26476544,
"scriptDurationMs": 124.26100000000001,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 599.3430000000899,
"styleRecalcs": 47,
"styleRecalcDurationMs": 12.579,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 418.66499999999996,
"heapDeltaBytes": -10388860,
"heapUsedBytes": 53773152,
"domNodes": 20,
"jsHeapTotalBytes": 26214400,
"scriptDurationMs": 121.05300000000001,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "subgraph-idle",
"durationMs": 1998.267999999996,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.466,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 457.89799999999997,
"heapDeltaBytes": 5221064,
"heapUsedBytes": 69295736,
"domNodes": 20,
"jsHeapTotalBytes": 24903680,
"scriptDurationMs": 13.965000000000002,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-idle",
"durationMs": 1999.0109999999959,
"styleRecalcs": 9,
"styleRecalcDurationMs": 7.707000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 486.39400000000006,
"heapDeltaBytes": 5232912,
"heapUsedBytes": 69252196,
"domNodes": 18,
"jsHeapTotalBytes": 25165824,
"scriptDurationMs": 18.291,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1725.387000000012,
"styleRecalcs": 78,
"styleRecalcDurationMs": 38.121,
"layouts": 16,
"layoutDurationMs": 4.29,
"taskDurationMs": 832.889,
"heapDeltaBytes": -16774576,
"heapUsedBytes": 47411792,
"domNodes": -279,
"jsHeapTotalBytes": 24096768,
"scriptDurationMs": 94.20299999999999,
"eventListeners": -153,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1697.5079999999707,
"styleRecalcs": 76,
"styleRecalcDurationMs": 35.675,
"layouts": 16,
"layoutDurationMs": 3.9260000000000006,
"taskDurationMs": 809.1930000000001,
"heapDeltaBytes": -4217048,
"heapUsedBytes": 60275928,
"domNodes": 65,
"jsHeapTotalBytes": 25165824,
"scriptDurationMs": 93.031,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-transition-enter",
"durationMs": 1434.5099999999888,
"styleRecalcs": 19,
"styleRecalcDurationMs": 31.27300000000001,
"layouts": 15,
"layoutDurationMs": 12.498000000000003,
"taskDurationMs": 978.2490000000001,
"heapDeltaBytes": 3416052,
"heapUsedBytes": 74834500,
"domNodes": 13673,
"jsHeapTotalBytes": 15466496,
"scriptDurationMs": 38.458999999999996,
"eventListeners": 2375,
"totalBlockingTimeMs": 149,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8507.715000000018,
"styleRecalcs": 250,
"styleRecalcDurationMs": 36.304,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4623.621999999999,
"heapDeltaBytes": 8618812,
"heapUsedBytes": 67971940,
"domNodes": -283,
"jsHeapTotalBytes": 5541888,
"scriptDurationMs": 1395.2340000000002,
"eventListeners": -131,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8366.361000000097,
"styleRecalcs": 251,
"styleRecalcDurationMs": 37.346000000000004,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4694.2029999999995,
"heapDeltaBytes": 11635620,
"heapUsedBytes": 71046432,
"domNodes": -281,
"jsHeapTotalBytes": 6066176,
"scriptDurationMs": 1423.946,
"eventListeners": -131,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 17359.217999999997,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 17314.338,
"heapDeltaBytes": -55199248,
"heapUsedBytes": 170803984,
"domNodes": -8312,
"jsHeapTotalBytes": -2826240,
"scriptDurationMs": 563.632,
"eventListeners": -16389,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-idle",
"durationMs": 17036.75899999996,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 17001.565,
"heapDeltaBytes": -40634340,
"heapUsedBytes": 165567592,
"domNodes": -8312,
"jsHeapTotalBytes": -8327168,
"scriptDurationMs": 593.544,
"eventListeners": -16387,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-pan",
"durationMs": 20702.364999999987,
"styleRecalcs": 140,
"styleRecalcDurationMs": 17.915000000000013,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 20658.152000000002,
"heapDeltaBytes": -23947580,
"heapUsedBytes": 182165488,
"domNodes": -8312,
"jsHeapTotalBytes": -8658944,
"scriptDurationMs": 893.231,
"eventListeners": -16383,
"totalBlockingTimeMs": 276,
"frameDurationMs": 18.333333333333332,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 20209.071999999993,
"styleRecalcs": 136,
"styleRecalcDurationMs": 15.875000000000028,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 20183.972,
"heapDeltaBytes": -51191800,
"heapUsedBytes": 176536356,
"domNodes": -8312,
"jsHeapTotalBytes": -11284480,
"scriptDurationMs": 867.13,
"eventListeners": -16383,
"totalBlockingTimeMs": 128,
"frameDurationMs": 17.77333333333336,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "workflow-execution",
"durationMs": 457.0359999999596,
"styleRecalcs": 14,
"styleRecalcDurationMs": 21.767,
"layouts": 4,
"layoutDurationMs": 1.3989999999999998,
"taskDurationMs": 117.41799999999999,
"heapDeltaBytes": 5312700,
"heapUsedBytes": 68468512,
"domNodes": 134,
"jsHeapTotalBytes": 5767168,
"scriptDurationMs": 10.200000000000001,
"eventListeners": 99,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "workflow-execution",
"durationMs": 460.5210000000852,
"styleRecalcs": 11,
"styleRecalcDurationMs": 18.038,
"layouts": 3,
"layoutDurationMs": 0.654,
"taskDurationMs": 108.99499999999999,
"heapDeltaBytes": 5076588,
"heapUsedBytes": 68057200,
"domNodes": 121,
"jsHeapTotalBytes": 4980736,
"scriptDurationMs": 8.911999999999999,
"eventListeners": 97,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
}
]
} |
📝 WalkthroughWalkthroughThe billing status contract now identifies pending subscription and top-up operations. Recovery uses this discriminator when starting the shared operation poller. A test verifies that top-up recovery preserves the operation type, ID, and payment action URL. ChangesBilling operation recovery
Estimated code review effort: 1 (Trivial) | ~5 minutes Possibly related PRs
Suggested labels: Suggested reviewers: Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 inconclusive)
✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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: 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/useWorkspaceBilling.test.ts`:
- Around line 288-308: Extend the recovery tests around setupBilling and
fetchStatus to cover a pending operation without pending_billing_op_type,
verifying mockStartOperation receives subscription as the fallback type. Also
assert the observable recovery result after polling, not only the startOperation
arguments, while preserving the existing explicit topup case.
🪄 Autofix
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: 2368623b-903c-4c61-81dc-d2d498346f74
📒 Files selected for processing (3)
src/platform/workspace/api/workspaceApi.tssrc/platform/workspace/composables/useWorkspaceBilling.test.tssrc/platform/workspace/composables/useWorkspaceBilling.ts
There was a problem hiding this comment.
🔍 Cursor Review — Consolidated panel
Triggered by @wei-hai.
Found 1 finding(s).
| Severity | Count |
|---|---|
| 🟢 Low | 1 |
Panel: 8/8 reviewers contributed findings.
Codecov Report✅ All modified and coverable lines are covered by tests. @@ Coverage Diff @@
## main #14715 +/- ##
=======================================
Coverage 79.58% 79.58%
=======================================
Files 1794 1794
Lines 116420 116421 +1
Branches 32844 32836 -8
=======================================
+ Hits 92652 92653 +1
+ Misses 23216 23210 -6
- Partials 552 558 +6
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 4 files with indirect coverage changes 🚀 New features to boost your workflow:
|
dante01yoon
left a comment
There was a problem hiding this comment.
Reviewed the pending billing-operation type propagation, fallback behavior, surrounding store consumers, and focused recovery coverage. The change preserves the existing subscription fallback and correctly restores top-up semantics. Required checks are passing.
…g silently (Comfy-Org#14759) Follow-up to Comfy-Org#14715. No behavior change against any value the API sends today — this is about what happens to the next one. Recovering a pending billing operation picked its client path with a two-way check, so anything that was not `topup` became `subscription` — including a value this client has never heard of. ```ts status.pending_billing_op_type === 'topup' ? 'topup' : 'subscription' ``` ### Before ```mermaid flowchart LR A["'subscription'"] --> S[subscription path] B["'topup'"] --> T[topup path] U[undefined] --> S N["any new value"] -.silently.-> S style N fill:#f8d7da,stroke:#b02a37,stroke-dasharray: 4 ``` ### After ```mermaid flowchart LR A["'subscription'"] --> S[subscription path] B["'topup'"] --> T[topup path] U[undefined] --> S N["any new value"] --> R["subscription path<br/>+ Sentry report"] style N fill:#d1e7dd,stroke:#0f5132 style R fill:#d1e7dd,stroke:#0f5132 ``` ## Why it matters `subscription` is the worst branch to default into. A mislabelled operation flips `isSettingUp`, so the panel covers the user's real plan with a "setting up your subscription" spinner for as long as the operation is pending; `topupActionOperation` never matches; and `handleSuccess` reconciles the subscription instead of refetching the balance. Same failure Comfy-Org#14715 fixed, waiting for the next value. ## The change An exhaustive `switch`: - `undefined` still resolves to `subscription` — the compatibility path for a server that does not send the field, and why Comfy-Org#14715 was safe to land alone. - An unknown value reports to Sentry (`billing_unknown_resume_mode`). - `const unexpected: never = type` in `default` means a widened union fails to compile. **The unknown case still resumes as a subscription, deliberately.** It is reachable only when this bundle is older than the server. In that window, dropping recovery strands a customer who cannot reach the payment page, while a wrong panel clears on reload — so recovery wins, and the report is what makes the skew visible. `JSON.stringify` rather than `String` in the report: `String` throws on a value with no callable `toString`/`valueOf`, and throwing out of the branch that exists to absorb a bad value would abort the whole status read. The value came from a parsed response, so it cannot be circular. ## Tests Three paths, all through `fetchStatus`: | Test | Covers | |---|---| | `recovers a pending subscription reported explicitly` | what the server sends today for an initial subscription or plan change | | `recovers a pending top-up as a top-up, not a subscription` | the `topup` path | | `reports a resume mode it does not recognize` | the unknown value — **verified red** against the old ternary | The absent-field compatibility path is already covered by the existing `recovers a pending subscription operation from billing status`. `vitest run --coverage` (76 passed, matching how CI runs unit tests), `pnpm typecheck`, `pnpm lint`, `pnpm format`. ## Scope note `BillingStatusResponse` is hand-written in `workspaceApi.ts` rather than taken from `@comfyorg/ingest-types`, so the `never` check is exhaustive over a hand-maintained union: it catches an edit to that union that misses this switch, but cannot widen on its own when the API changes. Single-sourcing it means regenerating the types package, which rewrites the whole generated surface and belongs in its own PR. Until then the Sentry report is the signal that matters here. The server-side counterpart is tracked separately and does not gate this PR. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Description
Billing status can return an in-flight billing operation (
pending_billing_op_id+action_url) so a client that lost its local reference can resume the payment — a reload, a cleared browser, a different device. The recovery path inuseWorkspaceBillingassumed that operation was always a subscription and passed a hardcoded'subscription'.That assumption is a latent bug regardless of what the server sends today: any non-subscription operation surfaced there takes the wrong branch throughout.
isSettingUpturns true, soSubscriptionPanelContentWorkspacereplaces the whole panel with a "setting up your subscription" spinner — hiding the user's actual plan for as long as the operation is pendingtopupActionOperationrequirestype === 'topup', so the top-up dialog's own recovery prompt never matcheshandleSuccessrunsreconcileSubscriptionSuccess()instead offetchStatus()+fetchBalance(), so credits just purchased never appearUses the operation type when the API reports one, falling back to
'subscription'when it does not — so this is inert against a server that never sends the field, and correct the moment one does.Status: the server side does not populate a top-up here yet. This is the client half, landing first because it is safe in isolation; without it, a server that starts reporting top-ups would drive the wrong UI. Treat it as removing a hardcoded assumption, not as shipping a user-visible fix on its own.
Checklist
recovers a pending top-up as a top-up, not a subscription, confirmed to fail without the fixpnpm test:unit(72 passed in the touched suite),pnpm typecheck,pnpm lint,pnpm formatFollow-up (not in this PR)
billingOperationStoregives a top-up 120s to discover an authentication URL (TIMEOUT_MS) while a subscription gets 5min (SUBSCRIPTION_ACTION_DISCOVERY_TIMEOUT_MS). Only bites when the payment page is slow to appear, so it is kept separate.