Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
39 changes: 22 additions & 17 deletions src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -1924,25 +1924,29 @@
},
"tiers": {
"founder": {
"name": "Founder's Edition Standard",
"name": "Founder's Edition",
"price": "20.00",
"benefits": {
"monthlyCredits": "5,460 monthly credits",
"maxDuration": "30 min max duration of each workflow run",
"rtx6000": "RTX 6000 Pro (96GB VRAM)",
"addCredits": "Add more credits whenever"
"monthlyCredits": "5,460",
"monthlyCreditsLabel": "monthly credits",
"maxDuration": "30 min",
"maxDurationLabel": "max duration of each workflow run",
"gpuLabel": "RTX 6000 Pro (96GB VRAM)",
"addCreditsLabel": "Add more credits whenever",
"customLoRAsLabel": "Import your own LoRAs"
}
},
"standard": {
"name": "Standard",
"price": "20.00",
"benefits": {
"monthlyCredits": "4,200 monthly credits",
"maxDuration": "30 min max duration of each workflow run",
"rtx6000": "RTX 6000 Pro (96GB VRAM)",
"addCredits": "Add more credits whenever",
"customLoRAs": "Import your own LoRAs",
"videoEstimate": "164"
"monthlyCredits": "4,200",
"monthlyCreditsLabel": "monthly credits",
"maxDuration": "30 min",
"maxDurationLabel": "max duration of each workflow run",
"gpuLabel": "RTX 6000 Pro (96GB VRAM)",
"addCreditsLabel": "Add more credits whenever",
"customLoRAsLabel": "Import your own LoRAs"
}
},
"creator": {
Expand All @@ -1962,12 +1966,13 @@
"name": "Pro",
"price": "100.00",
"benefits": {
"monthlyCredits": "21,100 monthly credits",
"maxDuration": "1 hr max duration of each workflow run",
"rtx6000": "RTX 6000 Pro (96GB VRAM)",
"addCredits": "Add more credits whenever",
"customLoRAs": "Import your own LoRAs",
"videoEstimate": "821"
"monthlyCredits": "21,100",
"monthlyCreditsLabel": "monthly credits",
"maxDuration": "1 hr",
"maxDurationLabel": "max duration of each workflow run",
"gpuLabel": "RTX 6000 Pro (96GB VRAM)",
"addCreditsLabel": "Add more credits whenever",
"customLoRAsLabel": "Import your own LoRAs"
}
}
},
Expand Down
37 changes: 27 additions & 10 deletions src/platform/cloud/subscription/components/SubscriptionPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -363,14 +363,30 @@ const {
isCancelled,
formattedRenewalDate,
formattedEndDate,
subscriptionTier,
Comment thread
christian-byrne marked this conversation as resolved.
handleInvoiceHistory
} = useSubscription()

const { show: showSubscriptionDialog } = useSubscriptionDialog()

// Tier data - hardcoded for Creator tier as requested
const tierName = computed(() => t('subscription.tiers.creator.name'))
const tierPrice = computed(() => t('subscription.tiers.creator.price'))
// Map API tier to i18n key
const tierKey = computed(() => {
switch (subscriptionTier.value) {
case 'STANDARD':
return 'standard'
case 'CREATOR':
return 'creator'
case 'PRO':
return 'pro'
case 'FOUNDERS_EDITION':
return 'founder'
default:
return 'standard' // fallback
}
})
Comment thread
christian-byrne marked this conversation as resolved.
Outdated

const tierName = computed(() => t(`subscription.tiers.${tierKey.value}.name`))
const tierPrice = computed(() => t(`subscription.tiers.${tierKey.value}.price`))

// Tier benefits for v-for loop
type BenefitType = 'metric' | 'feature'
Expand All @@ -383,33 +399,34 @@ interface Benefit {
}

