Skip to content

feat(billing): top-up dialog confirm step and outcome states - #14526

Draft
comfydesigner wants to merge 10 commits into
mainfrom
comfydesigner/topup-confirm-step
Draft

feat(billing): top-up dialog confirm step and outcome states#14526
comfydesigner wants to merge 10 commits into
mainfrom
comfydesigner/topup-confirm-step

Conversation

@comfydesigner

@comfydesigner comfydesigner commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

With a saved payment method the top-up charges immediately — the Stripe hand-off that acted as the implicit confirmation is gone. The dialog gains a step machine: amount → confirm → success | declined, plus a verifying re-entry state for pending 3DS charges.

Figma: New Top Up Flow (9 states, current tokens).

Live now (no BE dependency):

  • Outcome steps for every workspace top-up: success (previous/added/new balance tile, settings-linked note) and declined (reason tile, portal-routed Update payment method, back-to-confirm retry) — polled operation outcomes route into the dialog instead of stranding it behind the store toast
  • 3DS on top of fix(billing): support top-up 3DS verification #14490: Complete verification stacks above the demoted pay slot on confirm; verifying (DES-592's copy) is the landing for reopen/back during any pending charge — the amount step is unreachable while money is in flight, so a stale amount can never be toggled against a pending 3DS charge (Hunter's repro)
  • Back is disabled from Pay until the operation resolves, matching the checkout dialog's polling-disabled back
  • Post-click acknowledgment with zero layout shift: the demoted spinner slot becomes 'Waiting for your bank…' and the verifying body swaps to 'Complete the verification in the new tab — this updates automatically.' — only when the tab actually opened (popup-blocked clicks keep the original copy)
  • Fixed 512×min-500 frame (checkout confirm family), anchored CTA, vocabulary alignment ('Add additional credits' at 16px, 'Select an amount' — shared keys update the legacy dialog too), tabular stepper numerals

