fix(billing): show paid cancellation dates instead of leftover trial

This commit is contained in:
2026-09-08 15:17:19 +03:00
parent c0809e23bd
commit 2c890314c1
2 changed files with 76 additions and 4 deletions
@@ -492,7 +492,7 @@ export default function SettingsPage({ billingOnly = false }: { billingOnly?: bo
<p className="text-sm text-muted-foreground mt-1"> <p className="text-sm text-muted-foreground mt-1">
{billing.subscription.hasActiveSubscription {billing.subscription.hasActiveSubscription
? hasScheduledCancellation ? hasScheduledCancellation
? billing.subscription.hasActiveTrial ? billing.subscription.status === 'TRIALING'
? 'Trial canceled. Access remains active until the trial ends.' ? 'Trial canceled. Access remains active until the trial ends.'
: 'Subscription canceled. Access remains active until the end of the current billing period.' : 'Subscription canceled. Access remains active until the end of the current billing period.'
: 'Paid account with workspace creation unlocked.' : 'Paid account with workspace creation unlocked.'
@@ -512,11 +512,11 @@ export default function SettingsPage({ billingOnly = false }: { billingOnly?: bo
!billing.subscription.hasActiveSubscription ? ( !billing.subscription.hasActiveSubscription ? (
<p className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm font-medium text-destructive"> <p className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm font-medium text-destructive">
Your latest payment didn&apos;t go through. Update your payment method to keep Your latest payment didn&apos;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.
</p> </p>
) : null} ) : null}
{billing.subscription.hasActiveTrial && {billing.subscription.status === 'TRIALING' &&
billing.subscription.trialEndsAt && billing.subscription.trialEndsAt &&
hasScheduledCancellation ? ( hasScheduledCancellation ? (
<p className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm font-medium text-destructive"> <p className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm font-medium text-destructive">
@@ -535,7 +535,7 @@ export default function SettingsPage({ billingOnly = false }: { billingOnly?: bo
{hasScheduledCancellation && billing.subscription.cancelAt ? ( {hasScheduledCancellation && billing.subscription.cancelAt ? (
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Cancellation was scheduled on{' '} Cancellation takes effect on{' '}
{new Date(billing.subscription.cancelAt).toLocaleDateString()}. {new Date(billing.subscription.cancelAt).toLocaleDateString()}.
</p> </p>
) : null} ) : null}
+72
View File
@@ -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(<SettingsPage billingOnly />);
}
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()
);
});
});