From 30aca671b274e7959004fc37a9d0b1958ca701a3 Mon Sep 17 00:00:00 2001 From: Keiran Flanigan Date: Fri, 2 May 2025 15:16:01 -0500 Subject: [PATCH 1/4] add correct badging / cta for free plan --- .../PricingTable/PricingTableDefault.tsx | 116 ++++++++---------- .../src/ui/contexts/components/Plans.tsx | 4 +- .../ui/customizables/elementDescriptors.ts | 2 +- packages/types/src/appearance.ts | 2 +- 4 files changed, 56 insertions(+), 68 deletions(-) diff --git a/packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx b/packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx index 31bb9a7a9c8..8d813647797 100644 --- a/packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx +++ b/packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx @@ -22,7 +22,7 @@ import { Text, useAppearance, } from '../../customizables'; -import { Avatar, ReversibleContainer, SegmentedControl } from '../../elements'; +import { ReversibleContainer, SegmentedControl } from '../../elements'; import { usePrefersReducedMotion } from '../../hooks'; import { Check, InformationCircle, Plus } from '../../icons'; import type { ThemableCssProp } from '../../styledSystem'; @@ -111,7 +111,7 @@ function Card(props: CardProps) { const totalFeatures = features.length; const hasFeatures = totalFeatures > 0; - const { buttonPropsForPlan } = usePlansContext(); + const { buttonPropsForPlan, isDefaultPlanImplicitlyActiveOrUpcoming } = usePlansContext(); const showPlanDetails = (event?: React.MouseEvent) => { const portalRoot = getClosestProfileScrollBox(mode, event); @@ -175,7 +175,7 @@ function Card(props: CardProps) { /> ) : null} - {!plan.isDefault && ( + {(!plan.isDefault || !isDefaultPlanImplicitlyActiveOrUpcoming) && ( ({ @@ -217,7 +217,7 @@ const CardHeader = React.forwardRef((props, ref const prefersReducedMotion = usePrefersReducedMotion(); const { animations: layoutAnimations } = useAppearance().parsedLayout; const { plan, isCompact, planPeriod, setPlanPeriod } = props; - const { name, avatarUrl, annualMonthlyAmount } = plan; + const { name, annualMonthlyAmount } = plan; const isMotionSafe = !prefersReducedMotion && layoutAnimations === true; const pricingTableCardFeePeriodNoticeAnimation: ThemableCssProp = t => ({ transition: isMotionSafe @@ -231,11 +231,11 @@ const CardHeader = React.forwardRef((props, ref return planPeriod === 'annual' ? plan.annualMonthlyAmountFormatted : plan.amountFormatted; }, [annualMonthlyAmount, planPeriod, plan.amountFormatted, plan.annualMonthlyAmountFormatted]); - const { activeOrUpcomingSubscription, isDefaultPlanImplicitlyActive } = usePlansContext(); + const { activeOrUpcomingSubscription, isDefaultPlanImplicitlyActiveOrUpcoming, subscriptions } = usePlansContext(); const subscription = activeOrUpcomingSubscription(plan); - const isImplicitlyActive = isDefaultPlanImplicitlyActive && plan.isDefault; + const isImplicitlyActiveOrUpcoming = isDefaultPlanImplicitlyActiveOrUpcoming && plan.isDefault; - const showBadge = !!subscription || isImplicitlyActive; + const showBadge = !!subscription || isImplicitlyActiveOrUpcoming; return ( ((props, ref })} data-variant={isCompact ? 'compact' : 'default'} > - {avatarUrl || showBadge ? ( - ({ - marginBlockEnd: t.space.$3, - display: 'flex', - alignItems: 'flex-start', - justifyContent: 'space-between', - flexWrap: 'wrap', - gap: t.space.$3, - float: !avatarUrl && !showBadge ? 'right' : undefined, - })} - > - {avatarUrl ? ( - 40} - title={name} - initials={name[0]} - rounded={false} - imageUrl={avatarUrl} - /> - ) : null} - - {showBadge ? ( - - {isImplicitlyActive || subscription?.status === 'active' ? ( - - ) : ( - - )} - - ) : null} - - - ) : null} - ({ + width: '100%', + display: 'flex', + flexDirection: 'row-reverse', + alignItems: 'baseline', + justifyContent: 'flex-end', + flexWrap: 'wrap', + gap: t.space.$3, + marginBlockEnd: t.space.$3, + })} > - {plan.name} - + {showBadge ? ( + + {subscription?.status === 'active' || (isImplicitlyActiveOrUpcoming && subscriptions.length === 0) ? ( + + ) : ( + + )} + + ) : null} + + {name} + + {!isCompact && plan.description ? ( { ); // should the default plan be shown as active - const isDefaultPlanImplicitlyActive = useMemo(() => { + const isDefaultPlanImplicitlyActiveOrUpcoming = useMemo(() => { return ctx.subscriptions.length === 0; }, [ctx.subscriptions]); @@ -216,7 +216,7 @@ export const usePlansContext = () => { ...ctx, componentName, activeOrUpcomingSubscription, - isDefaultPlanImplicitlyActive, + isDefaultPlanImplicitlyActiveOrUpcoming, handleSelectPlan, buttonPropsForPlan, canManageSubscription, diff --git a/packages/clerk-js/src/ui/customizables/elementDescriptors.ts b/packages/clerk-js/src/ui/customizables/elementDescriptors.ts index 5891bf54a15..e7b2601a08d 100644 --- a/packages/clerk-js/src/ui/customizables/elementDescriptors.ts +++ b/packages/clerk-js/src/ui/customizables/elementDescriptors.ts @@ -245,7 +245,7 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([ 'pricingTableCardHeader', 'pricingTableCardTitle', 'pricingTableCardDescription', - 'pricingTableCardAvatarBadgeContainer', + 'pricingTableCardBadgeTitleContainer', 'pricingTableCardAvatar', 'pricingTableCardBadgeContainer', 'pricingTableCardBadge', diff --git a/packages/types/src/appearance.ts b/packages/types/src/appearance.ts index a2d35d8055d..56209fc7eaf 100644 --- a/packages/types/src/appearance.ts +++ b/packages/types/src/appearance.ts @@ -370,7 +370,7 @@ export type ElementsConfig = { pricingTable: WithOptions; pricingTableCard: WithOptions; pricingTableCardHeader: WithOptions; - pricingTableCardAvatarBadgeContainer: WithOptions; + pricingTableCardBadgeTitleContainer: WithOptions; pricingTableCardAvatar: WithOptions; pricingTableCardBadgeContainer: WithOptions; pricingTableCardBadge: WithOptions; From 613bb1a43bc4f8e65d1f361d614ac98456375031 Mon Sep 17 00:00:00 2001 From: Keiran Flanigan Date: Sat, 3 May 2025 09:13:21 -0500 Subject: [PATCH 2/4] display badge and CTAs for checking out free plan --- .../src/ui/components/Checkout/CheckoutComplete.tsx | 8 ++++---- .../ui/components/PricingTable/PricingTableDefault.tsx | 10 +++++++--- packages/clerk-js/src/ui/contexts/components/Plans.tsx | 3 ++- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx b/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx index 664d8116d31..54076404b42 100644 --- a/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx +++ b/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx @@ -79,7 +79,7 @@ export const CheckoutComplete = ({ checkout }: { checkout: __experimental_Commer as='h2' textVariant='h2' localizationKey={ - checkout.subscription?.status === 'active' + checkout.totals.totalDueNow.amount > 0 ? localizationKeys('__experimental_commerce.checkout.title__paymentSuccessful') : localizationKeys('__experimental_commerce.checkout.title__subscriptionSuccessful') } @@ -89,7 +89,7 @@ export const CheckoutComplete = ({ checkout }: { checkout: __experimental_Commer colorScheme='secondary' sx={t => ({ textAlign: 'center', paddingInline: t.space.$8, marginBlockStart: t.space.$2 })} localizationKey={ - checkout.subscription?.status === 'active' + checkout.totals.totalDueNow.amount > 0 ? localizationKeys('__experimental_commerce.checkout.description__paymentSuccessful') : localizationKeys('__experimental_commerce.checkout.description__subscriptionSuccessful') } @@ -113,14 +113,14 @@ export const CheckoutComplete = ({ checkout }: { checkout: __experimental_Commer 0 ? localizationKeys('__experimental_commerce.checkout.lineItems.title__paymentMethod') : localizationKeys('__experimental_commerce.checkout.lineItems.title__subscriptionBegins') } /> 0 ? checkout.paymentSource ? `${capitalize(checkout.paymentSource.cardType)} ⋯ ${checkout.paymentSource.last4}` : '–' diff --git a/packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx b/packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx index 8d813647797..0bd8315ebcb 100644 --- a/packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx +++ b/packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx @@ -276,9 +276,13 @@ const CardHeader = React.forwardRef((props, ref ) : ( )} diff --git a/packages/clerk-js/src/ui/contexts/components/Plans.tsx b/packages/clerk-js/src/ui/contexts/components/Plans.tsx index aedf6629e0b..d61d22eccf1 100644 --- a/packages/clerk-js/src/ui/contexts/components/Plans.tsx +++ b/packages/clerk-js/src/ui/contexts/components/Plans.tsx @@ -112,7 +112,8 @@ export const usePlansContext = () => { // should the default plan be shown as active const isDefaultPlanImplicitlyActiveOrUpcoming = useMemo(() => { - return ctx.subscriptions.length === 0; + // are there no subscriptions or are all subscriptions canceled + return ctx.subscriptions.length === 0 || !ctx.subscriptions.some(subscription => !subscription.canceledAt); }, [ctx.subscriptions]); const canManageSubscription = useCallback( From c5f679a832794d9a91c5b5c4e8dec2af7c86a62c Mon Sep 17 00:00:00 2001 From: Keiran Flanigan Date: Sat, 3 May 2025 09:14:42 -0500 Subject: [PATCH 3/4] changeset --- .changeset/eager-goats-vanish.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/eager-goats-vanish.md diff --git a/.changeset/eager-goats-vanish.md b/.changeset/eager-goats-vanish.md new file mode 100644 index 00000000000..3ef96d8be31 --- /dev/null +++ b/.changeset/eager-goats-vanish.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': patch +'@clerk/types': patch +--- + +Add support for switching to the free plan From 73f80bde7ff4e41df769a66f6590185fad570534 Mon Sep 17 00:00:00 2001 From: Keiran Flanigan Date: Sat, 3 May 2025 10:08:00 -0500 Subject: [PATCH 4/4] fix PlanDetails --- .../src/ui/components/Plans/PlanDetails.tsx | 171 +++++++++--------- .../PricingTable/PricingTableDefault.tsx | 3 +- 2 files changed, 89 insertions(+), 85 deletions(-) diff --git a/packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx b/packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx index d5c95322f00..0570238c163 100644 --- a/packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx +++ b/packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx @@ -31,13 +31,13 @@ export const PlanDetails = (props: __experimental_PlanDetailsProps) => { return ( - <_PlanDetails {...props} /> + ); }; -const _PlanDetails = ({ +const PlanDetailsInternal = ({ plan, onSubscriptionCancel, portalRoot, @@ -51,7 +51,8 @@ const _PlanDetails = ({ const [planPeriod, setPlanPeriod] = useState<__experimental_CommerceSubscriptionPlanPeriod>(_planPeriod); const { setIsOpen } = useDrawerContext(); - const { activeOrUpcomingSubscription, revalidate, buttonPropsForPlan } = usePlansContext(); + const { activeOrUpcomingSubscription, revalidate, buttonPropsForPlan, isDefaultPlanImplicitlyActiveOrUpcoming } = + usePlansContext(); const subscriberType = useSubscriberTypeContext(); if (!plan) { @@ -202,7 +203,7 @@ const _PlanDetails = ({ ) : null} - {plan.amount > 0 ? ( + {!plan.isDefault || !isDefaultPlanImplicitlyActiveOrUpcoming ? ( {subscription ? ( subscription.canceledAt ? ( @@ -315,7 +316,11 @@ interface HeaderProps { const Header = React.forwardRef((props, ref) => { const { plan, subscription, closeSlot, planPeriod, setPlanPeriod } = props; - const { captionForSubscription } = usePlansContext(); + const { captionForSubscription, isDefaultPlanImplicitlyActiveOrUpcoming, subscriptions } = usePlansContext(); + + const isImplicitlyActiveOrUpcoming = isDefaultPlanImplicitlyActiveOrUpcoming && plan.isDefault; + + const showBadge = !!subscription || isImplicitlyActiveOrUpcoming; return ( ((props, ref) => { })} /> ) : null} - {subscription ? ( + {showBadge ? ( ({ marginBlockEnd: t.space.$3, })} > - {subscription.status === 'active' ? ( + {subscription?.status === 'active' || (isImplicitlyActiveOrUpcoming && subscriptions.length === 0) ? ( ((props, ref) => { ) : null} - {plan.amount > 0 ? ( - ({ - marginTop: t.space.$3, - columnGap: t.space.$1x5, - })} - > - <> - ({ + marginTop: t.space.$3, + columnGap: t.space.$1x5, + })} + > + <> + + {plan.currencySymbol} + {(subscription && subscription.planPeriod === 'annual') || planPeriod === 'annual' + ? plan.annualMonthlyAmountFormatted + : plan.amountFormatted} + + ({ + textTransform: 'lowercase', + ':before': { + content: '"/"', + marginInlineEnd: t.space.$1, + }, + })} + localizationKey={localizationKeys('__experimental_commerce.month')} + /> + {plan.annualMonthlyAmount > 0 ? ( + ({ + width: '100%', + display: 'grid', + gridTemplateRows: + (subscription && subscription.planPeriod === 'annual') || planPeriod === 'annual' ? '1fr' : '0fr', + }), + ]} + // @ts-ignore - Needed until React 19 support + inert={ + (subscription && subscription.planPeriod === 'annual') || planPeriod === 'annual' ? 'true' : undefined + } > - {plan.currencySymbol} - {(subscription && subscription.planPeriod === 'annual') || planPeriod === 'annual' - ? plan.annualMonthlyAmountFormatted - : plan.amountFormatted} - - ({ - textTransform: 'lowercase', - ':before': { - content: '"/"', - marginInlineEnd: t.space.$1, - }, - })} - localizationKey={localizationKeys('__experimental_commerce.month')} - /> - {plan.annualMonthlyAmount > 0 ? ( ({ - width: '100%', - display: 'grid', - gridTemplateRows: - (subscription && subscription.planPeriod === 'annual') || planPeriod === 'annual' ? '1fr' : '0fr', - }), - ]} - // @ts-ignore - Needed until React 19 support - inert={ - (subscription && subscription.planPeriod === 'annual') || planPeriod === 'annual' ? 'true' : undefined - } + elementDescriptor={descriptors.planDetailFeePeriodNoticeInner} + sx={{ + overflow: 'hidden', + minHeight: 0, + }} > - ({ + width: '100%', + display: 'flex', + alignItems: 'center', + columnGap: t.space.$1, + })} > - ({ - width: '100%', - display: 'flex', - alignItems: 'center', - columnGap: t.space.$1, - })} - > - {' '} - - - + {' '} + + - ) : null} - - - ) : null} + + ) : null} + + {!!subscription && ( onClick={event => showPlanDetails(event)} variant='link' sx={t => ({ - marginBlockStart: t.space.$2, + marginBlockStart: 'auto', + paddingBlockStart: t.space.$2, gap: t.space.$1, })} >