Ships dark (behind the saved-methods BE contract):

  • The confirm step: credits figure, method row (1 method: Change → portal) or SingleSelect picker (2+: 'Add new payment method' → portal), total due today, Pay {amount}. savedMethods is a nullable prop — null (today's reality) keeps the dialog behaving exactly as on main
  • Contract needed: saved-methods list [{ type, brand?, last4?, id, isDefault }] + chosen method id on the charge; structured decline reason; op amount in status (verifying with amount, success on late resolution); challenge-completed polling granularity (window itself is BE-5770); cancel API for pending 3DS (abandon currently parks on Verifying until resolution — pre-existing lockout, now visible)

22 component tests. Draft until the contract lands and the confirm step is reachable outside tests.

  • Part of FE-1470 / DES-622

🤖 Generated with Claude Code

…pport

Branch rebuilt on main past #14490 (top-up 3DS verification). The
verification CTA integrates into the step machine per the confirm-flow
sequencing: it renders grouped above the primary action on the amount and
confirm steps whenever a pending operation carries an action_url, demoting
that action to tertiary; success/declined steps are unaffected. Ports the
#14490 verification tests and adds coverage that verification stays
reachable on the confirm step.
Reopening the dialog mid-verification previously landed on the amount step
with the verification stack attached, inviting a second charge while one was
in flight. With a pending action_url (and top-up permission) the dialog now
opens onto a dedicated verifying step — DES-592's settled copy ('Verify your
payment' / bank-verification line) with Complete verification as the only
action. The remounted dialog cannot know the pending charge's amount, so the
step shows none rather than the steppers' defaults.
A pending charge that later succeeded or failed during polling left the
dialog stranded on the confirm step with only the store's toast. handleBuy
now awaits the operation's terminal state: succeeded refreshes balances and
lands on the success step; failed lands on the declined step carrying the
operation's error message. Timeout keeps the current behavior (the pending
op persists inside the authentication window).
Declined is the flow's one non-terminal outcome — no charge happened — so
it gets the retry path back to the confirm step (amount and method intact),
clearing the stale decline reason. Success and verifying stay back-less:
one is terminal, the other is blocked on the pending charge. Declined title
joins the success/verifying type scale.
savedMethods becomes a nullable prop (the BE-contract seam, mirroring the
subscription confirm's savedMethodsForConfirm): null keeps the confirm step
unreachable, so production behavior is unchanged until the parent supplies
the list. The URL/keyboard demo hooks and mock methods are gone; tests arm
the flow through the prop.
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The workspace top-up dialog now supports confirmation, saved payment methods, verification, success and declined states, billing refreshes, and expanded localization. Tests cover the updated payment and operation flows.

Changes

Workspace top-up flow

Layer / File(s) Summary
Dialog steps and payment method contract
src/platform/workspace/components/TopUpCreditsDialogContentWorkspace.vue, src/components/ui/stepper/FormattedNumberStepper.vue, src/components/dialog/content/TopUpCreditsDialogContentLegacy.vue, src/locales/en/main.json
The dialog adds responsive step views, saved payment-method selection, confirmation totals, result displays, and updated billing and subscription translations. Number inputs use tabular numerals. Legacy dialog headers use a smaller title size.
Purchase lifecycle and result handling
src/platform/workspace/components/TopUpCreditsDialogContentWorkspace.vue
Top-up operations now handle pending, verification, success, declined, and unknown statuses. Billing data refreshes after completed operations.
Workflow validation coverage
src/platform/workspace/components/TopUpCreditsDialogContentWorkspace.test.ts
Tests cover payment methods, verification, polling outcomes, balance summaries, declined payments, billing navigation, and dialog closure.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WorkspaceUser
  participant TopUpCreditsDialogContentWorkspace
  participant BillingContext
  participant SubscriptionManagement

  WorkspaceUser->>TopUpCreditsDialogContentWorkspace: Select amount and payment method
  TopUpCreditsDialogContentWorkspace->>BillingContext: Start top-up operation
  BillingContext-->>TopUpCreditsDialogContentWorkspace: Return operation status
  TopUpCreditsDialogContentWorkspace->>BillingContext: Poll pending operation
  BillingContext-->>TopUpCreditsDialogContentWorkspace: Return success or failure
  WorkspaceUser->>SubscriptionManagement: Open payment-method management
Loading

Possibly related PRs

Suggested labels: size:XL, cloud/1.48, released:cloud

Suggested reviewers: dante01yoon


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
End-To-End Regression Coverage For Fixes ❓ Inconclusive The review context provides changed files and description, but no PR title or actual commit subjects; the required bug-fix signal cannot be verified. Provide the PR title and commit subjects to determine whether the missing browser_tests/ regression coverage triggers this check.
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Adr Compliance For Entity/Litegraph Changes ✅ Passed The changed files are UI, locale, and workspace top-up files; none are under src/lib/litegraph/, src/ecs/, or graph-entity paths.
Title check ✅ Passed The title clearly identifies the billing top-up dialog confirm step and outcome states, which are the main changes.
Description check ✅ Passed The description clearly covers the flow, behavior, dependencies, review details, and test scope, although it does not use the template headings.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch comfydesigner/topup-confirm-step
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch comfydesigner/topup-confirm-step

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Backing out of the confirm step during a pending verification landed on
the amount step with live steppers — the eventual 3DS completion charges
the original amount, not whatever the steppers now show (Hunter's repro).
With a pending action_url, back now routes to the verifying step, making
the amount step unreachable mid-verification by both reopen and back.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

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/TopUpCreditsDialogContentWorkspace.vue (1)

546-551: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep topup tracking clearing consistent on the success step.

The top-right X calls handleClose() with clearTracking = true, while the success “Close” button and billing link call handleClose(false). Make clearing deterministic after purchase succeeds, for example by skipping clearTopupTracking() when step.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/components/TopUpCreditsDialogContentWorkspace.vue`
around lines 546 - 551, Update handleClose so clearTopupTracking is skipped
whenever step.value is 'success', including calls that pass clearTracking as
false or true. Preserve tracking cleanup for non-success steps and keep
dialogStore.closeDialog 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/locales/en/main.json`:
- Line 2759: Update the translation value for monthlyCreditsRollover to use the
verb phrase “roll over” instead of “rollover,” matching the wording already used
by monthlyCreditsInfo.
- Around line 2770-2773: Update the locale entries partnerNodesBalance,
partnerNodesCredits, and partnerNodesPricingTable to use the singular,
capitalized compound adjective “Partner Node” before the following noun, while
preserving “Partner Nodes” only when it stands alone as a noun.

In
`@src/platform/workspace/components/TopUpCreditsDialogContentWorkspace.test.ts`:
- Around line 433-449: Add a declined-payment test alongside the existing
rejected-charge test that clicks the declined-step “Back” button, verifies the
dialog returns to the confirmation step, and confirms the displayed decline
reason is cleared. Reuse the existing dialog setup and payment-decline flow
helpers such as mockTopup, renderDialog, clickAddCredits, and the relevant
accessible button names.

In `@src/platform/workspace/components/TopUpCreditsDialogContentWorkspace.vue`:
- Around line 463-468: Wrap the manageSubscription() calls in both
handleChangePaymentMethod and the watch(selectedMethodId, ...) handler with the
same try/catch/finally error-handling pattern used by handleBuy. Ensure rejected
promises are routed through the existing handled error state and any required
cleanup runs in finally, while preserving the current payment-method selection
behavior.
- Around line 400-405: Export the SavedPaymentMethod interface in
TopUpCreditsDialogContentWorkspace.vue. In
src/platform/workspace/components/TopUpCreditsDialogContentWorkspace.test.ts
lines 176-182, import SavedPaymentMethod and define the VISA and MASTERCARD
fixtures using satisfies SavedPaymentMethod instead of as const.
- Around line 130-137: Replace the raw billing <button> in the billing template
with the shared Button component, matching the variant="link" and size="lg"
configuration used by handleChangePaymentMethod. Preserve the existing
handleViewBilling click behavior and translated billingAndInvoices label.
- Around line 581-602: Extract the shared success-finalization block from the
completed branch and the pending-to-succeeded branch into a local helper near
the surrounding top-up flow. Have the helper refresh balance and status, assign
successSummary, and set step.value to 'success', then replace both duplicated
blocks with calls to that helper while preserving existing control flow.

---

Outside diff comments:
In `@src/platform/workspace/components/TopUpCreditsDialogContentWorkspace.vue`:
- Around line 546-551: Update handleClose so clearTopupTracking is skipped
whenever step.value is 'success', including calls that pass clearTracking as
false or true. Preserve tracking cleanup for non-success steps and keep
dialogStore.closeDialog 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: 9114d1df-3041-49dc-bdb3-70d30ee333a7

📥 Commits

Reviewing files that changed from the base of the PR and between 6b66963 and 5b6dd73.

📒 Files selected for processing (4)
  • src/components/ui/stepper/FormattedNumberStepper.vue
  • src/locales/en/main.json
  • src/platform/workspace/components/TopUpCreditsDialogContentWorkspace.test.ts
  • src/platform/workspace/components/TopUpCreditsDialogContentWorkspace.vue

Comment thread src/locales/en/main.json
"monthlyCreditsInfo": "These credits refresh monthly and don't roll over",
"monthlyCreditsLabel": "Monthly credits",
"monthlyCreditsPerMemberLabel": "Monthly credits / member",
"monthlyCreditsRollover": "These credits will rollover to the next month",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fix verb form of "roll over".

monthlyCreditsRollover reads "These credits will rollover to the next month". "Rollover" is a noun/adjective. Use the verb phrase "roll over", as monthlyCreditsInfo does three lines above.

✏️ Proposed wording fix
-    "monthlyCreditsRollover": "These credits will rollover to the next month",
+    "monthlyCreditsRollover": "These credits will roll over to the next month",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"monthlyCreditsRollover": "These credits will rollover to the next month",
"monthlyCreditsRollover": "These credits will roll over to the next month",
🤖 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/locales/en/main.json` at line 2759, Update the translation value for
monthlyCreditsRollover to use the verb phrase “roll over” instead of “rollover,”
matching the wording already used by monthlyCreditsInfo.

Comment thread src/locales/en/main.json
Comment on lines +2770 to +2773
"partnerNodesBalance": "\"Partner Nodes\" Credit Balance",
"partnerNodesCredits": "Partner nodes pricing",
"partnerNodesDescription": "For running commercial/proprietary models",
"partnerNodesPricingTable": "Partner Nodes pricing table",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fix inconsistent "Partner Node(s)" wording.

partnerNodesCredits reads "Partner nodes pricing" and partnerNodesPricingTable reads "Partner Nodes pricing table". Both use "Partner Node(s)" as a compound adjective before "pricing". Keep it singular and capitalized: "Partner Node pricing" and "Partner Node pricing table". Apply the same singular form to partnerNodesBalance, since "Partner Nodes" there modifies "Credit Balance".

Based on learnings, "Partner Node(s)" as a compound adjective modifying another noun (e.g. "Partner Node pricing badge") stays singular; use "Partner Nodes" only as a standalone noun referring to the node set.

✏️ Proposed wording fix
-    "partnerNodesBalance": "\"Partner Nodes\" Credit Balance",
-    "partnerNodesCredits": "Partner nodes pricing",
+    "partnerNodesBalance": "\"Partner Node\" Credit Balance",
+    "partnerNodesCredits": "Partner Node pricing",
     "partnerNodesDescription": "For running commercial/proprietary models",
-    "partnerNodesPricingTable": "Partner Nodes pricing table",
+    "partnerNodesPricingTable": "Partner Node pricing table",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"partnerNodesBalance": "\"Partner Nodes\" Credit Balance",
"partnerNodesCredits": "Partner nodes pricing",
"partnerNodesDescription": "For running commercial/proprietary models",
"partnerNodesPricingTable": "Partner Nodes pricing table",
"partnerNodesBalance": "\"Partner Node\" Credit Balance",
"partnerNodesCredits": "Partner Node pricing",
"partnerNodesDescription": "For running commercial/proprietary models",
"partnerNodesPricingTable": "Partner Node pricing table",
🤖 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/locales/en/main.json` around lines 2770 - 2773, Update the locale entries
partnerNodesBalance, partnerNodesCredits, and partnerNodesPricingTable to use
the singular, capitalized compound adjective “Partner Node” before the following
noun, while preserving “Partner Nodes” only when it stands alone as a noun.

Source: Learnings

Comment on lines +433 to +449
it('shows the declined step with the failure reason after a rejected charge', async () => {
mockTopup.mockRejectedValue(new Error('Insufficient funds'))

renderDialog({ savedMethods: [VISA] })
await clickAddCredits()
const user = userEvent.setup()
await user.click(screen.getByRole('button', { name: 'Pay $50.00' }))

expect(screen.getByText('Payment declined')).toBeInTheDocument()
expect(screen.getByText('Insufficient funds')).toBeInTheDocument()
expect(mockToastAdd).not.toHaveBeenCalled()

await user.click(
screen.getByRole('button', { name: 'Update payment method' })
)
expect(mockManageSubscription).toHaveBeenCalledOnce()
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add a test for the declined-step retry flow.

The PR adds "Declined-step retry navigation back to confirmation," but no test in this diff clicks "Back" from the declined step and verifies the dialog returns to confirm with declineReason cleared. Add a test alongside these declined-payment tests.

Do you want me to generate this test?
As per path instructions, "Ensure behavioral coverage for every new flow: amount, confirmation, saved-method selection/null handling, verification reopening, polling, success, decline, retry, balance updates, and payment-method management."

🤖 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/TopUpCreditsDialogContentWorkspace.test.ts`
around lines 433 - 449, Add a declined-payment test alongside the existing
rejected-charge test that clicks the declined-step “Back” button, verifies the
dialog returns to the confirmation step, and confirms the displayed decline
reason is cleared. Reuse the existing dialog setup and payment-decline flow
helpers such as mockTopup, renderDialog, clickAddCredits, and the relevant
accessible button names.

Source: Path instructions

Comment on lines +130 to +137
<template #billing>
<button
class="cursor-pointer border-none bg-transparent p-0 font-inter text-sm text-base-foreground underline-offset-2 hover:underline"
@click="handleViewBilling"
>
{{ $t('subscription.billingAndInvoices') }}
</button>
</template>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the shared Button component for the billing link.

The success step renders a raw <button> for the "Billing & invoices" link, while handleChangePaymentMethod's "Change" control uses <Button variant="link" size="lg"> at line 67 in the same file. Use the same shared component for consistency.

♻️ Proposed fix
-            <button
-              class="cursor-pointer border-none bg-transparent p-0 font-inter text-sm text-base-foreground underline-offset-2 hover:underline"
-              `@click`="handleViewBilling"
-            >
-              {{ $t('subscription.billingAndInvoices') }}
-            </button>
+            <Button
+              variant="link"
+              size="sm"
+              class="h-auto p-0 text-sm"
+              `@click`="handleViewBilling"
+            >
+              {{ $t('subscription.billingAndInvoices') }}
+            </Button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<template #billing>
<button
class="cursor-pointer border-none bg-transparent p-0 font-inter text-sm text-base-foreground underline-offset-2 hover:underline"
@click="handleViewBilling"
>
{{ $t('subscription.billingAndInvoices') }}
</button>
</template>
<template `#billing`>
<Button
variant="link"
size="sm"
class="h-auto p-0 text-sm"
`@click`="handleViewBilling"
>
{{ $t('subscription.billingAndInvoices') }}
</Button>
</template>
🤖 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/TopUpCreditsDialogContentWorkspace.vue`
around lines 130 - 137, Replace the raw billing <button> in the billing template
with the shared Button component, matching the variant="link" and size="lg"
configuration used by handleChangePaymentMethod. Preserve the existing
handleViewBilling click behavior and translated billingAndInvoices label.

Comment on lines +400 to +405
interface SavedPaymentMethod {
id: string
type: 'card' | 'alipay' | 'bank'
brand?: string
last4?: string
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Export SavedPaymentMethod so the test file can type its fixtures. The SavedPaymentMethod interface is declared but not exported, so the test file cannot import it and instead relies on as const for its fixtures, losing shape validation against the real interface.

  • src/platform/workspace/components/TopUpCreditsDialogContentWorkspace.vue#L400-L405: Add export to the SavedPaymentMethod interface declaration.
  • src/platform/workspace/components/TopUpCreditsDialogContentWorkspace.test.ts#L176-L182: Import SavedPaymentMethod and define VISA/MASTERCARD with satisfies SavedPaymentMethod instead of as const.

As per path instructions, "Use explicit TypeScript types for saved payment methods... and test fixtures explicitly."

📍 Affects 2 files
  • src/platform/workspace/components/TopUpCreditsDialogContentWorkspace.vue#L400-L405 (this comment)
  • src/platform/workspace/components/TopUpCreditsDialogContentWorkspace.test.ts#L176-L182
🤖 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/TopUpCreditsDialogContentWorkspace.vue`
around lines 400 - 405, Export the SavedPaymentMethod interface in
TopUpCreditsDialogContentWorkspace.vue. In
src/platform/workspace/components/TopUpCreditsDialogContentWorkspace.test.ts
lines 176-182, import SavedPaymentMethod and define the VISA and MASTERCARD
fixtures using satisfies SavedPaymentMethod instead of as const.

Source: Path instructions

Comment on lines +463 to +468
watch(selectedMethodId, async (id, previous) => {
if (id === 'add-new') {
selectedMethodId.value = previous
await manageSubscription()
}
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Add error handling around manageSubscription() calls.

handleChangePaymentMethod and the watch(selectedMethodId, ...) handler both await manageSubscription() with no try/catch. handleBuy wraps its async work in try/catch/finally; these two call sites do not, so a rejection surfaces as an unhandled promise rejection instead of a handled error state.

🛡️ Proposed fix
 async function handleChangePaymentMethod() {
-  await manageSubscription()
+  try {
+    await manageSubscription()
+  } catch (error) {
+    console.error('Failed to open billing portal:', error)
+  }
 }
As per coding guidelines, "Implement proper error handling" and "Implement proper error propagation" apply to `src/**/*.{js,ts,vue}`.

Also applies to: 542-544

🤖 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/TopUpCreditsDialogContentWorkspace.vue`
around lines 463 - 468, Wrap the manageSubscription() calls in both
handleChangePaymentMethod and the watch(selectedMethodId, ...) handler with the
same try/catch/finally error-handling pattern used by handleBuy. Ensure rejected
promises are routed through the existing handled error state and any required
cleanup runs in finally, while preserving the current payment-method selection
behavior.

Source: Coding guidelines

Comment on lines 581 to +602
await Promise.allSettled([fetchBalance(), fetchStatus()])
handleClose(false)
settingsDialog.show('workspace')
successSummary.value = {
previous: previousCredits,
added: creditsModel.value
}
step.value = 'success'
} else if (response.status === 'pending') {
billingOperationStore.startOperation(response.billing_op_id, 'topup')
const operation = await billingOperationStore.startOperation(
response.billing_op_id,
'topup'
)
if (operation.status === 'succeeded') {
await Promise.allSettled([fetchBalance(), fetchStatus()])
successSummary.value = {
previous: previousCredits,
added: creditsModel.value
}
step.value = 'success'
} else if (operation.status === 'failed') {
declineReason.value = operation.errorMessage
step.value = 'declined'
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Extract the duplicated success-finalization logic.

The completed branch and the pending -> succeeded branch both refresh balance/status, set successSummary, and set step.value = 'success' with identical code. Extract a helper to avoid the duplication and reduce drift risk if the success flow changes later.

♻️ Proposed refactor
+async function finalizeSuccess(previousCredits: number) {
+  await Promise.allSettled([fetchBalance(), fetchStatus()])
+  successSummary.value = {
+    previous: previousCredits,
+    added: creditsModel.value
+  }
+  step.value = 'success'
+}
+
 if (response.status === 'completed') {
   telemetry?.trackBillingEvent({ ... })
-  await Promise.allSettled([fetchBalance(), fetchStatus()])
-  successSummary.value = {
-    previous: previousCredits,
-    added: creditsModel.value
-  }
-  step.value = 'success'
+  await finalizeSuccess(previousCredits)
 } else if (response.status === 'pending') {
   const operation = await billingOperationStore.startOperation(
     response.billing_op_id,
     'topup'
   )
   if (operation.status === 'succeeded') {
-    await Promise.allSettled([fetchBalance(), fetchStatus()])
-    successSummary.value = {
-      previous: previousCredits,
-      added: creditsModel.value
-    }
-    step.value = 'success'
+    await finalizeSuccess(previousCredits)
   } else if (operation.status === 'failed') {
     declineReason.value = operation.errorMessage
     step.value = 'declined'
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await Promise.allSettled([fetchBalance(), fetchStatus()])
handleClose(false)
settingsDialog.show('workspace')
successSummary.value = {
previous: previousCredits,
added: creditsModel.value
}
step.value = 'success'
} else if (response.status === 'pending') {
billingOperationStore.startOperation(response.billing_op_id, 'topup')
const operation = await billingOperationStore.startOperation(
response.billing_op_id,
'topup'
)
if (operation.status === 'succeeded') {
await Promise.allSettled([fetchBalance(), fetchStatus()])
successSummary.value = {
previous: previousCredits,
added: creditsModel.value
}
step.value = 'success'
} else if (operation.status === 'failed') {
declineReason.value = operation.errorMessage
step.value = 'declined'
}
async function finalizeSuccess(previousCredits: number) {
await Promise.allSettled([fetchBalance(), fetchStatus()])
successSummary.value = {
previous: previousCredits,
added: creditsModel.value
}
step.value = 'success'
}
await finalizeSuccess(previousCredits)
} else if (response.status === 'pending') {
const operation = await billingOperationStore.startOperation(
response.billing_op_id,
'topup'
)
if (operation.status === 'succeeded') {
await finalizeSuccess(previousCredits)
} else if (operation.status === 'failed') {
declineReason.value = operation.errorMessage
step.value = 'declined'
}
🤖 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/TopUpCreditsDialogContentWorkspace.vue`
around lines 581 - 602, Extract the shared success-finalization block from the
completed branch and the pending-to-succeeded branch into a local helper near
the surrounding top-up flow. Have the helper refresh balance and status, assign
successSummary, and set step.value to 'success', then replace both duplicated
blocks with calls to that helper while preserving existing control flow.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/components/TopUpCreditsDialogContentWorkspace.vue`:
- Around line 539-542: Update the Back-navigation branch that checks
topupActionUrl.value to also require permissions.value.canTopUp before setting
step.value to 'verifying'; otherwise fall through to the amount step. Add a
regression test covering permission revocation after the action URL appears and
verify that Back returns to the amount step rather than reopening verification.
🪄 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: 232ee8b4-77e4-464f-b33c-eb07754d84a0

📥 Commits

Reviewing files that changed from the base of the PR and between 5b6dd73 and 2d89da5.

📒 Files selected for processing (2)
  • src/platform/workspace/components/TopUpCreditsDialogContentWorkspace.test.ts
  • src/platform/workspace/components/TopUpCreditsDialogContentWorkspace.vue

Comment on lines +539 to +542
if (topupActionUrl.value) {
step.value = 'verifying'
return
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Apply the top-up permission check before reopening verification.

The initial state requires permissions.value.canTopUp. Line 539 drops that requirement. If permission is revoked while confirmation is open, Back opens the verification step and exposes the pending charge action.

Require permissions.value.canTopUp in this branch. Add a regression test that revokes permission after the action URL appears and verifies Back returns to the amount step.

Proposed fix
-  if (topupActionUrl.value) {
+  if (topupActionUrl.value && permissions.value.canTopUp) {
     step.value = 'verifying'
     return
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (topupActionUrl.value) {
step.value = 'verifying'
return
}
if (topupActionUrl.value && permissions.value.canTopUp) {
step.value = 'verifying'
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/workspace/components/TopUpCreditsDialogContentWorkspace.vue`
around lines 539 - 542, Update the Back-navigation branch that checks
topupActionUrl.value to also require permissions.value.canTopUp before setting
step.value to 'verifying'; otherwise fall through to the amount step. Add a
regression test covering permission revocation after the action URL appears and
verify that Back returns to the amount step rather than reopening verification.

Back routed to the amount step during the window before the bank link
arrives, and a user already sitting on the amount step never reached the
verifying state at all. Back now routes to verifying whenever a top-up is
polling; the verification CTA spins until the action_url arrives; and the
amount step jumps to verifying the moment the link lands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/TopUpCreditsDialogContentWorkspace.vue (1)

597-612: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Enter verifying immediately for pending top-ups.

A pending operation currently leaves the dialog on confirm. The test encodes that incorrect manual-Back flow.

  • src/platform/workspace/components/TopUpCreditsDialogContentWorkspace.vue#L597-L612: set step.value = 'verifying' when startOperation() returns pending.
  • src/platform/workspace/components/TopUpCreditsDialogContentWorkspace.test.ts#L356-L373: assert the verification view directly after the pending payment action. Do not require Back navigation.
🤖 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/TopUpCreditsDialogContentWorkspace.vue`
around lines 597 - 612, Update the pending branch in
TopUpCreditsDialogContentWorkspace.vue around startOperation to set step.value
to 'verifying' immediately when a pending operation is returned, while
preserving the existing succeeded and failed handling. In
src/platform/workspace/components/TopUpCreditsDialogContentWorkspace.test.ts
lines 356-373, assert the verification view directly after the pending payment
action and remove the manual-Back navigation requirement.

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.

Inline comments:
In `@src/platform/workspace/components/TopUpCreditsDialogContentWorkspace.vue`:
- Around line 450-452: Gate every transition into the verifying step in the
top-up flow on permissions.value.canTopUp, including the topupActionUrl watcher
and the polling transition around the existing verification logic. Reuse one
permission-gated computed condition for URL arrival and back navigation,
preserve the amount step when permission is denied, and add tests covering
revoked canTopUp permissions.

---

Outside diff comments:
In `@src/platform/workspace/components/TopUpCreditsDialogContentWorkspace.vue`:
- Around line 597-612: Update the pending branch in
TopUpCreditsDialogContentWorkspace.vue around startOperation to set step.value
to 'verifying' immediately when a pending operation is returned, while
preserving the existing succeeded and failed handling. In
src/platform/workspace/components/TopUpCreditsDialogContentWorkspace.test.ts
lines 356-373, assert the verification view directly after the pending payment
action and remove the manual-Back navigation requirement.
🪄 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: cbdac6fe-1112-4bd6-b866-beac553cf523

📥 Commits

Reviewing files that changed from the base of the PR and between dbef8f1 and dad2028.

📒 Files selected for processing (2)
  • src/platform/workspace/components/TopUpCreditsDialogContentWorkspace.test.ts
  • src/platform/workspace/components/TopUpCreditsDialogContentWorkspace.vue

Comment on lines +450 to +452
watch(topupActionUrl, (url) => {
if (url && step.value === 'amount') step.value = 'verifying'
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Require canTopUp before every verification transition.

Line 450 enters verifying when an action URL arrives after permission revocation. Lines 545-547 also enter verifying while polling without checking permission. Keep the initial-state permission rule consistent for URL arrival and back navigation.

Use one permission-gated computed condition for these transitions. Preserve the amount step when permissions.value.canTopUp is false.

As per path instructions, tests must cover permissions in the top-up flow.

Also applies to: 545-547

🤖 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/TopUpCreditsDialogContentWorkspace.vue`
around lines 450 - 452, Gate every transition into the verifying step in the
top-up flow on permissions.value.canTopUp, including the topupActionUrl watcher
and the polling transition around the existing verification logic. Reuse one
permission-gated computed condition for URL arrival and back navigation,
preserve the amount step when permission is denied, and add tests covering
revoked canTopUp permissions.

Source: Path instructions

…light

Matches the checkout dialog's polling-disabled back button. Back can't
honestly return to the amount step mid-charge, and routing it to the
verifying screen read as a surprise; the arrow now dims and locks until
the operation resolves. The verifying-on-back routing stays underneath as
a safety net.
After Complete verification opens the bank tab, the demoted pay spinner
tile becomes the status row — spinner plus 'Waiting for your bank…' — and
the verifying step's body swaps to 'Complete the verification in the new
tab — this updates automatically.' The verification CTA stays enabled for
reopening lost tabs; the flag only sets when the tab actually opened, so
popup-blocked clicks keep the original copy.
dante01yoon added a commit that referenced this pull request Aug 7, 2026
## Summary

Adds deterministic browser coverage for the current-main partner-node,
top-up, and responsive billing-cycle workflows. This PR is stacked on
the isolated maximum-warning prerequisite #14815, which is itself
stacked on #14713 so the Cloud Team recovery-banner slice remains
independently reviewable.

## Why this is needed

These journeys span stateful API polling, exact currency payloads,
workspace-scoped balances, partner-node recovery, permission boundaries,
and responsive pricing interactions. Main and #14713 did not
deterministically exercise them, leaving regressions dependent on live
billing state and timing.

The maximum top-up test also exposed a current frontend bug: manually
entering an amount at or above $10,000 clamps the input correctly, but
the enterprise guidance was unreachable because the warning only
appeared for values strictly greater than the maximum.

## Changes

- Adds a typed stateful billing API fixture for deterministic status,
balance, plans, preview, top-up, subscription, cancellation, and
operation polling responses.
- Adds partner-node coverage for price presentation, prompt execution
request, sufficient-credit balance deduction/refresh,
insufficient-credit recovery after a failed billing refresh, and member
privacy/permissions.
- Adds top-up coverage for exact-cent personal preset/custom payloads,
active Team workspace scoping, pending completion, HTTP failure/retry,
failed operation feedback, and the current $10,000/enterprise contract.
- Adds monthly-to-yearly Creator pricing interaction through the direct
supported pricing route on Cloud desktop and mobile, guarded by nonempty
interactive-plan assertions.
- Depends on #14815, which isolates the `showCeilingWarning` production
prerequisite.

## Root cause and fix

### AS IS

`showCeilingWarning` used `value > MAX_AMOUNT`. Because the input is
clamped to `MAX_AMOUNT`, the observable value could never remain above
the cap, hiding the warning and Contact us link at $10,000.

[AS IS
screenshot](https://ampcode.com/user-content/artifacts/6f5bcdc4d255307bdaed598273b88977160cebe70cc89f89baa56c5330e79c0f-file.png)

### TO BE

The warning uses `value >= MAX_AMOUNT`, preserving the cap while
exposing enterprise billing guidance at the reachable maximum.

[TO BE
screenshot](https://ampcode.com/user-content/artifacts/4ef8548ce8b54dee264330d27bcef202cc5633667fecacd04c46d170a3a4b44b-file.png)

## Review focus

- Stateful billing operation polling and mocked balance
refresh/deduction behavior.
- Team member permission/privacy expectations.
- The one-line maximum-warning prerequisite is isolated in #14815.
- #14526 is an active draft for a future saved-payment-method UI
contract with component tests; these browser tests remain scoped to
current-main behavior and do not mock provider metering.
- Workspace Activity API wiring and browser coverage are isolated in
#14811 and #14813 rather than duplicated here.

## Validation

- `pnpm typecheck` and `pnpm typecheck:browser`
- Focused Cloud partner-node/top-up/pricing Playwright
- Pricing mobile-chrome repeated 3 times and Cloud once after the
responsive route fix
- ESLint, oxfmt, oxlint, knip, and `git diff --check`

## Stacking

- Base: `dante/topup-max-enterprise-warning` (#14815)
- #14815 is based on `dante01yoon/test-billing-e2e-matrix` (#14713)
- Workspace Activity prerequisite/tests: #14811#14813

---------

Co-authored-by: Amp <amp@ampcode.com>

@christian-byrne christian-byrne left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are significant merge conflicts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants