Refactor(cloud)/yearly credits monthly (#7584)

## Summary

Add yearly total credits vs monthly. Also pulled out numerical values
from the main.json to avoid translation issues and used n() for better
currency support on prices.

## Changes

- **What**: PricingTable.vue, /en/main.json
- **Breaking**: <!-- Any breaking changes (if none, remove this line)
-->
- **Dependencies**: <!-- New dependencies (if none, remove this line)
-->

## Review Focus

<!-- Critical design decisions or edge cases that need attention -->

<!-- If this PR fixes an issue, uncomment and update the line below -->
<!-- Fixes #ISSUE_NUMBER -->

## Screenshots (if applicable)

<img width="2321" height="1538" alt="image"
src="https://github.com/user-attachments/assets/8c7b3eed-bfd8-4188-914f-3bfa5397a84f"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7584-Refactor-cloud-yearly-credits-monthly-2cc6d73d365081b28afbec7f9d22546f)
by [Unito](https://www.unito.io)
This commit is contained in:
Simula_r
2025-12-17 14:06:14 -08:00
committed by GitHub
parent 890ab2019f
commit a78e8c587f
2 changed files with 44 additions and 99 deletions

View File

@@ -1938,7 +1938,7 @@
"viewMoreDetails": "View more details",
"learnMore": "Learn more",
"billedMonthly": "Billed monthly",
"billedAnnually": "{total} Billed annually",
"billedYearly": "{total} Billed yearly",
"monthly": "Monthly",
"yearly": "Yearly",
"messageSupport": "Message support",
@@ -1950,72 +1950,16 @@
"yearlyDiscount": "20% DISCOUNT",
"tiers": {
"founder": {
"name": "Founder's Edition",
"price": "20",
"benefits": {
"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"
}
"name": "Founder's Edition"
},
"standard": {
"name": "Standard",
"price": {
"monthly": "20",
"yearly": "16",
"annualTotal": "$192"
},
"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",
"videoEstimate": "120"
}
"name": "Standard"
},
"creator": {
"name": "Creator",
"price": {
"monthly": "35",
"yearly": "28",
"annualTotal": "$336"
},
"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",
"videoEstimate": "288"
}
"name": "Creator"
},
"pro": {
"name": "Pro",
"price": {
"monthly": "100",
"yearly": "80",
"annualTotal": "$960"
},
"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",
"videoEstimate": "815"
}
"name": "Pro"
}
},
"required": {
@@ -2036,6 +1980,7 @@
"currentPlan": "Current Plan",
"subscribeTo": "Subscribe to {plan}",
"monthlyCreditsLabel": "Monthly credits",
"yearlyCreditsLabel": "Total yearly credits",
"maxDurationLabel": "Max duration of each workflow run",
"gpuLabel": "RTX 6000 Pro (96GB VRAM)",
"addCreditsLabel": "Add more credits whenever",
@@ -2047,11 +1992,6 @@
"upgradePlan": "Upgrade Plan",
"upgradeTo": "Upgrade to {plan}",
"changeTo": "Change to {plan}",
"credits": {
"standard": "4,200",
"creator": "7,400",
"pro": "21,100"
},
"maxDuration": {
"standard": "30 min",
"creator": "30 min",
@@ -2478,4 +2418,4 @@
"recentReleases": "Recent releases",
"helpCenterMenu": "Help Center Menu"
}
}
}

View File

