fix(billing): support subscription 3DS verification (FE-1436) - #14242
Conversation
📄 Knowledge reviewDosu skipped reviewing this PR because your organization has used its |
🎭 Playwright: ✅ 1753 passed, 0 failed · 3 flaky📊 Browser Reports
🎨 Storybook: ✅ Built — View Storybook📦 Bundle: 8.15 MB gzip 🔴 +1.04 kBDetailsSummary
Category Glance App Entry Points — 3.66 kB (baseline 3.66 kB) • ⚪ 0 BMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 1.28 MB (baseline 1.28 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: 12 added / 12 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 — 29.1 kB (baseline 29.1 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Status: 7 added / 7 removed / 3 unchanged Editors & Dialogs — 121 kB (baseline 121 kB) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
Status: 5 added / 5 removed / 1 unchanged UI Components — 64.8 kB (baseline 64.8 kB) • ⚪ 0 BReusable component library chunks
Status: 6 added / 6 removed / 8 unchanged Data & Services — 3.43 MB (baseline 3.42 MB) • 🔴 +2.71 kBStores, services, APIs, and repositories
Status: 14 added / 14 removed / 3 unchanged Utilities & Hooks — 371 kB (baseline 371 kB) • ⚪ 0 BHelpers, composables, and utility bundles
Status: 18 added / 18 removed / 17 unchanged Vendor & Third-Party — 15.7 MB (baseline 15.7 MB) • ⚪ 0 BExternal libraries and shared vendor chunks Status: 16 unchanged Other — 12.5 MB (baseline 12.5 MB) • 🔴 +3.39 kBBundles that do not match a named category
Status: 76 added / 76 removed / 200 unchanged ⚡ Performance Report
Show regressions
All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-07-29T20:45:37.093Z",
"gitSha": "6693096355de49bfa04e2e396abb230ca4b97d45",
"branch": "fix/billing-3ds-action-url",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2066.705000000013,
"styleRecalcs": 8,
"styleRecalcDurationMs": 9.103,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 544.388,
"heapDeltaBytes": 3757780,
"heapUsedBytes": 71716352,
"domNodes": 16,
"jsHeapTotalBytes": 20840448,
"scriptDurationMs": 23.281,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-idle",
"durationMs": 2035.9520000000657,
"styleRecalcs": 8,
"styleRecalcDurationMs": 8.322000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 596.4110000000001,
"heapDeltaBytes": 3835920,
"heapUsedBytes": 72026720,
"domNodes": 16,
"jsHeapTotalBytes": 20316160,
"scriptDurationMs": 27.996,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1854.5740000000137,
"styleRecalcs": 76,
"styleRecalcDurationMs": 41.840999999999994,
"layouts": 12,
"layoutDurationMs": 3.5319999999999996,
"taskDurationMs": 891.894,
"heapDeltaBytes": -1062808,
"heapUsedBytes": 66808740,
"domNodes": 59,
"jsHeapTotalBytes": 21102592,
"scriptDurationMs": 130.98200000000003,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1937.4599999999873,
"styleRecalcs": 77,
"styleRecalcDurationMs": 47.128,
"layouts": 12,
"layoutDurationMs": 3.549,
"taskDurationMs": 1026.354,
"heapDeltaBytes": -15578276,
"heapUsedBytes": 52355724,
"domNodes": -275,
"jsHeapTotalBytes": 20180992,
"scriptDurationMs": 139.19099999999997,
"eventListeners": -148,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1755.5070000000228,
"styleRecalcs": 31,
"styleRecalcDurationMs": 20.032,
"layouts": 6,
"layoutDurationMs": 0.7840000000000001,
"taskDurationMs": 417.5110000000001,
"heapDeltaBytes": 7189520,
"heapUsedBytes": 75340380,
"domNodes": 77,
"jsHeapTotalBytes": 20578304,
"scriptDurationMs": 23.616,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1771.9569999999294,
"styleRecalcs": 32,
"styleRecalcDurationMs": 18.926,
"layouts": 6,
"layoutDurationMs": 0.6020000000000001,
"taskDurationMs": 445.61400000000003,
"heapDeltaBytes": 7372712,
"heapUsedBytes": 75289056,
"domNodes": 77,
"jsHeapTotalBytes": 20054016,
"scriptDurationMs": 27.460999999999995,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "dom-widget-clipping",
"durationMs": 725.6090000000199,
"styleRecalcs": 12,
"styleRecalcDurationMs": 11.354999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 465.60699999999997,
"heapDeltaBytes": -12008432,
"heapUsedBytes": 56013192,
"domNodes": 20,
"jsHeapTotalBytes": 21102592,
"scriptDurationMs": 70.86500000000001,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "dom-widget-clipping",
"durationMs": 605.2230000000236,
"styleRecalcs": 11,
"styleRecalcDurationMs": 8.874,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 402.52099999999996,
"heapDeltaBytes": -12041032,
"heapUsedBytes": 55886000,
"domNodes": 18,
"jsHeapTotalBytes": 21102592,
"scriptDurationMs": 63.72,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-idle",
"durationMs": 2036.4749999999958,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.957999999999997,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 796.803,
"heapDeltaBytes": 6463976,
"heapUsedBytes": 65258824,
"domNodes": -276,
"jsHeapTotalBytes": 4521984,
"scriptDurationMs": 129.837,
"eventListeners": -146,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-idle",
"durationMs": 2017.0320000000856,
"styleRecalcs": 8,
"styleRecalcDurationMs": 8.796999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 760.7199999999999,
"heapDeltaBytes": 5864500,
"heapUsedBytes": 64906000,
"domNodes": -277,
"jsHeapTotalBytes": 5046272,
"scriptDurationMs": 123.695,
"eventListeners": -144,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-pan",
"durationMs": 2265.3889999999706,
"styleRecalcs": 68,
"styleRecalcDurationMs": 13.857999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1376.382,
"heapDeltaBytes": 1378116,
"heapUsedBytes": 61140212,
"domNodes": -278,
"jsHeapTotalBytes": 5513216,
"scriptDurationMs": 461.748,
"eventListeners": -144,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2298.2339999999795,
"styleRecalcs": 69,
"styleRecalcDurationMs": 14.850999999999996,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1419.965,
"heapDeltaBytes": 5091148,
"heapUsedBytes": 65219816,
"domNodes": -278,
"jsHeapTotalBytes": 5251072,
"scriptDurationMs": 490.818,
"eventListeners": -146,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3245.30900000002,
"styleRecalcs": 66,
"styleRecalcDurationMs": 16.411999999999995,
"layouts": 60,
"layoutDurationMs": 7.625,
"taskDurationMs": 1593.256,
"heapDeltaBytes": -273696,
"heapUsedBytes": 61295452,
"domNodes": -280,
"jsHeapTotalBytes": 8716288,
"scriptDurationMs": 555.0200000000001,
"eventListeners": -148,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-zoom",
"durationMs": 3496.498000000088,
"styleRecalcs": 66,
"styleRecalcDurationMs": 17.042,
"layouts": 60,
"layoutDurationMs": 7.875,
"taskDurationMs": 1718.441,
"heapDeltaBytes": 12069092,
"heapUsedBytes": 73597704,
"domNodes": -280,
"jsHeapTotalBytes": 7667712,
"scriptDurationMs": 597.78,
"eventListeners": -146,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666696,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2024.360999999999,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.622000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 731.355,
"heapDeltaBytes": 6283652,
"heapUsedBytes": 66763932,
"domNodes": -277,
"jsHeapTotalBytes": 4521984,
"scriptDurationMs": 122.466,
"eventListeners": -144,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2028.8590000000113,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.629999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 752.2310000000001,
"heapDeltaBytes": 6643468,
"heapUsedBytes": 67129020,
"domNodes": -278,
"jsHeapTotalBytes": 4259840,
"scriptDurationMs": 120.579,
"eventListeners": -144,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 646.1390000000051,
"styleRecalcs": 45,
"styleRecalcDurationMs": 13.759999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 426.578,
"heapDeltaBytes": -11585360,
"heapUsedBytes": 56461292,
"domNodes": 16,
"jsHeapTotalBytes": 21102592,
"scriptDurationMs": 123.32399999999998,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 622.4339999999984,
"styleRecalcs": 46,
"styleRecalcDurationMs": 10.786,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 425.39000000000004,
"heapDeltaBytes": -11456500,
"heapUsedBytes": 56568820,
"domNodes": 18,
"jsHeapTotalBytes": 21626880,
"scriptDurationMs": 123.42599999999999,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-idle",
"durationMs": 2020.4219999999964,
"styleRecalcs": 8,
"styleRecalcDurationMs": 8.901,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 569.975,
"heapDeltaBytes": 3641480,
"heapUsedBytes": 71515136,
"domNodes": 16,
"jsHeapTotalBytes": 20840448,
"scriptDurationMs": 24.211,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-idle",
"durationMs": 2021.2359999999308,
"styleRecalcs": 10,
"styleRecalcDurationMs": 10.603,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 597.4549999999999,
"heapDeltaBytes": 3543192,
"heapUsedBytes": 71503008,
"domNodes": 20,
"jsHeapTotalBytes": 20840448,
"scriptDurationMs": 23.766,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1741.5139999999951,
"styleRecalcs": 78,
"styleRecalcDurationMs": 42.405,
"layouts": 16,
"layoutDurationMs": 5.2,
"taskDurationMs": 889.0630000000001,
"heapDeltaBytes": -21311076,
"heapUsedBytes": 46827716,
"domNodes": -274,
"jsHeapTotalBytes": 20705280,
"scriptDurationMs": 104.458,
"eventListeners": -148,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1695.6609999999728,
"styleRecalcs": 75,
"styleRecalcDurationMs": 38.940999999999995,
"layouts": 16,
"layoutDurationMs": 4.571,
"taskDurationMs": 801.5210000000001,
"heapDeltaBytes": -5471712,
"heapUsedBytes": 62388840,
"domNodes": 63,
"jsHeapTotalBytes": 21102592,
"scriptDurationMs": 97.61600000000001,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-transition-enter",
"durationMs": 1417.9249999999683,
"styleRecalcs": 18,
"styleRecalcDurationMs": 33.4,
"layouts": 14,
"layoutDurationMs": 13.494,
"taskDurationMs": 1058.7680000000003,
"heapDeltaBytes": 32824688,
"heapUsedBytes": 100378796,
"domNodes": 13673,
"jsHeapTotalBytes": 13107200,
"scriptDurationMs": 39.688999999999986,
"eventListeners": 2371,
"totalBlockingTimeMs": 141,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "viewport-pan-sweep",
"durationMs": 9092.801000000009,
"styleRecalcs": 251,
"styleRecalcDurationMs": 40.391,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 5163.282999999999,
"heapDeltaBytes": 19591420,
"heapUsedBytes": 78502940,
"domNodes": -274,
"jsHeapTotalBytes": 8368128,
"scriptDurationMs": 1673.4859999999999,
"eventListeners": -126,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8441.324000000008,
"styleRecalcs": 249,
"styleRecalcDurationMs": 37.656,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4804.607,
"heapDeltaBytes": 7283216,
"heapUsedBytes": 66207220,
"domNodes": -277,
"jsHeapTotalBytes": 7057408,
"scriptDurationMs": 1478.601,
"eventListeners": -128,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 17442.015999999967,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 17426.397999999997,
"heapDeltaBytes": -40702272,
"heapUsedBytes": 166970944,
"domNodes": -8312,
"jsHeapTotalBytes": -14094336,
"scriptDurationMs": 584.7959999999999,
"eventListeners": -16387,
"totalBlockingTimeMs": 0,
"frameDurationMs": 18.333333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 17942.903000000002,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 17915.002999999997,
"heapDeltaBytes": -57298672,
"heapUsedBytes": 167042008,
"domNodes": -8312,
"jsHeapTotalBytes": -11730944,
"scriptDurationMs": 599.783,
"eventListeners": -16389,
"totalBlockingTimeMs": 0,
"frameDurationMs": 18.890000000000025,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 21333.02199999997,
"styleRecalcs": 144,
"styleRecalcDurationMs": 20.574000000000037,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 21297.025,
"heapDeltaBytes": -37685544,
"heapUsedBytes": 170830036,
"domNodes": -8312,
"jsHeapTotalBytes": -12869632,
"scriptDurationMs": 938.73,
"eventListeners": -16385,
"totalBlockingTimeMs": 327,
"frameDurationMs": 17.77333333333336,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-pan",
"durationMs": 22578.990999999973,
"styleRecalcs": 166,
"styleRecalcDurationMs": 24.978999999999974,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 22539.141,
"heapDeltaBytes": -55699816,
"heapUsedBytes": 164131808,
"domNodes": -8312,
"jsHeapTotalBytes": -13570048,
"scriptDurationMs": 1026.3980000000001,
"eventListeners": -16379,
"totalBlockingTimeMs": 558,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "workflow-execution",
"durationMs": 471.11599999993814,
"styleRecalcs": 14,
"styleRecalcDurationMs": 22.923,
"layouts": 3,
"layoutDurationMs": 0.7160000000000001,
"taskDurationMs": 148.698,
"heapDeltaBytes": -16014900,
"heapUsedBytes": 51020120,
"domNodes": 130,
"jsHeapTotalBytes": 7995392,
"scriptDurationMs": 18.386,
"eventListeners": 67,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "workflow-execution",
"durationMs": 468.36099999995895,
"styleRecalcs": 13,
"styleRecalcDurationMs": 21.201,
"layouts": 3,
"layoutDurationMs": 0.5149999999999998,
"taskDurationMs": 122.168,
"heapDeltaBytes": -16260948,
"heapUsedBytes": 50604460,
"domNodes": 119,
"jsHeapTotalBytes": 8257536,
"scriptDurationMs": 11.352,
"eventListeners": 65,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
}
]
} |
🌐 Website E2ETip All tests passed.
|
|
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:
📝 WalkthroughWalkthroughBilling operation responses now expose verification URLs. Workspace-scoped polling validates and preserves them, checkout propagates them to subscription dialogs, and UI buttons open verification actions securely. Top-up loading now tracks credit-add operations specifically. ChangesBilling verification flow
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant BillingOperationStore
participant useSubscriptionCheckout
participant SubscriptionPanelContentWorkspace
participant Customer
BillingOperationStore->>BillingOperationStore: Poll billing operation
BillingOperationStore-->>useSubscriptionCheckout: Expose active action URL
useSubscriptionCheckout-->>SubscriptionPanelContentWorkspace: Provide verification state
SubscriptionPanelContentWorkspace-->>Customer: Render Complete verification
Customer->>SubscriptionPanelContentWorkspace: Click verification button
SubscriptionPanelContentWorkspace->>Customer: Open protected HTTPS URL
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 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: 3
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/stores/billingOperationStore.ts (1)
213-234: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
scheduleNextPollnow has two near-identical timer arms.The
authenticationRequiredSeenbranch duplicates theintervals.set+setTimeout+timeouts.settail. Computing the delay first keeps one scheduling path.♻️ Single scheduling path
function scheduleNextPoll(opId: string) { const operation = operations.value.get(opId) if (!operation || operation.status !== 'pending') return - if (operation.authenticationRequiredSeen) { - intervals.set(opId, ACTION_REQUIRED_INTERVAL_MS) - const timeoutId = setTimeout( - () => void poll(opId), - ACTION_REQUIRED_INTERVAL_MS - ) - timeouts.set(opId, timeoutId) - return - } - const currentInterval = intervals.get(opId) ?? INITIAL_INTERVAL_MS - const nextInterval = Math.min( - currentInterval * BACKOFF_MULTIPLIER, - MAX_INTERVAL_MS - ) + let nextInterval = ACTION_REQUIRED_INTERVAL_MS + if (!operation.authenticationRequiredSeen) { + const currentInterval = intervals.get(opId) ?? INITIAL_INTERVAL_MS + nextInterval = Math.min( + currentInterval * BACKOFF_MULTIPLIER, + MAX_INTERVAL_MS + ) + } intervals.set(opId, nextInterval) const timeoutId = setTimeout(() => void poll(opId), nextInterval) timeouts.set(opId, timeoutId) }🤖 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/stores/billingOperationStore.ts` around lines 213 - 234, Refactor scheduleNextPoll to compute the polling delay first: use ACTION_REQUIRED_INTERVAL_MS when authenticationRequiredSeen is true, otherwise retain the existing backoff calculation and interval update. Then use one shared setTimeout and timeouts.set scheduling path, preserving the current pending-operation guard and delay behavior.
🤖 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/SubscriptionAddPaymentPreviewWorkspace.test.ts`:
- Around line 151-170: Update the verification-click test in
SubscriptionAddPaymentPreviewWorkspace to select the control with
screen.getByRole('button', { name: 'subscription.preview.completeVerification'
}) instead of getByText, while preserving the existing window.open assertions
and URL-safety checks.
In `@src/platform/workspace/components/SubscriptionPanelContentWorkspace.test.ts`:
- Around line 181-192: Move the mockIsSettingUp and
mockSubscriptionActionOperation ref declarations into a single vi.hoisted
callback before the billingOperationStore vi.mock factory, then have the factory
getters read those hoisted refs. Remove the current top-level declarations while
preserving the existing ref types and getter behavior.
In `@src/platform/workspace/stores/billingOperationStore.ts`:
- Around line 162-179: Update the polling flow around hasTimedOut,
handleTimeout, and the workspaceStore.activeWorkspaceId check so every operation
type, including subscriptions, is timeout-checked before returning from a
workspace mismatch. Preserve scheduleNextPoll only for operations that have not
timed out, ensuring inactive-workspace polling cannot re-arm indefinitely and
timed-out operations reach their terminal handling.
---
Outside diff comments:
In `@src/platform/workspace/stores/billingOperationStore.ts`:
- Around line 213-234: Refactor scheduleNextPoll to compute the polling delay
first: use ACTION_REQUIRED_INTERVAL_MS when authenticationRequiredSeen is true,
otherwise retain the existing backoff calculation and interval update. Then use
one shared setTimeout and timeouts.set scheduling path, preserving the current
pending-operation guard and delay behavior.
🪄 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: 24420f10-3ea0-4ec2-afbc-d010c8f45aca
📒 Files selected for processing (16)
packages/ingest-types/src/types.gen.tspackages/ingest-types/src/zod.gen.tssrc/locales/en/main.jsonsrc/platform/workspace/api/workspaceApi.tssrc/platform/workspace/components/SubscriptionAddPaymentPreviewWorkspace.test.tssrc/platform/workspace/components/SubscriptionAddPaymentPreviewWorkspace.vuesrc/platform/workspace/components/SubscriptionPanelContentWorkspace.test.tssrc/platform/workspace/components/SubscriptionPanelContentWorkspace.vuesrc/platform/workspace/components/SubscriptionRequiredDialogContentUnified.vuesrc/platform/workspace/components/SubscriptionRequiredDialogContentWorkspace.vuesrc/platform/workspace/components/TopUpCreditsDialogContentWorkspace.test.tssrc/platform/workspace/components/TopUpCreditsDialogContentWorkspace.vuesrc/platform/workspace/composables/useSubscriptionCheckout.test.tssrc/platform/workspace/composables/useSubscriptionCheckout.tssrc/platform/workspace/stores/billingOperationStore.test.tssrc/platform/workspace/stores/billingOperationStore.ts
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## main #14242 +/- ##
==========================================
+ Coverage 79.21% 79.48% +0.27%
==========================================
Files 1754 1754
Lines 114874 117804 +2930
Branches 38321 39764 +1443
==========================================
+ Hits 90998 93641 +2643
- Misses 23331 23565 +234
- Partials 545 598 +53
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 87 files with indirect coverage changes 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
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/stores/billingOperationStore.ts (1)
176-218: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy liftAdd a per-request timeout to
getBillingOpStatus.
workspaceApiClient.get(...)only passes{ headers }; theaxiosinstance has notimeoutorsignal, and Axios defaults request timeout to0. Since the polling code only checks elapsed time before starting a request or after it resolves, a hanging status request can keep the operation permanently in-flight without reachinghandleTimeout. Wrap this request with anAbortController/Axiostimeoutthat matches the billing operation lifetime.🤖 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/stores/billingOperationStore.ts` around lines 176 - 218, Add a per-request timeout to the `workspaceApi.getBillingOpStatus` call in the polling flow, using the billing operation lifetime and an AbortController or Axios timeout supported by the API client. Ensure a hung request is interrupted so the existing catch path can reach `handleTimeout` instead of remaining in-flight indefinitely.
🤖 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.
Outside diff comments:
In `@src/platform/workspace/stores/billingOperationStore.ts`:
- Around line 176-218: Add a per-request timeout to the
`workspaceApi.getBillingOpStatus` call in the polling flow, using the billing
operation lifetime and an AbortController or Axios timeout supported by the API
client. Ensure a hung request is interrupted so the existing catch path can
reach `handleTimeout` instead of remaining in-flight indefinitely.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 3319f81a-f022-48ab-be93-e993ddb1533d
📒 Files selected for processing (4)
src/platform/workspace/components/SubscriptionAddPaymentPreviewWorkspace.test.tssrc/platform/workspace/components/SubscriptionPanelContentWorkspace.test.tssrc/platform/workspace/stores/billingOperationStore.test.tssrc/platform/workspace/stores/billingOperationStore.ts
Amp-Thread-ID: https://ampcode.com/threads/T-019fabb4-5565-73c7-8629-7d9b4a0e6d1a Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019fabb4-5565-73c7-8629-7d9b4a0e6d1a Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019fabb4-5565-73c7-8629-7d9b4a0e6d1a Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019fabb4-5565-73c7-8629-7d9b4a0e6d1a Co-authored-by: Amp <amp@ampcode.com>
d11b1d9 to
e36e4e3
Compare
AS IS / TO BE — real app flowCaptured from the real Comfy Cloud frontend with controlled billing API responses matching the backend contract in cloud#5618. No bearer verification URL is rendered or persisted. AS ISBefore the billing operation exposes TO BEWhen polling exposes a valid HTTPS Flow |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@src/platform/workspace/components/SubscriptionRequiredDialogContentWorkspace.vue`:
- Line 88: Update the SubscriptionTransitionPreviewWorkspace usage in
SubscriptionRequiredDialogContentWorkspace.vue to pass
:action-url="activeCheckoutActionUrl", matching the existing prop wiring on the
new_subscription branch, so the transition preview always receives its required
action URL.
In
`@src/platform/workspace/components/SubscriptionTransitionPreviewWorkspace.vue`:
- Around line 191-194: Extract the duplicated openVerification logic into a
shared openVerificationUrl helper, then update the local openVerification usage
and the corresponding SubscriptionAddPaymentPreviewWorkspace/panel references to
import and reuse it. Preserve the null guard and the existing _blank,
noopener,noreferrer window features in the shared implementation.
In `@src/platform/workspace/composables/useSubscriptionCheckout.ts`:
- Around line 289-295: Update handleBackToPricing so users are not silently
prevented from leaving while isPolling remains active for the extended
subscription operation; either allow the pricing transition and clear the
checkout state while polling continues in the store, or explicitly disable and
annotate the dialog back affordances, including the Backspace handler in
SubscriptionRequiredDialogContentUnified.vue.
In `@src/platform/workspace/stores/billingOperationStore.ts`:
- Around line 206-218: In the catch path of the billing operation polling flow,
simplify the duplicated rescheduling logic after the hasTimedOut check: remove
the workspace-specific condition and retain a single scheduleNextPoll(opId) call
for all non-timeout operations. Keep the existing current-operation validation
and timeout handling 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: ce73bc33-fff2-48b6-8c29-1748f618507d
📒 Files selected for processing (18)
packages/ingest-types/src/types.gen.tspackages/ingest-types/src/zod.gen.tssrc/locales/en/main.jsonsrc/platform/workspace/api/workspaceApi.tssrc/platform/workspace/components/SubscriptionAddPaymentPreviewWorkspace.test.tssrc/platform/workspace/components/SubscriptionAddPaymentPreviewWorkspace.vuesrc/platform/workspace/components/SubscriptionPanelContentWorkspace.test.tssrc/platform/workspace/components/SubscriptionPanelContentWorkspace.vuesrc/platform/workspace/components/SubscriptionRequiredDialogContentUnified.vuesrc/platform/workspace/components/SubscriptionRequiredDialogContentWorkspace.vuesrc/platform/workspace/components/SubscriptionTransitionPreviewWorkspace.test.tssrc/platform/workspace/components/SubscriptionTransitionPreviewWorkspace.vuesrc/platform/workspace/components/TopUpCreditsDialogContentWorkspace.test.tssrc/platform/workspace/components/TopUpCreditsDialogContentWorkspace.vuesrc/platform/workspace/composables/useSubscriptionCheckout.test.tssrc/platform/workspace/composables/useSubscriptionCheckout.tssrc/platform/workspace/stores/billingOperationStore.test.tssrc/platform/workspace/stores/billingOperationStore.ts
There was a problem hiding this comment.
Overall: Solid, well-tested implementation of the 3DS action-url flow — HTTPS-only validation, no action_url in toasts/telemetry, and the CTA only opens via noopener,noreferrer from a real click are all correctly in place. A few things worth fixing before merge.
Issues
src/platform/workspace/stores/billingOperationStore.ts—poll()'scatchblock has a dead branch:if (operation.workspaceId !== workspaceStore.activeWorkspaceId) { scheduleNextPoll(opId); return }is immediately followed by an unconditionalscheduleNextPoll(opId)— both paths do the exact same thing, so theifis dead code. Collapse to a singlescheduleNextPoll(opId)call.- Same file — the timeout/workspace-guard check (
hasTimedOutplus "is this op's workspace still active") is duplicated three times insidepoll(): before the fetch, right after theawaitresolves, and again in thecatch. This is the race-sensitive part of the store, so worth extracting into one helper (e.g.resolveIfDone(opId, operation): boolean) rather than risking one copy drifting from the others on a future edit — matches the "is there a simpler way" guidance in AGENTS.md. - Minor UX inconsistency:
SubscriptionAddPaymentPreviewWorkspace.vue's "back to all plans" button gets:disabled="isLoading", butSubscriptionTransitionPreviewWorkspace.vue's equivalent button doesn't, even though both now render the same "Complete verification" CTA and both route throughhandleBackToPricing()'s newisPollingguard inuseSubscriptionCheckout.ts. The guard stops it from corrupting state, but on the transition preview the back button will just silently no-op while polling instead of visibly greying out — worth matching the two components. - Minor:
useSubscriptionCheckout.tsnow returnsactiveCheckoutOperationfrom the composable, but nothing outside its own test consumes it — only the derivedactiveCheckoutActionUrlandisPollingare used by components. Per AGENTS.md's guidance to minimize composable surface area, consider not exporting the raw operation and asserting against the derived values in the test instead.
Fast-follow ask: cloud#5704 (https://github.com/Comfy-Org/cloud/pull/5704) isn't merged yet, and this PR doesn't depend on it today — it adds pending_billing_op_id/action_url to a different, existing endpoint (GET /billing/status) than the one this PR already consumes (GET /billing/ops/{id}, from the already-merged cloud#5618). But once it lands it removes the "identity-scoped ownership" concern this PR's body cites for deferring reload recovery, since those fields are resolved server-side from the authenticated workspace with no client-supplied id. Since #5704 isn't merged either, there's no dependency-ordering reason this has to be a separate follow-up — could you fold seeding billingOperationStore from pending_billing_op_id/action_url on GET /billing/status into this PR, so a reload / workspace switch / second tab can pick up an in-flight 3DS operation instead of only seeding from a fresh subscribe() response?
Generated by Claude Code
Amp-Thread-ID: https://ampcode.com/threads/T-019fae4f-af7c-7307-a9b0-93acd3eec624 Co-authored-by: Amp <amp@ampcode.com>
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/composables/useSubscriptionCheckout.ts (1)
455-460: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winGuard the success transition by workspace.
activeCheckoutOperationIdis retained across a workspace switch. If the old operation resolves after its success reconciliation, this ID-only check can set the new workspace’s dialog tosuccess. Also requireoperation.workspaceId === workspaceStore.activeWorkspaceId.Proposed fix
if ( operation.status === 'succeeded' && - activeCheckoutOperationId.value === opId + activeCheckoutOperationId.value === opId && + operation.workspaceId === workspaceStore.activeWorkspaceId ) { checkoutStep.value = 'success' }🤖 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/useSubscriptionCheckout.ts` around lines 455 - 460, Update the success transition guard in the checkout reconciliation flow to also require operation.workspaceId to equal workspaceStore.activeWorkspaceId, alongside the existing succeeded status and activeCheckoutOperationId checks, so operations from a previous workspace cannot set the current dialog to success.
♻️ Duplicate comments (1)
src/platform/workspace/components/SubscriptionTransitionPreviewWorkspace.vue (1)
192-196: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicated
openVerificationhelper still not extracted.This matches a prior review comment on this same file/range: the
openVerificationlogic (null-guard +window.open(actionUrl, '_blank', 'noopener,noreferrer')) duplicates the implementation inSubscriptionAddPaymentPreviewWorkspace.vue. Extracting a sharedopenVerificationUrl()helper keeps the safe window-open flags in one place.♻️ Shared helper sketch
// src/platform/workspace/utils/openVerificationUrl.ts export function openVerificationUrl(actionUrl: string | null) { if (!actionUrl) return window.open(actionUrl, '_blank', 'noopener,noreferrer') }🤖 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/SubscriptionTransitionPreviewWorkspace.vue` around lines 192 - 196, Extract the duplicated verification-opening logic from openVerification in SubscriptionTransitionPreviewWorkspace into a shared openVerificationUrl helper, then reuse that helper in both this component and SubscriptionAddPaymentPreviewWorkspace. Preserve the null guard and the existing '_blank' with 'noopener,noreferrer' window flags, and remove the local duplicate implementation.
🤖 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/SubscriptionRequiredDialogContentUnified.test.ts`:
- Line 24: The dialog tests currently only provide fixture URLs without
verifying forwarding. In
src/platform/workspace/components/SubscriptionRequiredDialogContentUnified.test.ts#L24-L24,
make the action URL controllably non-null and assert it reaches every applicable
preview variant; in
src/platform/workspace/components/SubscriptionRequiredDialogContentWorkspace.test.ts#L123-L123,
do the same for both payment and transition previews. Retain click-only opening
coverage in the preview tests and verify behavioral outcomes rather than only
mocks.
---
Outside diff comments:
In `@src/platform/workspace/composables/useSubscriptionCheckout.ts`:
- Around line 455-460: Update the success transition guard in the checkout
reconciliation flow to also require operation.workspaceId to equal
workspaceStore.activeWorkspaceId, alongside the existing succeeded status and
activeCheckoutOperationId checks, so operations from a previous workspace cannot
set the current dialog to success.
---
Duplicate comments:
In
`@src/platform/workspace/components/SubscriptionTransitionPreviewWorkspace.vue`:
- Around line 192-196: Extract the duplicated verification-opening logic from
openVerification in SubscriptionTransitionPreviewWorkspace into a shared
openVerificationUrl helper, then reuse that helper in both this component and
SubscriptionAddPaymentPreviewWorkspace. Preserve the null guard and the existing
'_blank' with 'noopener,noreferrer' window flags, and remove the local duplicate
implementation.
🪄 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: 10423ab0-3d4c-4545-9093-e60426411600
📒 Files selected for processing (15)
packages/ingest-types/src/types.gen.tspackages/ingest-types/src/zod.gen.tssrc/platform/workspace/api/workspaceApi.test.tssrc/platform/workspace/api/workspaceApi.tssrc/platform/workspace/components/SubscriptionRequiredDialogContentUnified.test.tssrc/platform/workspace/components/SubscriptionRequiredDialogContentUnified.vuesrc/platform/workspace/components/SubscriptionRequiredDialogContentWorkspace.test.tssrc/platform/workspace/components/SubscriptionRequiredDialogContentWorkspace.vuesrc/platform/workspace/components/SubscriptionTransitionPreviewWorkspace.vuesrc/platform/workspace/composables/useSubscriptionCheckout.test.tssrc/platform/workspace/composables/useSubscriptionCheckout.tssrc/platform/workspace/composables/useWorkspaceBilling.test.tssrc/platform/workspace/composables/useWorkspaceBilling.tssrc/platform/workspace/stores/billingOperationStore.test.tssrc/platform/workspace/stores/billingOperationStore.ts
|
@dante01yoon Successfully backported to #14303 |
|
@dante01yoon Successfully backported to #14304 |
|
@dante01yoon Successfully backported to #14305 |
## Why this is needed A 3DS/SCA subscription can spend up to five minutes waiting for the billing operation to expose its verification action. If that discovery window timed out, retrying **Subscribe** opened checkout again but left the confirmation dialog idle: the pending/loading state did not return, polling did not restart, and **Complete verification** could not appear until the page was refreshed. This matches the [QA report in Slack](https://comfy-organization.slack.com/archives/C0932CGKT5K/p1785428216929989?thread_ts=1785373105.971169&cid=C0932CGKT5K). ## Root cause `billingOperationStore.startOperation()` deduplicated every previously-seen operation ID. After the five-minute poll timeout, the timed-out operation stayed cached. The backend intentionally replayed the same billing operation ID on checkout retry, so the store returned that cached terminal operation instead of creating a new pending operation and polling it again. ## Regression assessment This is a regression in the new 3DS/SCA retry flow added by Comfy-Org#14242. The operation-ID deduplication predates that change and remains correct for in-flight and completed operations, but the newly introduced five-minute action-discovery timeout made a terminal `timeout` operation retryable. That state was not covered by the original browser scenarios. ## How this changes - Treat only cached `timeout` operations as restartable. - Clear the timed-out operation's stale promise/timer bookkeeping before recreating it as pending. - Preserve existing deduplication for pending, succeeded, and failed operations. - Restart polling immediately when the backend replays the same operation ID, restoring checkout loading state and allowing **Complete verification** to appear without a refresh. ## Tests - **Unit regression:** timeout → retry same operation ID → pending/loading restored → polling succeeds. - **E2E regression:** real checkout dialog → five-minute timeout via Playwright Clock → retry same operation ID → pending UI disabled state returns → polling restarts → **Complete verification** appears. - The E2E test fails on the pre-fix implementation because the dialog remains idle, no second operation poll occurs, and the verification action stays hidden. ## AS IS After timeout and retry, checkout remains idle with **Subscribe to Creator** still available. The timeout toast remains, no loading state starts, and there is no **Complete verification** action.  ## TO BE Retrying the same backend operation ID recreates the pending operation. The CTA returns to loading, polling resumes, and the verification action appears as soon as the operation exposes its action URL.  ### Retry flow  ## Review focus The retry boundary in `startOperation()`: only timed-out operations are restarted; pending and other terminal operation deduplication is unchanged. --------- Co-authored-by: dante01yoon <6510430+dante01yoon@users.noreply.github.com> Co-authored-by: Amp <amp@ampcode.com>



Summary
Support the pending subscription authentication flow introduced by Comfy-Org/cloud#5704, so customers can complete 3DS/SCA instead of timing out after two minutes.
Changes
action_url, validate it as HTTPS, and surface it through explicit Complete verification actions in checkout and workspace billing settings.noopener,noreferrer.Review Focus
pending_billing_op_idandaction_urlfrom/api/billing/status, reconstructs in-memory polling, and does not resend/api/billing/subscribe.AS IS / TO BE — real app flow
Captured from the real Comfy Cloud frontend with controlled billing API responses matching the backend contract in cloud#5704. No bearer verification URL is rendered or persisted.
AS IS
Before the billing operation exposes
action_url, the upgrade remains in its pending state.Screen.Recording.2026-07-29.at.12.39.33.AM.mov
TO BE
don't need 3DS - keep AS IS
Screen.Recording.2026-07-29.at.1.13.56.AM.mov
need 3DS
When polling exposes a valid HTTPS
action_url, the same upgrade dialog shows Complete verification. This now covers plan changes as well as new subscriptions.Screen.Recording.2026-07-29.at.1.07.59.AM.mov
Flow