Files
ComfyUI_frontend/src/components/toast/GlobalToast.vue
Simula_r c5431de123 Feat/workspaces 6 billing (#8508)
## Summary

Implements billing infrastructure for team workspaces, separate from
legacy personal billing.

## Changes

- **Billing abstraction**: New `useBillingContext` composable that
switches between legacy (personal) and workspace billing based on
context
- **Workspace subscription flows**: Pricing tables, plan transitions,
cancellation dialogs, and payment preview components for workspace
billing
- **Top-up credits**: Workspace-specific top-up dialog with polling for
payment confirmation
- **Workspace API**: Extended with billing endpoints (subscriptions,
invoices, payment methods, credits top-up)
- **Workspace switcher**: Now displays tier badges for each workspace
- **Subscribe polling**: Added polling mechanisms
(`useSubscribePolling`, `useTopupPolling`) for async payment flows

## Review Focus

- Billing flow correctness for workspace vs legacy contexts
- Polling timeout and error handling in payment flows

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8508-Feat-workspaces-6-billing-2f96d73d365081f69f65c1ddf369010d)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 20:52:53 -08:00

100 lines
2.3 KiB
Vue

<template>
<Toast />
<Toast group="billing-operation" position="top-right">
<template #message="slotProps">
<div class="flex items-center gap-2">
<i class="pi pi-spin pi-spinner text-primary" />
<span>{{ slotProps.message.summary }}</span>
</div>
</template>
</Toast>
</template>
<script setup lang="ts">
import Toast from 'primevue/toast'
import { useToast } from 'primevue/usetoast'
import { nextTick, watch } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useToastStore } from '@/platform/updates/common/toastStore'
const toast = useToast()
const toastStore = useToastStore()
const settingStore = useSettingStore()
watch(
() => toastStore.messagesToAdd,
(newMessages) => {
if (newMessages.length === 0) {
return
}
newMessages.forEach((message) => {
toast.add(message)
})
toastStore.messagesToAdd = []
},
{ deep: true }
)
watch(
() => toastStore.messagesToRemove,
(messagesToRemove) => {
if (messagesToRemove.length === 0) {
return
}
messagesToRemove.forEach((message) => {
toast.remove(message)
})
toastStore.messagesToRemove = []
},
{ deep: true }
)
watch(
() => toastStore.removeAllRequested,
(requested) => {
if (requested) {
toast.removeAllGroups()
toastStore.removeAllRequested = false
}
}
)
function updateToastPosition() {
const styleElement =
document.getElementById('dynamic-toast-style') || createStyleElement()
const rect = document
.querySelector('.graph-canvas-container')
?.getBoundingClientRect()
if (!rect) return
styleElement.textContent = `
.p-toast.p-component.p-toast-top-right {
top: ${rect.top + 100}px !important;
right: ${window.innerWidth - (rect.left + rect.width) + 20}px !important;
z-index: 10000 !important;
}
`
}
function createStyleElement() {
const style = document.createElement('style')
style.id = 'dynamic-toast-style'
document.head.appendChild(style)
return style
}
watch(
() => settingStore.get('Comfy.UseNewMenu'),
() => nextTick(updateToastPosition),
{ immediate: true }
)
watch(
() => settingStore.get('Comfy.Sidebar.Location'),
() => nextTick(updateToastPosition),
{ immediate: true }
)
</script>