const tierBenefits = computed(() => {
const key = tierKey.value
const baseBenefits: Benefit[] = [
{
key: 'monthlyCredits',
type: 'metric',
value: t('subscription.tiers.creator.benefits.monthlyCredits'),
label: t('subscription.tiers.creator.benefits.monthlyCreditsLabel')
value: t(`subscription.tiers.${key}.benefits.monthlyCredits`),
label: t(`subscription.tiers.${key}.benefits.monthlyCreditsLabel`)
},
{
key: 'maxDuration',
type: 'metric',
value: t('subscription.tiers.creator.benefits.maxDuration'),
label: t('subscription.tiers.creator.benefits.maxDurationLabel')
value: t(`subscription.tiers.${key}.benefits.maxDuration`),
label: t(`subscription.tiers.${key}.benefits.maxDurationLabel`)
},
{
key: 'gpu',
type: 'feature',
label: t('subscription.tiers.creator.benefits.gpuLabel')
label: t(`subscription.tiers.${key}.benefits.gpuLabel`)
},
{
key: 'addCredits',
type: 'feature',
label: t('subscription.tiers.creator.benefits.addCreditsLabel')
label: t(`subscription.tiers.${key}.benefits.addCreditsLabel`)
},
{
key: 'customLoRAs',
type: 'feature',
label: t('subscription.tiers.creator.benefits.customLoRAsLabel')
label: t(`subscription.tiers.${key}.benefits.customLoRAsLabel`)
}
]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ import { computed, onBeforeUnmount, watch } from 'vue'

import CloudBadge from '@/components/topbar/CloudBadge.vue'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { MONTHLY_SUBSCRIPTION_PRICE } from '@/config/subscriptionPricesConfig'
import StripePricingTable from '@/platform/cloud/subscription/components/StripePricingTable.vue'
import SubscribeButton from '@/platform/cloud/subscription/components/SubscribeButton.vue'
import SubscriptionBenefits from '@/platform/cloud/subscription/components/SubscriptionBenefits.vue'
Expand All @@ -155,8 +156,10 @@ const emit = defineEmits<{
close: [subscribed: boolean]
}>()

const { formattedMonthlyPrice, fetchStatus, isActiveSubscription } =
useSubscription()
const { fetchStatus, isActiveSubscription } = useSubscription()

// Legacy price for non-tier flow
const formattedMonthlyPrice = `$${MONTHLY_SUBSCRIPTION_PRICE}`
Comment thread
christian-byrne marked this conversation as resolved.
Outdated
const { featureFlag } = useFeatureFlags()
const subscriptionTiersEnabled = featureFlag(
'subscription_tiers_enabled',
Expand Down
17 changes: 7 additions & 10 deletions src/platform/cloud/subscription/composables/useSubscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { getComfyApiBaseUrl, getComfyPlatformBaseUrl } from '@/config/comfyApi'
import { MONTHLY_SUBSCRIPTION_PRICE } from '@/config/subscriptionPricesConfig'
import { t } from '@/i18n'
import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
Expand All @@ -14,18 +13,16 @@ import {
useFirebaseAuthStore
} from '@/stores/firebaseAuthStore'
import { useDialogService } from '@/services/dialogService'
import type { operations } from '@/types/comfyRegistryTypes'
import { useSubscriptionCancellationWatcher } from './useSubscriptionCancellationWatcher'

type CloudSubscriptionCheckoutResponse = {
checkout_url: string
}

export type CloudSubscriptionStatusResponse = {
is_active: boolean
subscription_id: string
renewal_date: string | null
end_date?: string | null
}
export type CloudSubscriptionStatusResponse = NonNullable<
operations['GetCloudSubscriptionStatus']['responses']['200']['content']['application/json']
>

function useSubscriptionInternal() {
const subscriptionStatus = ref<CloudSubscriptionStatusResponse | null>(null)
Expand Down Expand Up @@ -72,8 +69,8 @@ function useSubscriptionInternal() {
})
})

const formattedMonthlyPrice = computed(
() => `$${MONTHLY_SUBSCRIPTION_PRICE.toFixed(0)}`
const subscriptionTier = computed(
() => subscriptionStatus.value?.subscription_tier ?? null
)

const buildApiUrl = (path: string) => `${getComfyApiBaseUrl()}${path}`
Expand Down Expand Up @@ -227,7 +224,7 @@ function useSubscriptionInternal() {
isCancelled,
formattedRenewalDate,
formattedEndDate,
formattedMonthlyPrice,
subscriptionTier,

// Actions
subscribe,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ const mockSubscriptionData = {
isCancelled: false,
formattedRenewalDate: '2024-12-31',
formattedEndDate: '2024-12-31',
formattedMonthlyPrice: '$9.99',
manageSubscription: vi.fn(),
subscriptionTier: 'CREATOR' as const,
Comment thread
christian-byrne marked this conversation as resolved.
Outdated
handleInvoiceHistory: vi.fn()
}

Expand Down Expand Up @@ -50,6 +49,15 @@ vi.mock(
})
)

vi.mock(
'@/platform/cloud/subscription/composables/useSubscriptionDialog',
() => ({
useSubscriptionDialog: () => ({
show: vi.fn()
})
})
)

// Create i18n instance for testing
const i18n = createI18n({
legacy: false,
Expand All @@ -58,24 +66,70 @@ const i18n = createI18n({
en: {
subscription: {
title: 'Subscription',
titleUnsubscribed: 'Subscribe',
perMonth: '/ month',
subscribeNow: 'Subscribe Now',
manageSubscription: 'Manage Subscription',
partnerNodesBalance: 'Partner Nodes Balance',
partnerNodesDescription: 'Credits for partner nodes',
totalCredits: 'Total Credits',
creditsRemainingThisMonth: 'Credits remaining this month',
creditsYouveAdded: "Credits you've added",
monthlyBonusDescription: 'Monthly bonus',
prepaidDescription: 'Prepaid credits',
monthlyCreditsRollover: 'Monthly credits rollover info',
prepaidCreditsInfo: 'Prepaid credits info',
viewUsageHistory: 'View Usage History',
addCredits: 'Add Credits',
yourPlanIncludes: 'Your plan includes',
viewMoreDetailsPlans: 'View more details about plans & pricing',
learnMore: 'Learn More',
messageSupport: 'Message Support',
invoiceHistory: 'Invoice History',
partnerNodesCredits: 'Partner nodes pricing',
renewsDate: 'Renews {date}',
expiresDate: 'Expires {date}'
expiresDate: 'Expires {date}',
tiers: {
standard: {
name: 'Standard',
price: '20.00',
benefits: {
monthlyCredits: '4,200',
monthlyCreditsLabel: 'monthly credits',
maxDuration: '30 min',
maxDurationLabel: 'max duration of each workflow run',
gpuLabel: 'RTX 6000 Pro (96GB VRAM)',
addCreditsLabel: 'Add more credits whenever',
customLoRAsLabel: 'Import your own LoRAs'
}
},
creator: {
name: 'Creator',
price: '35.00',
benefits: {
monthlyCredits: '7,400',
monthlyCreditsLabel: 'monthly credits',
maxDuration: '30 min',
maxDurationLabel: 'max duration of each workflow run',
gpuLabel: 'RTX 6000 Pro (96GB VRAM)',
addCreditsLabel: 'Add more credits whenever',
customLoRAsLabel: 'Import your own LoRAs'
}
},
pro: {
name: 'Pro',
price: '100.00',
benefits: {
monthlyCredits: '21,100',
monthlyCreditsLabel: 'monthly credits',
maxDuration: '1 hr',
maxDurationLabel: 'max duration of each workflow run',
gpuLabel: 'RTX 6000 Pro (96GB VRAM)',
addCreditsLabel: 'Add more credits whenever',
customLoRAsLabel: 'Import your own LoRAs'
}
}
}
Comment thread
christian-byrne marked this conversation as resolved.
}
}
}
Expand Down
24 changes: 21 additions & 3 deletions tests-ui/tests/platform/cloud/subscription/useSubscription.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,28 @@ describe('useSubscription', () => {
expect(formattedRenewalDate.value).toBe('')
})

it('should format monthly price correctly', () => {
const { formattedMonthlyPrice } = useSubscription()
it('should return subscription tier from status', async () => {
vi.mocked(global.fetch).mockResolvedValue({
ok: true,
json: async () => ({
is_active: true,
subscription_id: 'sub_123',
subscription_tier: 'CREATOR',
renewal_date: '2025-11-16T12:00:00Z'
})
} as Response)

mockIsLoggedIn.value = true
const { subscriptionTier, fetchStatus } = useSubscription()

await fetchStatus()
expect(subscriptionTier.value).toBe('CREATOR')
})

it('should return null when subscription tier is not available', () => {
const { subscriptionTier } = useSubscription()

expect(formattedMonthlyPrice.value).toBe('$20')
expect(subscriptionTier.value).toBeNull()
})
})

Expand Down
Loading