Skip to content

Commit 4a751f9

Browse files
Jaewon Yoonampagent
andcommitted
fix: scope pending top-ups to workspace
Amp-Thread-ID: https://ampcode.com/threads/T-019fc82c-9ff0-72bd-8994-3d39dd29a9ad Co-authored-by: Amp <amp@ampcode.com>
1 parent 9087705 commit 4a751f9

4 files changed

Lines changed: 69 additions & 24 deletions

File tree

src/platform/workspace/components/TopUpCreditsDialogContentWorkspace.test.ts

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ const mockPermissions = vi.hoisted(() => ({
2121
ref: undefined as { value: { canTopUp: boolean } } | undefined
2222
}))
2323
const mockShouldUseWorkspaceBilling = vi.hoisted(() => ({ value: true }))
24-
const mockIsAddingCredits = vi.hoisted(() => ({ value: false }))
25-
const mockTopupActionOperation = vi.hoisted(() => ({
26-
value: undefined as { actionUrl: string } | undefined
24+
const mockBillingOperationState = vi.hoisted(() => ({
25+
isAddingCredits: undefined as { value: boolean } | undefined,
26+
topupActionOperation: undefined as
27+
| { value: { actionUrl: string } | undefined }
28+
| undefined
2729
}))
2830

2931
vi.mock('@/composables/billing/useBillingContext', () => ({
@@ -34,18 +36,23 @@ vi.mock('@/composables/billing/useBillingContext', () => ({
3436
})
3537
}))
3638

37-
vi.mock('@/platform/workspace/stores/billingOperationStore', () => ({
38-
useBillingOperationStore: () => ({
39-
hasPendingOperations: true,
40-
get isAddingCredits() {
41-
return mockIsAddingCredits.value
42-
},
43-
get topupActionOperation() {
44-
return mockTopupActionOperation.value
45-
},
46-
startOperation: mockStartOperation
47-
})
48-
}))
39+
vi.mock('@/platform/workspace/stores/billingOperationStore', async () => {
40+
const { ref } = await import('vue')
41+
mockBillingOperationState.isAddingCredits = ref(false)
42+
mockBillingOperationState.topupActionOperation = ref(undefined)
43+
return {
44+
useBillingOperationStore: () => ({
45+
hasPendingOperations: true,
46+
get isAddingCredits() {
47+
return mockBillingOperationState.isAddingCredits?.value ?? false
48+
},
49+
get topupActionOperation() {
50+
return mockBillingOperationState.topupActionOperation?.value
51+
},
52+
startOperation: mockStartOperation
53+
})
54+
}
55+
})
4956

5057
vi.mock('@/platform/workspace/composables/useWorkspaceUI', async () => {
5158
const { ref } = await import('vue')
@@ -176,6 +183,20 @@ function setCanTopUp(canTopUp: boolean) {
176183
mockPermissions.ref.value = { canTopUp }
177184
}
178185

186+
function setIsAddingCredits(isAddingCredits: boolean) {
187+
if (!mockBillingOperationState.isAddingCredits) {
188+
throw new Error('Billing operation mock not initialized')
189+
}
190+
mockBillingOperationState.isAddingCredits.value = isAddingCredits
191+
}
192+
193+
function setTopupActionOperation(operation: { actionUrl: string } | undefined) {
194+
if (!mockBillingOperationState.topupActionOperation) {
195+
throw new Error('Billing operation mock not initialized')
196+
}
197+
mockBillingOperationState.topupActionOperation.value = operation
198+
}
199+
179200
async function clickAddCredits() {
180201
const user = userEvent.setup()
181202
await user.click(screen.getByRole('button', { name: 'Add credits' }))
@@ -186,12 +207,12 @@ describe('TopUpCreditsDialogContentWorkspace', () => {
186207
vi.clearAllMocks()
187208
setCanTopUp(true)
188209
mockShouldUseWorkspaceBilling.value = true
189-
mockIsAddingCredits.value = false
190-
mockTopupActionOperation.value = undefined
210+
setIsAddingCredits(false)
211+
setTopupActionOperation(undefined)
191212
mockFetchBalance.mockResolvedValue(undefined)
192213
mockFetchStatus.mockResolvedValue(undefined)
193214
mockStartOperation.mockImplementation(() => {
194-
mockIsAddingCredits.value = true
215+
setIsAddingCredits(true)
195216
return new Promise(() => {})
196217
})
197218
})
@@ -226,7 +247,7 @@ describe('TopUpCreditsDialogContentWorkspace', () => {
226247
it('reopens in verification without exposing the action URL', async () => {
227248
const actionUrl = 'https://verify.example/sensitive-token'
228249
const open = vi.spyOn(window, 'open').mockReturnValue({} as Window)
229-
mockTopupActionOperation.value = { actionUrl }
250+
setTopupActionOperation({ actionUrl })
230251

231252
const { container } = renderDialog()
232253

@@ -245,7 +266,7 @@ describe('TopUpCreditsDialogContentWorkspace', () => {
245266
})
246267

247268
it('reopens in verification while the action URL is loading', () => {
248-
mockIsAddingCredits.value = true
269+
setIsAddingCredits(true)
249270

250271
renderDialog()
251272

@@ -256,11 +277,24 @@ describe('TopUpCreditsDialogContentWorkspace', () => {
256277
expect(screen.queryByText('Select amount')).not.toBeInTheDocument()
257278
})
258279

280+
it('returns to amount selection when a reopened operation ends', async () => {
281+
setIsAddingCredits(true)
282+
283+
renderDialog()
284+
expect(screen.getByText('Verify your payment')).toBeInTheDocument()
285+
286+
setIsAddingCredits(false)
287+
await nextTick()
288+
289+
expect(screen.getByText('Select amount')).toBeInTheDocument()
290+
expect(screen.queryByText('Verify your payment')).not.toBeInTheDocument()
291+
})
292+
259293
it('hides topup verification after permission is revoked', () => {
260294
setCanTopUp(false)
261-
mockTopupActionOperation.value = {
295+
setTopupActionOperation({
262296
actionUrl: 'https://verify.example/sensitive-token'
263-
}
297+
})
264298

265299
renderDialog()
266300

src/platform/workspace/components/TopUpCreditsDialogContentWorkspace.vue

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@
249249

250250
<script setup lang="ts">
251251
import { useToast } from 'primevue/usetoast'
252-
import { computed, ref } from 'vue'
252+
import { computed, ref, watch } from 'vue'
253253
import { useI18n } from 'vue-i18n'
254254
255255
import { creditsToUsd, usdToCredits } from '@/base/credits/comfyCredits'
@@ -340,6 +340,12 @@ const paymentLocked = computed(
340340
!!topupActionUrl.value
341341
)
342342
343+
watch([isPolling, topupActionUrl], ([polling, actionUrl]) => {
344+
if (step.value === 'verifying' && !polling && !actionUrl) {
345+
step.value = 'amount'
346+
}
347+
})
348+
343349
// Utility functions
344350
function formatNumber(num: number): string {
345351
return num.toLocaleString('en-US')

src/platform/workspace/stores/billingOperationStore.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,10 +676,12 @@ describe('billingOperationStore', () => {
676676
await vi.advanceTimersByTimeAsync(0)
677677

678678
expect(store.topupActionOperation?.actionUrl).toBe(actionUrl)
679+
expect(store.isAddingCredits).toBe(true)
679680

680681
mockActiveWorkspaceId.value = 'workspace-2'
681682

682683
expect(store.topupActionOperation).toBeUndefined()
684+
expect(store.isAddingCredits).toBe(false)
683685
})
684686

685687
it('only exposes subscription actions for the active workspace', async () => {

src/platform/workspace/stores/billingOperationStore.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,10 @@ export const useBillingOperationStore = defineStore('billingOperation', () => {
8383

8484
const isAddingCredits = computed(() =>
8585
[...operations.value.values()].some(
86-
(op) => op.status === 'pending' && op.type === 'topup'
86+
(op) =>
87+
op.status === 'pending' &&
88+
op.type === 'topup' &&
89+
op.workspaceId === workspaceStore.activeWorkspaceId
8790
)
8891
)
8992

0 commit comments

Comments
 (0)