Skip to content

Commit cd59c0e

Browse files
authored
chore(clerk-js,shared): Expose experimental useSubscription (#6317)
1 parent 52a198b commit cd59c0e

File tree

25 files changed

+597
-191
lines changed

25 files changed

+597
-191
lines changed

.changeset/rare-readers-cough.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@clerk/clerk-js': minor
3+
'@clerk/types': minor
4+
---
5+
6+
[Billing Beta] Introduce top level subscription.
7+
8+
Updated `CommerceSubscriptionJSON` to describe the top level subscription and renamed the existing type to `CommerceSubscriptionItemJSON`.
9+
Deprecated `billing.getSubscriptions()` in favour of `billing.getSubscription`.

.changeset/wide-loops-decide.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@clerk/clerk-js': minor
3+
'@clerk/shared': minor
4+
---
5+
6+
[Billing Beta] Replace `useSubscriptionItems` with `useSubscription`.

.typedoc/__tests__/__snapshots__/file-structure.test.ts.snap

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = `
5252
"types/commerce-statement-status.mdx",
5353
"types/commerce-statement-totals-json.mdx",
5454
"types/commerce-statement-totals.mdx",
55+
"types/commerce-subscription-item-json.mdx",
56+
"types/commerce-subscription-item-resource.mdx",
5557
"types/commerce-subscription-json.mdx",
5658
"types/commerce-subscription-plan-period.mdx",
5759
"types/commerce-subscription-resource.mdx",
@@ -65,6 +67,7 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = `
6567
"types/get-payment-sources-params.mdx",
6668
"types/get-plans-params.mdx",
6769
"types/get-statements-params.mdx",
70+
"types/get-subscription-params.mdx",
6871
"types/get-subscriptions-params.mdx",
6972
"types/get-token.mdx",
7073
"types/id-selectors.mdx",

packages/clerk-js/src/core/modules/commerce/CommerceBilling.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ import type {
88
CommercePlanResource,
99
CommerceStatementJSON,
1010
CommerceStatementResource,
11+
CommerceSubscriptionItemJSON,
12+
CommerceSubscriptionItemResource,
1113
CommerceSubscriptionJSON,
1214
CommerceSubscriptionResource,
1315
CreateCheckoutParams,
1416
GetPaymentAttemptsParams,
1517
GetPlansParams,
1618
GetStatementsParams,
19+
GetSubscriptionParams,
1720
GetSubscriptionsParams,
1821
} from '@clerk/types';
1922

@@ -25,6 +28,7 @@ import {
2528
CommercePlan,
2629
CommerceStatement,
2730
CommerceSubscription,
31+
CommerceSubscriptionItem,
2832
} from '../../resources/internal';
2933

3034
export class CommerceBilling implements CommerceBillingNamespace {
@@ -45,6 +49,7 @@ export class CommerceBilling implements CommerceBillingNamespace {
4549
});
4650
};
4751

