Skip to content

Commit fbf3cf4

Browse files
authored
chore(clerk-js,types,localizations): Clean up empty subscription list (#5912)
1 parent 2b18d7a commit fbf3cf4

File tree

6 files changed

+186
-185
lines changed

6 files changed

+186
-185
lines changed

.changeset/hip-dancers-write.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@clerk/localizations': patch
3+
'@clerk/clerk-js': patch
4+
'@clerk/types': patch
5+
---
6+
7+
Display a better subscription list / button when empty and the free plan is hidden

packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationBillingPage.tsx

Lines changed: 14 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@ import {
55
SubscriberTypeContext,
66
useSubscriptions,
77
} from '../../contexts';
8-
import { Col, descriptors, Flex, localizationKeys } from '../../customizables';
8+
import { Col, descriptors, localizationKeys } from '../../customizables';
99
import {
1010
Card,
1111
Header,
12-
ProfileSection,
1312
Tab,
1413
TabPanel,
1514
TabPanels,
@@ -19,8 +18,6 @@ import {
1918
withCardStateProvider,
2019
} from '../../elements';
2120
import { useTabState } from '../../hooks/useTabState';
22-
import { ArrowsUpDown } from '../../icons';
23-
import { useRouter } from '../../router';
2421
import { PaymentSources } from '../PaymentSources';
2522
import { StatementsList } from '../Statements';
2623
import { SubscriptionsList } from '../Subscriptions';
@@ -35,7 +32,7 @@ const OrganizationBillingPageInternal = withCardStateProvider(() => {
3532
const card = useCardState();
3633
const { data: subscriptions } = useSubscriptions('org');
3734
const { selectedTab, handleTabChange } = useTabState(orgTabMap);
38-
const { navigate } = useRouter();
35+
3936
if (!Array.isArray(subscriptions?.data)) {
4037
return null;
4138
}
@@ -71,40 +68,18 @@ const OrganizationBillingPageInternal = withCardStateProvider(() => {
7168
</TabsList>
7269
<TabPanels>
7370
<TabPanel sx={{ width: '100%', flexDirection: 'column' }}>
74-
<Flex
75-
sx={{ width: '100%', flexDirection: 'column' }}
76-
gap={4}
77-
>
78-
<ProfileSection.Root
79-
id='subscriptionsList'
80-
title={localizationKeys('organizationProfile.billingPage.subscriptionsListSection.title')}
81-
centered={false}
82-
sx={t => ({
83-
borderTop: 'none',
84-
paddingTop: t.space.$1,
85-
})}
86-
>
87-
<SubscriptionsList />
88-
<ProfileSection.ArrowButton
89-
id='subscriptionsList'
90-
textLocalizationKey={localizationKeys(
91-
'organizationProfile.billingPage.subscriptionsListSection.actionLabel__switchPlan',
92-
)}
93-
sx={[
94-
t => ({
95-
justifyContent: 'start',
96-
height: t.sizes.$8,
97-
}),
98-
]}
99-
leftIcon={ArrowsUpDown}
100-
leftIconSx={t => ({ width: t.sizes.$4, height: t.sizes.$4 })}
101-
onClick={() => void navigate('plans')}
102-
/>
103-
</ProfileSection.Root>
104-
<Protect condition={has => has({ permission: 'org:sys_billing:manage' })}>
105-
<PaymentSources />
106-
</Protect>
107-
</Flex>
71+
<SubscriptionsList
72+
title={localizationKeys('organizationProfile.billingPage.subscriptionsListSection.title')}
73+
arrowButtonText={localizationKeys(
74+
'organizationProfile.billingPage.subscriptionsListSection.actionLabel__switchPlan',
75+
)}
76+
arrowButtonEmptyText={localizationKeys(
77+
'organizationProfile.billingPage.subscriptionsListSection.actionLabel__newSubscription',
78+
)}
79+
/>
80+
<Protect condition={has => has({ permission: 'org:sys_billing:manage' })}>
81+
<PaymentSources />
82+
</Protect>
10883
</TabPanel>
10984
<TabPanel sx={{ width: '100%' }}>
11085
<StatementsContextProvider>

packages/clerk-js/src/ui/components/Subscriptions/SubscriptionsList.tsx

Lines changed: 151 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { CommerceSubscriptionResource } from '@clerk/types';
22

33
import { useProtect } from '../../common';
44
import { usePlansContext, useSubscriberTypeContext } from '../../contexts';
5+
import type { LocalizationKey } from '../../customizables';
56
import {
67
Badge,
78
Button,
@@ -18,14 +19,25 @@ import {
1819
Thead,
1920
Tr,
2021
} from '../../customizables';
21-
import { CogFilled, Plans } from '../../icons';
22+
import { ProfileSection } from '../../elements';
23+
import { ArrowsUpDown, CogFilled, Plans, Plus } from '../../icons';
24+
import { useRouter } from '../../router';
2225

23-
export function SubscriptionsList() {
26+
export function SubscriptionsList({
27+
title,
28+
arrowButtonText,
29+
arrowButtonEmptyText,
30+
}: {
31+
title: LocalizationKey;
32+
arrowButtonText: LocalizationKey;
33+
arrowButtonEmptyText: LocalizationKey;
34+
}) {
2435
const { subscriptions, handleSelectPlan, captionForSubscription, canManageSubscription } = usePlansContext();
2536
const subscriberType = useSubscriberTypeContext();
2637
const canManageBilling = useProtect(
2738
has => has({ permission: 'org:sys_billing:manage' }) || subscriberType === 'user',
2839
);
40+
const { navigate } = useRouter();
2941
const handleSelectSubscription = (
3042
subscription: CommerceSubscriptionResource,
3143
event?: React.MouseEvent<HTMLElement>,
@@ -52,117 +64,146 @@ export function SubscriptionsList() {
5264
});
5365

5466
return (
55-
<Table tableHeadVisuallyHidden>
56-
<Thead>
57-
<Tr>
58-
<Th>Plan</Th>
59-
<Th>Start date</Th>
60-
<Th>Edit</Th>
61-
</Tr>
62-
</Thead>
63-
<Tbody>
64-
{sortedSubscriptions.map(subscription => (
65-
<Tr key={subscription.id}>
66-
<Td>
67-
<Col gap={1}>
68-
<Flex
69-
align='center'
70-
gap={1}
67+
<ProfileSection.Root
68+
id='subscriptionsList'
69+
title={title}
70+
centered={false}
71+
sx={t => ({
72+
borderTop: 'none',
73+
paddingTop: t.space.$1,
74+
})}
75+
>
76+
{subscriptions.length > 0 && (
77+
<Table tableHeadVisuallyHidden>
78+
<Thead>
79+
<Tr>
80+
<Th>Plan</Th>
81+
<Th>Start date</Th>
82+
<Th>Edit</Th>
83+
</Tr>
84+
</Thead>
85+
<Tbody>
86+
{sortedSubscriptions.map(subscription => (
87+
<Tr key={subscription.id}>
88+
<Td>
89+
<Col gap={1}>
90+
<Flex
91+
align='center'
92+
gap={1}
93+
>
94+
<Icon
95+
icon={Plans}
96+
sx={t => ({
97+
width: t.sizes.$4,
98+
height: t.sizes.$4,
99+
opacity: t.opacity.$inactive,
100+
})}
101+
/>
102+
<Text
103+
variant='subtitle'
104+
sx={t => ({ marginRight: t.sizes.$1 })}
105+
>
106+
{subscription.plan.name}
107+
</Text>
108+
{sortedSubscriptions.length > 1 || !!subscription.canceledAt ? (
109+
<Badge
110+
colorScheme={subscription.status === 'active' ? 'secondary' : 'primary'}
111+
localizationKey={
112+
subscription.status === 'active'
113+
? localizationKeys('badge__activePlan')
114+
: localizationKeys('badge__upcomingPlan')
115+
}
116+
/>
117+
) : null}
118+
</Flex>
119+
{(!subscription.plan.isDefault || subscription.status === 'upcoming') && (
120+
<Text
121+
variant='caption'
122+
colorScheme='secondary'
123+
localizationKey={captionForSubscription(subscription)}
124+
/>
125+
)}
126+
</Col>
127+
</Td>
128+
<Td
129+
sx={_ => ({
130+
textAlign: 'right',
131+
})}
71132
>
72-
<Icon
73-
icon={Plans}
74-
sx={t => ({
75-
width: t.sizes.$4,
76-
height: t.sizes.$4,
77-
opacity: t.opacity.$inactive,
78-
})}
79-
/>
80-
<Text
81-
variant='subtitle'
82-
sx={t => ({ marginRight: t.sizes.$1 })}
83-
>
84-
{subscription.plan.name}
133+
<Text variant='subtitle'>
134+
{subscription.plan.currencySymbol}
135+
{subscription.planPeriod === 'annual'
136+
? subscription.plan.annualAmountFormatted
137+
: subscription.plan.amountFormatted}
138+
{(subscription.plan.amount > 0 || subscription.plan.annualAmount > 0) && (
139+
<Span
140+
sx={t => ({
141+
color: t.colors.$colorTextSecondary,
142+
textTransform: 'lowercase',
143+
':before': {
144+
content: '"/"',
145+
marginInline: t.space.$1,
146+
},
147+
})}
148+
localizationKey={
149+
subscription.planPeriod === 'annual'
150+
? localizationKeys('commerce.year')
151+
: localizationKeys('commerce.month')
152+
}
153+
/>
154+
)}
85155
</Text>
86-
{sortedSubscriptions.length > 1 || !!subscription.canceledAt ? (
87-
<Badge
88-
colorScheme={subscription.status === 'active' ? 'secondary' : 'primary'}
89-
localizationKey={
90-
subscription.status === 'active'
91-
? localizationKeys('badge__activePlan')
92-
: localizationKeys('badge__upcomingPlan')
93-
}
94-
/>
95-
) : null}
96-
</Flex>
97-
{(!subscription.plan.isDefault || subscription.status === 'upcoming') && (
98-
<Text
99-
variant='caption'
100-
colorScheme='secondary'
101-
localizationKey={captionForSubscription(subscription)}
102-
/>
103-
)}
104-
</Col>
105-
</Td>
106-
<Td
107-
sx={_ => ({
108-
textAlign: 'right',
109-
})}
110-
>
111-
<Text variant='subtitle'>
112-
{subscription.plan.currencySymbol}
113-
{subscription.planPeriod === 'annual'
114-
? subscription.plan.annualAmountFormatted
115-
: subscription.plan.amountFormatted}
116-
{(subscription.plan.amount > 0 || subscription.plan.annualAmount > 0) && (
117-
<Span
118-
sx={t => ({
119-
color: t.colors.$colorTextSecondary,
120-
textTransform: 'lowercase',
121-
':before': {
122-
content: '"/"',
123-
marginInline: t.space.$1,
124-
},
125-
})}
126-
localizationKey={
127-
subscription.planPeriod === 'annual'
128-
? localizationKeys('commerce.year')
129-
: localizationKeys('commerce.month')
130-
}
131-
/>
132-
)}
133-
</Text>
134-
</Td>
135-
<Td
136-
sx={_ => ({
137-
textAlign: 'right',
138-
})}
139-
>
140-
{canManageSubscription({ subscription }) && subscription.id && !subscription.plan.isDefault && (
141-
<Button
142-
aria-label='Manage subscription'
143-
onClick={event => handleSelectSubscription(subscription, event)}
144-
variant='bordered'
145-
colorScheme='secondary'
146-
isDisabled={!canManageBilling}
147-
sx={t => ({
148-
width: t.sizes.$6,
149-
height: t.sizes.$6,
156+
</Td>
157+
<Td
158+
sx={_ => ({
159+
textAlign: 'right',
150160
})}
151161
>
152-
<Icon
153-
icon={CogFilled}
154-
sx={t => ({
155-
width: t.sizes.$4,
156-
height: t.sizes.$4,
157-
opacity: t.opacity.$inactive,
158-
})}
159-
/>
160-
</Button>
161-
)}
162-
</Td>
163-
</Tr>
164-
))}
165-
</Tbody>
166-
</Table>
162+
{canManageSubscription({ subscription }) && subscription.id && !subscription.plan.isDefault && (
163+
<Button
164+
aria-label='Manage subscription'
165+
onClick={event => handleSelectSubscription(subscription, event)}
166+
variant='bordered'
167+
colorScheme='secondary'
168+
isDisabled={!canManageBilling}
169+
sx={t => ({
170+
width: t.sizes.$6,
171+
height: t.sizes.$6,
172+
})}
173+
>
174+
<Icon
175+
icon={CogFilled}
176+
sx={t => ({
177+
width: t.sizes.$4,
178+
height: t.sizes.$4,
179+
opacity: t.opacity.$inactive,
180+
})}
181+
/>
182+
</Button>
183+
)}
184+
</Td>
185+
</Tr>
186+
))}
187+
</Tbody>
188+
</Table>
189+
)}
190+
191+
<ProfileSection.ArrowButton
192+
id='subscriptionsList'
193+
textLocalizationKey={subscriptions.length > 0 ? arrowButtonText : arrowButtonEmptyText}
194+
sx={[
195+
t => ({
196+
justifyContent: 'start',
197+
height: t.sizes.$8,
198+
}),
199+
]}
200+
leftIcon={subscriptions.length > 0 ? ArrowsUpDown : Plus}
201+
leftIconSx={t => ({
202+
width: t.sizes.$4,
203+
height: t.sizes.$4,
204+
})}
205+
onClick={() => void navigate('plans')}
206+
/>
207+
</ProfileSection.Root>
167208
);
168209
}

0 commit comments

Comments
 (0)