Skip to content

Commit 55490c3

Browse files
authored
chore(clerk-js,localizations): Reposition manage subscription button (#6428)
1 parent b2be471 commit 55490c3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+234
-100
lines changed

.changeset/better-results-jump.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@clerk/localizations': minor
3+
'@clerk/clerk-js': patch
4+
'@clerk/types': minor
5+
---
6+
7+
Change placement of the manage subscription button inside `<UserProfile/>` and `<OrganizationProfile/>`

integration/tests/pricing-table.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl
310310

311311
await expect(u.po.page.getByText(/Trial ends/i)).toBeVisible();
312312

313-
await u.po.page.getByRole('button', { name: 'Manage subscription' }).first().click();
313+
await u.po.page.getByRole('button', { name: 'Manage' }).first().click();
314314
await u.po.subscriptionDetails.waitForMounted();
315315
await u.po.subscriptionDetails.root.locator('.cl-menuButtonEllipsisBordered').click();
316316
await u.po.subscriptionDetails.root.getByText('Cancel free trial').click();
@@ -533,7 +533,10 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl
533533

534534
await u.page.waitForTimeout(1000);
535535
await expect(u.po.page.locator('.cl-profileSectionContent__subscriptionsList').getByText('Plus')).toBeVisible();
536-
await u.po.page.getByRole('button', { name: 'Manage subscription' }).first().click();
536+
await u.po.page
537+
.locator('.cl-profileSectionContent__subscriptionsList')
538+
.getByRole('button', { name: 'Manage' })
539+
.click();
537540
await u.po.subscriptionDetails.waitForMounted();
538541
await u.po.subscriptionDetails.root.locator('.cl-menuButtonEllipsisBordered').click();
539542
await u.po.subscriptionDetails.root.getByText('Cancel subscription').click();

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,15 @@ const OrganizationBillingPageInternal = withCardStateProvider(() => {
5757
<TabPanel sx={{ width: '100%', flexDirection: 'column' }}>
5858
<SubscriptionsList
5959
title={localizationKeys('organizationProfile.billingPage.subscriptionsListSection.title')}
60-
arrowButtonText={localizationKeys(
60+
switchPlansLabel={localizationKeys(
6161
'organizationProfile.billingPage.subscriptionsListSection.actionLabel__switchPlan',
6262
)}
63-
arrowButtonEmptyText={localizationKeys(
63+
newSubscriptionLabel={localizationKeys(
6464
'organizationProfile.billingPage.subscriptionsListSection.actionLabel__newSubscription',
6565
)}
66+
manageSubscriptionLabel={localizationKeys(
67+
'organizationProfile.billingPage.subscriptionsListSection.actionLabel__manageSubscription',
68+
)}
6669
/>
6770
<Protect condition={has => has({ permission: 'org:sys_billing:manage' })}>
6871
<PaymentSources />

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

Lines changed: 69 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import type { CommerceSubscriptionItemResource } from '@clerk/types';
1+
import type { CommercePlanResource, CommerceSubscriptionItemResource } from '@clerk/types';
22
import { useMemo } from 'react';
33

4+
import { useProtect } from '@/ui/common/Gate';
45
import { ProfileSection } from '@/ui/elements/Section';
56

6-
import { useProtect } from '../../common';
77
import {
88
normalizeFormatted,
99
useEnvironment,
@@ -13,39 +13,42 @@ import {
1313
useSubscription,
1414
} from '../../contexts';
1515
import type { LocalizationKey } from '../../customizables';
16-
import {
17-
Button,
18-
Col,
19-
Flex,
20-
Icon,
21-
localizationKeys,
22-
Span,
23-
Table,
24-
Tbody,
25-
Td,
26-
Text,
27-
Th,
28-
Thead,
29-
Tr,
30-
} from '../../customizables';
16+
import { Col, Flex, Icon, localizationKeys, Span, Table, Tbody, Td, Text, Th, Thead, Tr } from '../../customizables';
3117
import { ArrowsUpDown, CogFilled, Plans, Plus } from '../../icons';
3218
import { useRouter } from '../../router';
3319
import { SubscriptionBadge } from './badge';
3420

21+
const isFreePlan = (plan: CommercePlanResource) => !plan.hasBaseFee;
22+
3523
export function SubscriptionsList({
3624
title,
37-
arrowButtonText,
38-
arrowButtonEmptyText,
25+
switchPlansLabel,
26+
newSubscriptionLabel,
27+
manageSubscriptionLabel,
3928
}: {
4029
title: LocalizationKey;
41-
arrowButtonText: LocalizationKey;
42-
arrowButtonEmptyText: LocalizationKey;
30+
switchPlansLabel: LocalizationKey;
31+
newSubscriptionLabel: LocalizationKey;
32+
manageSubscriptionLabel: LocalizationKey;
4333
}) {
4434
const localizationRoot = useSubscriberTypeLocalizationRoot();
4535
const subscriberType = useSubscriberTypeContext();
46-
const { subscriptionItems } = useSubscription();
36+
const { subscriptionItems, data: subscription } = useSubscription();
37+
const canManageBilling =
38+
useProtect(has => has({ permission: 'org:sys_billing:manage' })) || subscriberType === 'user';
4739
const { navigate } = useRouter();
4840
const { commerceSettings } = useEnvironment();
41+
const { openSubscriptionDetails } = usePlansContext();
42+
43+
const billingPlansExist =
44+
(commerceSettings.billing.user.hasPaidPlans && subscriberType === 'user') ||
45+
(commerceSettings.billing.organization.hasPaidPlans && subscriberType === 'organization');
46+
47+
const hasActiveFreePlan = useMemo(() => {
48+
return subscriptionItems.some(sub => isFreePlan(sub.plan) && sub.status === 'active');
49+
}, [subscriptionItems]);
50+
51+
const isManageButtonVisible = canManageBilling && !!subscription && !hasActiveFreePlan;
4952

5053
const sortedSubscriptions = useMemo(
5154
() =>
@@ -88,11 +91,6 @@ export function SubscriptionsList({
8891
`${localizationRoot}.billingPage.subscriptionsListSection.tableHeader__startDate`,
8992
)}
9093
/>
91-
<Th
92-
localizationKey={localizationKeys(
93-
`${localizationRoot}.billingPage.subscriptionsListSection.tableHeader__edit`,
94-
)}
95-
/>
9694
</Tr>
9795
</Thead>
9896
<Tbody>
@@ -107,35 +105,56 @@ export function SubscriptionsList({
107105
</Table>
108106
)}
109107

110-
{(commerceSettings.billing.user.hasPaidPlans && subscriberType === 'user') ||
111-
(commerceSettings.billing.organization.hasPaidPlans && subscriberType === 'organization') ? (
112-
<ProfileSection.ArrowButton
113-
id='subscriptionsList'
114-
textLocalizationKey={subscriptionItems.length > 0 ? arrowButtonText : arrowButtonEmptyText}
115-
sx={[
116-
t => ({
117-
justifyContent: 'start',
118-
height: t.sizes.$8,
119-
}),
120-
]}
121-
leftIcon={subscriptionItems.length > 0 ? ArrowsUpDown : Plus}
122-
leftIconSx={t => ({
123-
width: t.sizes.$4,
124-
height: t.sizes.$4,
125-
})}
126-
onClick={() => void navigate('plans')}
127-
/>
128-
) : null}
108+
<ProfileSection.ButtonGroup id='subscriptionsList'>
109+
{billingPlansExist ? (
110+
<ProfileSection.ArrowButton
111+
id='subscriptionsList'
112+
textLocalizationKey={subscriptionItems.length > 0 ? switchPlansLabel : newSubscriptionLabel}
113+
sx={[
114+
t => ({
115+
justifyContent: 'start',
116+
height: t.sizes.$8,
117+
width: isManageButtonVisible ? 'unset' : undefined,
118+
}),
119+
]}
120+
leftIcon={subscriptionItems.length > 0 ? ArrowsUpDown : Plus}
121+
rightIcon={null}
122+
leftIconSx={t => ({
123+
width: t.sizes.$4,
124+
height: t.sizes.$4,
125+
})}
126+
onClick={() => void navigate('plans')}
127+
/>
128+
) : null}
129+
130+
{isManageButtonVisible ? (
131+
<ProfileSection.ArrowButton
132+
id='subscriptionsList'
133+
textLocalizationKey={manageSubscriptionLabel}
134+
sx={[
135+
t => ({
136+
justifyContent: 'start',
137+
height: t.sizes.$8,
138+
width: 'unset',
139+
}),
140+
]}
141+
rightIcon={null}
142+
leftIcon={CogFilled}
143+
leftIconSx={t => ({
144+
width: t.sizes.$4,
145+
height: t.sizes.$4,
146+
})}
147+
onClick={event => openSubscriptionDetails(event)}
148+
/>
149+
) : null}
150+
</ProfileSection.ButtonGroup>
129151
</ProfileSection.Root>
130152
);
131153
}
132154

133155
function SubscriptionRow({ subscription, length }: { subscription: CommerceSubscriptionItemResource; length: number }) {
134-
const subscriberType = useSubscriberTypeContext();
135-
const canManageBilling =
136-
useProtect(has => has({ permission: 'org:sys_billing:manage' })) || subscriberType === 'user';
137156
const fee = subscription.planPeriod === 'annual' ? subscription.plan.annualFee : subscription.plan.fee;
138-
const { captionForSubscription, openSubscriptionDetails } = usePlansContext();
157+
const { captionForSubscription } = usePlansContext();
139158

140159
const feeFormatted = useMemo(() => {
141160
return normalizeFormatted(fee.amountFormatted);
@@ -204,32 +223,6 @@ function SubscriptionRow({ subscription, length }: { subscription: CommerceSubsc
204223
)}
205224
</Text>
206225
</Td>
207-
<Td
208-
sx={_ => ({
209-
textAlign: 'right',
210-
})}
211-
>
212-
<Button
213-
aria-label='Manage subscription'
214-
onClick={event => openSubscriptionDetails(event)}
215-
variant='bordered'
216-
colorScheme='secondary'
217-
isDisabled={!canManageBilling}
218-
sx={t => ({
219-
width: t.sizes.$6,
220-
height: t.sizes.$6,
221-
})}
222-
>
223-
<Icon
224-
icon={CogFilled}
225-
sx={t => ({
226-
width: t.sizes.$4,
227-
height: t.sizes.$4,
228-
opacity: t.opacity.$inactive,
229-
})}
230-
/>
231-
</Button>
232-
</Td>
233226
</Tr>
234227
);
235228
}

packages/clerk-js/src/ui/components/UserProfile/BillingPage.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,15 @@ const BillingPageInternal = withCardStateProvider(() => {
5353
<TabPanel sx={_ => ({ width: '100%', flexDirection: 'column' })}>
5454
<SubscriptionsList
5555
title={localizationKeys('userProfile.billingPage.subscriptionsListSection.title')}
56-
arrowButtonText={localizationKeys(
56+
switchPlansLabel={localizationKeys(
5757
'userProfile.billingPage.subscriptionsListSection.actionLabel__switchPlan',
5858
)}
59-
arrowButtonEmptyText={localizationKeys(
59+
newSubscriptionLabel={localizationKeys(
6060
'userProfile.billingPage.subscriptionsListSection.actionLabel__newSubscription',
6161
)}
62+
manageSubscriptionLabel={localizationKeys(
63+
'userProfile.billingPage.subscriptionsListSection.actionLabel__manageSubscription',
64+
)}
6265
/>
6366
<PaymentSources />
6467
</TabPanel>

packages/clerk-js/src/ui/customizables/elementDescriptors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([
424424
'profileSectionSubtitleText',
425425
'profileSectionContent',
426426
'profileSectionPrimaryButton',
427+
'profileSectionButtonGroup',
427428
'profilePage',
428429

429430
'formattedPhoneNumber',

packages/clerk-js/src/ui/elements/ArrowBlockButton.tsx

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { ArrowRightIcon } from '../icons';
77
import type { PropsOfComponent, ThemableCssProp } from '../styledSystem';
88

99
type ArrowBlockButtonProps = PropsOfComponent<typeof Button> & {
10-
rightIcon?: React.ComponentType;
10+
rightIcon?: React.ComponentType | null;
1111
rightIconSx?: ThemableCssProp;
1212
leftIcon?: React.ComponentType | React.ReactElement;
1313
leftIconSx?: ThemableCssProp;
@@ -125,23 +125,25 @@ export const ArrowBlockButton = React.forwardRef<HTMLButtonElement, ArrowBlockBu
125125
</Text>
126126
{badge}
127127
</Flex>
128-
<Icon
129-
elementDescriptor={arrowElementDescriptor}
130-
elementId={arrowElementId}
131-
icon={rightIcon}
132-
sx={[
133-
theme => ({
134-
transition: 'all 100ms ease',
135-
minWidth: theme.sizes.$4,
136-
minHeight: theme.sizes.$4,
137-
width: '1em',
138-
height: '1em',
139-
opacity: `var(--arrow-opacity)`,
140-
transform: `var(--arrow-transform)`,
141-
}),
142-
rightIconSx,
143-
]}
144-
/>
128+
{rightIcon && (
129+
<Icon
130+
elementDescriptor={arrowElementDescriptor}
131+
elementId={arrowElementId}
132+
icon={rightIcon}
133+
sx={[
134+
theme => ({
135+
transition: 'all 100ms ease',
136+
minWidth: theme.sizes.$4,
137+
minHeight: theme.sizes.$4,
138+
width: '1em',
139+
height: '1em',
140+
opacity: `var(--arrow-opacity)`,
141+
transform: `var(--arrow-transform)`,
142+
}),
143+
rightIconSx,
144+
]}
145+
/>
146+
)}
145147
</SimpleButton>
146148
);
147149
});

packages/clerk-js/src/ui/elements/Section.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,26 @@ const ProfileSectionArrowButton = forwardRef<HTMLButtonElement, ProfileSectionBu
211211
);
212212
});
213213

214+
type ProfileSectionButtonGroupProps = PropsOfComponent<typeof Flex> & {
215+
id: ProfileSectionId;
216+
disableAnimation?: boolean;
217+
};
218+
219+
const ProfileSectionButtonGroup = (props: ProfileSectionButtonGroupProps) => {
220+
const { children, id, ...rest } = props;
221+
return (
222+
<Flex
223+
elementDescriptor={descriptors.profileSectionButtonGroup}
224+
elementId={descriptors.profileSectionButtonGroup.setId(id)}
225+
justify='between'
226+
gap={2}
227+
{...rest}
228+
>
229+
{children}
230+
</Flex>
231+
);
232+
};
233+
214234
export type ProfileSectionActionMenuItemProps = PropsOfComponent<typeof MenuItem> & {
215235
destructive?: boolean;
216236
leftIcon?: React.ComponentType | React.ReactElement;
@@ -318,6 +338,7 @@ export const ProfileSection = {
318338
Item: ProfileSectionItem,
319339
Button: ProfileSectionButton,
320340
ArrowButton: ProfileSectionArrowButton,
341+
ButtonGroup: ProfileSectionButtonGroup,
321342
ActionMenu: ProfileSectionActionMenu,
322343
ActionMenuItem: ProfileSectionActionMenuItem,
323344
};

packages/localizations/src/ar-SA.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@ export const arSA: LocalizationResource = {
316316
totalPaid: undefined,
317317
},
318318
subscriptionsListSection: {
319+
actionLabel__manageSubscription: undefined,
319320
actionLabel__newSubscription: undefined,
320321
actionLabel__switchPlan: undefined,
321322
tableHeader__edit: undefined,
@@ -993,6 +994,7 @@ export const arSA: LocalizationResource = {
993994
totalPaid: undefined,
994995
},
995996
subscriptionsListSection: {
997+
actionLabel__manageSubscription: undefined,
996998
actionLabel__newSubscription: undefined,
997999
actionLabel__switchPlan: undefined,
9981000
tableHeader__edit: undefined,

packages/localizations/src/be-BY.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,7 @@ export const beBY: LocalizationResource = {
317317
totalPaid: undefined,
318318
},
319319
subscriptionsListSection: {
320+
actionLabel__manageSubscription: undefined,
320321
actionLabel__newSubscription: undefined,
321322
actionLabel__switchPlan: undefined,
322323
tableHeader__edit: undefined,
@@ -1002,6 +1003,7 @@ export const beBY: LocalizationResource = {
10021003
totalPaid: undefined,
10031004
},
10041005
subscriptionsListSection: {
1006+
actionLabel__manageSubscription: undefined,
10051007
actionLabel__newSubscription: undefined,
10061008
actionLabel__switchPlan: undefined,
10071009
tableHeader__edit: undefined,

0 commit comments

Comments
 (0)