Skip to content

Commit cd13d00

Browse files
committed
feat(clerk-js,types): Make payment method optional for free trials
It's flag-based.
1 parent 2a815eb commit cd13d00

File tree

5 files changed

+59
-6
lines changed

5 files changed

+59
-6
lines changed

.changeset/bumpy-glasses-eat.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@clerk/clerk-js': minor
3+
'@clerk/types': minor
4+
---
5+
6+
Remove the requirement to add a payment method when subscribing to free trials, depending on a flag

packages/clerk-js/sandbox/template.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@
358358
<script
359359
type="text/javascript"
360360
src="/<%= htmlRspackPlugin.files.js[0] %>"
361-
data-clerk-publishable-key="pk_test_dG91Y2hlZC1sYWR5YmlyZC0yMy5jbGVyay5hY2NvdW50cy5kZXYk"
361+
data-clerk-publishable-key="pk_test_ZXZpZGVudC1lbGstMTQuY2xlcmsuYWNjb3VudHMubGNsY2xlcmsuY29tJA"
362362
></script>
363363
<script
364364
type="text/javascript"

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

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

3132
this.billing.stripePublishableKey = data.billing.stripe_publishable_key || '';
33+
this.billing.freeTrialRequiresPaymentMethod = data.billing.free_trial_requires_payment_method ?? true;
3234
this.billing.organization.enabled = data.billing.organization.enabled || false;
3335
this.billing.organization.hasPaidPlans = data.billing.organization.has_paid_plans || false;
3436
this.billing.user.enabled = data.billing.user.enabled || false;
@@ -41,6 +43,7 @@ export class CommerceSettings extends BaseResource implements CommerceSettingsRe
4143
return {
4244
billing: {
4345
stripe_publishable_key: this.billing.stripePublishableKey,
46+
free_trial_requires_payment_method: this.billing.freeTrialRequiresPaymentMethod,
4447
organization: {
4548
enabled: this.billing.organization.enabled,
4649
has_paid_plans: this.billing.organization.hasPaidPlans,

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

Lines changed: 42 additions & 5 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, usePaymentMethods } from '../../contexts';
15+
import { useCheckoutContext, useEnvironment, usePaymentMethods } from '../../contexts';
1616
import { Box, Button, Col, descriptors, Flex, Form, localizationKeys, Spinner, Text } from '../../customizables';
1717
import { ChevronUpDown, InformationCircle } from '../../icons';
1818
import * as AddPaymentMethod from '../PaymentMethods/AddPaymentMethod';
@@ -180,10 +180,13 @@ const useCheckoutMutations = () => {
180180
useTestCard: true,
181181
});
182182

183+
const subscribeWithoutPaymentMethod = () => confirmCheckout({});
184+
183185
return {
184186
payWithExistingPaymentMethod,
185187
addPaymentMethodAndPay,
186188
payWithTestCard,
189+
subscribeWithoutPaymentMethod,
187190
};
188191
};
189192

@@ -214,12 +217,19 @@ const CheckoutFormElementsInternal = () => {
214217
const { checkout } = useCheckout();
215218
const { id, totals, isImmediatePlanChange, freeTrialEndsAt } = checkout;
216219
const { data: paymentMethods } = usePaymentMethods();
220+
const environment = useEnvironment();
217221

218222
const [paymentMethodSource, setPaymentMethodSource] = useState<PaymentMethodSource>(() =>
219223
paymentMethods.length > 0 || __BUILD_DISABLE_RHC__ ? 'existing' : 'new',
220224
);
221225

222-
const showPaymentMethods = isImmediatePlanChange && (totals.totalDueNow.amount > 0 || !!freeTrialEndsAt);
226+
// Check if payment methods should be shown based on:
227+
// 1. Immediate plan change (not a downgrade)
228+
// 2. Either there's an amount due now OR it's a free trial that requires payment method
229+
const showPaymentMethods =
230+
isImmediatePlanChange &&
231+
(totals.totalDueNow.amount > 0 ||
232+
(!!freeTrialEndsAt && environment.commerceSettings.billing.freeTrialRequiresPaymentMethod));
223233

224234
if (!id) {
225235
return null;
@@ -254,14 +264,19 @@ const CheckoutFormElementsInternal = () => {
254264
</>
255265
)}
256266

257-
{paymentMethodSource === 'existing' && (
267+
{showPaymentMethods && paymentMethodSource === 'existing' && (
258268
<ExistingPaymentMethodForm
259269
paymentMethods={paymentMethods}
260270
totalDueNow={totals.totalDueNow}
261271
/>
262272
)}
263273

264-
{__BUILD_DISABLE_RHC__ ? null : paymentMethodSource === 'new' && <AddPaymentMethodForCheckout />}
274+
{__BUILD_DISABLE_RHC__
275+
? null
276+
: showPaymentMethods && paymentMethodSource === 'new' && <AddPaymentMethodForCheckout />}
277+
278+
{/* Show standalone subscribe button when payment methods are not needed */}
279+
{!showPaymentMethods && isImmediatePlanChange && <StandaloneSubscribeButton />}
265280
</Col>
266281
);
267282
};
@@ -374,6 +389,7 @@ const ExistingPaymentMethodForm = withCardStateProvider(
374389
const submitLabel = useSubmitLabel();
375390
const { checkout } = useCheckout();
376391
const { paymentMethod, isImmediatePlanChange, freeTrialEndsAt } = checkout;
392+
const environment = useEnvironment();
377393

378394
const { payWithExistingPaymentMethod } = useCheckoutMutations();
379395
const card = useCardState();
@@ -395,7 +411,10 @@ const ExistingPaymentMethodForm = withCardStateProvider(
395411
});
396412
}, [paymentMethods]);
397413

398-
const showPaymentMethods = isImmediatePlanChange && (totalDueNow.amount > 0 || !!freeTrialEndsAt);
414+
const showPaymentMethods =
415+
isImmediatePlanChange &&
416+
(totalDueNow.amount > 0 ||
417+
(!!freeTrialEndsAt && environment.commerceSettings.billing.freeTrialRequiresPaymentMethod));
399418

400419
return (
401420
<Form
@@ -462,3 +481,21 @@ const ExistingPaymentMethodForm = withCardStateProvider(
462481
);
463482
},
464483
);
484+
485+
const StandaloneSubscribeButton = withCardStateProvider(() => {
486+
const { subscribeWithoutPaymentMethod } = useCheckoutMutations();
487+
const submitLabel = useSubmitLabel();
488+
const card = useCardState();
489+
490+
return (
491+
<Button
492+
elementDescriptor={descriptors.formButtonPrimary}
493+
onClick={subscribeWithoutPaymentMethod}
494+
sx={{
495+
width: '100%',
496+
}}
497+
isLoading={card.isLoading}
498+
localizationKey={submitLabel}
499+
/>
500+
);
501+
});

packages/types/src/commerceSettings.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ 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;
89
organization: {
910
enabled: boolean;
1011
has_paid_plans: boolean;
@@ -19,6 +20,12 @@ export interface CommerceSettingsJSON extends ClerkResourceJSON {
1920
export interface CommerceSettingsResource extends ClerkResource {
2021
billing: {
2122
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;
2229
organization: {
2330
enabled: boolean;
2431
hasPaidPlans: boolean;

0 commit comments

Comments
 (0)