Skip to content

Commit 7168243

Browse files
authored
fix(clerk-js): Update checkout to list available methods at trial checkout (#6608)
1 parent 44a191f commit 7168243

File tree

3 files changed

+145
-4
lines changed

3 files changed

+145
-4
lines changed

.changeset/tall-worms-pull.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
---
4+
5+
Allow end users to select payment methods during trial checkout.

packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ const useCheckoutMutations = () => {
192192

193193
const CheckoutFormElements = () => {
194194
const { checkout } = useCheckout();
195-
const { id, totals } = checkout;
195+
const { id, totals, freeTrialEndsAt } = checkout;
196196
const { data: paymentSources } = usePaymentMethods();
197197

198198
const [paymentMethodSource, setPaymentMethodSource] = useState<PaymentMethodSource>(() =>
@@ -210,7 +210,7 @@ const CheckoutFormElements = () => {
210210
sx={t => ({ padding: t.space.$4 })}
211211
>
212212
{/* only show if there are payment sources and there is a total due now */}
213-
{paymentSources.length > 0 && totals.totalDueNow.amount > 0 && (
213+
{paymentSources.length > 0 && (totals.totalDueNow.amount > 0 || !!freeTrialEndsAt) && (
214214
<SegmentedControl.Root
215215
aria-label='Payment method source'
216216
value={paymentMethodSource}
@@ -370,7 +370,7 @@ const ExistingPaymentSourceForm = withCardStateProvider(
370370
});
371371
}, [paymentSources]);
372372

373-
const isSchedulePayment = totalDueNow.amount > 0 && !freeTrialEndsAt;
373+
const shouldDefaultBeUsed = totalDueNow.amount === 0 || !freeTrialEndsAt;
374374

375375
return (
376376
<Form
@@ -381,7 +381,7 @@ const ExistingPaymentSourceForm = withCardStateProvider(
381381
rowGap: t.space.$4,
382382
})}
383383
>
384-
{isSchedulePayment ? (
384+
{shouldDefaultBeUsed ? (
385385
<Select
386386
elementId='paymentSource'
387387
options={options}

packages/clerk-js/src/ui/components/Checkout/__tests__/Checkout.test.tsx

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,4 +465,140 @@ describe('Checkout', () => {
465465
expect(getByText('August 19, 2025')).toBeVisible();
466466
});
467467
});
468+
469+
it('renders existing payment sources during checkout confirmation', async () => {
470+
const { wrapper, fixtures } = await createFixtures(f => {
471+
f.withUser({ email_addresses: ['[email protected]'] });
472+
});
473+
474+
fixtures.clerk.user?.getPaymentSources.mockResolvedValue({
475+
data: [
476+
{
477+
id: 'pm_test_visa',
478+
last4: '4242',
479+
paymentMethod: 'card',
480+
cardType: 'visa',
481+
isDefault: true,
482+
isRemovable: true,
483+
status: 'active',
484+
walletType: undefined,
485+
remove: jest.fn(),
486+
makeDefault: jest.fn(),
487+
pathRoot: '/',
488+
reload: jest.fn(),
489+
},
490+
{
491+
id: 'pm_test_mastercard',
492+
last4: '5555',
493+
paymentMethod: 'card',
494+
cardType: 'mastercard',
495+
isDefault: false,
496+
isRemovable: true,
497+
status: 'active',
498+
walletType: undefined,
499+
remove: jest.fn(),
500+
makeDefault: jest.fn(),
501+
pathRoot: '/',
502+
reload: jest.fn(),
503+
},
504+
],
505+
total_count: 2,
506+
});
507+
508+
fixtures.clerk.billing.startCheckout.mockResolvedValue({
509+
id: 'chk_trial_2',
510+
status: 'needs_confirmation',
511+
externalClientSecret: 'cs_test_trial_2',
512+
externalGatewayId: 'gw_test',
513+
totals: {
514+
subtotal: { amount: 1000, amountFormatted: '10.00', currency: 'USD', currencySymbol: '$' },
515+
grandTotal: { amount: 1000, amountFormatted: '10.00', currency: 'USD', currencySymbol: '$' },
516+
taxTotal: { amount: 0, amountFormatted: '0.00', currency: 'USD', currencySymbol: '$' },
517+
credit: { amount: 0, amountFormatted: '0.00', currency: 'USD', currencySymbol: '$' },
518+
pastDue: { amount: 0, amountFormatted: '0.00', currency: 'USD', currencySymbol: '$' },
519+
totalDueNow: { amount: 0, amountFormatted: '0.00', currency: 'USD', currencySymbol: '$' },
520+
},
521+
isImmediatePlanChange: true,
522+
planPeriod: 'month',
523+
plan: {
524+
id: 'plan_trial',
525+
name: 'Pro',
526+
description: 'Pro plan',
527+
features: [],
528+
fee: {
529+
amount: 1000,
530+
amountFormatted: '10.00',
531+
currency: 'USD',
532+
currencySymbol: '$',
533+
},
534+
annualFee: {
535+
amount: 12000,
536+
amountFormatted: '120.00',
537+
currency: 'USD',
538+
currencySymbol: '$',
539+
},
540+
annualMonthlyFee: {
541+
amount: 1000,
542+
amountFormatted: '10.00',
543+
currency: 'USD',
544+
currencySymbol: '$',
545+
},
546+
slug: 'pro',
547+
avatarUrl: '',
548+
publiclyVisible: true,
549+
isDefault: true,
550+
isRecurring: true,
551+
hasBaseFee: false,
552+
forPayerType: 'user',
553+
freeTrialDays: 7,
554+
freeTrialEnabled: true,
555+
},
556+
paymentSource: undefined,
557+
confirm: jest.fn(),
558+
freeTrialEndsAt: new Date('2025-08-19'),
559+
} as any);
560+
561+
const { baseElement, getByText, getByRole, userEvent } = render(
562+
<Drawer.Root
563+
open
564+
onOpenChange={() => {}}
565+
>
566+
<Checkout
567+
planId='plan_with_payment_sources'
568+
planPeriod='month'
569+
/>
570+
</Drawer.Root>,
571+
{ wrapper },
572+
);
573+
574+
await waitFor(async () => {
575+
// Verify checkout title is displayed
576+
expect(getByRole('heading', { name: 'Checkout' })).toBeVisible();
577+
578+
// Verify segmented control for payment method source is rendered
579+
const paymentMethodsButton = getByText('Payment Methods');
580+
expect(paymentMethodsButton).toBeVisible();
581+
582+
const addPaymentMethodButton = getByText('Add payment method');
583+
expect(addPaymentMethodButton).toBeVisible();
584+
585+
await userEvent.click(paymentMethodsButton);
586+
});
587+
588+
await waitFor(() => {
589+
const visaPaymentSource = getByText('visa');
590+
expect(visaPaymentSource).toBeVisible();
591+
592+
const last4Digits = getByText('⋯ 4242');
593+
expect(last4Digits).toBeVisible();
594+
595+
// Verify the default badge is shown for the first payment source
596+
const defaultBadge = getByText('Default');
597+
expect(defaultBadge).toBeVisible();
598+
599+
// Verify the hidden input contains the correct payment source id
600+
const hiddenInput = baseElement.querySelector('input[name="payment_source_id"]');
601+
expect(hiddenInput).toHaveAttribute('value', 'pm_test_visa');
602+
});
603+
});
468604
});

0 commit comments

Comments
 (0)