From 69ee656da0639f13c0c7cd926d2424409ac9093b Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 23 Oct 2025 21:48:16 -0700 Subject: [PATCH 1/4] wip --- .../src/ui/components/Checkout/CheckoutForm.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx b/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx index 7b8e8ae9416..15b5cd3fbd0 100644 --- a/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx +++ b/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx @@ -228,6 +228,12 @@ const CheckoutFormElementsInternal = () => { (totals.totalDueNow.amount > 0 || (!!freeTrialEndsAt && environment.commerceSettings.billing.freeTrialRequiresPaymentMethod)); + // Only show the standalone free trial button when it's a free trial that does not + // require a payment method. Hiding payment methods (e.g. downgrade) should not imply + // showing the free trial button. + const showFreeTrialButton = + !showPaymentMethods && !!freeTrialEndsAt && !environment.commerceSettings.billing.freeTrialRequiresPaymentMethod; + if (!id) { return null; } @@ -261,18 +267,20 @@ const CheckoutFormElementsInternal = () => { )} - {showPaymentMethods && paymentMethodSource === 'existing' && ( + {paymentMethods.length > 0 && (paymentMethodSource === 'existing' || !showPaymentMethods) && ( )} + {/* {__BUILD_DISABLE_RHC__ ? null : paymentMethodSource === 'new' && } */} + {__BUILD_DISABLE_RHC__ ? null : showPaymentMethods && paymentMethodSource === 'new' && } - {!showPaymentMethods && } + {showFreeTrialButton && } ); }; From 7402cdd90c785e9a7e00cfe8d6516645ee94e6d9 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 23 Oct 2025 21:48:23 -0700 Subject: [PATCH 2/4] Revert "wip" This reverts commit 69ee656da0639f13c0c7cd926d2424409ac9093b. --- .../src/ui/components/Checkout/CheckoutForm.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx b/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx index 15b5cd3fbd0..7b8e8ae9416 100644 --- a/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx +++ b/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx @@ -228,12 +228,6 @@ const CheckoutFormElementsInternal = () => { (totals.totalDueNow.amount > 0 || (!!freeTrialEndsAt && environment.commerceSettings.billing.freeTrialRequiresPaymentMethod)); - // Only show the standalone free trial button when it's a free trial that does not - // require a payment method. Hiding payment methods (e.g. downgrade) should not imply - // showing the free trial button. - const showFreeTrialButton = - !showPaymentMethods && !!freeTrialEndsAt && !environment.commerceSettings.billing.freeTrialRequiresPaymentMethod; - if (!id) { return null; } @@ -267,20 +261,18 @@ const CheckoutFormElementsInternal = () => { )} - {paymentMethods.length > 0 && (paymentMethodSource === 'existing' || !showPaymentMethods) && ( + {showPaymentMethods && paymentMethodSource === 'existing' && ( )} - {/* {__BUILD_DISABLE_RHC__ ? null : paymentMethodSource === 'new' && } */} - {__BUILD_DISABLE_RHC__ ? null : showPaymentMethods && paymentMethodSource === 'new' && } - {showFreeTrialButton && } + {!showPaymentMethods && } ); }; From 3ca7cca0559e1f49d6ab5819c29312cc417326cd Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 23 Oct 2025 22:59:07 -0700 Subject: [PATCH 3/4] fix and write tests --- packages/clerk-js/src/test/fixture-helpers.ts | 23 +- .../ui/components/Checkout/CheckoutForm.tsx | 132 +++--- .../Checkout/__tests__/Checkout.test.tsx | 403 +++++++++++++++++- 3 files changed, 475 insertions(+), 83 deletions(-) diff --git a/packages/clerk-js/src/test/fixture-helpers.ts b/packages/clerk-js/src/test/fixture-helpers.ts index 143e7e6a7c3..816996d289b 100644 --- a/packages/clerk-js/src/test/fixture-helpers.ts +++ b/packages/clerk-js/src/test/fixture-helpers.ts @@ -362,11 +362,24 @@ const createOrganizationSettingsFixtureHelpers = (environment: EnvironmentJSON) const createBillingSettingsFixtureHelpers = (environment: EnvironmentJSON) => { const os = environment.commerce_settings.billing; - const withBilling = () => { - os.user.enabled = true; - os.user.has_paid_plans = true; - os.organization.enabled = true; - os.organization.has_paid_plans = true; + const withBilling = ({ + userEnabled = true, + userHasPaidPlans = true, + organizationEnabled = true, + organizationHasPaidPlans = true, + freeTrialRequiresPaymentMethod = true, + }: { + userEnabled?: boolean; + userHasPaidPlans?: boolean; + organizationEnabled?: boolean; + organizationHasPaidPlans?: boolean; + freeTrialRequiresPaymentMethod?: boolean; + } = {}) => { + os.user.enabled = userEnabled; + os.user.has_paid_plans = userHasPaidPlans; + os.organization.enabled = organizationEnabled; + os.organization.has_paid_plans = organizationHasPaidPlans; + os.free_trial_requires_payment_method = freeTrialRequiresPaymentMethod; }; return { withBilling }; diff --git a/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx b/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx index 7b8e8ae9416..4c240fb7637 100644 --- a/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx +++ b/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx @@ -15,6 +15,7 @@ import { DevOnly } from '../../common/DevOnly'; import { useCheckoutContext, useEnvironment, usePaymentMethods } from '../../contexts'; import { Box, Button, Col, descriptors, Flex, Form, localizationKeys, Spinner, Text } from '../../customizables'; import { ChevronUpDown, InformationCircle } from '../../icons'; +import type { PropsOfComponent, ThemableCssProp } from '../../styledSystem'; import * as AddPaymentMethod from '../PaymentMethods/AddPaymentMethod'; import { PaymentMethodRow } from '../PaymentMethods/PaymentMethodRow'; import { SubscriptionBadge } from '../Subscriptions/badge'; @@ -138,7 +139,7 @@ export const CheckoutForm = withCardStateProvider(() => { }); const useCheckoutMutations = () => { - const { for: _for, onSubscriptionComplete } = useCheckoutContext(); + const { onSubscriptionComplete } = useCheckoutContext(); const { checkout } = useCheckout(); const { id, confirm } = checkout; const card = useCardState(); @@ -172,6 +173,11 @@ const useCheckoutMutations = () => { }); }; + const payWithoutPaymentMethod = (e: React.FormEvent) => { + e.preventDefault(); + return confirmCheckout({}); + }; + const addPaymentMethodAndPay = (ctx: { gateway: 'stripe'; paymentToken: string }) => confirmCheckout(ctx); const payWithTestCard = () => @@ -184,6 +190,7 @@ const useCheckoutMutations = () => { payWithExistingPaymentMethod, addPaymentMethodAndPay, payWithTestCard, + payWithoutPaymentMethod, }; }; @@ -220,13 +227,9 @@ const CheckoutFormElementsInternal = () => { paymentMethods.length > 0 || __BUILD_DISABLE_RHC__ ? 'existing' : 'new', ); - // Check if payment methods should be shown based on: - // 1. Immediate plan change (not a downgrade) - // 2. Either there's an amount due now OR it's a free trial that requires payment method - const showPaymentMethods = - isImmediatePlanChange && - (totals.totalDueNow.amount > 0 || - (!!freeTrialEndsAt && environment.commerceSettings.billing.freeTrialRequiresPaymentMethod)); + const isFreeTrial = Boolean(freeTrialEndsAt); + const showTabs = isImmediatePlanChange && (totals.totalDueNow.amount > 0 || isFreeTrial); + const needsPaymentMethod = !(isFreeTrial && !environment.commerceSettings.billing.freeTrialRequiresPaymentMethod); if (!id) { return null; @@ -240,7 +243,7 @@ const CheckoutFormElementsInternal = () => { > {__BUILD_DISABLE_RHC__ ? null : ( <> - {paymentMethods.length > 0 && showPaymentMethods && ( + {paymentMethods.length > 0 && showTabs && needsPaymentMethod && ( { )} - {showPaymentMethods && paymentMethodSource === 'existing' && ( - - )} - - {__BUILD_DISABLE_RHC__ - ? null - : showPaymentMethods && paymentMethodSource === 'new' && } + {paymentMethodSource === 'existing' && + (needsPaymentMethod ? ( + + ) : ( + + ))} - {!showPaymentMethods && } + {__BUILD_DISABLE_RHC__ ? null : paymentMethodSource === 'new' && } ); }; @@ -356,52 +358,17 @@ const useSubmitLabel = () => { }; const FreeTrialButton = withCardStateProvider(() => { - const { for: _for, onSubscriptionComplete } = useCheckoutContext(); - const submitLabel = useSubmitLabel(); + const { for: _for } = useCheckoutContext(); + const { payWithoutPaymentMethod } = useCheckoutMutations(); const card = useCardState(); - const { checkout } = useCheckout(); - - const handleFreeTrialStart = async () => { - card.setLoading(); - card.setError(undefined); - - try { - // For free trials without payment method requirement, we can confirm without payment details - const { data, error } = await checkout.confirm({}); - - if (error) { - handleError(error, [], card.setError); - } else if (data) { - onSubscriptionComplete?.(); - } - } catch (error) { - handleError(error, [], card.setError); - } finally { - card.setIdle(); - } - }; return (
({ - display: 'flex', - flexDirection: 'column', - rowGap: t.space.$4, - })} + onSubmit={payWithoutPaymentMethod} + sx={formProps} > {card.error} -