Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -1945,6 +1945,7 @@
"billedYearly": "{total} Billed yearly",
"monthly": "Monthly",
"yearly": "Yearly",
"tierNameYearly": "{name} Yearly",
"messageSupport": "Message support",
"invoiceHistory": "Invoice history",
"benefits": {
Expand Down
39 changes: 25 additions & 14 deletions src/platform/cloud/subscription/components/PricingTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -344,18 +345,32 @@ 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)
}

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' =>
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -383,6 +383,7 @@ const {
formattedEndDate,
subscriptionTier,
subscriptionTierName,
isYearlySubscription,
handleInvoiceHistory
} = useSubscription()

Expand All @@ -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'
Expand Down Expand Up @@ -433,7 +436,7 @@ const tierBenefits = computed((): Benefit[] => {
}
]

if (TIER_FEATURES[key].customLoRAs) {
if (getTierFeatures(key).customLoRAs) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Consistent use of tier feature accessor.

Migrating from TIER_FEATURES[key] to getTierFeatures(key) improves encapsulation and maintains consistency with the refactored pricing constants.

🤖 Prompt for AI Agents
In src/platform/cloud/subscription/components/SubscriptionPanel.vue around line
439, replace the direct access to the old pricing constant
(TIER_FEATURES[key].customLoRAs) with the new accessor by calling
getTierFeatures(key).customLoRAs; update this single conditional to use
getTierFeatures(key) and remove any remaining uses of TIER_FEATURES[key] in
nearby lines so the component consistently uses the refactored pricing accessor.

benefits.push({
key: 'customLoRAs',
type: 'feature',
Expand Down
29 changes: 17 additions & 12 deletions src/platform/cloud/subscription/composables/useSubscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Good refactoring to centralize tier key mapping.

Importing TIER_TO_KEY from tierPricing.ts eliminates duplication and establishes a single source of truth for tier key resolution.

🤖 Prompt for AI Agents
In src/platform/cloud/subscription/composables/useSubscription.ts around line
16, replace any local tier-key mapping with the centralized TIER_TO_KEY import:
remove the duplicated mapping constant from this file, update all uses to
reference TIER_TO_KEY, and ensure exported/internal functions read from
TIER_TO_KEY (not a local copy); run/adjust any unit tests or callers that
referenced the removed local constant to use the imported TIER_TO_KEY.

import type { operations } from '@/types/comfyRegistryTypes'
import { useSubscriptionCancellationWatcher } from './useSubscriptionCancellationWatcher'

type CloudSubscriptionCheckoutResponse = NonNullable<
Expand All @@ -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()
Expand Down Expand Up @@ -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}`
Expand Down Expand Up @@ -241,6 +244,8 @@ function useSubscriptionInternal() {
formattedRenewalDate,
formattedEndDate,
subscriptionTier,
subscriptionDuration,
isYearlySubscription,
subscriptionTierName,
subscriptionStatus,

Expand Down
12 changes: 8 additions & 4 deletions src/platform/cloud/subscription/constants/tierPricing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -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]
}
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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()
}

Expand Down Expand Up @@ -212,6 +216,7 @@ describe('SubscriptionPanel', () => {
mockIsActiveSubscription.value = false
mockIsCancelled.value = false
mockSubscriptionTier.value = 'CREATOR'
mockIsYearlySubscription.value = false
})

describe('subscription state functionality', () => {
Expand Down