diff --git a/.changeset/hip-dancers-write.md b/.changeset/hip-dancers-write.md new file mode 100644 index 00000000000..0ec793cb4b7 --- /dev/null +++ b/.changeset/hip-dancers-write.md @@ -0,0 +1,7 @@ +--- +'@clerk/localizations': patch +'@clerk/clerk-js': patch +'@clerk/types': patch +--- + +Display a better subscription list / button when empty and the free plan is hidden diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationBillingPage.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationBillingPage.tsx index b96f12d74d0..30e8ecb2fcc 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationBillingPage.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationBillingPage.tsx @@ -5,11 +5,10 @@ import { SubscriberTypeContext, useSubscriptions, } from '../../contexts'; -import { Col, descriptors, Flex, localizationKeys } from '../../customizables'; +import { Col, descriptors, localizationKeys } from '../../customizables'; import { Card, Header, - ProfileSection, Tab, TabPanel, TabPanels, @@ -19,8 +18,6 @@ import { withCardStateProvider, } from '../../elements'; import { useTabState } from '../../hooks/useTabState'; -import { ArrowsUpDown } from '../../icons'; -import { useRouter } from '../../router'; import { PaymentSources } from '../PaymentSources'; import { StatementsList } from '../Statements'; import { SubscriptionsList } from '../Subscriptions'; @@ -35,7 +32,7 @@ const OrganizationBillingPageInternal = withCardStateProvider(() => { const card = useCardState(); const { data: subscriptions } = useSubscriptions('org'); const { selectedTab, handleTabChange } = useTabState(orgTabMap); - const { navigate } = useRouter(); + if (!Array.isArray(subscriptions?.data)) { return null; } @@ -71,40 +68,18 @@ const OrganizationBillingPageInternal = withCardStateProvider(() => { - - ({ - borderTop: 'none', - paddingTop: t.space.$1, - })} - > - - ({ - justifyContent: 'start', - height: t.sizes.$8, - }), - ]} - leftIcon={ArrowsUpDown} - leftIconSx={t => ({ width: t.sizes.$4, height: t.sizes.$4 })} - onClick={() => void navigate('plans')} - /> - - has({ permission: 'org:sys_billing:manage' })}> - - - + + has({ permission: 'org:sys_billing:manage' })}> + + diff --git a/packages/clerk-js/src/ui/components/Subscriptions/SubscriptionsList.tsx b/packages/clerk-js/src/ui/components/Subscriptions/SubscriptionsList.tsx index 6b1d5e7bcce..43a7b1c26b8 100644 --- a/packages/clerk-js/src/ui/components/Subscriptions/SubscriptionsList.tsx +++ b/packages/clerk-js/src/ui/components/Subscriptions/SubscriptionsList.tsx @@ -2,6 +2,7 @@ import type { CommerceSubscriptionResource } from '@clerk/types'; import { useProtect } from '../../common'; import { usePlansContext, useSubscriberTypeContext } from '../../contexts'; +import type { LocalizationKey } from '../../customizables'; import { Badge, Button, @@ -18,14 +19,25 @@ import { Thead, Tr, } from '../../customizables'; -import { CogFilled, Plans } from '../../icons'; +import { ProfileSection } from '../../elements'; +import { ArrowsUpDown, CogFilled, Plans, Plus } from '../../icons'; +import { useRouter } from '../../router'; -export function SubscriptionsList() { +export function SubscriptionsList({ + title, + arrowButtonText, + arrowButtonEmptyText, +}: { + title: LocalizationKey; + arrowButtonText: LocalizationKey; + arrowButtonEmptyText: LocalizationKey; +}) { const { subscriptions, handleSelectPlan, captionForSubscription, canManageSubscription } = usePlansContext(); const subscriberType = useSubscriberTypeContext(); const canManageBilling = useProtect( has => has({ permission: 'org:sys_billing:manage' }) || subscriberType === 'user', ); + const { navigate } = useRouter(); const handleSelectSubscription = ( subscription: CommerceSubscriptionResource, event?: React.MouseEvent, @@ -52,117 +64,146 @@ export function SubscriptionsList() { }); return ( - - - - - - - - - - {sortedSubscriptions.map(subscription => ( - - - ({ + borderTop: 'none', + paddingTop: t.space.$1, + })} + > + {subscriptions.length > 0 && ( +
PlanStart dateEdit
-
+ + + + + + + + + {sortedSubscriptions.map(subscription => ( + + + + ({ + width: t.sizes.$4, + height: t.sizes.$4, + opacity: t.opacity.$inactive, + })} + /> + ({ marginRight: t.sizes.$1 })} + > + {subscription.plan.name} + + {sortedSubscriptions.length > 1 || !!subscription.canceledAt ? ( + + ) : null} + + {(!subscription.plan.isDefault || subscription.status === 'upcoming') && ( + + )} + + + - - + - - ))} - -
PlanStart dateEdit
+
({ + textAlign: 'right', + })} > - ({ - width: t.sizes.$4, - height: t.sizes.$4, - opacity: t.opacity.$inactive, - })} - /> - ({ marginRight: t.sizes.$1 })} - > - {subscription.plan.name} + + {subscription.plan.currencySymbol} + {subscription.planPeriod === 'annual' + ? subscription.plan.annualAmountFormatted + : subscription.plan.amountFormatted} + {(subscription.plan.amount > 0 || subscription.plan.annualAmount > 0) && ( + ({ + color: t.colors.$colorTextSecondary, + textTransform: 'lowercase', + ':before': { + content: '"/"', + marginInline: t.space.$1, + }, + })} + localizationKey={ + subscription.planPeriod === 'annual' + ? localizationKeys('commerce.year') + : localizationKeys('commerce.month') + } + /> + )} - {sortedSubscriptions.length > 1 || !!subscription.canceledAt ? ( - - ) : null} - - {(!subscription.plan.isDefault || subscription.status === 'upcoming') && ( - - )} - - ({ - textAlign: 'right', - })} - > - - {subscription.plan.currencySymbol} - {subscription.planPeriod === 'annual' - ? subscription.plan.annualAmountFormatted - : subscription.plan.amountFormatted} - {(subscription.plan.amount > 0 || subscription.plan.annualAmount > 0) && ( - ({ - color: t.colors.$colorTextSecondary, - textTransform: 'lowercase', - ':before': { - content: '"/"', - marginInline: t.space.$1, - }, - })} - localizationKey={ - subscription.planPeriod === 'annual' - ? localizationKeys('commerce.year') - : localizationKeys('commerce.month') - } - /> - )} - - ({ - textAlign: 'right', - })} - > - {canManageSubscription({ subscription }) && subscription.id && !subscription.plan.isDefault && ( - ({ + textAlign: 'right', })} > - ({ - width: t.sizes.$4, - height: t.sizes.$4, - opacity: t.opacity.$inactive, - })} - /> - - )} -
+ {canManageSubscription({ subscription }) && subscription.id && !subscription.plan.isDefault && ( + + )} + + + ))} + + + )} + + 0 ? arrowButtonText : arrowButtonEmptyText} + sx={[ + t => ({ + justifyContent: 'start', + height: t.sizes.$8, + }), + ]} + leftIcon={subscriptions.length > 0 ? ArrowsUpDown : Plus} + leftIconSx={t => ({ + width: t.sizes.$4, + height: t.sizes.$4, + })} + onClick={() => void navigate('plans')} + /> + ); } diff --git a/packages/clerk-js/src/ui/components/UserProfile/BillingPage.tsx b/packages/clerk-js/src/ui/components/UserProfile/BillingPage.tsx index 647c97aa8d3..192cce90242 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/BillingPage.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/BillingPage.tsx @@ -8,7 +8,6 @@ import { Col, descriptors, localizationKeys } from '../../customizables'; import { Card, Header, - ProfileSection, Tab, TabPanel, TabPanels, @@ -18,8 +17,6 @@ import { withCardStateProvider, } from '../../elements'; import { useTabState } from '../../hooks/useTabState'; -import { ArrowsUpDown } from '../../icons'; -import { useRouter } from '../../router'; import { PaymentSources } from '../PaymentSources'; import { StatementsList } from '../Statements'; import { SubscriptionsList } from '../Subscriptions'; @@ -33,7 +30,6 @@ const tabMap = { const BillingPageInternal = withCardStateProvider(() => { const card = useCardState(); const { data: subscriptions } = useSubscriptions(); - const { navigate } = useRouter(); const { selectedTab, handleTabChange } = useTabState(tabMap); @@ -70,38 +66,16 @@ const BillingPageInternal = withCardStateProvider(() => { ({ width: '100%', flexDirection: 'column' })}> - <> - ({ - borderTop: 'none', - paddingTop: t.space.$1, - })} - > - - ({ - justifyContent: 'start', - height: t.sizes.$8, - }), - ]} - leftIcon={ArrowsUpDown} - leftIconSx={t => ({ - width: t.sizes.$4, - height: t.sizes.$4, - })} - onClick={() => void navigate('plans')} - /> - - - + + diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index a99d356efd5..5589a9c5707 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -176,6 +176,7 @@ export const enUS: LocalizationResource = { headerTitle__subscriptions: 'Subscriptions', }, subscriptionsListSection: { + actionLabel__newSubscription: 'Subscribe to a plan', actionLabel__switchPlan: 'Switch plans', title: 'Subscription', }, @@ -779,6 +780,7 @@ export const enUS: LocalizationResource = { headerTitle__subscriptions: 'Subscription', }, subscriptionsListSection: { + actionLabel__newSubscription: 'Subscribe to a plan', actionLabel__switchPlan: 'Switch plans', title: 'Subscription', }, diff --git a/packages/types/src/localization.ts b/packages/types/src/localization.ts index 445397a19cd..cee845f5784 100644 --- a/packages/types/src/localization.ts +++ b/packages/types/src/localization.ts @@ -705,6 +705,7 @@ type _LocalizationResource = { }; subscriptionsListSection: { title: LocalizationValue; + actionLabel__newSubscription: LocalizationValue; actionLabel__switchPlan: LocalizationValue; }; paymentSourcesSection: { @@ -908,6 +909,7 @@ type _LocalizationResource = { }; subscriptionsListSection: { title: LocalizationValue; + actionLabel__newSubscription: LocalizationValue; actionLabel__switchPlan: LocalizationValue; }; paymentSourcesSection: {