mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-17 01:07:56 +00:00
fix(billing): align yearly checkout confirm to updated Figma (FE-934)
Yearly headline now shows the monthly-equivalent price with a
"{total} Billed yearly" subtitle and "Each year credits refill to" (x12);
monthly is unchanged. Pass the selected billing cycle to the team confirm
so the subtitle renders, drop the redundant header credits/month line, and
reword "Starting today" to "Starts today".
This commit is contained in:
@@ -2631,10 +2631,11 @@
|
||||
"preview": {
|
||||
"confirmPayment": "Confirm your payment",
|
||||
"confirmPlanChange": "Confirm your plan change",
|
||||
"startingToday": "Starting today",
|
||||
"startingToday": "Starts today",
|
||||
"starting": "Starting {date}",
|
||||
"ends": "Ends {date}",
|
||||
"eachMonthCreditsRefill": "Each month credits refill to",
|
||||
"eachYearCreditsRefill": "Each year credits refill to",
|
||||
"everyMonthStarting": "Every month starting {date}",
|
||||
"creditsRefillTo": "Credits refill to",
|
||||
"youllBeCharged": "You'll be charged",
|
||||
|
||||
@@ -2,8 +2,42 @@ import userEvent from '@testing-library/user-event'
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type {
|
||||
PreviewSubscribeResponse,
|
||||
SubscriptionDuration
|
||||
} from '@/platform/workspace/api/workspaceApi'
|
||||
|
||||
import SubscriptionAddPaymentPreviewWorkspace from './SubscriptionAddPaymentPreviewWorkspace.vue'
|
||||
|
||||
function previewFixture(
|
||||
duration: SubscriptionDuration,
|
||||
priceCents: number
|
||||
): PreviewSubscribeResponse {
|
||||
return {
|
||||
allowed: true,
|
||||
transition_type: 'new_subscription',
|
||||
effective_at: '2026-06-19T00:00:00Z',
|
||||
is_immediate: true,
|
||||
cost_today_cents: priceCents,
|
||||
cost_next_period_cents: priceCents,
|
||||
credits_today_cents: 0,
|
||||
credits_next_period_cents: 0,
|
||||
new_plan: {
|
||||
slug: 'creator',
|
||||
tier: 'CREATOR',
|
||||
duration,
|
||||
price_cents: priceCents,
|
||||
credits_cents: 0,
|
||||
seat_summary: {
|
||||
seat_count: 1,
|
||||
total_cost_cents: priceCents,
|
||||
total_credits_cents: 0
|
||||
},
|
||||
period_end: '2027-06-19T00:00:00Z'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key,
|
||||
@@ -42,6 +76,67 @@ describe('SubscriptionAddPaymentPreviewWorkspace', () => {
|
||||
expect(screen.getByText('$380.00')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('shows the monthly-equivalent price and annual total for a yearly preview', () => {
|
||||
render(SubscriptionAddPaymentPreviewWorkspace, {
|
||||
props: {
|
||||
tierKey: 'creator',
|
||||
billingCycle: 'yearly',
|
||||
previewData: previewFixture('ANNUAL', 33_600)
|
||||
},
|
||||
global: globalOptions
|
||||
})
|
||||
expect(screen.getByText('subscription.usdPerMonth')).toBeTruthy()
|
||||
expect(screen.getByText('$28')).toBeTruthy()
|
||||
expect(screen.getByText('subscription.billedYearly')).toBeTruthy()
|
||||
expect(screen.getByText('$336.00')).toBeTruthy()
|
||||
expect(
|
||||
screen.getByText('subscription.preview.eachYearCreditsRefill')
|
||||
).toBeTruthy()
|
||||
expect(screen.getByText('88,800')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('divides the yearly price by twelve in the fallback path', () => {
|
||||
render(SubscriptionAddPaymentPreviewWorkspace, {
|
||||
props: { tierKey: 'creator', billingCycle: 'yearly' },
|
||||
global: globalOptions
|
||||
})
|
||||
expect(screen.getByText('$28')).toBeTruthy()
|
||||
expect(screen.getByText('subscription.billedYearly')).toBeTruthy()
|
||||
expect(screen.getByText('$336.00')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('omits the billed-yearly note for a monthly subscription', () => {
|
||||
render(SubscriptionAddPaymentPreviewWorkspace, {
|
||||
props: {
|
||||
tierKey: 'creator',
|
||||
billingCycle: 'monthly',
|
||||
previewData: previewFixture('MONTHLY', 3_500)
|
||||
},
|
||||
global: globalOptions
|
||||
})
|
||||
expect(screen.getByText('$35')).toBeTruthy()
|
||||
expect(screen.queryByText('subscription.billedYearly')).toBeNull()
|
||||
expect(
|
||||
screen.getByText('subscription.preview.eachMonthCreditsRefill')
|
||||
).toBeTruthy()
|
||||
expect(
|
||||
screen.queryByText('subscription.preview.eachYearCreditsRefill')
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('shows the annual total for a yearly team plan', () => {
|
||||
render(SubscriptionAddPaymentPreviewWorkspace, {
|
||||
props: {
|
||||
billingCycle: 'yearly',
|
||||
teamPlan: { usd: 400, credits: 84_400, discountedUsd: 380 }
|
||||
},
|
||||
global: globalOptions
|
||||
})
|
||||
expect(screen.getByText('$380')).toBeTruthy()
|
||||
expect(screen.getByText('subscription.billedYearly')).toBeTruthy()
|
||||
expect(screen.getByText('$4560.00')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('emits addCreditCard from the team confirm CTA', async () => {
|
||||
const { emitted } = render(SubscriptionAddPaymentPreviewWorkspace, {
|
||||
props: { teamPlan: { usd: 400, credits: 84_400, discountedUsd: 380 } },
|
||||
|
||||
@@ -19,13 +19,9 @@
|
||||
{{ $t('subscription.usdPerMonth') }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="teamPlan"
|
||||
class="flex items-center gap-1 text-sm text-muted-foreground"
|
||||
>
|
||||
<i class="icon-[comfy--credits] size-3.5 shrink-0 bg-amber-400" />
|
||||
<span>{{ displayCredits }} {{ $t('subscription.perMonth') }}</span>
|
||||
</div>
|
||||
<span v-if="isYearly" class="text-muted-foreground">
|
||||
{{ $t('subscription.billedYearly', { total: annualTotalFormatted }) }}
|
||||
</span>
|
||||
<span class="text-muted-foreground">
|
||||
{{ $t('subscription.preview.startingToday') }}
|
||||
</span>
|
||||
@@ -35,12 +31,12 @@
|
||||
<div class="flex flex-col gap-3 pt-16 pb-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-base-foreground">
|
||||
{{ $t('subscription.preview.eachMonthCreditsRefill') }}
|
||||
{{ $t(creditsRefillLabelKey) }}
|
||||
</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<i class="icon-[comfy--credits] size-4 shrink-0 bg-amber-400" />
|
||||
<span class="font-bold text-base-foreground">
|
||||
{{ displayCredits }}
|
||||
{{ refillCredits }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,16 +206,41 @@ const tierName = computed(() =>
|
||||
: t(`subscription.tiers.${tierKey}.name`)
|
||||
)
|
||||
|
||||
const isYearly = computed(() =>
|
||||
previewData?.new_plan
|
||||
? previewData.new_plan.duration === 'ANNUAL'
|
||||
: billingCycle === 'yearly'
|
||||
)
|
||||
|
||||
const displayPrice = computed(() => {
|
||||
if (teamPlan) return teamPlan.discountedUsd
|
||||
if (previewData?.new_plan) {
|
||||
return (previewData.new_plan.price_cents / 100).toFixed(0)
|
||||
const cents = previewData.new_plan.price_cents
|
||||
return ((isYearly.value ? cents / 12 : cents) / 100).toFixed(0)
|
||||
}
|
||||
return tierKey ? getTierPrice(tierKey, billingCycle === 'yearly') : 0
|
||||
return tierKey ? getTierPrice(tierKey, isYearly.value) : 0
|
||||
})
|
||||
|
||||
const displayCredits = computed(() =>
|
||||
n(teamPlan ? teamPlan.credits : tierKey ? (getTierCredits(tierKey) ?? 0) : 0)
|
||||
const annualTotalUsd = computed(() => {
|
||||
if (teamPlan) return teamPlan.discountedUsd * 12
|
||||
if (previewData?.new_plan) return previewData.new_plan.price_cents / 100
|
||||
return tierKey ? getTierPrice(tierKey, true) * 12 : 0
|
||||
})
|
||||
|
||||
const annualTotalFormatted = computed(() => `$${n(annualTotalUsd.value)}`)
|
||||
|
||||
const monthlyCredits = computed(() =>
|
||||
teamPlan ? teamPlan.credits : tierKey ? (getTierCredits(tierKey) ?? 0) : 0
|
||||
)
|
||||
|
||||
const refillCredits = computed(() =>
|
||||
n(isYearly.value ? monthlyCredits.value * 12 : monthlyCredits.value)
|
||||
)
|
||||
|
||||
const creditsRefillLabelKey = computed(() =>
|
||||
isYearly.value
|
||||
? 'subscription.preview.eachYearCreditsRefill'
|
||||
: 'subscription.preview.eachMonthCreditsRefill'
|
||||
)
|
||||
|
||||
const teamPerks = computed(() => [
|
||||
@@ -235,16 +256,18 @@ const hasCustomLoRAs = computed(() =>
|
||||
const maxDuration = computed(() => t(`subscription.maxDuration.${tierKey}`))
|
||||
|
||||
const totalDueToday = computed(() => {
|
||||
if (teamPlan) return teamPlan.discountedUsd.toFixed(2)
|
||||
if (teamPlan) {
|
||||
const total = isYearly.value
|
||||
? teamPlan.discountedUsd * 12
|
||||
: teamPlan.discountedUsd
|
||||
return total.toFixed(2)
|
||||
}
|
||||
if (previewData) {
|
||||
return (previewData.cost_today_cents / 100).toFixed(2)
|
||||
}
|
||||
if (!tierKey) return '0.00'
|
||||
const priceValue = getTierPrice(tierKey, billingCycle === 'yearly')
|
||||
if (billingCycle === 'yearly') {
|
||||
return (priceValue * 12).toFixed(2)
|
||||
}
|
||||
return priceValue.toFixed(2)
|
||||
const priceValue = getTierPrice(tierKey, isYearly.value)
|
||||
return (isYearly.value ? priceValue * 12 : priceValue).toFixed(2)
|
||||
})
|
||||
|
||||
const nextPaymentDate = computed(() => {
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
<SubscriptionAddPaymentPreviewWorkspace
|
||||
v-else-if="checkoutStep === 'preview' && selectedTeamStop"
|
||||
:team-plan="selectedTeamStop"
|
||||
:billing-cycle="selectedBillingCycle"
|
||||
:is-loading="isSubscribing || isPolling"
|
||||
@add-credit-card="handleTeamSubscribe"
|
||||
@back="handleBackToPricing"
|
||||
|
||||
Reference in New Issue
Block a user