From 2c890314c1622f4104231e38bac5f575d34f9e75 Mon Sep 17 00:00:00 2001 From: yusufipk Date: Tue, 8 Sep 2026 15:17:19 +0300 Subject: [PATCH] fix(billing): show paid cancellation dates instead of leftover trial --- .../settings/settings-page-client.tsx | 8 +-- tests/component/settings-billing.test.tsx | 72 +++++++++++++++++++ 2 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 tests/component/settings-billing.test.tsx diff --git a/app/(dashboard)/settings/settings-page-client.tsx b/app/(dashboard)/settings/settings-page-client.tsx index c3a6383..149ae91 100644 --- a/app/(dashboard)/settings/settings-page-client.tsx +++ b/app/(dashboard)/settings/settings-page-client.tsx @@ -492,7 +492,7 @@ export default function SettingsPage({ billingOnly = false }: { billingOnly?: bo

{billing.subscription.hasActiveSubscription ? hasScheduledCancellation - ? billing.subscription.hasActiveTrial + ? billing.subscription.status === 'TRIALING' ? 'Trial canceled. Access remains active until the trial ends.' : 'Subscription canceled. Access remains active until the end of the current billing period.' : 'Paid account with workspace creation unlocked.' @@ -512,11 +512,11 @@ export default function SettingsPage({ billingOnly = false }: { billingOnly?: bo !billing.subscription.hasActiveSubscription ? (

Your latest payment didn't go through. Update your payment method to keep - your subscription — starting a new one would create a duplicate. + your subscription. Starting a new one would create a duplicate.

) : null} - {billing.subscription.hasActiveTrial && + {billing.subscription.status === 'TRIALING' && billing.subscription.trialEndsAt && hasScheduledCancellation ? (

@@ -535,7 +535,7 @@ export default function SettingsPage({ billingOnly = false }: { billingOnly?: bo {hasScheduledCancellation && billing.subscription.cancelAt ? (

- Cancellation was scheduled on{' '} + Cancellation takes effect on{' '} {new Date(billing.subscription.cancelAt).toLocaleDateString()}.

) : null} diff --git a/tests/component/settings-billing.test.tsx b/tests/component/settings-billing.test.tsx new file mode 100644 index 0000000..4f1c4a9 --- /dev/null +++ b/tests/component/settings-billing.test.tsx @@ -0,0 +1,72 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import SettingsPage from '@/app/(dashboard)/settings/settings-page-client'; + +function renderScheduledCancellation(status: 'ACTIVE' | 'TRIALING') { + vi.stubGlobal( + 'fetch', + vi.fn(async (url: string) => { + if (url !== '/api/billing') return { ok: false }; + return { + ok: true, + json: async () => ({ + data: { + isEnabled: true, + isConfigured: true, + checkoutAvailable: false, + portalAvailable: true, + cancelAvailable: false, + needsPaymentFix: false, + openInvoice: null, + workspaceCreation: { canCreateWorkspace: true, canStartTrial: false }, + subscription: { + status, + label: status === 'ACTIVE' ? 'Active' : 'Trialing', + hasActiveSubscription: true, + hasRecoverableSubscription: true, + hasActiveTrial: true, + hasBillingAccess: true, + currentPeriodEnd: '2026-10-08T12:00:00Z', + trialEndsAt: '2026-09-15T12:00:00Z', + cancelAtPeriodEnd: true, + cancelAt: '2026-10-08T12:00:00Z', + }, + }, + }), + }; + }) + ); + render(); +} + +afterEach(() => vi.unstubAllGlobals()); + +describe('scheduled cancellation in billing settings', () => { + it('keeps a paid subscription distinct from its remaining cardless trial', async () => { + renderScheduledCancellation('ACTIVE'); + + expect( + await screen.findByText( + 'Subscription canceled. Access remains active until the end of the current billing period.' + ) + ).toBeInTheDocument(); + expect(screen.queryByText(/Trial canceled/)).not.toBeInTheDocument(); + expect(screen.queryByText(/Access ends on/)).not.toBeInTheDocument(); + expect(screen.getByText(/Your subscription ends on/)).toHaveTextContent( + new Date('2026-10-08T12:00:00Z').toLocaleDateString() + ); + expect(screen.getByText(/Cancellation takes effect on/)).toBeInTheDocument(); + expect(screen.queryByText(/Cancellation was scheduled on/)).not.toBeInTheDocument(); + }); + + it('still explains the trial end for a Stripe trial subscription', async () => { + renderScheduledCancellation('TRIALING'); + + expect( + await screen.findByText('Trial canceled. Access remains active until the trial ends.') + ).toBeInTheDocument(); + expect(screen.getByText(/Access ends on/)).toHaveTextContent( + new Date('2026-09-15T12:00:00Z').toLocaleDateString() + ); + }); +});