52+
// Inconsistent API
4853
getPlan = async (params: { id: string }): Promise<CommercePlanResource> => {
4954
const plan = (await BaseResource._fetch({
5055
path: `/commerce/plans/${params.id}`,
@@ -53,9 +58,16 @@ export class CommerceBilling implements CommerceBillingNamespace {
5358
return new CommercePlan(plan);
5459
};
5560

61+
getSubscription = async (params: GetSubscriptionParams): Promise<CommerceSubscriptionResource> => {
62+
return await BaseResource._fetch({
63+
path: params.orgId ? `/organizations/${params.orgId}/commerce/subscription` : `/me/commerce/subscription`,
64+
method: 'GET',
65+
}).then(res => new CommerceSubscription(res?.response as CommerceSubscriptionJSON));
66+
};
67+
5668
getSubscriptions = async (
5769
params: GetSubscriptionsParams,
58-
): Promise<ClerkPaginatedResponse<CommerceSubscriptionResource>> => {
70+
): Promise<ClerkPaginatedResponse<CommerceSubscriptionItemResource>> => {
5971
const { orgId, ...rest } = params;
6072

6173
return await BaseResource._fetch({
@@ -64,11 +76,11 @@ export class CommerceBilling implements CommerceBillingNamespace {
6476
search: convertPageToOffsetSearchParams(rest),
6577
}).then(res => {
6678
const { data: subscriptions, total_count } =
67-
res?.response as unknown as ClerkPaginatedResponse<CommerceSubscriptionJSON>;
79+
res?.response as unknown as ClerkPaginatedResponse<CommerceSubscriptionItemJSON>;
6880

6981
return {
7082
total_count,
71-
data: subscriptions.map(subscription => new CommerceSubscription(subscription)),
83+
data: subscriptions.map(subscription => new CommerceSubscriptionItem(subscription)),
7284
};
7385
});
7486
};

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,27 @@ import type {
33
CommercePaymentChargeType,
44
CommercePaymentJSON,
55
CommercePaymentResource,
6+
CommercePaymentSourceResource,
67
CommercePaymentStatus,
8+
CommerceSubscriptionItemResource,
79
} from '@clerk/types';
810

911
import { commerceMoneyFromJSON } from '../../utils';
1012
import { unixEpochToDate } from '../../utils/date';
11-
import { BaseResource, CommercePaymentSource, CommerceSubscription } from './internal';
13+
import { BaseResource, CommercePaymentSource, CommerceSubscriptionItem } from './internal';
1214

1315
export class CommercePayment extends BaseResource implements CommercePaymentResource {
1416
id!: string;
1517
amount!: CommerceMoney;
1618
failedAt?: Date;
1719
paidAt?: Date;
1820
updatedAt!: Date;
19-
paymentSource!: CommercePaymentSource;
20-
subscription!: CommerceSubscription;
21-
subscriptionItem!: CommerceSubscription;
21+
paymentSource!: CommercePaymentSourceResource;
22+
/**
23+
* @deprecated
24+
*/
25+
subscription!: CommerceSubscriptionItemResource;
26+
subscriptionItem!: CommerceSubscriptionItemResource;
2227
chargeType!: CommercePaymentChargeType;
2328
status!: CommercePaymentStatus;
2429

@@ -38,8 +43,8 @@ export class CommercePayment extends BaseResource implements CommercePaymentReso
3843
this.failedAt = data.failed_at ? unixEpochToDate(data.failed_at) : undefined;
3944
this.updatedAt = unixEpochToDate(data.updated_at);
4045
this.paymentSource = new CommercePaymentSource(data.payment_source);
41-
this.subscription = new CommerceSubscription(data.subscription);
42-
this.subscriptionItem = new CommerceSubscription(data.subscription_item);
46+
this.subscription = new CommerceSubscriptionItem(data.subscription);
47+
this.subscriptionItem = new CommerceSubscriptionItem(data.subscription_item);
4348
this.chargeType = data.charge_type;
4449
this.status = data.status;
4550
return this;

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

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type {
22
CancelSubscriptionParams,
33
CommerceMoney,
4+
CommerceSubscriptionItemJSON,
5+
CommerceSubscriptionItemResource,
46
CommerceSubscriptionJSON,
57
CommerceSubscriptionPlanPeriod,
68
CommerceSubscriptionResource,
@@ -14,6 +16,46 @@ import { commerceMoneyFromJSON } from '../../utils';
1416
import { BaseResource, CommercePlan, DeletedObject } from './internal';
1517

1618
export class CommerceSubscription extends BaseResource implements CommerceSubscriptionResource {
19+
id!: string;
20+
status!: Extract<CommerceSubscriptionStatus, 'active' | 'past_due'>;
21+
activeAt!: Date;
22+
createdAt!: Date;
23+
pastDueAt!: Date | null;
24+
updatedAt!: Date | null;
25+
nextPayment: {
26+
amount: CommerceMoney;
27+
date: Date;
28+
} | null = null;
29+
subscriptionItems!: CommerceSubscriptionItemResource[];
30+
31+
constructor(data: CommerceSubscriptionJSON) {
32+
super();
33+
this.fromJSON(data);
34+
}
35+
36+
protected fromJSON(data: CommerceSubscriptionJSON | null): this {
37+
if (!data) {
38+
return this;
39+
}
40+
41+
this.id = data.id;
42+
this.status = data.status;
43+
this.createdAt = unixEpochToDate(data.created_at);
44+
this.updatedAt = data.updated_at ? unixEpochToDate(data.updated_at) : null;
45+
this.activeAt = unixEpochToDate(data.active_at);
46+
this.pastDueAt = data.past_due_at ? unixEpochToDate(data.past_due_at) : null;
47+
this.nextPayment = data.next_payment
48+
? {
49+
amount: commerceMoneyFromJSON(data.next_payment.amount),
50+
date: unixEpochToDate(data.next_payment.date),
51+
}
52+
: null;
53+
this.subscriptionItems = (data.subscription_items || []).map(item => new CommerceSubscriptionItem(item));
54+
return this;
55+
}
56+
}
57+
58+
export class CommerceSubscriptionItem extends BaseResource implements CommerceSubscriptionItemResource {
1759
id!: string;
1860
paymentSourceId!: string;
1961
plan!: CommercePlan;
@@ -27,17 +69,18 @@ export class CommerceSubscription extends BaseResource implements CommerceSubscr
2769
periodStart!: number;
2870
periodEnd!: number;
2971
canceledAt!: number | null;
72+
//TODO(@COMMERCE): Why can this be undefined ?
3073
amount?: CommerceMoney;
3174
credit?: {
3275
amount: CommerceMoney;
3376
};
3477

35-
constructor(data: CommerceSubscriptionJSON) {
78+
constructor(data: CommerceSubscriptionItemJSON) {
3679
super();
3780
this.fromJSON(data);
3881
}
3982

40-
protected fromJSON(data: CommerceSubscriptionJSON | null): this {
83+
protected fromJSON(data: CommerceSubscriptionItemJSON | null): this {
4184
if (!data) {
4285
return this;
4386
}

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import type {
22
AddMemberParams,
33
ClerkPaginatedResponse,
44
ClerkResourceReloadParams,
5-
CommerceSubscriptionJSON,
6-
CommerceSubscriptionResource,
5+
CommerceSubscriptionItemJSON,
6+
CommerceSubscriptionItemResource,
77
CreateOrganizationParams,
88
GetDomainsParams,
99
GetInvitationsParams,
@@ -32,7 +32,7 @@ import type {
3232
import { convertPageToOffsetSearchParams } from '../../utils/convertPageToOffsetSearchParams';
3333
import { unixEpochToDate } from '../../utils/date';
3434
import { addPaymentSource, getPaymentSources, initializePaymentSource } from '../modules/commerce';
35-
import { BaseResource, CommerceSubscription, OrganizationInvitation, OrganizationMembership } from './internal';
35+
import { BaseResource, CommerceSubscriptionItem, OrganizationInvitation, OrganizationMembership } from './internal';
3636
import { OrganizationDomain } from './OrganizationDomain';
3737
import { OrganizationMembershipRequest } from './OrganizationMembershipRequest';
3838
import { Role } from './Role';
@@ -235,18 +235,18 @@ export class Organization extends BaseResource implements OrganizationResource {
235235

236236
getSubscriptions = async (
237237
getSubscriptionsParams?: GetSubscriptionsParams,
238-
): Promise<ClerkPaginatedResponse<CommerceSubscriptionResource>> => {
238+
): Promise<ClerkPaginatedResponse<CommerceSubscriptionItemResource>> => {
239239
return await BaseResource._fetch({
240240
path: `/organizations/${this.id}/commerce/subscriptions`,
241241
method: 'GET',
242242
search: convertPageToOffsetSearchParams(getSubscriptionsParams),
243243
}).then(res => {
244244
const { data: subscriptions, total_count } =
245-
res?.response as unknown as ClerkPaginatedResponse<CommerceSubscriptionJSON>;
245+
res?.response as unknown as ClerkPaginatedResponse<CommerceSubscriptionItemJSON>;
246246

247247
return {
248248
total_count,
249-
data: subscriptions.map(subscription => new CommerceSubscription(subscription)),
249+
data: subscriptions.map(subscription => new CommerceSubscriptionItem(subscription)),
250250
};
251251
});
252252
};

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,29 @@ import { useClerk } from '@clerk/shared/react';
22
import type { CommercePlanResource, CommerceSubscriptionPlanPeriod, PricingTableProps } from '@clerk/types';
33
import { useEffect, useMemo, useState } from 'react';
44

5-
import { usePaymentMethods, usePlans, usePlansContext, usePricingTableContext, useSubscriptions } from '../../contexts';
6-
import { Flow } from '../../customizables';
5+
import { Flow } from '@/ui/customizables/Flow';
6+
7+
import { usePaymentMethods, usePlans, usePlansContext, usePricingTableContext, useSubscription } from '../../contexts';
78
import { PricingTableDefault } from './PricingTableDefault';
89
import { PricingTableMatrix } from './PricingTableMatrix';
910

1011
const PricingTableRoot = (props: PricingTableProps) => {
1112
const clerk = useClerk();
1213
const { mode = 'mounted', signInMode = 'redirect' } = usePricingTableContext();
1314
const isCompact = mode === 'modal';
14-
const { data: subscriptions } = useSubscriptions();
15+
const { subscriptionItems } = useSubscription();
1516
const { data: plans } = usePlans();
1617
const { handleSelectPlan } = usePlansContext();
1718

1819
const defaultPlanPeriod = useMemo(() => {
1920
if (isCompact) {
20-
const upcomingSubscription = subscriptions?.find(sub => sub.status === 'upcoming');
21+
const upcomingSubscription = subscriptionItems?.find(sub => sub.status === 'upcoming');
2122
if (upcomingSubscription) {
2223
return upcomingSubscription.planPeriod;
2324
}
2425

2526
// don't pay attention to the default plan
26-
const activeSubscription = subscriptions?.find(
27+
const activeSubscription = subscriptionItems?.find(
2728
sub => !sub.canceledAtDate && sub.status === 'active' && !sub.plan.isDefault,
2829
);
2930
if (activeSubscription) {
@@ -32,7 +33,7 @@ const PricingTableRoot = (props: PricingTableProps) => {
3233
}
3334

3435
return 'annual';
35-
}, [isCompact, subscriptions]);
36+
}, [isCompact, subscriptionItems]);
3637

3738
const [planPeriod, setPlanPeriod] = useState<CommerceSubscriptionPlanPeriod>(defaultPlanPeriod);
3839

0 commit comments

Comments
 (0)