From 2bbbf65321b5dfaa20aa829f7181fc4ea15c6d11 Mon Sep 17 00:00:00 2001 From: dante01yoon Date: Fri, 19 Jun 2026 18:24:43 +0900 Subject: [PATCH] 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". --- src/locales/en/main.json | 3 +- ...criptionAddPaymentPreviewWorkspace.test.ts | 95 +++++++++++++++++++ ...SubscriptionAddPaymentPreviewWorkspace.vue | 61 ++++++++---- ...bscriptionRequiredDialogContentUnified.vue | 1 + 4 files changed, 140 insertions(+), 20 deletions(-) diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 863dea1d4f..5d00d38d66 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -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", diff --git a/src/platform/workspace/components/SubscriptionAddPaymentPreviewWorkspace.test.ts b/src/platform/workspace/components/SubscriptionAddPaymentPreviewWorkspace.test.ts index 2511f653fa..0744b16789 100644 --- a/src/platform/workspace/components/SubscriptionAddPaymentPreviewWorkspace.test.ts +++ b/src/platform/workspace/components/SubscriptionAddPaymentPreviewWorkspace.test.ts @@ -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 } }, diff --git a/src/platform/workspace/components/SubscriptionAddPaymentPreviewWorkspace.vue b/src/platform/workspace/components/SubscriptionAddPaymentPreviewWorkspace.vue index 199c16b317..b2a8baf7cd 100644 --- a/src/platform/workspace/components/SubscriptionAddPaymentPreviewWorkspace.vue +++ b/src/platform/workspace/components/SubscriptionAddPaymentPreviewWorkspace.vue @@ -19,13 +19,9 @@ {{ $t('subscription.usdPerMonth') }} -
- - {{ displayCredits }} {{ $t('subscription.perMonth') }} -
+ + {{ $t('subscription.billedYearly', { total: annualTotalFormatted }) }} + {{ $t('subscription.preview.startingToday') }} @@ -35,12 +31,12 @@
- {{ $t('subscription.preview.eachMonthCreditsRefill') }} + {{ $t(creditsRefillLabelKey) }}
- {{ displayCredits }} + {{ refillCredits }}
@@ -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(() => { diff --git a/src/platform/workspace/components/SubscriptionRequiredDialogContentUnified.vue b/src/platform/workspace/components/SubscriptionRequiredDialogContentUnified.vue index c8aa0cb62e..47ef8e908d 100644 --- a/src/platform/workspace/components/SubscriptionRequiredDialogContentUnified.vue +++ b/src/platform/workspace/components/SubscriptionRequiredDialogContentUnified.vue @@ -81,6 +81,7 @@