Skip to content

Commit f122be2

Browse files
committed
refactor: use needs_payment_method from FAPI
1 parent a2b55d9 commit f122be2

File tree

9 files changed

+30
-68
lines changed

9 files changed

+30
-68
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export class BillingCheckout extends BaseResource implements BillingCheckoutReso
2929
totals!: BillingCheckoutTotals;
3030
isImmediatePlanChange!: boolean;
3131
freeTrialEndsAt!: Date | null;
32+
needsPaymentMethod!: boolean;
3233
payer!: BillingPayerResource;
3334

3435
constructor(data: BillingCheckoutJSON) {
@@ -52,6 +53,7 @@ export class BillingCheckout extends BaseResource implements BillingCheckoutReso
5253
this.totals = billingTotalsFromJSON(data.totals);
5354
this.isImmediatePlanChange = data.is_immediate_plan_change;
5455
this.freeTrialEndsAt = data.free_trial_ends_at ? unixEpochToDate(data.free_trial_ends_at) : null;
56+
this.needsPaymentMethod = data.needs_payment_method;
5557
this.payer = new BillingPayer(data.payer);
5658
return this;
5759
}

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: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -362,24 +362,11 @@ const createOrganizationSettingsFixtureHelpers = (environment: EnvironmentJSON)
362362

