[backport cloud/1.35] Fix(cloud)/pricing annual misc (#7704)

Backport of #7701 to `cloud/1.35`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7704-backport-cloud-1-35-Fix-cloud-pricing-annual-misc-2d16d73d365081868447db732608a2c7)
by [Unito](https://www.unito.io)

Co-authored-by: Simula_r <18093452+simula-r@users.noreply.github.com>
Co-authored-by: bymyself <cbyrne@comfy.org>
This commit is contained in:
Comfy Org PR Bot
2025-12-22 21:21:24 +09:00
committed by GitHub
parent b81f5fee48
commit 10389e216e
6 changed files with 65 additions and 36 deletions

View File

@@ -1921,6 +1921,7 @@
"billedYearly": "{total} Billed yearly",
"monthly": "Monthly",
"yearly": "Yearly",
"tierNameYearly": "{name} Yearly",
"messageSupport": "Message support",
"invoiceHistory": "Invoice history",
"benefits": {

View File

@@ -331,8 +331,9 @@ const tiers: PricingTierConfig[] = [
const { n } = useI18n()
const { getAuthHeader } = useFirebaseAuthStore()
const { isActiveSubscription, subscriptionTier } = useSubscription()
const { accessBillingPortal, reportError } = useFirebaseAuthActions()
const { isActiveSubscription, subscriptionTier, isYearlySubscription } =
useSubscription()
const { reportError } = useFirebaseAuthActions()
const { wrapWithErrorHandlingAsync } = useErrorHandling()
const isLoading = ref(false)
@@ -344,8 +345,16 @@ const currentTierKey = computed<TierKey | null>(() =>
subscriptionTier.value ? TIER_TO_KEY[subscriptionTier.value] : null
)
const isCurrentPlan = (tierKey: CheckoutTierKey): boolean =>
currentTierKey.value === tierKey
const isCurrentPlan = (tierKey: CheckoutTierKey): boolean => {
if (!currentTierKey.value) return false
const selectedIsYearly = currentBillingCycle.value === 'yearly'
return (
currentTierKey.value === tierKey &&
isYearlySubscription.value === selectedIsYearly
)
}
const togglePopover = (event: Event) => {
popover.value.toggle(event)
@@ -353,9 +362,15 @@ const togglePopover = (event: Event) => {
const getButtonLabel = (tier: PricingTierConfig): string => {
if (isCurrentPlan(tier.key)) return t('subscription.currentPlan')
if (!isActiveSubscription.value)
return t('subscription.subscribeTo', { plan: tier.name })
return t('subscription.changeTo', { plan: tier.name })
const planName =
currentBillingCycle.value === 'yearly'
? t('subscription.tierNameYearly', { name: tier.name })
: tier.name
return isActiveSubscription.value
? t('subscription.changeTo', { plan: planName })
: t('subscription.subscribeTo', { plan: planName })
}
const getButtonSeverity = (tier: PricingTierConfig): 'primary' | 'secondary' =>
@@ -428,13 +443,9 @@ const handleSubscribe = wrapWithErrorHandlingAsync(
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')
}
const response = await initiateCheckout(tierKey)
if (response.checkout_url) {
window.open(response.checkout_url, '_blank')
}
} finally {
isLoading.value = false

View File

@@ -365,9 +365,9 @@ import { useSubscriptionCredits } from '@/platform/cloud/subscription/composable
import { useSubscriptionDialog } from '@/platform/cloud/subscription/composables/useSubscriptionDialog'
import {
DEFAULT_TIER_KEY,
TIER_FEATURES,
TIER_TO_KEY,
getTierCredits,
getTierFeatures,
getTierPrice
} from '@/platform/cloud/subscription/constants/tierPricing'
import { cn } from '@/utils/tailwindUtil'
@@ -383,6 +383,7 @@ const {
formattedEndDate,
subscriptionTier,
subscriptionTierName,
isYearlySubscription,
handleInvoiceHistory
} = useSubscription()
@@ -393,7 +394,9 @@ const tierKey = computed(() => {
if (!tier) return DEFAULT_TIER_KEY
return TIER_TO_KEY[tier] ?? DEFAULT_TIER_KEY
})
const tierPrice = computed(() => getTierPrice(tierKey.value))
const tierPrice = computed(() =>
getTierPrice(tierKey.value, isYearlySubscription.value)
)
// Tier benefits for v-for loop
type BenefitType = 'metric' | 'feature'
@@ -433,7 +436,7 @@ const tierBenefits = computed((): Benefit[] => {
}
]
if (TIER_FEATURES[key].customLoRAs) {
if (getTierFeatures(key).customLoRAs) {
benefits.push({
key: 'customLoRAs',
type: 'feature',

View File

@@ -13,7 +13,8 @@ import {
useFirebaseAuthStore
} from '@/stores/firebaseAuthStore'
import { useDialogService } from '@/services/dialogService'
import type { components, operations } from '@/types/comfyRegistryTypes'
import { TIER_TO_KEY } from '@/platform/cloud/subscription/constants/tierPricing'
import type { operations } from '@/types/comfyRegistryTypes'
import { useSubscriptionCancellationWatcher } from './useSubscriptionCancellationWatcher'
type CloudSubscriptionCheckoutResponse = NonNullable<
@@ -24,15 +25,6 @@ export type CloudSubscriptionStatusResponse = NonNullable<
operations['GetCloudSubscriptionStatus']['responses']['200']['content']['application/json']
>
type SubscriptionTier = components['schemas']['SubscriptionTier']
const TIER_TO_I18N_KEY: Record<SubscriptionTier, string> = {
STANDARD: 'standard',
CREATOR: 'creator',
PRO: 'pro',
FOUNDERS_EDITION: 'founder'
}
function useSubscriptionInternal() {
const subscriptionStatus = ref<CloudSubscriptionStatusResponse | null>(null)
const telemetry = useTelemetry()
@@ -82,11 +74,22 @@ function useSubscriptionInternal() {
() => subscriptionStatus.value?.subscription_tier ?? null
)
const subscriptionDuration = computed(
() => subscriptionStatus.value?.subscription_duration ?? null
)
const isYearlySubscription = computed(
() => subscriptionDuration.value === 'ANNUAL'
)
const subscriptionTierName = computed(() => {
const tier = subscriptionTier.value
if (!tier) return ''
const key = TIER_TO_I18N_KEY[tier] ?? 'standard'
return t(`subscription.tiers.${key}.name`)
const key = TIER_TO_KEY[tier] ?? 'standard'
const baseName = t(`subscription.tiers.${key}.name`)
return isYearlySubscription.value
? t('subscription.tierNameYearly', { name: baseName })
: baseName
})
const buildApiUrl = (path: string) => `${getComfyApiBaseUrl()}${path}`
@@ -241,6 +244,8 @@ function useSubscriptionInternal() {
formattedRenewalDate,
formattedEndDate,
subscriptionTier,
subscriptionDuration,
isYearlySubscription,
subscriptionTierName,
subscriptionStatus,

View File

@@ -28,7 +28,7 @@ interface TierFeatures {
customLoRAs: boolean
}
export const TIER_FEATURES: Record<TierKey, TierFeatures> = {
const TIER_FEATURES: Record<TierKey, TierFeatures> = {
standard: { customLoRAs: false },
creator: { customLoRAs: true },
pro: { customLoRAs: true },
@@ -37,16 +37,20 @@ export const TIER_FEATURES: Record<TierKey, TierFeatures> = {
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 {
export function getTierPrice(tierKey: TierKey, isYearly = false): number {
if (tierKey === 'founder') return FOUNDER_MONTHLY_PRICE
return TIER_PRICING[tierKey].monthly
const pricing = TIER_PRICING[tierKey]
return isYearly ? pricing.yearly : pricing.monthly
}
export function getTierCredits(tierKey: TierKey): number {
if (tierKey === 'founder') return FOUNDER_MONTHLY_CREDITS
return TIER_PRICING[tierKey].credits
}
export function getTierFeatures(tierKey: TierKey): TierFeatures {
return TIER_FEATURES[tierKey]
}

View File

@@ -12,6 +12,7 @@ const mockIsCancelled = ref(false)
const mockSubscriptionTier = ref<
'STANDARD' | 'CREATOR' | 'PRO' | 'FOUNDERS_EDITION' | null
>('CREATOR')
const mockIsYearlySubscription = ref(false)
const TIER_TO_NAME: Record<string, string> = {
STANDARD: 'Standard',
@@ -27,9 +28,12 @@ const mockSubscriptionData = {
formattedRenewalDate: computed(() => '2024-12-31'),
formattedEndDate: computed(() => '2024-12-31'),
subscriptionTier: computed(() => mockSubscriptionTier.value),
subscriptionTierName: computed(() =>
mockSubscriptionTier.value ? TIER_TO_NAME[mockSubscriptionTier.value] : ''
),
subscriptionTierName: computed(() => {
if (!mockSubscriptionTier.value) return ''
const baseName = TIER_TO_NAME[mockSubscriptionTier.value]
return mockIsYearlySubscription.value ? `${baseName} Yearly` : baseName
}),
isYearlySubscription: computed(() => mockIsYearlySubscription.value),
handleInvoiceHistory: vi.fn()
}
@@ -212,6 +216,7 @@ describe('SubscriptionPanel', () => {
mockIsActiveSubscription.value = false
mockIsCancelled.value = false
mockSubscriptionTier.value = 'CREATOR'
mockIsYearlySubscription.value = false
})
describe('subscription state functionality', () => {