Skip to content

Commit a75c973

Browse files
committed
feat: use needs_payment_method field from FAPI
1 parent a2b55d9 commit a75c973

File tree

10 files changed

+42
-56
lines changed

10 files changed

+42
-56
lines changed

packages/clerk-js/src/core/modules/checkout/__tests__/manager.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const createMockCheckoutResource = (overrides: Partial<BillingCheckoutResource>
2121
isImmediatePlanChange: false,
2222
planPeriod: 'month',
2323
freeTrialEndsAt: null,
24+
needsPaymentMethod: true,
2425
payer: {
2526
id: 'payer_123',
2627
createdAt: new Date('2025-01-01'),

packages/clerk-js/src/core/resources/CommerceSettings.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { BaseResource } from './internal';
88
export class CommerceSettings extends BaseResource implements CommerceSettingsResource {
99
billing: CommerceSettingsResource['billing'] = {
1010
stripePublishableKey: '',
11-
freeTrialRequiresPaymentMethod: true,
1211
organization: {
1312
enabled: false,
1413
hasPaidPlans: false,
@@ -30,7 +29,6 @@ export class CommerceSettings extends BaseResource implements CommerceSettingsRe
3029
}
3130

3231
this.billing.stripePublishableKey = data.billing.stripe_publishable_key || '';
33-
this.billing.freeTrialRequiresPaymentMethod = data.billing.free_trial_requires_payment_method ?? true;
3432
this.billing.organization.enabled = data.billing.organization.enabled || false;
3533
this.billing.organization.hasPaidPlans = data.billing.organization.has_paid_plans || false;
3634
this.billing.user.enabled = data.billing.user.enabled || false;
@@ -43,7 +41,6 @@ export class CommerceSettings extends BaseResource implements CommerceSettingsRe
4341
return {
4442
billing: {
4543
stripe_publishable_key: this.billing.stripePublishableKey,
46-
free_trial_requires_payment_method: this.billing.freeTrialRequiresPaymentMethod,
4744
organization: {
4845
enabled: this.billing.organization.enabled,
4946
has_paid_plans: this.billing.organization.hasPaidPlans,

packages/clerk-js/src/test/fixture-helpers.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -367,19 +367,16 @@ const createBillingSettingsFixtureHelpers = (environment: EnvironmentJSON) => {
367367
userHasPaidPlans = true,
368368
organizationEnabled = true,
369369
organizationHasPaidPlans = true,
370-
freeTrialRequiresPaymentMethod = true,
371370
}: {
372371
userEnabled?: boolean;
373372
userHasPaidPlans?: boolean;
374373
organizationEnabled?: boolean;
375374
organizationHasPaidPlans?: boolean;
376-
freeTrialRequiresPaymentMethod?: boolean;
377375
} = {}) => {
378376
os.user.enabled = userEnabled;
379377
os.user.has_paid_plans = userHasPaidPlans;
380378
os.organization.enabled = organizationEnabled;
381379
os.organization.has_paid_plans = organizationHasPaidPlans;
382-
os.free_trial_requires_payment_method = freeTrialRequiresPaymentMethod;
383380
};
384381

385382
return { withBilling };

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

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Drawer, useDrawerContext } from '@/ui/elements/Drawer';
55
import { LineItems } from '@/ui/elements/LineItems';
66
import { formatDate } from '@/ui/utils/formatDate';
77

8-
import { useCheckoutContext, useEnvironment } from '../../contexts';
8+
import { useCheckoutContext } from '../../contexts';
99
import { Box, Button, descriptors, Heading, localizationKeys, Span, Text, useAppearance } from '../../customizables';
1010
import { transitionDurationValues, transitionTiming } from '../../foundations/transitions';
1111
import { usePrefersReducedMotion } from '../../hooks';
@@ -161,8 +161,7 @@ export const CheckoutComplete = () => {
161161
const { setIsOpen } = useDrawerContext();
162162
const { newSubscriptionRedirectUrl } = useCheckoutContext();
163163
const { checkout } = useCheckout();
164-
const { totals, paymentMethod, planPeriodStart, freeTrialEndsAt } = checkout;
165-
const environment = useEnvironment();
164+
const { totals, paymentMethod, planPeriodStart, freeTrialEndsAt, needsPaymentMethod } = checkout;
166165
const [mousePosition, setMousePosition] = useState({ x: 256, y: 256 });
167166

168167
const prefersReducedMotion = usePrefersReducedMotion();
@@ -332,7 +331,7 @@ export const CheckoutComplete = () => {
332331
localizationKey={
333332
freeTrialEndsAt
334333
? localizationKeys('billing.checkout.title__trialSuccess')
335-
: totals.totalDueNow.amount > 0
334+
: needsPaymentMethod
336335
? localizationKeys('billing.checkout.title__paymentSuccessful')
337336
: localizationKeys('billing.checkout.title__subscriptionSuccessful')
338337
}
@@ -387,7 +386,7 @@ export const CheckoutComplete = () => {
387386
}),
388387
})}
389388
localizationKey={
390-
totals.totalDueNow.amount > 0
389+
needsPaymentMethod
391390
? localizationKeys('billing.checkout.description__paymentSuccessful')
392391
: localizationKeys('billing.checkout.description__subscriptionSuccessful')
393392
}
@@ -431,16 +430,14 @@ export const CheckoutComplete = () => {
431430
<LineItems.Group variant='secondary'>
432431
<LineItems.Title
433432
title={
434-
totals.totalDueNow.amount > 0 ||
435-
(freeTrialEndsAt !== null && environment.commerceSettings.billing.freeTrialRequiresPaymentMethod)
433+
needsPaymentMethod
436434
? localizationKeys('billing.checkout.lineItems.title__paymentMethod')
437435
: localizationKeys('billing.checkout.lineItems.title__subscriptionBegins')
438436
}
439437
/>
440438
<LineItems.Description
441439
text={
442-
totals.totalDueNow.amount > 0 ||
443-
(freeTrialEndsAt !== null && environment.commerceSettings.billing.freeTrialRequiresPaymentMethod)
440+
needsPaymentMethod
444441
? paymentMethod
445442
? paymentMethod.paymentType !== 'card'
446443
? `${capitalize(paymentMethod.paymentType)}`

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

Lines changed: 17 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { Tooltip } from '@/ui/elements/Tooltip';
1212
import { handleError } from '@/ui/utils/errorHandler';
1313

1414
import { DevOnly } from '../../common/DevOnly';
15-
import { useCheckoutContext, useEnvironment, usePaymentMethods } from '../../contexts';
15+
import { useCheckoutContext, usePaymentMethods } from '../../contexts';
1616
import { Box, Button, Col, descriptors, Flex, Form, localizationKeys, Spinner, Text } from '../../customizables';
1717
import { ChevronUpDown, InformationCircle } from '../../icons';
1818
import type { PropsOfComponent, ThemableCssProp } from '../../styledSystem';
@@ -219,17 +219,14 @@ const CheckoutFormElements = () => {
219219

220220
const CheckoutFormElementsInternal = () => {
221221
const { checkout } = useCheckout();
222-
const { id, totals, isImmediatePlanChange, freeTrialEndsAt } = checkout;
222+
const { id, totals, isImmediatePlanChange, needsPaymentMethod } = checkout;
223223
const { data: paymentMethods } = usePaymentMethods();
224-
const environment = useEnvironment();
225224

226225
const [paymentMethodSource, setPaymentMethodSource] = useState<PaymentMethodSource>(() =>
227226
paymentMethods.length > 0 || __BUILD_DISABLE_RHC__ ? 'existing' : 'new',
228227
);
229228

230-
const isFreeTrial = Boolean(freeTrialEndsAt);
231-
const showTabs = isImmediatePlanChange && (totals.totalDueNow.amount > 0 || isFreeTrial);
232-
const needsPaymentMethod = !(isFreeTrial && !environment.commerceSettings.billing.freeTrialRequiresPaymentMethod);
229+
const showTabs = isImmediatePlanChange && needsPaymentMethod;
233230

234231
if (!id) {
235232
return null;
@@ -243,7 +240,7 @@ const CheckoutFormElementsInternal = () => {
243240
>
244241
{__BUILD_DISABLE_RHC__ ? null : (
245242
<>
246-
{paymentMethods.length > 0 && showTabs && needsPaymentMethod && (
243+
{paymentMethods.length > 0 && showTabs && (
247244
<SegmentedControl.Root
248245
aria-label='Payment method source'
249246
value={paymentMethodSource}
@@ -264,17 +261,16 @@ const CheckoutFormElementsInternal = () => {
264261
</>
265262
)}
266263

267-
{paymentMethodSource === 'existing' &&
268-
(needsPaymentMethod ? (
269-
<ExistingPaymentMethodForm
270-
paymentMethods={paymentMethods}
271-
totalDueNow={totals.totalDueNow}
272-
/>
273-
) : (
274-
<FreeTrialButton />
275-
))}
276-
277-
{__BUILD_DISABLE_RHC__ ? null : paymentMethodSource === 'new' && <AddPaymentMethodForCheckout />}
264+
{!needsPaymentMethod ? (
265+
<FreeTrialButton />
266+
) : paymentMethodSource === 'existing' ? (
267+
<ExistingPaymentMethodForm
268+
paymentMethods={paymentMethods}
269+
totalDueNow={totals.totalDueNow}
270+
/>
271+
) : (
272+
!__BUILD_DISABLE_RHC__ && paymentMethodSource === 'new' && <AddPaymentMethodForCheckout />
273+
)}
278274
</Col>
279275
);
280276
};
@@ -419,16 +415,9 @@ const formProps: ThemableCssProp = t => ({
419415
});
420416

421417
const ExistingPaymentMethodForm = withCardStateProvider(
422-
({
423-
totalDueNow,
424-
paymentMethods,
425-
}: {
426-
totalDueNow: BillingMoneyAmount;
427-
paymentMethods: BillingPaymentMethodResource[];
428-
}) => {
418+
({ paymentMethods }: { totalDueNow: BillingMoneyAmount; paymentMethods: BillingPaymentMethodResource[] }) => {
429419
const { checkout } = useCheckout();
430-
const { paymentMethod, isImmediatePlanChange, freeTrialEndsAt } = checkout;
431-
const environment = useEnvironment();
420+
const { paymentMethod, isImmediatePlanChange, needsPaymentMethod } = checkout;
432421

433422
const { payWithExistingPaymentMethod } = useCheckoutMutations();
434423
const card = useCardState();
@@ -450,10 +439,7 @@ const ExistingPaymentMethodForm = withCardStateProvider(
450439
});
451440
}, [paymentMethods]);
452441

453-
const showPaymentMethods =
454-
isImmediatePlanChange &&
455-
(totalDueNow.amount > 0 ||
456-
(!!freeTrialEndsAt && environment.commerceSettings.billing.freeTrialRequiresPaymentMethod));
442+
const showPaymentMethods = isImmediatePlanChange && needsPaymentMethod;
457443

458444
return (
459445
<Form

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,7 @@ describe('Checkout', () => {
545545
planPeriodStart: new Date('2025-08-19'),
546546
confirm: vi.fn(),
547547
freeTrialEndsAt: null,
548+
needsPaymentMethod: false,
548549
} as any);
549550

550551
const { getByText } = render(
@@ -636,6 +637,7 @@ describe('Checkout', () => {
636637
},
637638
confirm: vi.fn(),
638639
freeTrialEndsAt: null,
640+
needsPaymentMethod: true,
639641
} as any);
640642

641643
const { getByText } = render(
@@ -748,6 +750,7 @@ describe('Checkout', () => {
748750
paymentMethod: undefined,
749751
confirm: vi.fn(),
750752
freeTrialEndsAt: new Date('2025-08-19'),
753+
needsPaymentMethod: true,
751754
} as any);
752755

753756
const { baseElement, getByText, getByRole, userEvent } = render(
@@ -887,6 +890,7 @@ describe('Checkout', () => {
887890
paymentMethod: undefined,
888891
confirm: vi.fn(),
889892
freeTrialEndsAt: null,
893+
needsPaymentMethod: true,
890894
} as any);
891895

892896
const { baseElement, getByText, getByRole, userEvent } = render(
@@ -1013,6 +1017,7 @@ describe('Checkout', () => {
10131017
paymentMethod: undefined,
10141018
confirm: vi.fn(),
10151019
freeTrialEndsAt: new Date('2025-08-19'),
1020+
needsPaymentMethod: true,
10161021
} as any);
10171022

10181023
const { getByText, getByRole, userEvent } = render(
@@ -1043,7 +1048,7 @@ describe('Checkout', () => {
10431048
it('prompts for adding payment method for free trial if none exists and requires payment method', async () => {
10441049
const { wrapper, fixtures } = await createFixtures(f => {
10451050
f.withUser({ email_addresses: ['[email protected]'] });
1046-
f.withBilling({ freeTrialRequiresPaymentMethod: true });
1051+
f.withBilling();
10471052
});
10481053

10491054
fixtures.clerk.user?.getPaymentMethods.mockResolvedValue({
@@ -1102,6 +1107,7 @@ describe('Checkout', () => {
11021107
paymentMethod: undefined,
11031108
confirm: vi.fn(),
11041109
freeTrialEndsAt: new Date('2025-08-19'),
1110+
needsPaymentMethod: true,
11051111
} as any);
11061112

11071113
const { queryByText, getByRole } = render(
@@ -1133,7 +1139,7 @@ describe('Checkout', () => {
11331139
it('does not prompt payment methods for free trial when not required', async () => {
11341140
const { wrapper, fixtures } = await createFixtures(f => {
11351141
f.withUser({ email_addresses: ['[email protected]'] });
1136-
f.withBilling({ freeTrialRequiresPaymentMethod: false });
1142+
f.withBilling();
11371143
});
11381144

11391145
fixtures.clerk.user?.getPaymentMethods.mockResolvedValue({
@@ -1192,6 +1198,7 @@ describe('Checkout', () => {
11921198
paymentMethod: undefined,
11931199
confirm: vi.fn(),
11941200
freeTrialEndsAt: new Date('2025-08-19'),
1201+
needsPaymentMethod: false,
11951202
} as any);
11961203

11971204
const { queryByText, getByRole, baseElement } = render(
@@ -1229,7 +1236,7 @@ describe('Checkout', () => {
12291236
it('does not prompt payment methods for free trial when not required, even with stored payment methods', async () => {
12301237
const { wrapper, fixtures } = await createFixtures(f => {
12311238
f.withUser({ email_addresses: ['[email protected]'] });
1232-
f.withBilling({ freeTrialRequiresPaymentMethod: false });
1239+
f.withBilling();
12331240
});
12341241

12351242
fixtures.clerk.user?.getPaymentMethods.mockResolvedValue({
@@ -1296,6 +1303,7 @@ describe('Checkout', () => {
12961303
paymentMethod: undefined,
12971304
confirm: vi.fn(),
12981305
freeTrialEndsAt: new Date('2025-08-19'),
1306+
needsPaymentMethod: false,
12991307
} as any);
13001308

13011309
const { queryByText, getByRole, baseElement } = render(
@@ -1427,6 +1435,7 @@ describe('Checkout', () => {
14271435
paymentMethod: undefined,
14281436
confirm: vi.fn(),
14291437
freeTrialEndsAt: null,
1438+
needsPaymentMethod: true,
14301439
} as any);
14311440

14321441
const { baseElement, queryByText, queryByRole, getByText } = render(

packages/shared/src/react/hooks/useCheckout.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export const useCheckout = (options?: Params): __experimental_UseCheckoutReturn
114114
paymentMethod: null,
115115
freeTrialEndsAt: null,
116116
payer: null,
117+
needsPaymentMethod: null,
117118
};
118119
}
119120
const {

packages/types/src/billing.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,10 @@ export interface BillingCheckoutResource extends ClerkResource {
783783
* The payer associated with the checkout.
784784
*/
785785
payer: BillingPayerResource;
786+
/**
787+
* Whether a payment method is required for this checkout.
788+
*/
789+
needsPaymentMethod: boolean;
786790
}
787791

788792
/**

packages/types/src/commerceSettings.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import type { CommerceSettingsJSONSnapshot } from './snapshots';
55
export interface CommerceSettingsJSON extends ClerkResourceJSON {
66
billing: {
77
stripe_publishable_key: string;
8-
free_trial_requires_payment_method: boolean;
98
organization: {
109
enabled: boolean;
1110
has_paid_plans: boolean;
@@ -20,12 +19,6 @@ export interface CommerceSettingsJSON extends ClerkResourceJSON {
2019
export interface CommerceSettingsResource extends ClerkResource {
2120
billing: {
2221
stripePublishableKey: string;
23-
/**
24-
* Whether payment methods are required when starting a free trial.
25-
* When false, users can start free trials without providing payment methods.
26-
* @default true
27-
*/
28-
freeTrialRequiresPaymentMethod: boolean;
2922
organization: {
3023
enabled: boolean;
3124
hasPaidPlans: boolean;

packages/types/src/json.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,7 @@ export interface BillingCheckoutJSON extends ClerkResourceJSON {
816816
// TODO(@COMMERCE): Remove optional after GA.
817817
free_trial_ends_at: number | null;
818818
payer: BillingPayerJSON;
819+
needs_payment_method: boolean;
819820
}
820821

821822
/**

0 commit comments

Comments
 (0)