Skip to content

Commit 462b5b2

Browse files
authored
chore(clerk-js,types,localizations): Break out subscriptions and plans within User/OrgProfile (#5727)
1 parent b02e766 commit 462b5b2

File tree

22 files changed

+907
-487
lines changed

22 files changed

+907
-487
lines changed

.changeset/orange-moles-wish.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@clerk/localizations': patch
3+
'@clerk/clerk-js': patch
4+
'@clerk/types': patch
5+
---
6+
7+
- Break out subscriptions and plans into different pages within `UserProfile` and `OrgProfile`
8+
- Display free plan row when "active" and plan has features
9+
- Tidy up design of subscription rows and badging
10+
- Adds `SubscriptionDetails` support for plans without a current subscription

packages/clerk-js/bundlewatch.config.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
{ "path": "./dist/pricingTable*.js", "maxSize": "4.02KB" },
2424
{ "path": "./dist/checkout*.js", "maxSize": "4.92KB" },
2525
{ "path": "./dist/paymentSources*.js", "maxSize": "8.5KB" },
26-
{ "path": "./dist/up-billing-page*.js", "maxSize": "1.78KB" },
27-
{ "path": "./dist/op-billing-page*.js", "maxSize": "1.76KB" },
26+
{ "path": "./dist/up-billing-page*.js", "maxSize": "2.4KB" },
27+
{ "path": "./dist/op-billing-page*.js", "maxSize": "2.4KB" },
2828
{ "path": "./dist/sessionTasks*.js", "maxSize": "1KB" }
2929
]
3030
}
Lines changed: 82 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import {
22
__experimental_PricingTableContext,
33
InvoicesContextProvider,
4+
PlansContextProvider,
45
SubscriberTypeContext,
56
useSubscriptions,
6-
withPlans,
77
} from '../../contexts';
8-
import { Col, descriptors, Heading, Hr, localizationKeys } from '../../customizables';
8+
import { Button, Col, descriptors, localizationKeys } from '../../customizables';
99
import {
1010
Card,
1111
Header,
@@ -18,6 +18,7 @@ import {
1818
withCardStateProvider,
1919
} from '../../elements';
2020
import { useTabState } from '../../hooks/useTabState';
21+
import { useRouter } from '../../router';
2122
import { InvoicesList } from '../Invoices';
2223
import { __experimental_PaymentSources } from '../PaymentSources/PaymentSources';
2324
import { __experimental_PricingTable } from '../PricingTable';
@@ -29,95 +30,98 @@ const orgTabMap = {
2930
2: 'payment-methods',
3031
} as const;
3132

32-
const OrganizationBillingPageInternal = withPlans(
33-
withCardStateProvider(() => {
34-
const card = useCardState();
35-
const { data: subscriptions } = useSubscriptions('org');
36-
const { selectedTab, handleTabChange } = useTabState(orgTabMap);
33+
const OrganizationBillingPageInternal = withCardStateProvider(() => {
34+
const card = useCardState();
35+
const { data: subscriptions } = useSubscriptions('org');
36+
const { selectedTab, handleTabChange } = useTabState(orgTabMap);
37+
const { navigate } = useRouter();
38+
if (!Array.isArray(subscriptions?.data)) {
39+
return null;
40+
}
3741

38-
if (!Array.isArray(subscriptions?.data)) {
39-
return null;
40-
}
41-
42-
return (
42+
return (
43+
<Col
44+
elementDescriptor={descriptors.page}
45+
sx={t => ({ gap: t.space.$8, color: t.colors.$colorText })}
46+
>
4347
<Col
44-
elementDescriptor={descriptors.page}
45-
sx={t => ({ gap: t.space.$8, color: t.colors.$colorText })}
48+
elementDescriptor={descriptors.profilePage}
49+
elementId={descriptors.profilePage.setId('billing')}
50+
gap={4}
4651
>
47-
<Col
48-
elementDescriptor={descriptors.profilePage}
49-
elementId={descriptors.profilePage.setId('billing')}
50-
gap={4}
51-
>
52-
<Header.Root>
53-
<Header.Title
54-
localizationKey={localizationKeys('userProfile.__experimental_billingPage.title')}
55-
textVariant='h2'
56-
/>
57-
</Header.Root>
58-
59-
<Card.Alert>{card.error}</Card.Alert>
52+
<Header.Root>
53+
<Header.Title
54+
localizationKey={localizationKeys('userProfile.__experimental_billingPage.title')}
55+
textVariant='h2'
56+
/>
57+
</Header.Root>
6058

61-
<Tabs
62-
value={selectedTab}
63-
onChange={handleTabChange}
64-
>
65-
<TabsList sx={t => ({ gap: t.space.$6 })}>
66-
<Tab
67-
localizationKey={
68-
subscriptions.data.length > 0
69-
? localizationKeys('userProfile.__experimental_billingPage.start.headerTitle__subscriptions')
70-
: localizationKeys('userProfile.__experimental_billingPage.start.headerTitle__plans')
71-
}
72-
/>
73-
<Tab
74-
localizationKey={localizationKeys('userProfile.__experimental_billingPage.start.headerTitle__invoices')}
75-
/>
76-
<Tab
77-
localizationKey={localizationKeys(
78-
'userProfile.__experimental_billingPage.start.headerTitle__paymentMethods',
79-
)}
80-
/>
81-
</TabsList>
82-
<TabPanels>
83-
<TabPanel sx={{ width: '100%', flexDirection: 'column' }}>
84-
{subscriptions.data.length > 0 && (
85-
<>
86-
<SubscriptionsList />
87-
<Hr sx={t => ({ marginBlock: t.space.$6 })} />
88-
<Heading
89-
textVariant='h3'
90-
as='h2'
91-
localizationKey='Available Plans'
92-
sx={t => ({ marginBlockEnd: t.space.$4, fontWeight: t.fontWeights.$medium })}
93-
/>
94-
</>
95-
)}
59+
<Card.Alert>{card.error}</Card.Alert>
9660

61+
<Tabs
62+
value={selectedTab}
63+
onChange={handleTabChange}
64+
>
65+
<TabsList sx={t => ({ gap: t.space.$6 })}>
66+
<Tab
67+
localizationKey={
68+
subscriptions.data.length > 0
69+
? localizationKeys('userProfile.__experimental_billingPage.start.headerTitle__subscriptions')
70+
: localizationKeys('userProfile.__experimental_billingPage.start.headerTitle__plans')
71+
}
72+
/>
73+
<Tab
74+
localizationKey={localizationKeys('userProfile.__experimental_billingPage.start.headerTitle__invoices')}
75+
/>
76+
<Tab
77+
localizationKey={localizationKeys(
78+
'userProfile.__experimental_billingPage.start.headerTitle__paymentMethods',
79+
)}
80+
/>
81+
</TabsList>
82+
<TabPanels>
83+
<TabPanel sx={{ width: '100%', flexDirection: 'column' }}>
84+
{subscriptions.data.length > 0 ? (
85+
<>
86+
<SubscriptionsList />
87+
<Button
88+
localizationKey='View all plans'
89+
hasArrow
90+
variant='ghost'
91+
onClick={() => navigate('plans')}
92+
sx={t => ({
93+
width: 'fit-content',
94+
marginTop: t.space.$4,
95+
})}
96+
/>
97+
</>
98+
) : (
9799
<__experimental_PricingTableContext.Provider value={{ componentName: 'PricingTable', mode: 'modal' }}>
98100
<__experimental_PricingTable />
99101
</__experimental_PricingTableContext.Provider>
100-
</TabPanel>
101-
<TabPanel sx={{ width: '100%' }}>
102-
<InvoicesContextProvider>
103-
<InvoicesList />
104-
</InvoicesContextProvider>
105-
</TabPanel>
106-
<TabPanel sx={{ width: '100%' }}>
107-
<__experimental_PaymentSources />
108-
</TabPanel>
109-
</TabPanels>
110-
</Tabs>
111-
</Col>
102+
)}
103+
</TabPanel>
104+
<TabPanel sx={{ width: '100%' }}>
105+
<InvoicesContextProvider>
106+
<InvoicesList />
107+
</InvoicesContextProvider>
108+
</TabPanel>
109+
<TabPanel sx={{ width: '100%' }}>
110+
<__experimental_PaymentSources />
111+
</TabPanel>
112+
</TabPanels>
113+
</Tabs>
112114
</Col>
113-
);
114-
}),
115-
);
115+
</Col>
116+
);
117+
});
116118

117119
export const OrganizationBillingPage = () => {
118120
return (
119121
<SubscriberTypeContext.Provider value='org'>
120-
<OrganizationBillingPageInternal />
122+
<PlansContextProvider>
123+
<OrganizationBillingPageInternal />
124+
</PlansContextProvider>
121125
</SubscriberTypeContext.Provider>
122126
);
123127
};
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { __experimental_PricingTableContext, PlansContextProvider, SubscriberTypeContext } from '../../contexts';
2+
import { Header } from '../../elements';
3+
import { useRouter } from '../../router';
4+
import { __experimental_PricingTable } from '../PricingTable/PricingTable';
5+
6+
const OrganizationPlansPageInternal = () => {
7+
const { navigate } = useRouter();
8+
9+
return (
10+
<>
11+
<Header.Root sx={t => ({ marginBlockEnd: t.space.$4 })}>
12+
<Header.BackLink onClick={() => void navigate('../', { searchParams: new URLSearchParams('tab=plans') })}>
13+
<Header.Title
14+
localizationKey='Available Plans'
15+
textVariant='h2'
16+
/>
17+
</Header.BackLink>
18+
</Header.Root>
19+
20+
<__experimental_PricingTableContext.Provider value={{ componentName: 'PricingTable', mode: 'modal' }}>
21+
<__experimental_PricingTable />
22+
</__experimental_PricingTableContext.Provider>
23+
</>
24+
);
25+
};
26+
27+
export const OrganizationPlansPage = () => {
28+
return (
29+
<SubscriberTypeContext.Provider value='org'>
30+
<PlansContextProvider>
31+
<OrganizationPlansPageInternal />
32+
</PlansContextProvider>
33+
</SubscriberTypeContext.Provider>
34+
);
35+
};

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Route, Switch } from '../../router';
77
import { OrganizationGeneralPage } from './OrganizationGeneralPage';
88
import { OrganizationInvoicePage } from './OrganizationInvoicePage';
99
import { OrganizationMembers } from './OrganizationMembers';
10+
import { OrganizationPlansPage } from './OrganizationPlansPage';
1011

