mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-14 01:36:14 +00:00
## Summary Adds 20 component tests for `SubscriptionRequiredDialogContentWorkspace.vue` covering: - **Initial rendering**: pricing table display, close/back button visibility, out_of_credits reason message - **Close button**: calls onClose callback - **Subscribe click flow**: pricing→preview transitions (new subscription & upgrade), error toasts for disallowed/missing/failed previews, monthly billing cycle - **Back button**: returns from preview to pricing step - **Add credit card**: handles subscribed status (success toast + close), needs_payment_method (opens Stripe URL), error state - **Confirm transition**: success path with close emit, error toast on failure - **Resubscribe**: success path with toast + close, error toast on failure ## Testing ```bash pnpm test:unit -- src/platform/workspace/components/SubscriptionRequiredDialogContentWorkspace.test.ts ``` All 20 tests pass. Quality gates (typecheck, lint, format, knip) pass. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-11396-test-add-component-tests-for-SubscriptionRequiredDialogContentWorkspace-3476d73d36508156a218dcb67a2a334e) by [Unito](https://www.unito.io)
211 lines
6.0 KiB
TypeScript
211 lines
6.0 KiB
TypeScript
import { useToast } from 'primevue/usetoast'
|
|
import { computed, ref } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
import { useBillingContext } from '@/composables/billing/useBillingContext'
|
|
import { getComfyPlatformBaseUrl } from '@/config/comfyApi'
|
|
import type { TierKey } from '@/platform/cloud/subscription/constants/tierPricing'
|
|
import type { BillingCycle } from '@/platform/cloud/subscription/utils/subscriptionTierRank'
|
|
import { useTelemetry } from '@/platform/telemetry'
|
|
import type {
|
|
Plan,
|
|
PreviewSubscribeResponse
|
|
} from '@/platform/workspace/api/workspaceApi'
|
|
import { workspaceApi } from '@/platform/workspace/api/workspaceApi'
|
|
import { useBillingOperationStore } from '@/platform/workspace/stores/billingOperationStore'
|
|
|
|
type CheckoutStep = 'pricing' | 'preview'
|
|
type CheckoutTierKey = Exclude<TierKey, 'free' | 'founder'>
|
|
|
|
export function findPlanSlug(
|
|
plans: Plan[],
|
|
tierKey: CheckoutTierKey,
|
|
billingCycle: BillingCycle
|
|
): string | null {
|
|
const apiDuration = billingCycle === 'yearly' ? 'ANNUAL' : 'MONTHLY'
|
|
const apiTier = tierKey.toUpperCase()
|
|
const plan = plans.find(
|
|
(p) => p.tier === apiTier && p.duration === apiDuration
|
|
)
|
|
return plan?.slug ?? null
|
|
}
|
|
|
|
export function useSubscriptionCheckout(emit: {
|
|
(e: 'close', subscribed: boolean): void
|
|
}) {
|
|
const { t } = useI18n()
|
|
const toast = useToast()
|
|
const { subscribe, previewSubscribe, plans, fetchStatus, fetchBalance } =
|
|
useBillingContext()
|
|
const telemetry = useTelemetry()
|
|
const billingOperationStore = useBillingOperationStore()
|
|
|
|
const checkoutStep = ref<CheckoutStep>('pricing')
|
|
const isLoadingPreview = ref(false)
|
|
const loadingTier = ref<CheckoutTierKey | null>(null)
|
|
const isSubscribing = ref(false)
|
|
const isResubscribing = ref(false)
|
|
const previewData = ref<PreviewSubscribeResponse | null>(null)
|
|
const selectedTierKey = ref<CheckoutTierKey | null>(null)
|
|
const selectedBillingCycle = ref<BillingCycle>('yearly')
|
|
const isPolling = computed(() => billingOperationStore.hasPendingOperations)
|
|
|
|
function getApiPlanSlug(
|
|
tierKey: CheckoutTierKey,
|
|
billingCycle: BillingCycle
|
|
): string | null {
|
|
return findPlanSlug(plans.value, tierKey, billingCycle)
|
|
}
|
|
|
|
async function handleSubscribeClick(payload: {
|
|
tierKey: CheckoutTierKey
|
|
billingCycle: BillingCycle
|
|
}) {
|
|
const { tierKey, billingCycle } = payload
|
|
|
|
isLoadingPreview.value = true
|
|
loadingTier.value = tierKey
|
|
selectedTierKey.value = tierKey
|
|
selectedBillingCycle.value = billingCycle
|
|
|
|
try {
|
|
const planSlug = getApiPlanSlug(tierKey, billingCycle)
|
|
if (!planSlug) {
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Unable to subscribe',
|
|
detail: 'This plan is not available'
|
|
})
|
|
return
|
|
}
|
|
const response = await previewSubscribe(planSlug)
|
|
|
|
if (!response || !response.allowed) {
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Unable to subscribe',
|
|
detail: response?.reason || 'This plan is not available'
|
|
})
|
|
return
|
|
}
|
|
|
|
previewData.value = response
|
|
checkoutStep.value = 'preview'
|
|
} catch (error) {
|
|
const message =
|
|
error instanceof Error
|
|
? error.message
|
|
: 'Failed to load subscription preview'
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Error',
|
|
detail: message
|
|
})
|
|
} finally {
|
|
isLoadingPreview.value = false
|
|
loadingTier.value = null
|
|
}
|
|
}
|
|
|
|
function handleBackToPricing() {
|
|
checkoutStep.value = 'pricing'
|
|
previewData.value = null
|
|
}
|
|
|
|
async function handleSubscription() {
|
|
if (!selectedTierKey.value) return
|
|
|
|
isSubscribing.value = true
|
|
try {
|
|
const planSlug = getApiPlanSlug(
|
|
selectedTierKey.value,
|
|
selectedBillingCycle.value
|
|
)
|
|
if (!planSlug) return
|
|
const response = await subscribe(
|
|
planSlug,
|
|
`${getComfyPlatformBaseUrl()}/payment/success`,
|
|
`${getComfyPlatformBaseUrl()}/payment/failed`
|
|
)
|
|
|
|
if (!response) return
|
|
|
|
if (response.status === 'subscribed') {
|
|
telemetry?.trackMonthlySubscriptionSucceeded()
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: t('subscription.required.pollingSuccess'),
|
|
life: 5000
|
|
})
|
|
await Promise.all([fetchStatus(), fetchBalance()])
|
|
emit('close', true)
|
|
} else if (
|
|
response.status === 'needs_payment_method' &&
|
|
response.payment_method_url
|
|
) {
|
|
window.open(response.payment_method_url, '_blank')
|
|
billingOperationStore.startOperation(
|
|
response.billing_op_id,
|
|
'subscription'
|
|
)
|
|
} else if (response.status === 'pending_payment') {
|
|
billingOperationStore.startOperation(
|
|
response.billing_op_id,
|
|
'subscription'
|
|
)
|
|
}
|
|
} catch (error) {
|
|
const message =
|
|
error instanceof Error ? error.message : 'Failed to subscribe'
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Error',
|
|
detail: message
|
|
})
|
|
} finally {
|
|
isSubscribing.value = false
|
|
}
|
|
}
|
|
|
|
async function handleResubscribe() {
|
|
isResubscribing.value = true
|
|
try {
|
|
await workspaceApi.resubscribe()
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: t('subscription.resubscribeSuccess'),
|
|
life: 5000
|
|
})
|
|
await Promise.all([fetchStatus(), fetchBalance()])
|
|
emit('close', true)
|
|
} catch (error) {
|
|
const message =
|
|
error instanceof Error ? error.message : 'Failed to resubscribe'
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Error',
|
|
detail: message
|
|
})
|
|
} finally {
|
|
isResubscribing.value = false
|
|
}
|
|
}
|
|
|
|
return {
|
|
checkoutStep,
|
|
isLoadingPreview,
|
|
loadingTier,
|
|
isSubscribing,
|
|
isResubscribing,
|
|
previewData,
|
|
selectedTierKey,
|
|
selectedBillingCycle,
|
|
isPolling,
|
|
handleSubscribeClick,
|
|
handleBackToPricing,
|
|
handleAddCreditCard: handleSubscription,
|
|
handleConfirmTransition: handleSubscription,
|
|
handleResubscribe
|
|
}
|
|
}
|