Files
ComfyUI_frontend/src/platform/cloud/subscription/composables/useSubscriptionDialog.ts
2026-06-05 08:00:56 +00:00

170 lines
5.1 KiB
TypeScript

import { defineAsyncComponent } from 'vue'
import { useDialogService } from '@/services/dialogService'
import { useDialogStore } from '@/stores/dialogStore'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
import { isCloud } from '@/platform/distribution/types'
import { useTeamWorkspaceStore } from '@/platform/workspace/stores/teamWorkspaceStore'
const DIALOG_KEY = 'subscription-required'
const FREE_TIER_DIALOG_KEY = 'free-tier-info'
const RESUME_PRICING_KEY = 'comfy:resume-team-pricing'
export type SubscriptionDialogReason =
| 'subscription_required'
| 'out_of_credits'
| 'top_up_blocked'
export const useSubscriptionDialog = () => {
const { flags } = useFeatureFlags()
const dialogService = useDialogService()
const dialogStore = useDialogStore()
const workspaceStore = useTeamWorkspaceStore()
const { isFreeTier } = useSubscription()
function hide() {
dialogStore.closeDialog({ key: DIALOG_KEY })
dialogStore.closeDialog({ key: FREE_TIER_DIALOG_KEY })
}
function showPricingTable(options?: { reason?: SubscriptionDialogReason }) {
if (!isCloud) return
// Shared dialog shell styling for both variants.
const dialogComponentProps = {
style: 'width: min(1328px, 95vw); max-height: 958px;',
pt: {
root: {
class: 'rounded-2xl bg-transparent h-full'
},
content: {
class:
'!p-0 rounded-2xl border border-border-default bg-base-background/60 backdrop-blur-md shadow-[0_25px_80px_rgba(5,6,12,0.45)] h-full'
}
}
}
// Jun-5 model: a single unified pricing table (personal/team plan toggle on
// one workspace) when team workspaces are enabled. Replaces the old
// personal-vs-team workspace fork. Flag-off keeps the legacy table.
if (flags.teamWorkspacesEnabled) {
dialogService.showLayoutDialog({
key: DIALOG_KEY,
component: defineAsyncComponent(
() =>
import('@/platform/workspace/components/SubscriptionRequiredDialogContentUnified.vue')
),
props: { onClose: hide, reason: options?.reason },
dialogComponentProps
})
return
}
dialogService.showLayoutDialog({
key: DIALOG_KEY,
component: defineAsyncComponent(
() =>
import('@/platform/cloud/subscription/components/SubscriptionRequiredDialogContent.vue')
),
props: {
onClose: hide,
reason: options?.reason,
onChooseTeam: () => startTeamWorkspaceUpgradeFlow()
},
dialogComponentProps
})
}
function show(options?: { reason?: SubscriptionDialogReason }) {
if (isFreeTier.value && workspaceStore.isInPersonalWorkspace) {
const component = defineAsyncComponent(
() =>
import('@/platform/cloud/subscription/components/FreeTierDialogContent.vue')
)
dialogService.showLayoutDialog({
key: FREE_TIER_DIALOG_KEY,
component,
props: {
reason: options?.reason,
onClose: hide,
onUpgrade: () => {
hide()
showPricingTable(options)
}
},
dialogComponentProps: {
style: 'width: min(640px, 95vw);',
pt: {
root: {
class: 'rounded-2xl bg-transparent'
},
content: {
class:
'!p-0 rounded-2xl border border-border-default bg-base-background/60 backdrop-blur-md shadow-[0_25px_80px_rgba(5,6,12,0.45)]'
}
}
}
})
return
}
showPricingTable(options)
}
/**
* Start the two-stage team workspace upgrade flow:
* 1. Close the current pricing dialog
* 2. Open the create workspace dialog
* 3. On successful creation, persist a resume intent so the team pricing
* dialog reopens automatically after the page reload
*
* Uses sessionStorage (not a store) because the intent must survive
* a full page reload triggered by workspace switching.
*/
function startTeamWorkspaceUpgradeFlow() {
hide()
dialogService
.showTeamWorkspacesDialog(() => {
try {
sessionStorage.setItem(RESUME_PRICING_KEY, '1')
} catch {
// sessionStorage may be unavailable
}
})
.catch((error) => {
console.error(
'[useSubscriptionDialog] Failed to open team workspaces dialog:',
error
)
showPricingTable()
})
}
/**
* Check for and consume a pending team pricing resume intent.
* Call once after workspace initialization on app boot.
*/
function resumePendingPricingFlow() {
try {
const pending = sessionStorage.getItem(RESUME_PRICING_KEY)
if (!pending) return
sessionStorage.removeItem(RESUME_PRICING_KEY)
if (!workspaceStore.isInPersonalWorkspace) {
showPricingTable()
}
} catch {
// sessionStorage may be unavailable
}
}
return {
show,
showPricingTable,
hide,
startTeamWorkspaceUpgradeFlow,
resumePendingPricingFlow
}
}