diff --git a/.changeset/sweet-humans-poke.md b/.changeset/sweet-humans-poke.md
new file mode 100644
index 00000000000..b6c6294b1f2
--- /dev/null
+++ b/.changeset/sweet-humans-poke.md
@@ -0,0 +1,5 @@
+---
+'@clerk/clerk-js': patch
+---
+
+Hide CTA for `` when the user is does not have an active organization selected.
diff --git a/packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx b/packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx
index 40ef79be015..8c34bcd7af9 100644
--- a/packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx
+++ b/packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx
@@ -1,4 +1,4 @@
-import { useClerk, useSession } from '@clerk/shared/react';
+import { useClerk, useOrganization, useSession } from '@clerk/shared/react';
import type { BillingPlanResource, BillingSubscriptionPlanPeriod, PricingTableProps } from '@clerk/types';
import * as React from 'react';
@@ -24,6 +24,7 @@ import {
import { Check, Plus } from '../../icons';
import { common, InternalThemeProvider } from '../../styledSystem';
import { SubscriptionBadge } from '../Subscriptions/badge';
+import { getPricingFooterState } from './utils/pricing-footer-state';
interface PricingTableDefaultProps {
plans?: BillingPlanResource[] | null;
@@ -103,6 +104,7 @@ function Card(props: CardProps) {
const { isSignedIn } = useSession();
const { mode = 'mounted', ctaPosition: ctxCtaPosition } = usePricingTableContext();
const subscriberType = useSubscriberTypeContext();
+ const { organization } = useOrganization();
const ctaPosition = pricingTableProps.ctaPosition || ctxCtaPosition || 'bottom';
const collapseFeatures = pricingTableProps.collapseFeatures || false;
@@ -129,35 +131,14 @@ function Card(props: CardProps) {
);
const hasFeatures = plan.features.length > 0;
- const showStatusRow = !!subscription;
- let shouldShowFooter = false;
- let shouldShowFooterNotice = false;
-
- if (!subscription) {
- shouldShowFooter = true;
- shouldShowFooterNotice = false;
- } else if (subscription.status === 'upcoming') {
- shouldShowFooter = true;
- shouldShowFooterNotice = true;
- } else if (subscription.status === 'active') {
- if (subscription.canceledAt) {
- shouldShowFooter = true;
- shouldShowFooterNotice = false;
- } else if (planPeriod !== subscription.planPeriod && plan.annualMonthlyFee.amount > 0) {
- shouldShowFooter = true;
- shouldShowFooterNotice = false;
- } else if (plan.freeTrialEnabled && subscription.isFreeTrial) {
- shouldShowFooter = true;
- shouldShowFooterNotice = true;
- } else {
- shouldShowFooter = false;
- shouldShowFooterNotice = false;
- }
- } else {
- shouldShowFooter = false;
- shouldShowFooterNotice = false;
- }
+ const { shouldShowFooter, shouldShowFooterNotice } = getPricingFooterState({
+ subscription,
+ plan,
+ planPeriod,
+ forOrganizations: pricingTableProps.forOrganizations,
+ hasActiveOrganization: !!organization,
+ });
return (
) : undefined
}
diff --git a/packages/clerk-js/src/ui/components/PricingTable/utils/pricing-footer-state.spec.ts b/packages/clerk-js/src/ui/components/PricingTable/utils/pricing-footer-state.spec.ts
new file mode 100644
index 00000000000..90964d76b85
--- /dev/null
+++ b/packages/clerk-js/src/ui/components/PricingTable/utils/pricing-footer-state.spec.ts
@@ -0,0 +1,126 @@
+import type { BillingPlanResource, BillingSubscriptionItemResource, BillingSubscriptionPlanPeriod } from '@clerk/types';
+import { describe, expect, it } from 'vitest';
+
+import { getPricingFooterState } from './pricing-footer-state';
+
+const basePlan: BillingPlanResource = {
+ id: 'plan_1',
+ name: 'Pro',
+ fee: { amount: 1000, amountFormatted: '10.00', currency: 'USD', currencySymbol: '$' },
+ annualFee: { amount: 10000, amountFormatted: '100.00', currency: 'USD', currencySymbol: '$' },
+ annualMonthlyFee: { amount: 833, amountFormatted: '8.33', currency: 'USD', currencySymbol: '$' },
+ description: 'desc',
+ isDefault: false,
+ isRecurring: true,
+ hasBaseFee: true,
+ forPayerType: 'user',
+ publiclyVisible: true,
+ slug: 'pro',
+ avatarUrl: '',
+ features: [],
+ freeTrialDays: 14,
+ freeTrialEnabled: true,
+ pathRoot: '',
+ reload: async () => undefined as any,
+};
+
+const makeSub = (overrides: Partial): BillingSubscriptionItemResource => ({
+ id: 'si_1',
+ plan: basePlan,
+ planPeriod: 'month',
+ status: 'active',
+ createdAt: new Date('2021-01-01'),
+ paymentSourceId: 'src_1',
+ pastDueAt: null,
+ periodStart: new Date('2021-01-01'),
+ periodEnd: new Date('2021-01-31'),
+ canceledAt: null,
+ isFreeTrial: false,
+ cancel: async () => undefined as any,
+ pathRoot: '',
+ reload: async () => undefined as any,
+ ...overrides,
+});
+
+const run = (args: {
+ subscription?: BillingSubscriptionItemResource;
+ plan?: BillingPlanResource;
+ planPeriod?: BillingSubscriptionPlanPeriod;
+ forOrganizations?: boolean;
+ hasActiveOrganization?: boolean;
+}) =>
+ getPricingFooterState({
+ subscription: args.subscription,
+ plan: args.plan ?? basePlan,
+ planPeriod: args.planPeriod ?? 'month',
+ forOrganizations: args.forOrganizations,
+ hasActiveOrganization: args.hasActiveOrganization ?? false,
+ });
+
+describe('usePricingFooterState', () => {
+ it('hides footer when org plans and no active org', () => {
+ const res = run({ subscription: undefined, forOrganizations: true, hasActiveOrganization: false });
+ expect(res).toEqual({ shouldShowFooter: false, shouldShowFooterNotice: false });
+ });
+
+ it('shows footer when no subscription and user plans', () => {
+ const res = run({ subscription: undefined, forOrganizations: false });
+ expect(res).toEqual({ shouldShowFooter: true, shouldShowFooterNotice: false });
+ });
+
+ it('shows notice when subscription is upcoming', () => {
+ const res = run({ subscription: makeSub({ status: 'upcoming' }) });
+ expect(res).toEqual({ shouldShowFooter: true, shouldShowFooterNotice: true });
+ });
+
+ it('shows footer when active but canceled', () => {
+ const res = run({ subscription: makeSub({ status: 'active', canceledAt: new Date('2021-02-01') }) });
+ expect(res).toEqual({ shouldShowFooter: true, shouldShowFooterNotice: false });
+ });
+
+ it('shows footer when switching period to paid annual', () => {
+ const res = run({
+ subscription: makeSub({ status: 'active', planPeriod: 'month' }),
+ planPeriod: 'annual',
+ plan: basePlan,
+ });
+ expect(res).toEqual({ shouldShowFooter: true, shouldShowFooterNotice: false });
+ });
+
+ it('shows notice when active free trial', () => {
+ const res = run({ subscription: makeSub({ status: 'active', isFreeTrial: true }) });
+ expect(res).toEqual({ shouldShowFooter: true, shouldShowFooterNotice: true });
+ });
+
+ it('hides footer when active and matching period without trial', () => {
+ const res = run({ subscription: makeSub({ status: 'active', planPeriod: 'month', isFreeTrial: false }) });
+ expect(res).toEqual({ shouldShowFooter: false, shouldShowFooterNotice: false });
+ });
+
+ it('shows footer when switching period to paid monthly', () => {
+ const res = run({
+ subscription: makeSub({ status: 'active', planPeriod: 'annual' }),
+ planPeriod: 'month',
+ plan: basePlan,
+ });
+ expect(res).toEqual({ shouldShowFooter: true, shouldShowFooterNotice: false });
+ });
+
+ it('does not show footer when switching period if annualMonthlyFee is 0', () => {
+ const freeAnnualPlan: BillingPlanResource = {
+ ...basePlan,
+ annualMonthlyFee: { ...basePlan.annualMonthlyFee, amount: 0, amountFormatted: '0.00' },
+ };
+ const res = run({
+ subscription: makeSub({ status: 'active', planPeriod: 'month' }),
+ planPeriod: 'annual',
+ plan: freeAnnualPlan,
+ });
+ expect(res).toEqual({ shouldShowFooter: false, shouldShowFooterNotice: false });
+ });
+
+ it('hides footer when subscription is past_due', () => {
+ const res = run({ subscription: makeSub({ status: 'past_due' }) });
+ expect(res).toEqual({ shouldShowFooter: false, shouldShowFooterNotice: false });
+ });
+});
diff --git a/packages/clerk-js/src/ui/components/PricingTable/utils/pricing-footer-state.ts b/packages/clerk-js/src/ui/components/PricingTable/utils/pricing-footer-state.ts
new file mode 100644
index 00000000000..441b55829f3
--- /dev/null
+++ b/packages/clerk-js/src/ui/components/PricingTable/utils/pricing-footer-state.ts
@@ -0,0 +1,58 @@
+import type { BillingPlanResource, BillingSubscriptionItemResource, BillingSubscriptionPlanPeriod } from '@clerk/types';
+
+type UsePricingFooterStateParams = {
+ subscription: BillingSubscriptionItemResource | undefined;
+ plan: BillingPlanResource;
+ planPeriod: BillingSubscriptionPlanPeriod;
+ forOrganizations?: boolean;
+ hasActiveOrganization: boolean;
+};
+
+/**
+ * Calculates the correct show/hide state for the footer of a card in the `` component.
+ * @returns [shouldShowFooter, shouldShowFooterNotice]
+ */
+const valueResolution = (params: UsePricingFooterStateParams): [boolean, boolean] => {
+ const { subscription, plan, planPeriod, forOrganizations, hasActiveOrganization } = params;
+ const show_with_notice: [boolean, boolean] = [true, true];
+ const show_without_notice: [boolean, boolean] = [true, false];
+ const hide: [boolean, boolean] = [false, false];
+
+ // No subscription
+ if (!subscription) {
+ if (forOrganizations && !hasActiveOrganization) {
+ return hide;
+ }
+ return show_without_notice;
+ }
+
+ // Upcoming subscription
+ if (subscription.status === 'upcoming') {
+ return show_with_notice;
+ }
+
+ // Active subscription
+ if (subscription.status === 'active') {
+ const isCanceled = !!subscription.canceledAt;
+ const isSwitchingPaidPeriod = planPeriod !== subscription.planPeriod && plan.annualMonthlyFee.amount > 0;
+ const isActiveFreeTrial = plan.freeTrialEnabled && subscription.isFreeTrial;
+
+ if (isCanceled || isSwitchingPaidPeriod) {
+ return show_without_notice;
+ }
+
+ if (isActiveFreeTrial) {
+ return show_with_notice;
+ }
+
+ return hide;
+ }
+ return hide;
+};
+
+export const getPricingFooterState = (
+ params: UsePricingFooterStateParams,
+): { shouldShowFooter: boolean; shouldShowFooterNotice: boolean } => {
+ const [shouldShowFooter, shouldShowFooterNotice] = valueResolution(params);
+ return { shouldShowFooter, shouldShowFooterNotice };
+};