Skip to content

Commit f8887b2

Browse files
authored
feat(clerk-js): Support upcoming and expiring plans (#5601)
1 parent a7f3ebc commit f8887b2

Some content is hidden

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

64 files changed

+557
-310
lines changed

.changeset/yummy-bats-take.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+
Updates `PricingTable` and `SubscriptionDetailDrawer` to handle `upcoming` and "expiring" subscriptions.

packages/clerk-js/bundlewatch.config.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"files": [
33
{ "path": "./dist/clerk.js", "maxSize": "590kB" },
4-
{ "path": "./dist/clerk.browser.js", "maxSize": "73.83KB" },
4+
{ "path": "./dist/clerk.browser.js", "maxSize": "73.88KB" },
55
{ "path": "./dist/clerk.headless*.js", "maxSize": "55KB" },
66
{ "path": "./dist/ui-common*.js", "maxSize": "99KB" },
77
{ "path": "./dist/vendors*.js", "maxSize": "36KB" },
@@ -19,7 +19,7 @@
1919
{ "path": "./dist/onetap*.js", "maxSize": "1KB" },
2020
{ "path": "./dist/waitlist*.js", "maxSize": "1.3KB" },
2121
{ "path": "./dist/keylessPrompt*.js", "maxSize": "5.9KB" },
22-
{ "path": "./dist/pricingTable*.js", "maxSize": "5KB" },
22+
{ "path": "./dist/pricingTable*.js", "maxSize": "5.28KB" },
2323
{ "path": "./dist/checkout*.js", "maxSize": "3KB" },
2424
{ "path": "./dist/paymentSources*.js", "maxSize": "8.2KB" },
2525
{ "path": "./dist/up-billing-page*.js", "maxSize": "1KB" },

packages/clerk-js/src/core/resources/CommercePlan.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ export class __experimental_CommercePlan extends BaseResource implements __exper
1212
currencySymbol!: string;
1313
currency!: string;
1414
description!: string;
15+
isDefault!: boolean;
1516
isRecurring!: boolean;
1617
hasBaseFee!: boolean;
1718
payerType!: string[];
1819
publiclyVisible!: boolean;
1920
slug!: string;
2021
avatarUrl!: string;
2122
features!: __experimental_CommerceFeature[];
22-
subscriptionIdForCurrentSubscriber: string | undefined;
2323

2424
constructor(data: __experimental_CommercePlanJSON) {
2525
super();
@@ -40,6 +40,7 @@ export class __experimental_CommercePlan extends BaseResource implements __exper
4040
this.currencySymbol = data.currency_symbol;
4141
this.currency = data.currency;
4242
this.description = data.description;
43+
this.isDefault = data.is_default;
4344
this.isRecurring = data.is_recurring;
4445
this.hasBaseFee = data.has_base_fee;
4546
this.payerType = data.payer_type;

packages/clerk-js/src/core/resources/CommerceSubscription.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ export class __experimental_CommerceSubscription
1818
plan!: __experimental_CommercePlan;
1919
planPeriod!: __experimental_CommerceSubscriptionPlanPeriod;
2020
status!: __experimental_CommerceSubscriptionStatus;
21-
21+
periodStart!: number;
22+
periodEnd!: number;
23+
canceledAt!: number | null;
2224
constructor(data: __experimental_CommerceSubscriptionJSON) {
2325
super();
2426
this.fromJSON(data);
@@ -34,7 +36,9 @@ export class __experimental_CommerceSubscription
3436
this.plan = new __experimental_CommercePlan(data.plan);
3537
this.planPeriod = data.plan_period;
3638
this.status = data.status;
37-
39+
this.periodStart = data.period_start;
40+
this.periodEnd = data.period_end;
41+
this.canceledAt = data.canceled_at;
3842
return this;
3943
}
4044

Lines changed: 65 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { __experimental_PaymentSourcesContext, __experimental_PricingTableContext } from '../../contexts';
1+
import {
2+
__experimental_PaymentSourcesContext,
3+
__experimental_PricingTableContext,
4+
usePlansContext,
5+
withPlans,
6+
} from '../../contexts';
27
import { Col, descriptors, localizationKeys } from '../../customizables';
38
import {
49
Card,
@@ -14,61 +19,68 @@ import {
1419
import { __experimental_PaymentSources } from '../PaymentSources/PaymentSources';
1520
import { __experimental_PricingTable } from '../PricingTable';
1621

17-
export const OrganizationBillingPage = withCardStateProvider(() => {
18-
const card = useCardState();
22+
export const OrganizationBillingPage = withPlans(
23+
withCardStateProvider(() => {
24+
const card = useCardState();
25+
const { subscriptions } = usePlansContext();
1926

20-
return (
21-
<Col
22-
elementDescriptor={descriptors.page}
23-
sx={t => ({ gap: t.space.$8, color: t.colors.$colorText })}
24-
>
27+
return (
2528
<Col
26-
elementDescriptor={descriptors.profilePage}
27-
elementId={descriptors.profilePage.setId('billing')}
28-
gap={4}
29+
elementDescriptor={descriptors.page}
30+
sx={t => ({ gap: t.space.$8, color: t.colors.$colorText })}
2931
>
30-
<Header.Root>
31-
<Header.Title
32-
localizationKey={localizationKeys('userProfile.__experimental_billingPage.title')}
33-
textVariant='h2'
34-
/>
35-
</Header.Root>
32+
<Col
33+
elementDescriptor={descriptors.profilePage}
34+
elementId={descriptors.profilePage.setId('billing')}
35+
gap={4}
36+
>
37+
<Header.Root>
38+
<Header.Title
39+
localizationKey={localizationKeys('userProfile.__experimental_billingPage.title')}
40+
textVariant='h2'
41+
/>
42+
</Header.Root>
3643

37-
<Card.Alert>{card.error}</Card.Alert>
44+
<Card.Alert>{card.error}</Card.Alert>
3845

39-
<Tabs>
40-
<TabsList sx={t => ({ gap: t.space.$6 })}>
41-
<Tab
42-
localizationKey={localizationKeys('userProfile.__experimental_billingPage.start.headerTitle__plans')}
43-
/>
44-
<Tab
45-
localizationKey={localizationKeys('userProfile.__experimental_billingPage.start.headerTitle__invoices')}
46-
/>
47-
<Tab
48-
localizationKey={localizationKeys(
49-
'userProfile.__experimental_billingPage.start.headerTitle__paymentSources',
50-
)}
51-
/>
52-
</TabsList>
53-
<TabPanels>
54-
<TabPanel sx={{ width: '100%' }}>
55-
<__experimental_PricingTableContext.Provider
56-
value={{ componentName: 'PricingTable', mode: 'modal', subscriberType: 'org' }}
57-
>
58-
<__experimental_PricingTable />
59-
</__experimental_PricingTableContext.Provider>
60-
</TabPanel>
61-
<TabPanel sx={{ width: '100%' }}>Invoices</TabPanel>
62-
<TabPanel sx={{ width: '100%' }}>
63-
<__experimental_PaymentSourcesContext.Provider
64-
value={{ componentName: 'PaymentSources', subscriberType: 'org' }}
65-
>
66-
<__experimental_PaymentSources />
67-
</__experimental_PaymentSourcesContext.Provider>
68-
</TabPanel>
69-
</TabPanels>
70-
</Tabs>
46+
<Tabs>
47+
<TabsList sx={t => ({ gap: t.space.$6 })}>
48+
<Tab
49+
localizationKey={
50+
subscriptions.length > 0
51+
? localizationKeys('userProfile.__experimental_billingPage.start.headerTitle__subscriptions')
52+
: localizationKeys('userProfile.__experimental_billingPage.start.headerTitle__plans')
53+
}
54+
/>
55+
<Tab
56+
localizationKey={localizationKeys('userProfile.__experimental_billingPage.start.headerTitle__invoices')}
57+
/>
58+
<Tab
59+
localizationKey={localizationKeys(
60+
'userProfile.__experimental_billingPage.start.headerTitle__paymentSources',
61+
)}
62+
/>
63+
</TabsList>
64+
<TabPanels>
65+
<TabPanel sx={{ width: '100%' }}>
66+
<__experimental_PricingTableContext.Provider
67+
value={{ componentName: 'PricingTable', mode: 'modal', subscriberType: 'org' }}
68+
>
69+
<__experimental_PricingTable />
70+
</__experimental_PricingTableContext.Provider>
71+
</TabPanel>
72+
<TabPanel sx={{ width: '100%' }}>Invoices</TabPanel>
73+
<TabPanel sx={{ width: '100%' }}>
74+
<__experimental_PaymentSourcesContext.Provider
75+
value={{ componentName: 'PaymentSources', subscriberType: 'org' }}
76+
>
77+
<__experimental_PaymentSources />
78+
</__experimental_PaymentSourcesContext.Provider>
79+
</TabPanel>
80+
</TabPanels>
81+
</Tabs>
82+
</Col>
7183
</Col>
72-
</Col>
73-
);
74-
});
84+
);
85+
}),
86+
);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export const OrganizationProfileRoutes = () => {
6666
<Switch>
6767
<Route index>
6868
<Suspense fallback={''}>
69-
<OrganizationBillingPage />
69+
<OrganizationBillingPage providerProps={{ subscriberType: 'org' }} />
7070
</Suspense>
7171
</Route>
7272
</Switch>

packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@ import type {
88
import { useState } from 'react';
99

1010
import { PROFILE_CARD_SCROLLBOX_ID } from '../../constants';
11-
import { usePricingTableContext } from '../../contexts';
11+
import { usePlansContext, usePricingTableContext } from '../../contexts';
1212
import { AppearanceProvider } from '../../customizables';
13-
import { usePlans } from '../../hooks';
1413
import { PricingTableDefault } from './PricingTableDefault';
1514
import { PricingTableMatrix } from './PricingTableMatrix';
1615
import { SubscriptionDetailDrawer } from './SubscriptionDetailDrawer';
@@ -20,7 +19,7 @@ const PricingTable = (props: __experimental_PricingTableProps) => {
2019
const { mode = 'mounted', subscriberType } = usePricingTableContext();
2120
const isCompact = mode === 'modal';
2221

23-
const { plans, subscriptions, revalidate } = usePlans({ subscriberType });
22+
const { plans, revalidate, activeOrUpcomingSubscription } = usePlansContext();
2423

2524
const [planPeriod, setPlanPeriod] = useState<__experimental_CommerceSubscriptionPlanPeriod>('month');
2625
const [detailSubscription, setDetailSubscription] = useState<__experimental_CommerceSubscriptionResource>();
@@ -31,9 +30,11 @@ const PricingTable = (props: __experimental_PricingTableProps) => {
3130
if (!clerk.isSignedIn) {
3231
void clerk.redirectToSignIn();
3332
}
34-
const activeSubscription = subscriptions.find(sub => sub.id === plan.subscriptionIdForCurrentSubscriber);
35-
if (activeSubscription) {
36-
setDetailSubscription(activeSubscription);
33+
34+
const subscription = activeOrUpcomingSubscription(plan);
35+
36+
if (subscription && !subscription.canceledAt) {
37+
setDetailSubscription(subscription);
3738
setShowSubscriptionDetailDrawer(true);
3839
} else {
3940
clerk.__internal_openCheckout({

0 commit comments

Comments
 (0)