mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 10:59:53 +00:00
Fix(cloud)/subscription panel (#7628)
## Summary Fix subscription panel to use new shared consts for pricing info and misc plan related items. ## Changes - **What**: SubscriptionPanel.vue, /en/main.json - **Breaking**: <!-- Any breaking changes (if none, remove this line) --> - **Dependencies**: <!-- New dependencies (if none, remove this line) --> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7628-Fix-cloud-subscription-panel-2ce6d73d36508119846dd537b37a0d59) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -192,7 +192,9 @@ const formattedBalance = computed(() => {
|
||||
|
||||
const canUpgrade = computed(() => {
|
||||
const tier = subscriptionTier.value
|
||||
return tier === 'STANDARD' || tier === 'CREATOR'
|
||||
return (
|
||||
tier === 'FOUNDERS_EDITION' || tier === 'STANDARD' || tier === 'CREATOR'
|
||||
)
|
||||
})
|
||||
|
||||
const handleOpenUserSettings = () => {
|
||||
|
||||
@@ -1988,7 +1988,7 @@
|
||||
"gpuLabel": "RTX 6000 Pro (96GB VRAM)",
|
||||
"addCreditsLabel": "Add more credits whenever",
|
||||
"customLoRAsLabel": "Import your own LoRAs",
|
||||
"videoEstimateLabel": "Approx. number of 5s videos generated with Wan Fun Control template",
|
||||
"videoEstimateLabel": "Number of 5s videos generated with Wan Fun Control template",
|
||||
"videoEstimateHelp": "What is this?",
|
||||
"videoEstimateExplanation": "These estimates are based on the Wan Fun Control template for 5-second videos.",
|
||||
"videoEstimateTryTemplate": "Try the Wan Fun Control template →",
|
||||
@@ -1998,7 +1998,8 @@
|
||||
"maxDuration": {
|
||||
"standard": "30 min",
|
||||
"creator": "30 min",
|
||||
"pro": "1 hr"
|
||||
"pro": "1 hr",
|
||||
"founder": "30 min"
|
||||
}
|
||||
},
|
||||
"userSettings": {
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
<span
|
||||
class="font-inter text-sm font-bold leading-normal text-base-foreground"
|
||||
>
|
||||
{{ n(tier.pricing.videoEstimate) }}
|
||||
~{{ n(tier.pricing.videoEstimate) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -253,6 +253,14 @@ import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { getComfyApiBaseUrl } from '@/config/comfyApi'
|
||||
import { t } from '@/i18n'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import {
|
||||
TIER_PRICING,
|
||||
TIER_TO_KEY
|
||||
} from '@/platform/cloud/subscription/constants/tierPricing'
|
||||
import type {
|
||||
TierKey,
|
||||
TierPricing
|
||||
} from '@/platform/cloud/subscription/constants/tierPricing'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import {
|
||||
FirebaseAuthStoreError,
|
||||
@@ -261,13 +269,13 @@ import {
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type SubscriptionTier = components['schemas']['SubscriptionTier']
|
||||
type TierKey = 'standard' | 'creator' | 'pro'
|
||||
type CheckoutTier = TierKey | `${TierKey}-yearly`
|
||||
type CheckoutTierKey = Exclude<TierKey, 'founder'>
|
||||
type CheckoutTier = CheckoutTierKey | `${CheckoutTierKey}-yearly`
|
||||
|
||||
type BillingCycle = 'monthly' | 'yearly'
|
||||
|
||||
const getCheckoutTier = (
|
||||
tierKey: TierKey,
|
||||
tierKey: CheckoutTierKey,
|
||||
billingCycle: BillingCycle
|
||||
): CheckoutTier => (billingCycle === 'yearly' ? `${tierKey}-yearly` : tierKey)
|
||||
|
||||
@@ -276,22 +284,9 @@ interface BillingCycleOption {
|
||||
value: BillingCycle
|
||||
}
|
||||
|
||||
interface TierPricing {
|
||||
monthly: number
|
||||
yearly: number
|
||||
credits: number
|
||||
videoEstimate: number
|
||||
}
|
||||
|
||||
const TIER_PRICING: Record<TierKey, TierPricing> = {
|
||||
standard: { monthly: 20, yearly: 16, credits: 4200, videoEstimate: 164 },
|
||||
creator: { monthly: 35, yearly: 28, credits: 7400, videoEstimate: 288 },
|
||||
pro: { monthly: 100, yearly: 80, credits: 21100, videoEstimate: 821 }
|
||||
} as const
|
||||
|
||||
interface PricingTierConfig {
|
||||
id: SubscriptionTier
|
||||
key: TierKey
|
||||
key: CheckoutTierKey
|
||||
name: string
|
||||
pricing: TierPricing
|
||||
maxDuration: string
|
||||
@@ -304,13 +299,6 @@ const billingCycleOptions: BillingCycleOption[] = [
|
||||
{ label: t('subscription.monthly'), value: 'monthly' }
|
||||
]
|
||||
|
||||
const TIER_TO_KEY: Record<SubscriptionTier, TierKey> = {
|
||||
STANDARD: 'standard',
|
||||
CREATOR: 'creator',
|
||||
PRO: 'pro',
|
||||
FOUNDERS_EDITION: 'standard'
|
||||
}
|
||||
|
||||
const tiers: PricingTierConfig[] = [
|
||||
{
|
||||
id: 'STANDARD',
|
||||
@@ -348,7 +336,7 @@ const { accessBillingPortal, reportError } = useFirebaseAuthActions()
|
||||
const { wrapWithErrorHandlingAsync } = useErrorHandling()
|
||||
|
||||
const isLoading = ref(false)
|
||||
const loadingTier = ref<TierKey | null>(null)
|
||||
const loadingTier = ref<CheckoutTierKey | null>(null)
|
||||
const popover = ref()
|
||||
const currentBillingCycle = ref<BillingCycle>('yearly')
|
||||
|
||||
@@ -356,7 +344,7 @@ const currentTierKey = computed<TierKey | null>(() =>
|
||||
subscriptionTier.value ? TIER_TO_KEY[subscriptionTier.value] : null
|
||||
)
|
||||
|
||||
const isCurrentPlan = (tierKey: TierKey): boolean =>
|
||||
const isCurrentPlan = (tierKey: CheckoutTierKey): boolean =>
|
||||
currentTierKey.value === tierKey
|
||||
|
||||
const togglePopover = (event: Event) => {
|
||||
@@ -391,7 +379,7 @@ const getAnnualTotal = (tier: PricingTierConfig): number =>
|
||||
const getCreditsDisplay = (tier: PricingTierConfig): number =>
|
||||
tier.pricing.credits * (currentBillingCycle.value === 'yearly' ? 12 : 1)
|
||||
|
||||
const initiateCheckout = async (tierKey: TierKey) => {
|
||||
const initiateCheckout = async (tierKey: CheckoutTierKey) => {
|
||||
const authHeader = await getAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
@@ -432,24 +420,27 @@ const initiateCheckout = async (tierKey: TierKey) => {
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
const handleSubscribe = wrapWithErrorHandlingAsync(async (tierKey: TierKey) => {
|
||||
if (!isCloud || isLoading.value || isCurrentPlan(tierKey)) return
|
||||
const handleSubscribe = wrapWithErrorHandlingAsync(
|
||||
async (tierKey: CheckoutTierKey) => {
|
||||
if (!isCloud || isLoading.value || isCurrentPlan(tierKey)) return
|
||||
|
||||
isLoading.value = true
|
||||
loadingTier.value = tierKey
|
||||
isLoading.value = true
|
||||
loadingTier.value = tierKey
|
||||
|
||||
try {
|
||||
if (isActiveSubscription.value) {
|
||||
await accessBillingPortal()
|
||||
} else {
|
||||
const response = await initiateCheckout(tierKey)
|
||||
if (response.checkout_url) {
|
||||
window.open(response.checkout_url, '_blank')
|
||||
try {
|
||||
if (isActiveSubscription.value) {
|
||||
await accessBillingPortal()
|
||||
} else {
|
||||
const response = await initiateCheckout(tierKey)
|
||||
if (response.checkout_url) {
|
||||
window.open(response.checkout_url, '_blank')
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
loadingTier.value = null
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
loadingTier.value = null
|
||||
}
|
||||
}, reportError)
|
||||
},
|
||||
reportError
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<TabPanel value="PlanCredits" class="subscription-container h-full">
|
||||
<div class="flex h-full flex-col gap-6">
|
||||
<div class="flex items-baseline gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-2xl font-inter font-semibold leading-tight">
|
||||
{{
|
||||
isActiveSubscription
|
||||
@@ -9,10 +9,12 @@
|
||||
: $t('subscription.titleUnsubscribed')
|
||||
}}
|
||||
</span>
|
||||
<CloudBadge
|
||||
reverse-order
|
||||
background-color="var(--p-dialog-background)"
|
||||
/>
|
||||
<div class="pt-1">
|
||||
<CloudBadge
|
||||
reverse-order
|
||||
background-color="var(--p-dialog-background)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grow overflow-auto">
|
||||
@@ -361,26 +363,18 @@ import { useSubscription } from '@/platform/cloud/subscription/composables/useSu
|
||||
import { useSubscriptionActions } from '@/platform/cloud/subscription/composables/useSubscriptionActions'
|
||||
import { useSubscriptionCredits } from '@/platform/cloud/subscription/composables/useSubscriptionCredits'
|
||||
import { useSubscriptionDialog } from '@/platform/cloud/subscription/composables/useSubscriptionDialog'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import {
|
||||
DEFAULT_TIER_KEY,
|
||||
TIER_FEATURES,
|
||||
TIER_TO_KEY,
|
||||
getTierCredits,
|
||||
getTierPrice
|
||||
} from '@/platform/cloud/subscription/constants/tierPricing'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
type SubscriptionTier = components['schemas']['SubscriptionTier']
|
||||
|
||||
/** Maps API subscription tier values to i18n translation keys */
|
||||
const TIER_TO_I18N_KEY = {
|
||||
STANDARD: 'standard',
|
||||
CREATOR: 'creator',
|
||||
PRO: 'pro',
|
||||
FOUNDERS_EDITION: 'founder'
|
||||
} as const satisfies Record<SubscriptionTier, string>
|
||||
|
||||
type TierKey = (typeof TIER_TO_I18N_KEY)[SubscriptionTier]
|
||||
|
||||
const DEFAULT_TIER_KEY: TierKey = 'standard'
|
||||
|
||||
const { buildDocsUrl, docsPaths } = useExternalLink()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const { t } = useI18n()
|
||||
const { t, n } = useI18n()
|
||||
|
||||
const {
|
||||
isActiveSubscription,
|
||||
@@ -397,9 +391,9 @@ const { show: showSubscriptionDialog } = useSubscriptionDialog()
|
||||
const tierKey = computed(() => {
|
||||
const tier = subscriptionTier.value
|
||||
if (!tier) return DEFAULT_TIER_KEY
|
||||
return TIER_TO_I18N_KEY[tier] ?? DEFAULT_TIER_KEY
|
||||
return TIER_TO_KEY[tier] ?? DEFAULT_TIER_KEY
|
||||
})
|
||||
const tierPrice = computed(() => t(`subscription.tiers.${tierKey.value}.price`))
|
||||
const tierPrice = computed(() => getTierPrice(tierKey.value))
|
||||
|
||||
// Tier benefits for v-for loop
|
||||
type BenefitType = 'metric' | 'feature'
|
||||
@@ -411,49 +405,43 @@ interface Benefit {
|
||||
value?: string
|
||||
}
|
||||
|
||||
const BENEFITS_BY_TIER: Record<
|
||||
TierKey,
|
||||
ReadonlyArray<Omit<Benefit, 'label' | 'value'>>
|
||||
> = {
|
||||
standard: [
|
||||
{ key: 'monthlyCredits', type: 'metric' },
|
||||
{ key: 'maxDuration', type: 'metric' },
|
||||
{ key: 'gpu', type: 'feature' },
|
||||
{ key: 'addCredits', type: 'feature' }
|
||||
],
|
||||
creator: [
|
||||
{ key: 'monthlyCredits', type: 'metric' },
|
||||
{ key: 'maxDuration', type: 'metric' },
|
||||
{ key: 'gpu', type: 'feature' },
|
||||
{ key: 'addCredits', type: 'feature' },
|
||||
{ key: 'customLoRAs', type: 'feature' }
|
||||
],
|
||||
pro: [
|
||||
{ key: 'monthlyCredits', type: 'metric' },
|
||||
{ key: 'maxDuration', type: 'metric' },
|
||||
{ key: 'gpu', type: 'feature' },
|
||||
{ key: 'addCredits', type: 'feature' },
|
||||
{ key: 'customLoRAs', type: 'feature' }
|
||||
],
|
||||
founder: [
|
||||
{ key: 'monthlyCredits', type: 'metric' },
|
||||
{ key: 'maxDuration', type: 'metric' },
|
||||
{ key: 'gpu', type: 'feature' },
|
||||
{ key: 'addCredits', type: 'feature' }
|
||||
]
|
||||
}
|
||||
|
||||
const tierBenefits = computed(() => {
|
||||
const tierBenefits = computed((): Benefit[] => {
|
||||
const key = tierKey.value
|
||||
const benefitConfig = BENEFITS_BY_TIER[key]
|
||||
|
||||
return benefitConfig.map((config) => ({
|
||||
...config,
|
||||
...(config.type === 'metric' && {
|
||||
value: t(`subscription.tiers.${key}.benefits.${config.key}`)
|
||||
}),
|
||||
label: t(`subscription.tiers.${key}.benefits.${config.key}Label`)
|
||||
}))
|
||||
const benefits: Benefit[] = [
|
||||
{
|
||||
key: 'monthlyCredits',
|
||||
type: 'metric',
|
||||
value: n(getTierCredits(key)),
|
||||
label: t('subscription.monthlyCreditsLabel')
|
||||
},
|
||||
{
|
||||
key: 'maxDuration',
|
||||
type: 'metric',
|
||||
value: t(`subscription.maxDuration.${key}`),
|
||||
label: t('subscription.maxDurationLabel')
|
||||
},
|
||||
{
|
||||
key: 'gpu',
|
||||
type: 'feature',
|
||||
label: t('subscription.gpuLabel')
|
||||
},
|
||||
{
|
||||
key: 'addCredits',
|
||||
type: 'feature',
|
||||
label: t('subscription.addCreditsLabel')
|
||||
}
|
||||
]
|
||||
|
||||
if (TIER_FEATURES[key].customLoRAs) {
|
||||
benefits.push({
|
||||
key: 'customLoRAs',
|
||||
type: 'feature',
|
||||
label: t('subscription.customLoRAsLabel')
|
||||
})
|
||||
}
|
||||
|
||||
return benefits
|
||||
})
|
||||
|
||||
const { totalCredits, monthlyBonusCredits, prepaidCredits, isLoadingBalance } =
|
||||
|
||||
52
src/platform/cloud/subscription/constants/tierPricing.ts
Normal file
52
src/platform/cloud/subscription/constants/tierPricing.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type SubscriptionTier = components['schemas']['SubscriptionTier']
|
||||
|
||||
export type TierKey = 'standard' | 'creator' | 'pro' | 'founder'
|
||||
|
||||
export const TIER_TO_KEY: Record<SubscriptionTier, TierKey> = {
|
||||
STANDARD: 'standard',
|
||||
CREATOR: 'creator',
|
||||
PRO: 'pro',
|
||||
FOUNDERS_EDITION: 'founder'
|
||||
}
|
||||
|
||||
export interface TierPricing {
|
||||
monthly: number
|
||||
yearly: number
|
||||
credits: number
|
||||
videoEstimate: number
|
||||
}
|
||||
|
||||
export const TIER_PRICING: Record<Exclude<TierKey, 'founder'>, TierPricing> = {
|
||||
standard: { monthly: 20, yearly: 16, credits: 4200, videoEstimate: 164 },
|
||||
creator: { monthly: 35, yearly: 28, credits: 7400, videoEstimate: 288 },
|
||||
pro: { monthly: 100, yearly: 80, credits: 21100, videoEstimate: 821 }
|
||||
}
|
||||
|
||||
interface TierFeatures {
|
||||
customLoRAs: boolean
|
||||
}
|
||||
|
||||
export const TIER_FEATURES: Record<TierKey, TierFeatures> = {
|
||||
standard: { customLoRAs: false },
|
||||
creator: { customLoRAs: true },
|
||||
pro: { customLoRAs: true },
|
||||
founder: { customLoRAs: false }
|
||||
}
|
||||
|
||||
export const DEFAULT_TIER_KEY: TierKey = 'standard'
|
||||
|
||||
// Founder tier pricing: legacy tier with fixed values not in TIER_PRICING
|
||||
const FOUNDER_MONTHLY_PRICE = 20
|
||||
const FOUNDER_MONTHLY_CREDITS = 5460
|
||||
|
||||
export function getTierPrice(tierKey: TierKey): number {
|
||||
if (tierKey === 'founder') return FOUNDER_MONTHLY_PRICE
|
||||
return TIER_PRICING[tierKey].monthly
|
||||
}
|
||||
|
||||
export function getTierCredits(tierKey: TierKey): number {
|
||||
if (tierKey === 'founder') return FOUNDER_MONTHLY_CREDITS
|
||||
return TIER_PRICING[tierKey].credits
|
||||
}
|
||||
Reference in New Issue
Block a user