Fix(cloud)/pricing annual misc (#7701)

## Summary

Fix: PricingTable showed "Current Plan" on the wrong billing cycle
(e.g., showing it on Yearly when subscribed to Monthly) because we
weren't checking subscription_duration. Now we check for ANNUAL |
MONTHLY match.

Fix: Subscribed users were being sent to billing portal instead of
checkout. Now routes to checkout.

Improved: Types now use openapi.yml as source of truth. Tier names in
user popover and subscription panels now reflect the billing cycle
(YEARLY/MONTHLY).

Recommended to merge this before
https://github.com/Comfy-Org/ComfyUI_frontend/pull/7692

---------

Co-authored-by: bymyself <cbyrne@comfy.org>
This commit is contained in:
Simula_r
2025-12-22 04:01:32 -08:00
committed by GitHub
parent 970861e677
commit 9514e5d36c
6 changed files with 65 additions and 36 deletions

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]
}