@@ -73,7 +73,7 @@
v-show="currentBillingCycle === 'yearly'"
class="line-through text-2xl text-muted-foreground"
>
${{ tier.price.monthly }}
${{ tier.pricing.monthly }}
</span>
${{ getPrice(tier) }}
</span>
@@ -87,8 +87,8 @@
<span class="text-sm text-muted-foreground">
{{
currentBillingCycle === 'yearly'
? t('subscription.billedAnnually', {
total: tier.price.annualTotal
? t('subscription.billedYearly', {
total: `$${getAnnualTotal(tier)}`
})
: t('subscription.billedMonthly')
}}
@@ -102,14 +102,18 @@
<span
class="font-inter text-sm font-normal leading-normal text-foreground"
>
{{ t('subscription.monthlyCreditsLabel') }}
{{
currentBillingCycle === 'yearly'
? t('subscription.yearlyCreditsLabel')
: t('subscription.monthlyCreditsLabel')
}}
</span>
<div class="flex flex-row items-center gap-1">
<i class="icon-[lucide--component] text-amber-400 text-sm" />
<span
class="font-inter text-sm font-bold leading-normal text-base-foreground"
>
{{ tier.credits }}
{{ n(getCreditsDisplay(tier)) }}
</span>
</div>
</div>
@@ -171,7 +175,7 @@
<span
class="font-inter text-sm font-bold leading-normal text-base-foreground"
>
{{ tier.videoEstimate }}
{{ n(tier.pricing.videoEstimate) }}
</span>
</div>
</div>
@@ -242,6 +246,7 @@ import Popover from 'primevue/popover'
import SelectButton from 'primevue/selectbutton'
import type { ToggleButtonPassThroughMethodOptions } from 'primevue/togglebutton'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { useErrorHandling } from '@/composables/useErrorHandling'
@@ -271,15 +276,26 @@ 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
name: string
price: Record<BillingCycle, string> & { annualTotal: string }
credits: string
pricing: TierPricing
maxDuration: string
customLoRAs: boolean
videoEstimate: string
isPopular?: boolean
}
@@ -300,49 +316,32 @@ const tiers: PricingTierConfig[] = [
id: 'STANDARD',
key: 'standard',
name: t('subscription.tiers.standard.name'),
price: {
monthly: t('subscription.tiers.standard.price.monthly'),
yearly: t('subscription.tiers.standard.price.yearly'),
annualTotal: t('subscription.tiers.standard.price.annualTotal')
},
credits: t('subscription.credits.standard'),
pricing: TIER_PRICING.standard,
maxDuration: t('subscription.maxDuration.standard'),
customLoRAs: false,
videoEstimate: t('subscription.tiers.standard.benefits.videoEstimate'),
isPopular: false
},
{
id: 'CREATOR',
key: 'creator',
name: t('subscription.tiers.creator.name'),
price: {
monthly: t('subscription.tiers.creator.price.monthly'),
yearly: t('subscription.tiers.creator.price.yearly'),
annualTotal: t('subscription.tiers.creator.price.annualTotal')
},
credits: t('subscription.credits.creator'),
pricing: TIER_PRICING.creator,
maxDuration: t('subscription.maxDuration.creator'),
customLoRAs: true,
videoEstimate: t('subscription.tiers.creator.benefits.videoEstimate'),
isPopular: true
},
{
id: 'PRO',
key: 'pro',
name: t('subscription.tiers.pro.name'),
price: {
monthly: t('subscription.tiers.pro.price.monthly'),
yearly: t('subscription.tiers.pro.price.yearly'),
annualTotal: t('subscription.tiers.pro.price.annualTotal')
},
credits: t('subscription.credits.pro'),
pricing: TIER_PRICING.pro,
maxDuration: t('subscription.maxDuration.pro'),
customLoRAs: true,
videoEstimate: t('subscription.tiers.pro.benefits.videoEstimate'),
isPopular: false
}
]
const { n } = useI18n()
const { getAuthHeader } = useFirebaseAuthStore()
const { isActiveSubscription, subscriptionTier } = useSubscription()
const { accessBillingPortal, reportError } = useFirebaseAuthActions()
@@ -383,8 +382,14 @@ const getButtonTextClass = (tier: PricingTierConfig): string =>
? 'font-inter text-sm font-bold leading-normal text-base-background'
: 'font-inter text-sm font-bold leading-normal text-primary-foreground'
const getPrice = (tier: PricingTierConfig): string =>
tier.price[currentBillingCycle.value]
const getPrice = (tier: PricingTierConfig): number =>
tier.pricing[currentBillingCycle.value]
const getAnnualTotal = (tier: PricingTierConfig): number =>
tier.pricing.yearly * 12
const getCreditsDisplay = (tier: PricingTierConfig): number =>
tier.pricing.credits * (currentBillingCycle.value === 'yearly' ? 12 : 1)
const initiateCheckout = async (tierKey: TierKey) => {
const authHeader = await getAuthHeader()