363363
const createBillingSettingsFixtureHelpers = (environment: EnvironmentJSON) => {
364364
const os = environment.commerce_settings.billing;
365-
const withBilling = ({
366-
userEnabled = true,
367-
userHasPaidPlans = true,
368-
organizationEnabled = true,
369-
organizationHasPaidPlans = true,
370-
freeTrialRequiresPaymentMethod = true,
371-
}: {
372-
userEnabled?: boolean;
373-
userHasPaidPlans?: boolean;
374-
organizationEnabled?: boolean;
375-
organizationHasPaidPlans?: boolean;
376-
freeTrialRequiresPaymentMethod?: boolean;
377-
} = {}) => {
378-
os.user.enabled = userEnabled;
379-
os.user.has_paid_plans = userHasPaidPlans;
380-
os.organization.enabled = organizationEnabled;
381-
os.organization.has_paid_plans = organizationHasPaidPlans;
382-
os.free_trial_requires_payment_method = freeTrialRequiresPaymentMethod;
365+
const withBilling = () => {
366+
os.user.enabled = true;
367+
os.user.has_paid_plans = true;
368+
os.organization.enabled = true;
369+
os.organization.has_paid_plans = true;
383370
};
384371

385372
return { withBilling };

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

Lines changed: 4 additions & 7 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();
@@ -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: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { __experimental_useCheckout as useCheckout } from '@clerk/shared/react';
2-
import type { BillingMoneyAmount, BillingPaymentMethodResource, ConfirmCheckoutParams } from '@clerk/types';
2+
import type { BillingPaymentMethodResource, ConfirmCheckoutParams } from '@clerk/types';
33
import { useMemo, useState } from 'react';
44

55
import { Card } from '@/ui/elements/Card';
@@ -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,18 +219,13 @@ const CheckoutFormElements = () => {
219219

220220
const CheckoutFormElementsInternal = () => {
221221
const { checkout } = useCheckout();
222-
const { id, totals, isImmediatePlanChange, freeTrialEndsAt } = checkout;
222+
const { id, 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);
233-
234229
if (!id) {
235230
return null;
236231
}
@@ -243,7 +238,7 @@ const CheckoutFormElementsInternal = () => {
243238
>
244239
{__BUILD_DISABLE_RHC__ ? null : (
245240
<>
246-
{paymentMethods.length > 0 && showTabs && needsPaymentMethod && (
241+
{paymentMethods.length > 0 && needsPaymentMethod && (
247242
<SegmentedControl.Root
248243
aria-label='Payment method source'
249244
value={paymentMethodSource}
@@ -265,14 +260,7 @@ const CheckoutFormElementsInternal = () => {
265260
)}
266261

267262
{paymentMethodSource === 'existing' &&
268-
(needsPaymentMethod ? (
269-
<ExistingPaymentMethodForm
270-
paymentMethods={paymentMethods}
271-
totalDueNow={totals.totalDueNow}
272-
/>
273-
) : (
274-
<FreeTrialButton />
275-
))}
263+
(needsPaymentMethod ? <ExistingPaymentMethodForm paymentMethods={paymentMethods} /> : <FreeTrialButton />)}
276264

277265
{__BUILD_DISABLE_RHC__ ? null : paymentMethodSource === 'new' && <AddPaymentMethodForCheckout />}
278266
</Col>
@@ -419,16 +407,9 @@ const formProps: ThemableCssProp = t => ({
419407
});
420408

421409
const ExistingPaymentMethodForm = withCardStateProvider(
422-
({
423-
totalDueNow,
424-
paymentMethods,
425-
}: {
426-
totalDueNow: BillingMoneyAmount;
427-
paymentMethods: BillingPaymentMethodResource[];
428-
}) => {
410+
({ paymentMethods }: { paymentMethods: BillingPaymentMethodResource[] }) => {
429411
const { checkout } = useCheckout();
430-
const { paymentMethod, isImmediatePlanChange, freeTrialEndsAt } = checkout;
431-
const environment = useEnvironment();
412+
const { paymentMethod, needsPaymentMethod } = checkout;
432413

433414
const { payWithExistingPaymentMethod } = useCheckoutMutations();
434415
const card = useCardState();
@@ -450,10 +431,7 @@ const ExistingPaymentMethodForm = withCardStateProvider(
450431
});
451432
}, [paymentMethods]);
452433

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

458436
return (
459437
<Form

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,7 @@ describe('Checkout', () => {
10131013
paymentMethod: undefined,
10141014
confirm: vi.fn(),
10151015
freeTrialEndsAt: new Date('2025-08-19'),
1016+
needsPaymentMethod: false,
10161017
} as any);
10171018

10181019
const { getByText, getByRole, userEvent } = render(
@@ -1043,7 +1044,7 @@ describe('Checkout', () => {
10431044
it('prompts for adding payment method for free trial if none exists and requires payment method', async () => {
10441045
const { wrapper, fixtures } = await createFixtures(f => {
10451046
f.withUser({ email_addresses: ['[email protected]'] });
1046-
f.withBilling({ freeTrialRequiresPaymentMethod: true });
1047+
f.withBilling();
10471048
});
10481049

10491050
fixtures.clerk.user?.getPaymentMethods.mockResolvedValue({
@@ -1102,6 +1103,7 @@ describe('Checkout', () => {
11021103
paymentMethod: undefined,
11031104
confirm: vi.fn(),
11041105
freeTrialEndsAt: new Date('2025-08-19'),
1106+
needsPaymentMethod: false,
11051107
} as any);
11061108

11071109
const { queryByText, getByRole } = render(
@@ -1133,7 +1135,6 @@ describe('Checkout', () => {
11331135
it('does not prompt payment methods for free trial when not required', async () => {
11341136
const { wrapper, fixtures } = await createFixtures(f => {
11351137
f.withUser({ email_addresses: ['[email protected]'] });
1136-
f.withBilling({ freeTrialRequiresPaymentMethod: false });
11371138
});
11381139

11391140
fixtures.clerk.user?.getPaymentMethods.mockResolvedValue({
@@ -1192,6 +1193,7 @@ describe('Checkout', () => {
11921193
paymentMethod: undefined,
11931194
confirm: vi.fn(),
11941195
freeTrialEndsAt: new Date('2025-08-19'),
1196+
needsPaymentMethod: false,
11951197
} as any);
11961198

11971199
const { queryByText, getByRole, baseElement } = render(
@@ -1229,7 +1231,6 @@ describe('Checkout', () => {
12291231
it('does not prompt payment methods for free trial when not required, even with stored payment methods', async () => {
12301232
const { wrapper, fixtures } = await createFixtures(f => {
12311233
f.withUser({ email_addresses: ['[email protected]'] });
1232-
f.withBilling({ freeTrialRequiresPaymentMethod: false });
12331234
});
12341235

12351236
fixtures.clerk.user?.getPaymentMethods.mockResolvedValue({
@@ -1296,6 +1297,7 @@ describe('Checkout', () => {
12961297
paymentMethod: undefined,
12971298
confirm: vi.fn(),
12981299
freeTrialEndsAt: new Date('2025-08-19'),
1300+
needsPaymentMethod: false,
12991301
} as any);
13001302

13011303
const { queryByText, getByRole, baseElement } = render(

packages/types/src/billing.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,11 @@ export interface BillingCheckoutResource extends ClerkResource {
779779
* Unix timestamp (milliseconds) of when the free trial ends.
780780
*/
781781
freeTrialEndsAt: Date | null;
782+
/**
783+
* Whether a payment method is required for this checkout.
784+
* This determines if the payment method form should be shown to the user.
785+
*/
786+
needsPaymentMethod: boolean;
782787
/**
783788
* The payer associated with the checkout.
784789
*/

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
@@ -815,6 +815,7 @@ export interface BillingCheckoutJSON extends ClerkResourceJSON {
815815
is_immediate_plan_change: boolean;
816816
// TODO(@COMMERCE): Remove optional after GA.
817817
free_trial_ends_at: number | null;
818+
needs_payment_method: boolean;
818819
payer: BillingPayerJSON;
819820
}
820821

0 commit comments

Comments
 (0)