1112
const OrganizationBillingPage = lazy(() =>
1213
import(/* webpackChunkName: "op-billing-page"*/ './OrganizationBillingPage').then(module => ({
@@ -67,6 +68,12 @@ export const OrganizationProfileRoutes = () => {
6768
<OrganizationBillingPage />
6869
</Suspense>
6970
</Route>
71+
<Route path='plans'>
72+
{/* TODO(@commerce): Should this be lazy loaded ? */}
73+
<Suspense fallback={''}>
74+
<OrganizationPlansPage />
75+
</Suspense>
76+
</Route>
7077
<Route path='invoice/:invoiceId'>
7178
{/* TODO(@commerce): Should this be lazy loaded ? */}
7279
<Suspense fallback={''}>
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { useClerk } from '@clerk/shared/react';
2+
3+
import { ORGANIZATION_PROFILE_CARD_SCROLLBOX_ID, USER_PROFILE_CARD_SCROLLBOX_ID } from '../../constants';
4+
import { usePlansContext, usePricingTableContext, useSubscriberTypeContext } from '../../contexts';
5+
import { Button, Col, Flex, Icon, localizationKeys, Table, Tbody, Td, Text, Th, Thead, Tr } from '../../customizables';
6+
import { Plans } from '../../icons';
7+
import { InternalThemeProvider } from '../../styledSystem';
8+
9+
export const FreePlanRow = () => {
10+
const clerk = useClerk();
11+
const { mode = 'mounted' } = usePricingTableContext();
12+
const subscriberType = useSubscriberTypeContext();
13+
14+
const { isLoading, defaultFreePlan, isDefaultPlanImplicitlyActive } = usePlansContext();
15+
16+
const handleSelect = () => {
17+
if (!defaultFreePlan) {
18+
return;
19+
}
20+
21+
clerk.__internal_openSubscriptionDetails({
22+
plan: defaultFreePlan,
23+
subscriberType: subscriberType,
24+
portalId:
25+
mode === 'modal'
26+
? subscriberType === 'user'
27+
? USER_PROFILE_CARD_SCROLLBOX_ID
28+
: ORGANIZATION_PROFILE_CARD_SCROLLBOX_ID
29+
: undefined,
30+
});
31+
};
32+
33+
if (isLoading || !defaultFreePlan || !isDefaultPlanImplicitlyActive || defaultFreePlan.features.length === 0) {
34+
return null;
35+
}
36+
37+
return (
38+
<InternalThemeProvider>
39+
<Table
40+
tableHeadVisuallyHidden
41+
sx={t => ({
42+
marginBlockEnd: t.sizes.$4,
43+
'tr > td': {
44+
paddingInline: t.sizes.$4,
45+
paddingBlock: t.sizes.$2,
46+
},
47+
'tr > th': {
48+
paddingInline: t.sizes.$4,
49+
paddingBlock: t.sizes.$2,
50+
},
51+
})}
52+
>
53+
<Thead>
54+
<Tr>
55+
<Th>Plan</Th>
56+
<Th>Start date</Th>
57+
<Th>Edit</Th>
58+
</Tr>
59+
</Thead>
60+
<Tbody>
61+
<Tr>
62+
<Td>
63+
<Col gap={1}>
64+
<Flex
65+
align='center'
66+
gap={1}
67+
>
68+
<Icon
69+
icon={Plans}
70+
sx={t => ({
71+
width: t.sizes.$4,
72+
height: t.sizes.$4,
73+
opacity: t.opacity.$inactive,
74+
})}
75+
/>
76+
<Text
77+
variant='subtitle'
78+
sx={t => ({ marginRight: t.sizes.$1 })}
79+
>
80+
{defaultFreePlan.name}
81+
</Text>
82+
</Flex>
83+
<Text
84+
variant='caption'
85+
colorScheme='secondary'
86+
localizationKey={localizationKeys('__experimental_commerce.defaultFreePlanActive')}
87+
/>
88+
</Col>
89+
</Td>
90+
<Td
91+
sx={_ => ({
92+
textAlign: 'right',
93+
})}
94+
>
95+
<Button
96+
onClick={() => handleSelect()}
97+
variant='bordered'
98+
colorScheme='secondary'
99+
localizationKey={localizationKeys('__experimental_commerce.viewFeatures')}
100+
/>
101+
</Td>
102+
</Tr>
103+
</Tbody>
104+
</Table>
105+
</InternalThemeProvider>
106+
);
107+
};

0 commit comments

Comments
 (0)