Skip to content

Commit e20fb6b

Browse files
authored
feat(clerk-js): OrgProfile billing page and new subscription methods (#5423)
1 parent 7fac110 commit e20fb6b

Some content is hidden

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

43 files changed

+538
-141
lines changed

.changeset/yellow-hairs-refuse.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@clerk/localizations': patch
3+
'@clerk/clerk-js': patch
4+
'@clerk/shared': patch
5+
'@clerk/types': patch
6+
---
7+
8+
Add billing page to `OrgProfile`, use new `usePlans` hook, and adds new subscription methods

packages/clerk-js/bundlewatch.config.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"files": [
3-
{ "path": "./dist/clerk.js", "maxSize": "580.7kB" },
4-
{ "path": "./dist/clerk.browser.js", "maxSize": "79.25kB" },
3+
{ "path": "./dist/clerk.js", "maxSize": "581.5kB" },
4+
{ "path": "./dist/clerk.browser.js", "maxSize": "79.30kB" },
55
{ "path": "./dist/clerk.headless.js", "maxSize": "55KB" },
66
{ "path": "./dist/ui-common*.js", "maxSize": "96KB" },
77
{ "path": "./dist/vendors*.js", "maxSize": "30KB" },

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

+9
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ import {
8383
createAllowedRedirectOrigins,
8484
createBeforeUnloadTracker,
8585
createPageLifecycle,
86+
disabledCommerceFeature,
8687
disabledOrganizationsFeature,
8788
errorThrower,
8889
generateSignatureWithCoinbaseWallet,
@@ -921,6 +922,14 @@ export class Clerk implements ClerkInterface {
921922

922923
public __experimental_mountPricingTable = (node: HTMLDivElement, props?: __experimental_PricingTableProps): void => {
923924
this.assertComponentsReady(this.#componentControls);
925+
if (disabledCommerceFeature(this, this.environment)) {
926+
if (this.#instanceType === 'development') {
927+
throw new ClerkRuntimeError(warnings.cannotRenderAnyCommerceComponent('PricingTable'), {
928+
code: 'cannot_render_commerce_disabled',
929+
});
930+
}
931+
return;
932+
}
924933
void this.#componentControls.ensureMounted({ preloadHint: 'PricingTable' }).then(controls =>
925934
controls.mountComponent({
926935
name: 'PricingTable',

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

+24-3
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,47 @@ import type {
33
__experimental_CommerceCheckoutJSON,
44
__experimental_CommercePlanResource,
55
__experimental_CommerceProductJSON,
6+
__experimental_CommerceSubscriptionJSON,
7+
__experimental_CommerceSubscriptionResource,
68
__experimental_CreateCheckoutParams,
79
__experimental_GetPlansParams,
810
ClerkPaginatedResponse,
911
} from '@clerk/types';
1012

11-
import { convertPageToOffsetSearchParams } from '../../../utils/convertPageToOffsetSearchParams';
12-
import { __experimental_CommerceCheckout, __experimental_CommercePlan, BaseResource } from '../../resources/internal';
13+
import {
14+
__experimental_CommerceCheckout,
15+
__experimental_CommercePlan,
16+
__experimental_CommerceSubscription,
17+
BaseResource,
18+
} from '../../resources/internal';
1319

1420
export class __experimental_CommerceBilling implements __experimental_CommerceBillingNamespace {
1521
getPlans = async (params?: __experimental_GetPlansParams): Promise<__experimental_CommercePlanResource[]> => {
1622
const { data: products } = (await BaseResource._fetch({
1723
path: `/commerce/products`,
1824
method: 'GET',
19-
search: convertPageToOffsetSearchParams(params),
25+
search: { payerType: params?.subscriberType || '' },
2026
})) as unknown as ClerkPaginatedResponse<__experimental_CommerceProductJSON>;
2127

2228
const defaultProduct = products.find(product => product.is_default);
2329
return defaultProduct?.plans.map(plan => new __experimental_CommercePlan(plan)) || [];
2430
};
2531

32+
getSubscriptions = async (): Promise<ClerkPaginatedResponse<__experimental_CommerceSubscriptionResource>> => {
33+
return await BaseResource._fetch({
34+
path: `/me/subscriptions`,
35+
method: 'GET',
36+
}).then(res => {
37+
const { data: subscriptions, total_count } =
38+
res?.response as unknown as ClerkPaginatedResponse<__experimental_CommerceSubscriptionJSON>;
39+
40+
return {
41+
total_count,
42+
data: subscriptions.map(subscription => new __experimental_CommerceSubscription(subscription)),
43+
};
44+
});
45+
};
46+
2647
startCheckout = async (params: __experimental_CreateCheckoutParams) => {
2748
const json = (
2849
await BaseResource._fetch<__experimental_CommerceCheckoutJSON>({

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {
22
__experimental_CommerceCheckoutJSON,
33
__experimental_CommerceCheckoutResource,
4+
__experimental_CommerceSubscriptionPlanPeriod,
45
__experimental_CommerceTotals,
56
__experimental_ConfirmCheckoutParams,
67
} from '@clerk/types';
@@ -23,7 +24,7 @@ export class __experimental_CommerceCheckout extends BaseResource implements __e
2324
invoice?: __experimental_CommerceInvoice;
2425
paymentSource?: __experimental_CommercePaymentSource;
2526
plan!: __experimental_CommercePlan;
26-
planPeriod!: string;
27+
planPeriod!: __experimental_CommerceSubscriptionPlanPeriod;
2728
status!: string;
2829
subscription?: __experimental_CommerceSubscription;
2930
totals!: __experimental_CommerceTotals;

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

+1-2
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-
isActiveForPayer!: boolean;
1615
isRecurring!: boolean;
1716
hasBaseFee!: boolean;
1817
payerType!: string[];
1918
publiclyVisible!: boolean;
2019
slug!: string;
2120
avatarUrl!: string;
2221
features!: __experimental_CommerceFeature[];
22+
subscriptionIdForCurrentSubscriber: string | undefined;
2323

2424
constructor(data: __experimental_CommercePlanJSON) {
2525
super();
@@ -40,7 +40,6 @@ 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.isActiveForPayer = data.is_active_for_payer;
4443
this.isRecurring = data.is_recurring;
4544
this.hasBaseFee = data.has_base_fee;
4645
this.payerType = data.payer_type;

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

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { BaseResource } from './internal';
1111
*/
1212
export class __experimental_CommerceSettings extends BaseResource implements __experimental_CommerceSettingsResource {
1313
stripePublishableKey: string = '';
14+
enabled: boolean = false;
1415

1516
public constructor(
1617
data: __experimental_CommerceSettingsJSON | __experimental_CommerceSettingsJSONSnapshot | null = null,
@@ -26,12 +27,14 @@ export class __experimental_CommerceSettings extends BaseResource implements __e
2627
return this;
2728
}
2829
this.stripePublishableKey = data.stripe_publishable_key;
30+
this.enabled = data.enabled;
2931
return this;
3032
}
3133

3234
public __internal_toSnapshot(): __experimental_CommerceSettingsJSONSnapshot {
3335
return {
3436
stripe_publishable_key: this.stripePublishableKey,
37+
enabled: this.enabled,
3538
} as unknown as __experimental_CommerceSettingsJSONSnapshot;
3639
}
3740
}

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

+9-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import type {
22
__experimental_CommerceSubscriptionJSON,
3+
__experimental_CommerceSubscriptionPlanPeriod,
34
__experimental_CommerceSubscriptionResource,
5+
__experimental_CommerceSubscriptionStatus,
6+
DeletedObjectJSON,
47
} from '@clerk/types';
58

6-
import { __experimental_CommercePlan, BaseResource } from './internal';
9+
import { __experimental_CommercePlan, BaseResource, DeletedObject } from './internal';
710

811
export class __experimental_CommerceSubscription
912
extends BaseResource
@@ -12,8 +15,8 @@ export class __experimental_CommerceSubscription
1215
id!: string;
1316
paymentSourceId!: string;
1417
plan!: __experimental_CommercePlan;
15-
planPeriod!: string;
16-
status!: string;
18+
planPeriod!: __experimental_CommerceSubscriptionPlanPeriod;
19+
status!: __experimental_CommerceSubscriptionStatus;
1720

1821
constructor(data: __experimental_CommerceSubscriptionJSON) {
1922
super();
@@ -40,7 +43,8 @@ export class __experimental_CommerceSubscription
4043
path: `/me/commerce/subscriptions/${this.id}`,
4144
method: 'DELETE',
4245
})
43-
)?.response;
44-
return json;
46+
)?.response as unknown as DeletedObjectJSON;
47+
48+
return new DeletedObject(json);
4549
}
4650
}

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

+32-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import type {
2+
__experimental_CommerceSubscriptionJSON,
3+
__experimental_CommerceSubscriptionResource,
4+
__experimental_GetSubscriptionsParams,
25
AddMemberParams,
36
ClerkPaginatedResponse,
47
ClerkResourceReloadParams,
@@ -28,7 +31,12 @@ import type {
2831

2932
import { convertPageToOffsetSearchParams } from '../../utils/convertPageToOffsetSearchParams';
3033
import { unixEpochToDate } from '../../utils/date';
31-
import { BaseResource, OrganizationInvitation, OrganizationMembership } from './internal';
34+
import {
35+
__experimental_CommerceSubscription,
36+
BaseResource,
37+
OrganizationInvitation,
38+
OrganizationMembership,
39+
} from './internal';
3240
import { OrganizationDomain } from './OrganizationDomain';
3341
import { OrganizationMembershipRequest } from './OrganizationMembershipRequest';
3442
import { Role } from './Role';
@@ -229,6 +237,29 @@ export class Organization extends BaseResource implements OrganizationResource {
229237
}).then(res => new OrganizationMembership(res?.response as OrganizationMembershipJSON));
230238
};
231239

240+
__experimental_getSubscriptions = async (
241+
getSubscriptionsParams?: __experimental_GetSubscriptionsParams,
242+
): Promise<ClerkPaginatedResponse<__experimental_CommerceSubscriptionResource>> => {
243+
return await BaseResource._fetch(
244+
{
245+
path: `/organizations/${this.id}/subscriptions`,
246+
method: 'GET',
247+
search: convertPageToOffsetSearchParams(getSubscriptionsParams),
248+
},
249+
{
250+
forceUpdateClient: true,
251+
},
252+
).then(res => {
253+
const { data: subscriptions, total_count } =
254+
res?.response as unknown as ClerkPaginatedResponse<__experimental_CommerceSubscriptionJSON>;
255+
256+
return {
257+
total_count,
258+
data: subscriptions.map(subscription => new __experimental_CommerceSubscription(subscription)),
259+
};
260+
});
261+
};
262+
232263
destroy = async (): Promise<void> => {
233264
return this._baseDelete();
234265
};

packages/clerk-js/src/core/resources/__tests__/__snapshots__/Environment.test.ts.snap

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ exports[`Environment __internal_toSnapshot() 1`] = `
1010
"single_session_mode": true,
1111
},
1212
"commerce_settings": {
13+
"enabled": false,
1314
"stripe_publishable_key": "",
1415
},
1516
"display_config": {
@@ -271,6 +272,7 @@ exports[`Environment __internal_toSnapshot() 1`] = `
271272
exports[`Environment defaults values when instantiated without arguments 1`] = `
272273
Environment {
273274
"__experimental_commerceSettings": __experimental_CommerceSettings {
275+
"enabled": false,
274276
"pathRoot": "",
275277
"stripePublishableKey": "",
276278
},
@@ -497,6 +499,7 @@ Environment {
497499
exports[`Environment has the same initial properties 1`] = `
498500
Environment {
499501
"__experimental_commerceSettings": __experimental_CommerceSettings {
502+
"enabled": false,
500503
"pathRoot": "",
501504
"stripePublishableKey": "",
502505
},

packages/clerk-js/src/core/resources/__tests__/__snapshots__/Organization.test.ts.snap

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
exports[`Organization has the same initial properties 1`] = `
44
Organization {
5+
"__experimental_getSubscriptions": [Function],
56
"addMember": [Function],
67
"adminDeleteEnabled": true,
78
"createDomain": [Function],

packages/clerk-js/src/core/resources/__tests__/__snapshots__/OrganizationMembership.test.ts.snap

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ OrganizationMembership {
66
"destroy": [Function],
77
"id": "test_id",
88
"organization": Organization {
9+
"__experimental_getSubscriptions": [Function],
910
"addMember": [Function],
1011
"adminDeleteEnabled": true,
1112
"createDomain": [Function],

packages/clerk-js/src/core/warnings.ts

+6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ const createMessageForDisabledOrganizations = (
1111
`The <${componentName}/> cannot be rendered when the feature is turned off. Visit 'dashboard.clerk.com' to enable the feature. Since the feature is turned off, this is no-op.`,
1212
);
1313
};
14+
const createMessageForDisabledCommerce = (componentName: 'PricingTable' | 'Checkout') => {
15+
return formatWarning(
16+
`The <${componentName}/> component cannot be rendered when commerce is disabled. Visit 'https://dashboard.clerk.com/last-active?path=commerce/settings' to follow the necessary steps to enable commerce. Since commerce is disabled, this is no-op.`,
17+
);
18+
};
1419
const warnings = {
1520
cannotRenderComponentWhenSessionExists:
1621
'The <SignUp/> and <SignIn/> components cannot render when a user is already signed in, unless the application allows multiple sessions. Since a user is signed in and this application only allows a single session, Clerk is redirecting to the Home URL instead.',
@@ -26,6 +31,7 @@ const warnings = {
2631
'<UserProfile/> cannot render unless a user is signed in. Since no user is signed in, this is no-op.',
2732
cannotRenderComponentWhenOrgDoesNotExist: `<OrganizationProfile/> cannot render unless an organization is active. Since no organization is currently active, this is no-op.`,
2833
cannotRenderAnyOrganizationComponent: createMessageForDisabledOrganizations,
34+
cannotRenderAnyCommerceComponent: createMessageForDisabledCommerce,
2935
cannotOpenUserProfile:
3036
'The UserProfile modal cannot render unless a user is signed in. Since no user is signed in, this is no-op.',
3137
cannotOpenSignInOrSignUp:

packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ const CheckoutFormElements = ({
125125
const [isSubmitting, setIsSubmitting] = useState(false);
126126
const [submitError, setSubmitError] = useState<ClerkRuntimeError | ClerkAPIError | string | undefined>();
127127

128-
const { data } = useFetch(__experimental_commerce?.getPaymentSources, {});
128+
const { data } = useFetch(__experimental_commerce?.getPaymentSources, 'commerce-payment-sources');
129129
const { data: paymentSources } = data || { data: [] };
130130

131131
const didExpandStripePaymentMethods = useCallback(() => {

packages/clerk-js/src/ui/components/Checkout/CheckoutPage.tsx

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { __experimental_CheckoutProps } from '@clerk/types';
1+
import type { __experimental_CheckoutProps, __experimental_CommerceCheckoutResource } from '@clerk/types';
22
import type { Stripe } from '@stripe/stripe-js';
33
import { loadStripe } from '@stripe/stripe-js';
44
import { useEffect, useRef, useState } from 'react';
@@ -10,14 +10,15 @@ import { CheckoutComplete } from './CheckoutComplete';
1010
import { CheckoutForm } from './CheckoutForm';
1111

1212
export const CheckoutPage = (props: __experimental_CheckoutProps) => {
13-
const { planId, planPeriod } = props;
13+
const { planId, planPeriod, orgId, onSubscriptionComplete } = props;
1414
const stripePromiseRef = useRef<Promise<Stripe | null> | null>(null);
1515
const [stripe, setStripe] = useState<Stripe | null>(null);
1616
const { __experimental_commerceSettings } = useEnvironment();
1717

1818
const { checkout, updateCheckout, isLoading } = useCheckout({
1919
planId,
2020
planPeriod,
21+
orgId,
2122
});
2223

2324
useEffect(() => {
@@ -35,6 +36,11 @@ export const CheckoutPage = (props: __experimental_CheckoutProps) => {
3536
}
3637
}, [checkout?.externalGatewayId, __experimental_commerceSettings]);
3738

39+
const onCheckoutComplete = (newCheckout: __experimental_CommerceCheckoutResource) => {
40+
updateCheckout(newCheckout);
41+
onSubscriptionComplete?.();
42+
};
43+
3844
if (isLoading) {
3945
return (
4046
<Spinner
@@ -69,7 +75,7 @@ export const CheckoutPage = (props: __experimental_CheckoutProps) => {
6975
<CheckoutForm
7076
stripe={stripe}
7177
checkout={checkout}
72-
onCheckoutComplete={updateCheckout}
78+
onCheckoutComplete={onCheckoutComplete}
7379
/>
7480
);
7581
};

0 commit comments

Comments
 (0)