Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
1be9179
add pricing table basics
aeliox Jan 16, 2025
b23b7b4
pricing table as customizables / elements
aeliox Jan 29, 2025
3411e5b
add some variants to plan cards
aeliox Jan 30, 2025
6f4aef0
plan annual price toggle
aeliox Jan 31, 2025
8e36198
plan card width, plan sorting
aeliox Jan 31, 2025
fb04388
bump
aeliox Feb 5, 2025
db1b6eb
checkout blade work
aeliox Feb 7, 2025
c0ede3b
checkout happy path
aeliox Feb 14, 2025
01d7259
commerce blade component
aeliox Feb 21, 2025
646cdc6
cancel subscription from plan detail blade, wip
aeliox Feb 26, 2025
92dab17
Conclude merge and update changes
aeliox Feb 26, 2025
e93d3c1
sprinkle in a bunch of __experimental_
aeliox Feb 27, 2025
a2bfdd8
address PR feedback
aeliox Mar 5, 2025
66849fd
wip
alexcarpenter Mar 6, 2025
e979c41
Update pnpm-lock.yaml
alexcarpenter Mar 6, 2025
eac3fff
SegmentedControl
alexcarpenter Mar 6, 2025
87d4597
text alignment
alexcarpenter Mar 6, 2025
b24e790
sandbox tweaks
alexcarpenter Mar 6, 2025
95a2c29
Update template.html
alexcarpenter Mar 6, 2025
4a0fa41
secondary bordered button
alexcarpenter Mar 7, 2025
9b59ae9
remove text-center
alexcarpenter Mar 7, 2025
be4c0ed
disclosure
alexcarpenter Mar 7, 2025
73b2afc
fix
alexcarpenter Mar 7, 2025
b4e088a
feat(clerk-js): `<LineItems />` component (#5302)
alexcarpenter Mar 7, 2025
585942d
animations
alexcarpenter Mar 7, 2025
c305b58
some checkout work, WIP
aeliox Mar 8, 2025
2b288a9
clean up useCheckout state
aeliox Mar 10, 2025
0392107
PR feedback
aeliox Mar 11, 2025
111fee2
Merge branch 'main' into keiran/commerce-pricing-table
aeliox Mar 11, 2025
283cd46
fix payment method line on checkout complete
aeliox Mar 11, 2025
b710814
toggle features
alexcarpenter Mar 11, 2025
e269613
address remaining pieces of feedback
aeliox Mar 11, 2025
2420725
tag missing localization TODOs
aeliox Mar 11, 2025
33aa00c
Merge branch 'main' into keiran/commerce-pricing-table
aeliox Mar 12, 2025
fbde87a
fix tests
aeliox Mar 12, 2025
42feca9
Merge branch 'main' into keiran/commerce-pricing-table
aeliox Mar 12, 2025
cef81aa
add changeset
aeliox Mar 12, 2025
5842e6b
Include stripe dependencies to only Checkout
panteliselef Mar 12, 2025
f008948
bump bundlewatch sizes, pin stripe package versions
aeliox Mar 12, 2025
3d93892
pnpm dedupe
aeliox Mar 12, 2025
2c1c7ee
another bundlewatch bump
aeliox Mar 12, 2025
6c203b6
linter fix
aeliox Mar 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/large-feet-hammer.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
---
'@clerk/localizations': patch
'@clerk/clerk-js': patch
'@clerk/clerk-react': patch
'@clerk/types': patch
---

Introduce experimental billing APIs and components
14 changes: 8 additions & 6 deletions packages/clerk-js/bundlewatch.config.json
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
{
"files": [
{ "path": "./dist/clerk.js", "maxSize": "560kB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "75kB" },
{ "path": "./dist/clerk.headless.js", "maxSize": "48.3KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "89KB" },
{ "path": "./dist/vendors*.js", "maxSize": "25.1KB" },
{ "path": "./dist/clerk.js", "maxSize": "570kB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "76kB" },
{ "path": "./dist/clerk.headless.js", "maxSize": "50KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "92KB" },
{ "path": "./dist/vendors*.js", "maxSize": "26.5KB" },
{ "path": "./dist/coinbase*.js", "maxSize": "35.5KB" },
{ "path": "./dist/createorganization*.js", "maxSize": "5KB" },
{ "path": "./dist/impersonationfab*.js", "maxSize": "5KB" },
Expand All@@ -18,6 +18,8 @@
{ "path": "./dist/userverification*.js", "maxSize": "5KB" },
{ "path": "./dist/onetap*.js", "maxSize": "1KB" },
{ "path": "./dist/waitlist*.js", "maxSize": "1.3KB" },
{ "path": "./dist/keylessPrompt*.js", "maxSize": "5.9KB" }
{ "path": "./dist/keylessPrompt*.js", "maxSize": "5.9KB" },
{ "path": "./dist/pricingTable*.js", "maxSize": "3.5KB" },
{ "path": "./dist/checkout*.js", "maxSize": "8.8KB" }
]
}
2 changes: 2 additions & 0 deletions packages/clerk-js/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,6 +63,8 @@
"@floating-ui/react": "0.25.4",
"@floating-ui/react-dom": "^2.0.2",
"@formkit/auto-animate": "^0.8.1",
"@stripe/react-stripe-js": "3.1.1",
"@stripe/stripe-js": "5.6.0",
"@swc/helpers": "^0.5.13",
"@zxcvbn-ts/core": "3.0.4",
"@zxcvbn-ts/language-common": "3.0.4",
Expand Down
9 changes: 9 additions & 0 deletions packages/clerk-js/rspack.config.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -93,6 +93,15 @@ const common = ({ mode, disableRHC = false }) => {
name: 'signup',
test: module => module.resource && module.resource.includes('/ui/components/SignUp'),
},
checkout: {
minChunks: 1,
name: 'checkout',
test: module =>
module.resource &&
(module.resource.includes('/ui/components/Checkout') ||
// Include `@stripe/react-stripe-js` and `@stripe/stripe-js` in the checkout chunk
module.resource.includes('/node_modules/@stripe')),
},
common: {
minChunks: 1,
name: 'ui-common',
Expand Down
8 changes: 7 additions & 1 deletion packages/clerk-js/sandbox/app.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@ const AVAILABLE_COMPONENTS = [
'organizationProfile',
'organizationSwitcher',
'waitlist',
'pricingTable',
] as const;

const COMPONENT_PROPS_NAMESPACE = 'clerk-js-sandbox';
Expand DownExpand Up@@ -73,6 +74,7 @@ const componentControls: Record<(typeof AVAILABLE_COMPONENTS)[number], Component
organizationProfile: buildComponentControls('organizationProfile'),
organizationSwitcher: buildComponentControls('organizationSwitcher'),
waitlist: buildComponentControls('waitlist'),
pricingTable: buildComponentControls('pricingTable'),
};

declare global {
Expand All@@ -95,7 +97,7 @@ const app = document.getElementById('app') as HTMLDivElement;
function mountIndex(element: HTMLDivElement) {
assertClerkIsLoaded(Clerk);
const user = Clerk.user;
element.innerHTML = `<pre><code>${JSON.stringify({ user }, null, 2)}</code></pre>`;
element.innerHTML = `<pre class="text-left whitespace-pre overflow-x-auto bg-white p-4 border border-gray-100 rounded-md text-sm"><code>${JSON.stringify({ user }, null, 2)}</code></pre>`;
}

function mountOpenSignInButton(element: HTMLDivElement, props) {
Expand DownExpand Up@@ -167,6 +169,9 @@ function addCurrentRouteIndicator(currentRoute: string) {
},
});
},
'/pricing-table': () => {
Clerk.__experimental_mountPricingTable(app, componentControls.pricingTable.getProps() ?? {});
},
'/open-sign-in': () => {
mountOpenSignInButton(app, componentControls.signIn.getProps() ?? {});
},
Expand All@@ -183,6 +188,7 @@ function addCurrentRouteIndicator(currentRoute: string) {
...(componentControls.clerk.getProps() ?? {}),
signInUrl: '/sign-in',
signUpUrl: '/sign-up',
experimental: { commerce: true },
});
renderCurrentRoute();
} else {
Expand Down
10 changes: 9 additions & 1 deletion packages/clerk-js/sandbox/template.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -249,6 +249,14 @@
Keyless
</a>
</li>
<li class="relative">
<a
class="relative isolate flex w-full rounded-md border border-white px-2 py-[0.4375rem] text-sm hover:bg-gray-50 aria-[current]:bg-gray-50"
href="/pricing-table"
>
PricingTable
</a>
</li>
<li class="relative">
<a
class="relative isolate flex w-full rounded-md border border-white px-2 py-[0.4375rem] text-sm hover:bg-gray-50 aria-[current]:bg-gray-50"
Expand All@@ -274,7 +282,7 @@
>
<div
id="app"
class="max-w-full p-12"
class="container w-full p-12"
></div>
</main>

Expand Down
40 changes: 40 additions & 0 deletions packages/clerk-js/src/core/clerk.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,8 @@ import {
import { addClerkPrefix, isAbsoluteUrl, stripScheme } from '@clerk/shared/url';
import { handleValueOrFn, noop } from '@clerk/shared/utils';
import type {
__experimental_CommerceNamespace,
__experimental_PricingTableProps,
__internal_UserVerificationModalProps,
AuthenticateWithCoinbaseWalletParams,
AuthenticateWithGoogleOneTapParams,
Expand DownExpand Up@@ -119,6 +121,7 @@ import {
import { eventBus, events } from './events';
import type { FapiClient, FapiRequestCallback } from './fapiClient';
import { createFapiClient } from './fapiClient';
import { __experimental_Commerce } from './modules/commerce';
import {
BaseResource,
Client,
Expand DownExpand Up@@ -166,6 +169,7 @@ export class Clerk implements ClerkInterface {
version: __PKG_VERSION__,
environment: process.env.NODE_ENV || 'production',
};
private static _commerce: __experimental_CommerceNamespace;

public client: ClientResource | undefined;
public session: SignedInSessionResource | null | undefined;
Expand DownExpand Up@@ -287,6 +291,19 @@ export class Clerk implements ClerkInterface {
return this.#options.standardBrowser || false;
}

get __experimental_commerce(): __experimental_CommerceNamespace {
if (!this.#options.experimental?.commerce) {
throw new Error(
'Clerk: commerce functionality is currently in an experimental state. To enable, pass `experimental.commerce = true`.',
);
}

if (!Clerk._commerce) {
Clerk._commerce = new __experimental_Commerce();
}
return Clerk._commerce;
}

public __internal_getOption<K extends keyof ClerkOptions>(key: K): ClerkOptions[K] {
return this.#options[key];
}
Expand DownExpand Up@@ -886,6 +903,29 @@ export class Clerk implements ClerkInterface {
void this.#componentControls?.ensureMounted().then(controls => controls.unmountComponent({ node }));
};

public __experimental_mountPricingTable = (node: HTMLDivElement, props?: __experimental_PricingTableProps): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls.ensureMounted({ preloadHint: 'PricingTable' }).then(controls =>
controls.mountComponent({
name: 'PricingTable',
appearanceKey: 'pricingTable',
node,
props,
}),
);

this.telemetry?.record(eventPrebuiltComponentMounted('PricingTable', props));
};

public __experimental_unmountPricingTable = (node: HTMLDivElement): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls.ensureMounted().then(controls =>
controls.unmountComponent({
node,
}),
);
};

/**
* `setActive` can be used to set the active session and/or organization.
*/
Expand Down
46 changes: 46 additions & 0 deletions packages/clerk-js/src/core/modules/commerce/Commerce.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
import type {
__experimental_AddPaymentSourceParams,
__experimental_CommerceBillingNamespace,
__experimental_CommerceNamespace,
__experimental_CommercePaymentSourceJSON,
ClerkPaginatedResponse,
} from '@clerk/types';

import { __experimental_CommercePaymentSource, BaseResource } from '../../resources/internal';
import { __experimental_CommerceBilling } from './CommerceBilling';

export class __experimental_Commerce implements __experimental_CommerceNamespace {
private static _billing: __experimental_CommerceBillingNamespace;

get __experimental_billing(): __experimental_CommerceBillingNamespace {
if (!__experimental_Commerce._billing) {
__experimental_Commerce._billing = new __experimental_CommerceBilling();
}
return __experimental_Commerce._billing;
}

addPaymentSource = async (params: __experimental_AddPaymentSourceParams) => {
const json = (
await BaseResource._fetch({
path: `/me/commerce/payment_sources`,
method: 'POST',
body: params as any,
})
)?.response as unknown as __experimental_CommercePaymentSourceJSON;
return new __experimental_CommercePaymentSource(json);
};

getPaymentSources = async () => {
return await BaseResource._fetch({
path: `/me/commerce/payment_sources`,
method: 'GET',
}).then(res => {
const { data: paymentSources, total_count } =
res as unknown as ClerkPaginatedResponse<__experimental_CommercePaymentSourceJSON>;
return {
total_count,
data: paymentSources.map(paymentSource => new __experimental_CommercePaymentSource(paymentSource)),
};
});
};
}
37 changes: 37 additions & 0 deletions packages/clerk-js/src/core/modules/commerce/CommerceBilling.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
import type {
__experimental_CommerceBillingNamespace,
__experimental_CommerceCheckoutJSON,
__experimental_CommercePlanResource,
__experimental_CommerceProductJSON,
__experimental_CreateCheckoutParams,
__experimental_GetPlansParams,
ClerkPaginatedResponse,
} from '@clerk/types';

import { convertPageToOffsetSearchParams } from '../../../utils/convertPageToOffsetSearchParams';
import { __experimental_CommerceCheckout, __experimental_CommercePlan, BaseResource } from '../../resources/internal';

export class __experimental_CommerceBilling implements __experimental_CommerceBillingNamespace {
getPlans = async (params?: __experimental_GetPlansParams): Promise<__experimental_CommercePlanResource[]> => {
const { data: products } = (await BaseResource._fetch({
path: `/commerce/products`,
method: 'GET',
search: convertPageToOffsetSearchParams(params),
})) as unknown as ClerkPaginatedResponse<__experimental_CommerceProductJSON>;

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

startCheckout = async (params: __experimental_CreateCheckoutParams) => {
const json = (
await BaseResource._fetch<__experimental_CommerceCheckoutJSON>({
path: `/me/commerce/checkouts`,
method: 'POST',
body: params as any,
})
)?.response as unknown as __experimental_CommerceCheckoutJSON;

return new __experimental_CommerceCheckout(json);
};
}
2 changes: 2 additions & 0 deletions packages/clerk-js/src/core/modules/commerce/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
export * from './Commerce';
export * from './CommerceBilling';
2 changes: 1 addition & 1 deletion packages/clerk-js/src/core/resources/Base.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,7 +62,7 @@ export abstract class BaseResource {
return !this.id;
}

protected static async _fetch<J extends ClerkResourceJSON | DeletedObjectJSON | null>(
static async _fetch<J extends ClerkResourceJSON | DeletedObjectJSON | null>(
Comment thread
panteliselef marked this conversation as resolved.
requestInit: FapiRequestInit,
opts: BaseFetchOptions = {},
): Promise<FapiResponseJSON<J> | null> {
Expand Down
63 changes: 63 additions & 0 deletions packages/clerk-js/src/core/resources/CommerceCheckout.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
import type {
__experimental_CommerceCheckoutJSON,
__experimental_CommerceCheckoutResource,
__experimental_CommerceTotals,
__experimental_ConfirmCheckoutParams,
} from '@clerk/types';

import { commerceTotalsFromJSON } from '../../utils';
import {
__experimental_CommerceInvoice,
__experimental_CommercePaymentSource,
__experimental_CommercePlan,
__experimental_CommerceSubscription,
BaseResource,
} from './internal';

export class __experimental_CommerceCheckout extends BaseResource implements __experimental_CommerceCheckoutResource {
pathRoot = '/me/commerce/checkouts';

id!: string;
Comment thread
aeliox marked this conversation as resolved.
externalClientSecret!: string;
externalGatewayId!: string;
invoice?: __experimental_CommerceInvoice;
paymentSource?: __experimental_CommercePaymentSource;
plan!: __experimental_CommercePlan;
planPeriod!: string;
status!: string;
subscription?: __experimental_CommerceSubscription;
totals!: __experimental_CommerceTotals;

constructor(data: __experimental_CommerceCheckoutJSON) {
super();
this.fromJSON(data);
}

protected fromJSON(data: __experimental_CommerceCheckoutJSON | null): this {
if (!data) {
return this;
}

this.id = data.id;
this.externalClientSecret = data.external_client_secret;
this.externalGatewayId = data.external_gateway_id;
this.invoice = data.invoice ? new __experimental_CommerceInvoice(data.invoice) : undefined;
this.paymentSource = data.payment_source
? new __experimental_CommercePaymentSource(data.payment_source)
: undefined;
this.plan = new __experimental_CommercePlan(data.plan);
this.planPeriod = data.plan_period;
this.status = data.status;
this.subscription = data.subscription ? new __experimental_CommerceSubscription(data.subscription) : undefined;
this.totals = commerceTotalsFromJSON(data.totals);

return this;
}

confirm = (params?: __experimental_ConfirmCheckoutParams): Promise<this> => {
return this._basePatch({
path: this.path('confirm'),
body: params as any,
});
};
}
30 changes: 30 additions & 0 deletions packages/clerk-js/src/core/resources/CommerceFeature.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
import type { __experimental_CommerceFeatureJSON, __experimental_CommerceFeatureResource } from '@clerk/types';

import { BaseResource } from './internal';

export class __experimental_CommerceFeature extends BaseResource implements __experimental_CommerceFeatureResource {
id!: string;
name!: string;
description!: string;
slug!: string;
avatarUrl!: string;

constructor(data: __experimental_CommerceFeatureJSON) {
super();
this.fromJSON(data);
}

protected fromJSON(data: __experimental_CommerceFeatureJSON | null): this {
if (!data) {
return this;
}

this.id = data.id;
this.name = data.name;
this.description = data.description;
this.slug = data.slug;
this.avatarUrl = data.avatar_url;

return this;
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
1be9179
add pricing table basics
aeliox Jan 16, 2025
b23b7b4
pricing table as customizables / elements
aeliox Jan 29, 2025
3411e5b
add some variants to plan cards
aeliox Jan 30, 2025
6f4aef0
plan annual price toggle
aeliox Jan 31, 2025
8e36198
plan card width, plan sorting
aeliox Jan 31, 2025
fb04388
bump
aeliox Feb 5, 2025
db1b6eb
checkout blade work
aeliox Feb 7, 2025
c0ede3b
checkout happy path
aeliox Feb 14, 2025
01d7259
commerce blade component
aeliox Feb 21, 2025
646cdc6
cancel subscription from plan detail blade, wip
aeliox Feb 26, 2025
92dab17
Conclude merge and update changes
aeliox Feb 26, 2025
e93d3c1
sprinkle in a bunch of __experimental_
aeliox Feb 27, 2025
a2bfdd8
address PR feedback
aeliox Mar 5, 2025
66849fd
wip
alexcarpenter Mar 6, 2025
e979c41
Update pnpm-lock.yaml
alexcarpenter Mar 6, 2025
eac3fff
SegmentedControl
alexcarpenter Mar 6, 2025
87d4597
text alignment
alexcarpenter Mar 6, 2025
b24e790
sandbox tweaks
alexcarpenter Mar 6, 2025
95a2c29
Update template.html
alexcarpenter Mar 6, 2025
4a0fa41
secondary bordered button
alexcarpenter Mar 7, 2025
9b59ae9
remove text-center
alexcarpenter Mar 7, 2025
be4c0ed
disclosure
alexcarpenter Mar 7, 2025
73b2afc
fix
alexcarpenter Mar 7, 2025
b4e088a
feat(clerk-js): `<LineItems />` component (#5302)
alexcarpenter Mar 7, 2025
585942d
animations
alexcarpenter Mar 7, 2025
c305b58
some checkout work, WIP
aeliox Mar 8, 2025
2b288a9
clean up useCheckout state
aeliox Mar 10, 2025
0392107
PR feedback
aeliox Mar 11, 2025
111fee2
Merge branch 'main' into keiran/commerce-pricing-table
aeliox Mar 11, 2025
283cd46
fix payment method line on checkout complete
aeliox Mar 11, 2025
b710814
toggle features
alexcarpenter Mar 11, 2025
e269613
address remaining pieces of feedback
aeliox Mar 11, 2025
2420725
tag missing localization TODOs
aeliox Mar 11, 2025
33aa00c
Merge branch 'main' into keiran/commerce-pricing-table
aeliox Mar 12, 2025
fbde87a
fix tests
aeliox Mar 12, 2025
42feca9
Merge branch 'main' into keiran/commerce-pricing-table
aeliox Mar 12, 2025
cef81aa
add changeset
aeliox Mar 12, 2025
5842e6b
Include stripe dependencies to only Checkout
panteliselef Mar 12, 2025
f008948
bump bundlewatch sizes, pin stripe package versions
aeliox Mar 12, 2025
3d93892
pnpm dedupe
aeliox Mar 12, 2025
2c1c7ee
another bundlewatch bump
aeliox Mar 12, 2025
6c203b6
linter fix
aeliox Mar 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/large-feet-hammer.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
---
'@clerk/localizations': patch
'@clerk/clerk-js': patch
'@clerk/clerk-react': patch
'@clerk/types': patch
---

Introduce experimental billing APIs and components
14 changes: 8 additions & 6 deletions packages/clerk-js/bundlewatch.config.json
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
{
"files": [
{ "path": "./dist/clerk.js", "maxSize": "560kB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "75kB" },
{ "path": "./dist/clerk.headless.js", "maxSize": "48.3KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "89KB" },
{ "path": "./dist/vendors*.js", "maxSize": "25.1KB" },
{ "path": "./dist/clerk.js", "maxSize": "570kB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "76kB" },
{ "path": "./dist/clerk.headless.js", "maxSize": "50KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "92KB" },
{ "path": "./dist/vendors*.js", "maxSize": "26.5KB" },
{ "path": "./dist/coinbase*.js", "maxSize": "35.5KB" },
{ "path": "./dist/createorganization*.js", "maxSize": "5KB" },
{ "path": "./dist/impersonationfab*.js", "maxSize": "5KB" },
Expand All@@ -18,6 +18,8 @@
{ "path": "./dist/userverification*.js", "maxSize": "5KB" },
{ "path": "./dist/onetap*.js", "maxSize": "1KB" },
{ "path": "./dist/waitlist*.js", "maxSize": "1.3KB" },
{ "path": "./dist/keylessPrompt*.js", "maxSize": "5.9KB" }
{ "path": "./dist/keylessPrompt*.js", "maxSize": "5.9KB" },
{ "path": "./dist/pricingTable*.js", "maxSize": "3.5KB" },
{ "path": "./dist/checkout*.js", "maxSize": "8.8KB" }
]
}
2 changes: 2 additions & 0 deletions packages/clerk-js/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,6 +63,8 @@
"@floating-ui/react": "0.25.4",
"@floating-ui/react-dom": "^2.0.2",
"@formkit/auto-animate": "^0.8.1",
"@stripe/react-stripe-js": "3.1.1",
"@stripe/stripe-js": "5.6.0",
"@swc/helpers": "^0.5.13",
"@zxcvbn-ts/core": "3.0.4",
"@zxcvbn-ts/language-common": "3.0.4",
Expand Down
9 changes: 9 additions & 0 deletions packages/clerk-js/rspack.config.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -93,6 +93,15 @@ const common = ({ mode, disableRHC = false }) => {
name: 'signup',
test: module => module.resource && module.resource.includes('/ui/components/SignUp'),
},
checkout: {
minChunks: 1,
name: 'checkout',
test: module =>
module.resource &&
(module.resource.includes('/ui/components/Checkout') ||
// Include `@stripe/react-stripe-js` and `@stripe/stripe-js` in the checkout chunk
module.resource.includes('/node_modules/@stripe')),
},
common: {
minChunks: 1,
name: 'ui-common',
Expand Down
8 changes: 7 additions & 1 deletion packages/clerk-js/sandbox/app.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@ const AVAILABLE_COMPONENTS = [
'organizationProfile',
'organizationSwitcher',
'waitlist',
'pricingTable',
] as const;

const COMPONENT_PROPS_NAMESPACE = 'clerk-js-sandbox';
Expand DownExpand Up@@ -73,6 +74,7 @@ const componentControls: Record<(typeof AVAILABLE_COMPONENTS)[number], Component
organizationProfile: buildComponentControls('organizationProfile'),
organizationSwitcher: buildComponentControls('organizationSwitcher'),
waitlist: buildComponentControls('waitlist'),
pricingTable: buildComponentControls('pricingTable'),
};

declare global {
Expand All@@ -95,7 +97,7 @@ const app = document.getElementById('app') as HTMLDivElement;
function mountIndex(element: HTMLDivElement) {
assertClerkIsLoaded(Clerk);
const user = Clerk.user;
element.innerHTML = `<pre><code>${JSON.stringify({ user }, null, 2)}</code></pre>`;
element.innerHTML = `<pre class="text-left whitespace-pre overflow-x-auto bg-white p-4 border border-gray-100 rounded-md text-sm"><code>${JSON.stringify({ user }, null, 2)}</code></pre>`;
}

function mountOpenSignInButton(element: HTMLDivElement, props) {
Expand DownExpand Up@@ -167,6 +169,9 @@ function addCurrentRouteIndicator(currentRoute: string) {
},
});
},
'/pricing-table': () => {
Clerk.__experimental_mountPricingTable(app, componentControls.pricingTable.getProps() ?? {});
},
'/open-sign-in': () => {
mountOpenSignInButton(app, componentControls.signIn.getProps() ?? {});
},
Expand All@@ -183,6 +188,7 @@ function addCurrentRouteIndicator(currentRoute: string) {
...(componentControls.clerk.getProps() ?? {}),
signInUrl: '/sign-in',
signUpUrl: '/sign-up',
experimental: { commerce: true },
});
renderCurrentRoute();
} else {
Expand Down
10 changes: 9 additions & 1 deletion packages/clerk-js/sandbox/template.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -249,6 +249,14 @@
Keyless
</a>
</li>
<li class="relative">
<a
class="relative isolate flex w-full rounded-md border border-white px-2 py-[0.4375rem] text-sm hover:bg-gray-50 aria-[current]:bg-gray-50"
href="/pricing-table"
>
PricingTable
</a>
</li>
<li class="relative">
<a
class="relative isolate flex w-full rounded-md border border-white px-2 py-[0.4375rem] text-sm hover:bg-gray-50 aria-[current]:bg-gray-50"
Expand All@@ -274,7 +282,7 @@
>
<div
id="app"
class="max-w-full p-12"
class="container w-full p-12"
></div>
</main>

Expand Down
40 changes: 40 additions & 0 deletions packages/clerk-js/src/core/clerk.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,8 @@ import {
import { addClerkPrefix, isAbsoluteUrl, stripScheme } from '@clerk/shared/url';
import { handleValueOrFn, noop } from '@clerk/shared/utils';
import type {
__experimental_CommerceNamespace,
__experimental_PricingTableProps,
__internal_UserVerificationModalProps,
AuthenticateWithCoinbaseWalletParams,
AuthenticateWithGoogleOneTapParams,
Expand DownExpand Up@@ -119,6 +121,7 @@ import {
import { eventBus, events } from './events';
import type { FapiClient, FapiRequestCallback } from './fapiClient';
import { createFapiClient } from './fapiClient';
import { __experimental_Commerce } from './modules/commerce';
import {
BaseResource,
Client,
Expand DownExpand Up@@ -166,6 +169,7 @@ export class Clerk implements ClerkInterface {
version: __PKG_VERSION__,
environment: process.env.NODE_ENV || 'production',
};
private static _commerce: __experimental_CommerceNamespace;

public client: ClientResource | undefined;
public session: SignedInSessionResource | null | undefined;
Expand DownExpand Up@@ -287,6 +291,19 @@ export class Clerk implements ClerkInterface {
return this.#options.standardBrowser || false;
}

get __experimental_commerce(): __experimental_CommerceNamespace {
if (!this.#options.experimental?.commerce) {
throw new Error(
'Clerk: commerce functionality is currently in an experimental state. To enable, pass `experimental.commerce = true`.',
);
}

if (!Clerk._commerce) {
Clerk._commerce = new __experimental_Commerce();
}
return Clerk._commerce;
}

public __internal_getOption<K extends keyof ClerkOptions>(key: K): ClerkOptions[K] {
return this.#options[key];
}
Expand DownExpand Up@@ -886,6 +903,29 @@ export class Clerk implements ClerkInterface {
void this.#componentControls?.ensureMounted().then(controls => controls.unmountComponent({ node }));
};

public __experimental_mountPricingTable = (node: HTMLDivElement, props?: __experimental_PricingTableProps): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls.ensureMounted({ preloadHint: 'PricingTable' }).then(controls =>
controls.mountComponent({
name: 'PricingTable',
appearanceKey: 'pricingTable',
node,
props,
}),
);

this.telemetry?.record(eventPrebuiltComponentMounted('PricingTable', props));
};

public __experimental_unmountPricingTable = (node: HTMLDivElement): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls.ensureMounted().then(controls =>
controls.unmountComponent({
node,
}),
);
};

/**
* `setActive` can be used to set the active session and/or organization.
*/
Expand Down
46 changes: 46 additions & 0 deletions packages/clerk-js/src/core/modules/commerce/Commerce.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
import type {
__experimental_AddPaymentSourceParams,
__experimental_CommerceBillingNamespace,
__experimental_CommerceNamespace,
__experimental_CommercePaymentSourceJSON,
ClerkPaginatedResponse,
} from '@clerk/types';

import { __experimental_CommercePaymentSource, BaseResource } from '../../resources/internal';
import { __experimental_CommerceBilling } from './CommerceBilling';

export class __experimental_Commerce implements __experimental_CommerceNamespace {
private static _billing: __experimental_CommerceBillingNamespace;

get __experimental_billing(): __experimental_CommerceBillingNamespace {
if (!__experimental_Commerce._billing) {
__experimental_Commerce._billing = new __experimental_CommerceBilling();
}
return __experimental_Commerce._billing;
}

addPaymentSource = async (params: __experimental_AddPaymentSourceParams) => {
const json = (
await BaseResource._fetch({
path: `/me/commerce/payment_sources`,
method: 'POST',
body: params as any,
})
)?.response as unknown as __experimental_CommercePaymentSourceJSON;
return new __experimental_CommercePaymentSource(json);
};

getPaymentSources = async () => {
return await BaseResource._fetch({
path: `/me/commerce/payment_sources`,
method: 'GET',
}).then(res => {
const { data: paymentSources, total_count } =
res as unknown as ClerkPaginatedResponse<__experimental_CommercePaymentSourceJSON>;
return {
total_count,
data: paymentSources.map(paymentSource => new __experimental_CommercePaymentSource(paymentSource)),
};
});
};
}
37 changes: 37 additions & 0 deletions packages/clerk-js/src/core/modules/commerce/CommerceBilling.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
import type {
__experimental_CommerceBillingNamespace,
__experimental_CommerceCheckoutJSON,
__experimental_CommercePlanResource,
__experimental_CommerceProductJSON,
__experimental_CreateCheckoutParams,
__experimental_GetPlansParams,
ClerkPaginatedResponse,
} from '@clerk/types';

import { convertPageToOffsetSearchParams } from '../../../utils/convertPageToOffsetSearchParams';
import { __experimental_CommerceCheckout, __experimental_CommercePlan, BaseResource } from '../../resources/internal';

export class __experimental_CommerceBilling implements __experimental_CommerceBillingNamespace {
getPlans = async (params?: __experimental_GetPlansParams): Promise<__experimental_CommercePlanResource[]> => {
const { data: products } = (await BaseResource._fetch({
path: `/commerce/products`,
method: 'GET',
search: convertPageToOffsetSearchParams(params),
})) as unknown as ClerkPaginatedResponse<__experimental_CommerceProductJSON>;

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

startCheckout = async (params: __experimental_CreateCheckoutParams) => {
const json = (
await BaseResource._fetch<__experimental_CommerceCheckoutJSON>({
path: `/me/commerce/checkouts`,
method: 'POST',
body: params as any,
})
)?.response as unknown as __experimental_CommerceCheckoutJSON;

return new __experimental_CommerceCheckout(json);
};
}
2 changes: 2 additions & 0 deletions packages/clerk-js/src/core/modules/commerce/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
export * from './Commerce';
export * from './CommerceBilling';
2 changes: 1 addition & 1 deletion packages/clerk-js/src/core/resources/Base.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,7 +62,7 @@ export abstract class BaseResource {
return !this.id;
}

protected static async _fetch<J extends ClerkResourceJSON | DeletedObjectJSON | null>(
static async _fetch<J extends ClerkResourceJSON | DeletedObjectJSON | null>(
Comment thread
panteliselef marked this conversation as resolved.
requestInit: FapiRequestInit,
opts: BaseFetchOptions = {},
): Promise<FapiResponseJSON<J> | null> {
Expand Down
63 changes: 63 additions & 0 deletions packages/clerk-js/src/core/resources/CommerceCheckout.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
import type {
__experimental_CommerceCheckoutJSON,
__experimental_CommerceCheckoutResource,
__experimental_CommerceTotals,
__experimental_ConfirmCheckoutParams,
} from '@clerk/types';

import { commerceTotalsFromJSON } from '../../utils';
import {
__experimental_CommerceInvoice,
__experimental_CommercePaymentSource,
__experimental_CommercePlan,
__experimental_CommerceSubscription,
BaseResource,
} from './internal';

export class __experimental_CommerceCheckout extends BaseResource implements __experimental_CommerceCheckoutResource {
pathRoot = '/me/commerce/checkouts';

id!: string;
Comment thread
aeliox marked this conversation as resolved.
externalClientSecret!: string;
externalGatewayId!: string;
invoice?: __experimental_CommerceInvoice;
paymentSource?: __experimental_CommercePaymentSource;
plan!: __experimental_CommercePlan;
planPeriod!: string;
status!: string;
subscription?: __experimental_CommerceSubscription;
totals!: __experimental_CommerceTotals;

constructor(data: __experimental_CommerceCheckoutJSON) {
super();
this.fromJSON(data);
}

protected fromJSON(data: __experimental_CommerceCheckoutJSON | null): this {
if (!data) {
return this;
}

this.id = data.id;
this.externalClientSecret = data.external_client_secret;
this.externalGatewayId = data.external_gateway_id;
this.invoice = data.invoice ? new __experimental_CommerceInvoice(data.invoice) : undefined;
this.paymentSource = data.payment_source
? new __experimental_CommercePaymentSource(data.payment_source)
: undefined;
this.plan = new __experimental_CommercePlan(data.plan);
this.planPeriod = data.plan_period;
this.status = data.status;
this.subscription = data.subscription ? new __experimental_CommerceSubscription(data.subscription) : undefined;
this.totals = commerceTotalsFromJSON(data.totals);

return this;
}

confirm = (params?: __experimental_ConfirmCheckoutParams): Promise<this> => {
return this._basePatch({
path: this.path('confirm'),
body: params as any,
});
};
}
30 changes: 30 additions & 0 deletions packages/clerk-js/src/core/resources/CommerceFeature.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
import type { __experimental_CommerceFeatureJSON, __experimental_CommerceFeatureResource } from '@clerk/types';

import { BaseResource } from './internal';

export class __experimental_CommerceFeature extends BaseResource implements __experimental_CommerceFeatureResource {
id!: string;
name!: string;
description!: string;
slug!: string;
avatarUrl!: string;

constructor(data: __experimental_CommerceFeatureJSON) {
super();
this.fromJSON(data);
}

protected fromJSON(data: __experimental_CommerceFeatureJSON | null): this {
if (!data) {
return this;
}

this.id = data.id;
this.name = data.name;
this.description = data.description;
this.slug = data.slug;
this.avatarUrl = data.avatar_url;

return this;
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
1be9179
add pricing table basics
aeliox Jan 16, 2025
b23b7b4
pricing table as customizables / elements
aeliox Jan 29, 2025
3411e5b
add some variants to plan cards
aeliox Jan 30, 2025
6f4aef0
plan annual price toggle
aeliox Jan 31, 2025
8e36198
plan card width, plan sorting
aeliox Jan 31, 2025
fb04388
bump
aeliox Feb 5, 2025
db1b6eb
checkout blade work
aeliox Feb 7, 2025
c0ede3b
checkout happy path
aeliox Feb 14, 2025
01d7259
commerce blade component
aeliox Feb 21, 2025
646cdc6
cancel subscription from plan detail blade, wip
aeliox Feb 26, 2025
92dab17
Conclude merge and update changes
aeliox Feb 26, 2025
e93d3c1
sprinkle in a bunch of __experimental_
aeliox Feb 27, 2025
a2bfdd8
address PR feedback
aeliox Mar 5, 2025
66849fd
wip
alexcarpenter Mar 6, 2025
e979c41
Update pnpm-lock.yaml
alexcarpenter Mar 6, 2025
eac3fff
SegmentedControl
alexcarpenter Mar 6, 2025
87d4597
text alignment
alexcarpenter Mar 6, 2025
b24e790
sandbox tweaks
alexcarpenter Mar 6, 2025
95a2c29
Update template.html
alexcarpenter Mar 6, 2025
4a0fa41
secondary bordered button
alexcarpenter Mar 7, 2025
9b59ae9
remove text-center
alexcarpenter Mar 7, 2025
be4c0ed
disclosure
alexcarpenter Mar 7, 2025
73b2afc
fix
alexcarpenter Mar 7, 2025
b4e088a
feat(clerk-js): `<LineItems />` component (#5302)
alexcarpenter Mar 7, 2025
585942d
animations
alexcarpenter Mar 7, 2025
c305b58
some checkout work, WIP
aeliox Mar 8, 2025
2b288a9
clean up useCheckout state
aeliox Mar 10, 2025
0392107
PR feedback
aeliox Mar 11, 2025
111fee2
Merge branch 'main' into keiran/commerce-pricing-table
aeliox Mar 11, 2025
283cd46
fix payment method line on checkout complete
aeliox Mar 11, 2025
b710814
toggle features
alexcarpenter Mar 11, 2025
e269613
address remaining pieces of feedback
aeliox Mar 11, 2025
2420725
tag missing localization TODOs
aeliox Mar 11, 2025
33aa00c
Merge branch 'main' into keiran/commerce-pricing-table
aeliox Mar 12, 2025
fbde87a
fix tests
aeliox Mar 12, 2025
42feca9
Merge branch 'main' into keiran/commerce-pricing-table
aeliox Mar 12, 2025
cef81aa
add changeset
aeliox Mar 12, 2025
5842e6b
Include stripe dependencies to only Checkout
panteliselef Mar 12, 2025
f008948
bump bundlewatch sizes, pin stripe package versions
aeliox Mar 12, 2025
3d93892
pnpm dedupe
aeliox Mar 12, 2025
2c1c7ee
another bundlewatch bump
aeliox Mar 12, 2025
6c203b6
linter fix
aeliox Mar 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/large-feet-hammer.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
---
'@clerk/localizations': patch
'@clerk/clerk-js': patch
'@clerk/clerk-react': patch
'@clerk/types': patch
---

Introduce experimental billing APIs and components
14 changes: 8 additions & 6 deletions packages/clerk-js/bundlewatch.config.json
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
{
"files": [
{ "path": "./dist/clerk.js", "maxSize": "560kB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "75kB" },
{ "path": "./dist/clerk.headless.js", "maxSize": "48.3KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "89KB" },
{ "path": "./dist/vendors*.js", "maxSize": "25.1KB" },
{ "path": "./dist/clerk.js", "maxSize": "570kB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "76kB" },
{ "path": "./dist/clerk.headless.js", "maxSize": "50KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "92KB" },
{ "path": "./dist/vendors*.js", "maxSize": "26.5KB" },
{ "path": "./dist/coinbase*.js", "maxSize": "35.5KB" },
{ "path": "./dist/createorganization*.js", "maxSize": "5KB" },
{ "path": "./dist/impersonationfab*.js", "maxSize": "5KB" },
Expand All@@ -18,6 +18,8 @@
{ "path": "./dist/userverification*.js", "maxSize": "5KB" },
{ "path": "./dist/onetap*.js", "maxSize": "1KB" },
{ "path": "./dist/waitlist*.js", "maxSize": "1.3KB" },
{ "path": "./dist/keylessPrompt*.js", "maxSize": "5.9KB" }
{ "path": "./dist/keylessPrompt*.js", "maxSize": "5.9KB" },
{ "path": "./dist/pricingTable*.js", "maxSize": "3.5KB" },
{ "path": "./dist/checkout*.js", "maxSize": "8.8KB" }
]
}
2 changes: 2 additions & 0 deletions packages/clerk-js/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,6 +63,8 @@
"@floating-ui/react": "0.25.4",
"@floating-ui/react-dom": "^2.0.2",
"@formkit/auto-animate": "^0.8.1",
"@stripe/react-stripe-js": "3.1.1",
"@stripe/stripe-js": "5.6.0",
"@swc/helpers": "^0.5.13",
"@zxcvbn-ts/core": "3.0.4",
"@zxcvbn-ts/language-common": "3.0.4",
Expand Down
9 changes: 9 additions & 0 deletions packages/clerk-js/rspack.config.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -93,6 +93,15 @@ const common = ({ mode, disableRHC = false }) => {
name: 'signup',
test: module => module.resource && module.resource.includes('/ui/components/SignUp'),
},
checkout: {
minChunks: 1,
name: 'checkout',
test: module =>
module.resource &&
(module.resource.includes('/ui/components/Checkout') ||
// Include `@stripe/react-stripe-js` and `@stripe/stripe-js` in the checkout chunk
module.resource.includes('/node_modules/@stripe')),
},
common: {
minChunks: 1,
name: 'ui-common',
Expand Down
8 changes: 7 additions & 1 deletion packages/clerk-js/sandbox/app.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@ const AVAILABLE_COMPONENTS = [
'organizationProfile',
'organizationSwitcher',
'waitlist',
'pricingTable',
] as const;

const COMPONENT_PROPS_NAMESPACE = 'clerk-js-sandbox';
Expand DownExpand Up@@ -73,6 +74,7 @@ const componentControls: Record<(typeof AVAILABLE_COMPONENTS)[number], Component
organizationProfile: buildComponentControls('organizationProfile'),
organizationSwitcher: buildComponentControls('organizationSwitcher'),
waitlist: buildComponentControls('waitlist'),
pricingTable: buildComponentControls('pricingTable'),
};

declare global {
Expand All@@ -95,7 +97,7 @@ const app = document.getElementById('app') as HTMLDivElement;
function mountIndex(element: HTMLDivElement) {
assertClerkIsLoaded(Clerk);
const user = Clerk.user;
element.innerHTML = `<pre><code>${JSON.stringify({ user }, null, 2)}</code></pre>`;
element.innerHTML = `<pre class="text-left whitespace-pre overflow-x-auto bg-white p-4 border border-gray-100 rounded-md text-sm"><code>${JSON.stringify({ user }, null, 2)}</code></pre>`;
}

function mountOpenSignInButton(element: HTMLDivElement, props) {
Expand DownExpand Up@@ -167,6 +169,9 @@ function addCurrentRouteIndicator(currentRoute: string) {
},
});
},
'/pricing-table': () => {
Clerk.__experimental_mountPricingTable(app, componentControls.pricingTable.getProps() ?? {});
},
'/open-sign-in': () => {
mountOpenSignInButton(app, componentControls.signIn.getProps() ?? {});
},
Expand All@@ -183,6 +188,7 @@ function addCurrentRouteIndicator(currentRoute: string) {
...(componentControls.clerk.getProps() ?? {}),
signInUrl: '/sign-in',
signUpUrl: '/sign-up',
experimental: { commerce: true },
});
renderCurrentRoute();
} else {
Expand Down
10 changes: 9 additions & 1 deletion packages/clerk-js/sandbox/template.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -249,6 +249,14 @@
Keyless
</a>
</li>
<li class="relative">
<a
class="relative isolate flex w-full rounded-md border border-white px-2 py-[0.4375rem] text-sm hover:bg-gray-50 aria-[current]:bg-gray-50"
href="/pricing-table"
>
PricingTable
</a>
</li>
<li class="relative">
<a
class="relative isolate flex w-full rounded-md border border-white px-2 py-[0.4375rem] text-sm hover:bg-gray-50 aria-[current]:bg-gray-50"
Expand All@@ -274,7 +282,7 @@
>
<div
id="app"
class="max-w-full p-12"
class="container w-full p-12"
></div>
</main>

Expand Down
40 changes: 40 additions & 0 deletions packages/clerk-js/src/core/clerk.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,8 @@ import {
import { addClerkPrefix, isAbsoluteUrl, stripScheme } from '@clerk/shared/url';
import { handleValueOrFn, noop } from '@clerk/shared/utils';
import type {
__experimental_CommerceNamespace,
__experimental_PricingTableProps,
__internal_UserVerificationModalProps,
AuthenticateWithCoinbaseWalletParams,
AuthenticateWithGoogleOneTapParams,
Expand DownExpand Up@@ -119,6 +121,7 @@ import {
import { eventBus, events } from './events';
import type { FapiClient, FapiRequestCallback } from './fapiClient';
import { createFapiClient } from './fapiClient';
import { __experimental_Commerce } from './modules/commerce';
import {
BaseResource,
Client,
Expand DownExpand Up@@ -166,6 +169,7 @@ export class Clerk implements ClerkInterface {
version: __PKG_VERSION__,
environment: process.env.NODE_ENV || 'production',
};
private static _commerce: __experimental_CommerceNamespace;

public client: ClientResource | undefined;
public session: SignedInSessionResource | null | undefined;
Expand DownExpand Up@@ -287,6 +291,19 @@ export class Clerk implements ClerkInterface {
return this.#options.standardBrowser || false;
}

get __experimental_commerce(): __experimental_CommerceNamespace {
if (!this.#options.experimental?.commerce) {
throw new Error(
'Clerk: commerce functionality is currently in an experimental state. To enable, pass `experimental.commerce = true`.',
);
}

if (!Clerk._commerce) {
Clerk._commerce = new __experimental_Commerce();
}
return Clerk._commerce;
}

public __internal_getOption<K extends keyof ClerkOptions>(key: K): ClerkOptions[K] {
return this.#options[key];
}
Expand DownExpand Up@@ -886,6 +903,29 @@ export class Clerk implements ClerkInterface {
void this.#componentControls?.ensureMounted().then(controls => controls.unmountComponent({ node }));
};

public __experimental_mountPricingTable = (node: HTMLDivElement, props?: __experimental_PricingTableProps): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls.ensureMounted({ preloadHint: 'PricingTable' }).then(controls =>
controls.mountComponent({
name: 'PricingTable',
appearanceKey: 'pricingTable',
node,
props,
}),
);

this.telemetry?.record(eventPrebuiltComponentMounted('PricingTable', props));
};

public __experimental_unmountPricingTable = (node: HTMLDivElement): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls.ensureMounted().then(controls =>
controls.unmountComponent({
node,
}),
);
};

/**
* `setActive` can be used to set the active session and/or organization.
*/
Expand Down
46 changes: 46 additions & 0 deletions packages/clerk-js/src/core/modules/commerce/Commerce.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
import type {
__experimental_AddPaymentSourceParams,
__experimental_CommerceBillingNamespace,
__experimental_CommerceNamespace,
__experimental_CommercePaymentSourceJSON,
ClerkPaginatedResponse,
} from '@clerk/types';

import { __experimental_CommercePaymentSource, BaseResource } from '../../resources/internal';
import { __experimental_CommerceBilling } from './CommerceBilling';

export class __experimental_Commerce implements __experimental_CommerceNamespace {
private static _billing: __experimental_CommerceBillingNamespace;

get __experimental_billing(): __experimental_CommerceBillingNamespace {
if (!__experimental_Commerce._billing) {
__experimental_Commerce._billing = new __experimental_CommerceBilling();
}
return __experimental_Commerce._billing;
}

addPaymentSource = async (params: __experimental_AddPaymentSourceParams) => {
const json = (
await BaseResource._fetch({
path: `/me/commerce/payment_sources`,
method: 'POST',
body: params as any,
})
)?.response as unknown as __experimental_CommercePaymentSourceJSON;
return new __experimental_CommercePaymentSource(json);
};

getPaymentSources = async () => {
return await BaseResource._fetch({
path: `/me/commerce/payment_sources`,
method: 'GET',
}).then(res => {
const { data: paymentSources, total_count } =
res as unknown as ClerkPaginatedResponse<__experimental_CommercePaymentSourceJSON>;
return {
total_count,
data: paymentSources.map(paymentSource => new __experimental_CommercePaymentSource(paymentSource)),
};
});
};
}
37 changes: 37 additions & 0 deletions packages/clerk-js/src/core/modules/commerce/CommerceBilling.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
import type {
__experimental_CommerceBillingNamespace,
__experimental_CommerceCheckoutJSON,
__experimental_CommercePlanResource,
__experimental_CommerceProductJSON,
__experimental_CreateCheckoutParams,
__experimental_GetPlansParams,
ClerkPaginatedResponse,
} from '@clerk/types';

import { convertPageToOffsetSearchParams } from '../../../utils/convertPageToOffsetSearchParams';
import { __experimental_CommerceCheckout, __experimental_CommercePlan, BaseResource } from '../../resources/internal';

export class __experimental_CommerceBilling implements __experimental_CommerceBillingNamespace {
getPlans = async (params?: __experimental_GetPlansParams): Promise<__experimental_CommercePlanResource[]> => {
const { data: products } = (await BaseResource._fetch({
path: `/commerce/products`,
method: 'GET',
search: convertPageToOffsetSearchParams(params),
})) as unknown as ClerkPaginatedResponse<__experimental_CommerceProductJSON>;

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

startCheckout = async (params: __experimental_CreateCheckoutParams) => {
const json = (
await BaseResource._fetch<__experimental_CommerceCheckoutJSON>({
path: `/me/commerce/checkouts`,
method: 'POST',
body: params as any,
})
)?.response as unknown as __experimental_CommerceCheckoutJSON;

return new __experimental_CommerceCheckout(json);
};
}
2 changes: 2 additions & 0 deletions packages/clerk-js/src/core/modules/commerce/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
export * from './Commerce';
export * from './CommerceBilling';
2 changes: 1 addition & 1 deletion packages/clerk-js/src/core/resources/Base.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,7 +62,7 @@ export abstract class BaseResource {
return !this.id;
}

protected static async _fetch<J extends ClerkResourceJSON | DeletedObjectJSON | null>(
static async _fetch<J extends ClerkResourceJSON | DeletedObjectJSON | null>(
Comment thread
panteliselef marked this conversation as resolved.
requestInit: FapiRequestInit,
opts: BaseFetchOptions = {},
): Promise<FapiResponseJSON<J> | null> {
Expand Down
63 changes: 63 additions & 0 deletions packages/clerk-js/src/core/resources/CommerceCheckout.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
import type {
__experimental_CommerceCheckoutJSON,
__experimental_CommerceCheckoutResource,
__experimental_CommerceTotals,
__experimental_ConfirmCheckoutParams,
} from '@clerk/types';

import { commerceTotalsFromJSON } from '../../utils';
import {
__experimental_CommerceInvoice,
__experimental_CommercePaymentSource,
__experimental_CommercePlan,
__experimental_CommerceSubscription,
BaseResource,
} from './internal';

export class __experimental_CommerceCheckout extends BaseResource implements __experimental_CommerceCheckoutResource {
pathRoot = '/me/commerce/checkouts';

id!: string;
Comment thread
aeliox marked this conversation as resolved.
externalClientSecret!: string;
externalGatewayId!: string;
invoice?: __experimental_CommerceInvoice;
paymentSource?: __experimental_CommercePaymentSource;
plan!: __experimental_CommercePlan;
planPeriod!: string;
status!: string;
subscription?: __experimental_CommerceSubscription;
totals!: __experimental_CommerceTotals;

constructor(data: __experimental_CommerceCheckoutJSON) {
super();
this.fromJSON(data);
}

protected fromJSON(data: __experimental_CommerceCheckoutJSON | null): this {
if (!data) {
return this;
}

this.id = data.id;
this.externalClientSecret = data.external_client_secret;
this.externalGatewayId = data.external_gateway_id;
this.invoice = data.invoice ? new __experimental_CommerceInvoice(data.invoice) : undefined;
this.paymentSource = data.payment_source
? new __experimental_CommercePaymentSource(data.payment_source)
: undefined;
this.plan = new __experimental_CommercePlan(data.plan);
this.planPeriod = data.plan_period;
this.status = data.status;
this.subscription = data.subscription ? new __experimental_CommerceSubscription(data.subscription) : undefined;
this.totals = commerceTotalsFromJSON(data.totals);

return this;
}

confirm = (params?: __experimental_ConfirmCheckoutParams): Promise<this> => {
return this._basePatch({
path: this.path('confirm'),
body: params as any,
});
};
}
30 changes: 30 additions & 0 deletions packages/clerk-js/src/core/resources/CommerceFeature.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
import type { __experimental_CommerceFeatureJSON, __experimental_CommerceFeatureResource } from '@clerk/types';

import { BaseResource } from './internal';

export class __experimental_CommerceFeature extends BaseResource implements __experimental_CommerceFeatureResource {
id!: string;
name!: string;
description!: string;
slug!: string;
avatarUrl!: string;

constructor(data: __experimental_CommerceFeatureJSON) {
super();
this.fromJSON(data);
}

protected fromJSON(data: __experimental_CommerceFeatureJSON | null): this {
if (!data) {
return this;
}

this.id = data.id;
this.name = data.name;
this.description = data.description;
this.slug = data.slug;
this.avatarUrl = data.avatar_url;

return this;
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
1be9179
add pricing table basics
aeliox Jan 16, 2025
b23b7b4
pricing table as customizables / elements
aeliox Jan 29, 2025
3411e5b
add some variants to plan cards
aeliox Jan 30, 2025
6f4aef0
plan annual price toggle
aeliox Jan 31, 2025
8e36198
plan card width, plan sorting
aeliox Jan 31, 2025
fb04388
bump
aeliox Feb 5, 2025
db1b6eb
checkout blade work
aeliox Feb 7, 2025
c0ede3b
checkout happy path
aeliox Feb 14, 2025
01d7259
commerce blade component
aeliox Feb 21, 2025
646cdc6
cancel subscription from plan detail blade, wip
aeliox Feb 26, 2025
92dab17
Conclude merge and update changes
aeliox Feb 26, 2025
e93d3c1
sprinkle in a bunch of __experimental_
aeliox Feb 27, 2025
a2bfdd8
address PR feedback
aeliox Mar 5, 2025
66849fd
wip
alexcarpenter Mar 6, 2025
e979c41
Update pnpm-lock.yaml
alexcarpenter Mar 6, 2025
eac3fff
SegmentedControl
alexcarpenter Mar 6, 2025
87d4597
text alignment
alexcarpenter Mar 6, 2025
b24e790
sandbox tweaks
alexcarpenter Mar 6, 2025
95a2c29
Update template.html
alexcarpenter Mar 6, 2025
4a0fa41
secondary bordered button
alexcarpenter Mar 7, 2025
9b59ae9
remove text-center
alexcarpenter Mar 7, 2025
be4c0ed
disclosure
alexcarpenter Mar 7, 2025
73b2afc
fix
alexcarpenter Mar 7, 2025
b4e088a
feat(clerk-js): `<LineItems />` component (#5302)
alexcarpenter Mar 7, 2025
585942d
animations
alexcarpenter Mar 7, 2025
c305b58
some checkout work, WIP
aeliox Mar 8, 2025
2b288a9
clean up useCheckout state
aeliox Mar 10, 2025
0392107
PR feedback
aeliox Mar 11, 2025
111fee2
Merge branch 'main' into keiran/commerce-pricing-table
aeliox Mar 11, 2025
283cd46
fix payment method line on checkout complete
aeliox Mar 11, 2025
b710814
toggle features
alexcarpenter Mar 11, 2025
e269613
address remaining pieces of feedback
aeliox Mar 11, 2025
2420725
tag missing localization TODOs
aeliox Mar 11, 2025
33aa00c
Merge branch 'main' into keiran/commerce-pricing-table
aeliox Mar 12, 2025
fbde87a
fix tests
aeliox Mar 12, 2025
42feca9
Merge branch 'main' into keiran/commerce-pricing-table
aeliox Mar 12, 2025
cef81aa
add changeset
aeliox Mar 12, 2025
5842e6b
Include stripe dependencies to only Checkout
panteliselef Mar 12, 2025
f008948
bump bundlewatch sizes, pin stripe package versions
aeliox Mar 12, 2025
3d93892
pnpm dedupe
aeliox Mar 12, 2025
2c1c7ee
another bundlewatch bump
aeliox Mar 12, 2025
6c203b6
linter fix
aeliox Mar 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/large-feet-hammer.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
---
'@clerk/localizations': patch
'@clerk/clerk-js': patch
'@clerk/clerk-react': patch
'@clerk/types': patch
---

Introduce experimental billing APIs and components
14 changes: 8 additions & 6 deletions packages/clerk-js/bundlewatch.config.json
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
{
"files": [
{ "path": "./dist/clerk.js", "maxSize": "560kB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "75kB" },
{ "path": "./dist/clerk.headless.js", "maxSize": "48.3KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "89KB" },
{ "path": "./dist/vendors*.js", "maxSize": "25.1KB" },
{ "path": "./dist/clerk.js", "maxSize": "570kB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "76kB" },
{ "path": "./dist/clerk.headless.js", "maxSize": "50KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "92KB" },
{ "path": "./dist/vendors*.js", "maxSize": "26.5KB" },
{ "path": "./dist/coinbase*.js", "maxSize": "35.5KB" },
{ "path": "./dist/createorganization*.js", "maxSize": "5KB" },
{ "path": "./dist/impersonationfab*.js", "maxSize": "5KB" },
Expand All@@ -18,6 +18,8 @@
{ "path": "./dist/userverification*.js", "maxSize": "5KB" },
{ "path": "./dist/onetap*.js", "maxSize": "1KB" },
{ "path": "./dist/waitlist*.js", "maxSize": "1.3KB" },
{ "path": "./dist/keylessPrompt*.js", "maxSize": "5.9KB" }
{ "path": "./dist/keylessPrompt*.js", "maxSize": "5.9KB" },
{ "path": "./dist/pricingTable*.js", "maxSize": "3.5KB" },
{ "path": "./dist/checkout*.js", "maxSize": "8.8KB" }
]
}
2 changes: 2 additions & 0 deletions packages/clerk-js/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,6 +63,8 @@
"@floating-ui/react": "0.25.4",
"@floating-ui/react-dom": "^2.0.2",
"@formkit/auto-animate": "^0.8.1",
"@stripe/react-stripe-js": "3.1.1",
"@stripe/stripe-js": "5.6.0",
"@swc/helpers": "^0.5.13",
"@zxcvbn-ts/core": "3.0.4",
"@zxcvbn-ts/language-common": "3.0.4",
Expand Down
9 changes: 9 additions & 0 deletions packages/clerk-js/rspack.config.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -93,6 +93,15 @@ const common = ({ mode, disableRHC = false }) => {
name: 'signup',
test: module => module.resource && module.resource.includes('/ui/components/SignUp'),
},
checkout: {
minChunks: 1,
name: 'checkout',
test: module =>
module.resource &&
(module.resource.includes('/ui/components/Checkout') ||
// Include `@stripe/react-stripe-js` and `@stripe/stripe-js` in the checkout chunk
module.resource.includes('/node_modules/@stripe')),
},
common: {
minChunks: 1,
name: 'ui-common',
Expand Down
8 changes: 7 additions & 1 deletion packages/clerk-js/sandbox/app.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@ const AVAILABLE_COMPONENTS = [
'organizationProfile',
'organizationSwitcher',
'waitlist',
'pricingTable',
] as const;

const COMPONENT_PROPS_NAMESPACE = 'clerk-js-sandbox';
Expand DownExpand Up@@ -73,6 +74,7 @@ const componentControls: Record<(typeof AVAILABLE_COMPONENTS)[number], Component
organizationProfile: buildComponentControls('organizationProfile'),
organizationSwitcher: buildComponentControls('organizationSwitcher'),
waitlist: buildComponentControls('waitlist'),
pricingTable: buildComponentControls('pricingTable'),
};

declare global {
Expand All@@ -95,7 +97,7 @@ const app = document.getElementById('app') as HTMLDivElement;
function mountIndex(element: HTMLDivElement) {
assertClerkIsLoaded(Clerk);
const user = Clerk.user;
element.innerHTML = `<pre><code>${JSON.stringify({ user }, null, 2)}</code></pre>`;
element.innerHTML = `<pre class="text-left whitespace-pre overflow-x-auto bg-white p-4 border border-gray-100 rounded-md text-sm"><code>${JSON.stringify({ user }, null, 2)}</code></pre>`;
}

function mountOpenSignInButton(element: HTMLDivElement, props) {
Expand DownExpand Up@@ -167,6 +169,9 @@ function addCurrentRouteIndicator(currentRoute: string) {
},
});
},
'/pricing-table': () => {
Clerk.__experimental_mountPricingTable(app, componentControls.pricingTable.getProps() ?? {});
},
'/open-sign-in': () => {
mountOpenSignInButton(app, componentControls.signIn.getProps() ?? {});
},
Expand All@@ -183,6 +188,7 @@ function addCurrentRouteIndicator(currentRoute: string) {
...(componentControls.clerk.getProps() ?? {}),
signInUrl: '/sign-in',
signUpUrl: '/sign-up',
experimental: { commerce: true },
});
renderCurrentRoute();
} else {
Expand Down
10 changes: 9 additions & 1 deletion packages/clerk-js/sandbox/template.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -249,6 +249,14 @@
Keyless
</a>
</li>
<li class="relative">
<a
class="relative isolate flex w-full rounded-md border border-white px-2 py-[0.4375rem] text-sm hover:bg-gray-50 aria-[current]:bg-gray-50"
href="/pricing-table"
>
PricingTable
</a>
</li>
<li class="relative">
<a
class="relative isolate flex w-full rounded-md border border-white px-2 py-[0.4375rem] text-sm hover:bg-gray-50 aria-[current]:bg-gray-50"
Expand All@@ -274,7 +282,7 @@
>
<div
id="app"
class="max-w-full p-12"
class="container w-full p-12"
></div>
</main>

Expand Down
40 changes: 40 additions & 0 deletions packages/clerk-js/src/core/clerk.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,8 @@ import {
import { addClerkPrefix, isAbsoluteUrl, stripScheme } from '@clerk/shared/url';
import { handleValueOrFn, noop } from '@clerk/shared/utils';
import type {
__experimental_CommerceNamespace,
__experimental_PricingTableProps,
__internal_UserVerificationModalProps,
AuthenticateWithCoinbaseWalletParams,
AuthenticateWithGoogleOneTapParams,
Expand DownExpand Up@@ -119,6 +121,7 @@ import {
import { eventBus, events } from './events';
import type { FapiClient, FapiRequestCallback } from './fapiClient';
import { createFapiClient } from './fapiClient';
import { __experimental_Commerce } from './modules/commerce';
import {
BaseResource,
Client,
Expand DownExpand Up@@ -166,6 +169,7 @@ export class Clerk implements ClerkInterface {
version: __PKG_VERSION__,
environment: process.env.NODE_ENV || 'production',
};
private static _commerce: __experimental_CommerceNamespace;

public client: ClientResource | undefined;
public session: SignedInSessionResource | null | undefined;
Expand DownExpand Up@@ -287,6 +291,19 @@ export class Clerk implements ClerkInterface {
return this.#options.standardBrowser || false;
}

get __experimental_commerce(): __experimental_CommerceNamespace {
if (!this.#options.experimental?.commerce) {
throw new Error(
'Clerk: commerce functionality is currently in an experimental state. To enable, pass `experimental.commerce = true`.',
);
}

if (!Clerk._commerce) {
Clerk._commerce = new __experimental_Commerce();
}
return Clerk._commerce;
}

public __internal_getOption<K extends keyof ClerkOptions>(key: K): ClerkOptions[K] {
return this.#options[key];
}
Expand DownExpand Up@@ -886,6 +903,29 @@ export class Clerk implements ClerkInterface {
void this.#componentControls?.ensureMounted().then(controls => controls.unmountComponent({ node }));
};

public __experimental_mountPricingTable = (node: HTMLDivElement, props?: __experimental_PricingTableProps): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls.ensureMounted({ preloadHint: 'PricingTable' }).then(controls =>
controls.mountComponent({
name: 'PricingTable',
appearanceKey: 'pricingTable',
node,
props,
}),
);

this.telemetry?.record(eventPrebuiltComponentMounted('PricingTable', props));
};

public __experimental_unmountPricingTable = (node: HTMLDivElement): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls.ensureMounted().then(controls =>
controls.unmountComponent({
node,
}),
);
};

/**
* `setActive` can be used to set the active session and/or organization.
*/
Expand Down
46 changes: 46 additions & 0 deletions packages/clerk-js/src/core/modules/commerce/Commerce.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
import type {
__experimental_AddPaymentSourceParams,
__experimental_CommerceBillingNamespace,
__experimental_CommerceNamespace,
__experimental_CommercePaymentSourceJSON,
ClerkPaginatedResponse,
} from '@clerk/types';

import { __experimental_CommercePaymentSource, BaseResource } from '../../resources/internal';
import { __experimental_CommerceBilling } from './CommerceBilling';

export class __experimental_Commerce implements __experimental_CommerceNamespace {
private static _billing: __experimental_CommerceBillingNamespace;

get __experimental_billing(): __experimental_CommerceBillingNamespace {
if (!__experimental_Commerce._billing) {
__experimental_Commerce._billing = new __experimental_CommerceBilling();
}
return __experimental_Commerce._billing;
}

addPaymentSource = async (params: __experimental_AddPaymentSourceParams) => {
const json = (
await BaseResource._fetch({
path: `/me/commerce/payment_sources`,
method: 'POST',
body: params as any,
})
)?.response as unknown as __experimental_CommercePaymentSourceJSON;
return new __experimental_CommercePaymentSource(json);
};

getPaymentSources = async () => {
return await BaseResource._fetch({
path: `/me/commerce/payment_sources`,
method: 'GET',
}).then(res => {
const { data: paymentSources, total_count } =
res as unknown as ClerkPaginatedResponse<__experimental_CommercePaymentSourceJSON>;
return {
total_count,
data: paymentSources.map(paymentSource => new __experimental_CommercePaymentSource(paymentSource)),
};
});
};
}
37 changes: 37 additions & 0 deletions packages/clerk-js/src/core/modules/commerce/CommerceBilling.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
import type {
__experimental_CommerceBillingNamespace,
__experimental_CommerceCheckoutJSON,
__experimental_CommercePlanResource,
__experimental_CommerceProductJSON,
__experimental_CreateCheckoutParams,
__experimental_GetPlansParams,
ClerkPaginatedResponse,
} from '@clerk/types';

import { convertPageToOffsetSearchParams } from '../../../utils/convertPageToOffsetSearchParams';
import { __experimental_CommerceCheckout, __experimental_CommercePlan, BaseResource } from '../../resources/internal';

export class __experimental_CommerceBilling implements __experimental_CommerceBillingNamespace {
getPlans = async (params?: __experimental_GetPlansParams): Promise<__experimental_CommercePlanResource[]> => {
const { data: products } = (await BaseResource._fetch({
path: `/commerce/products`,
method: 'GET',
search: convertPageToOffsetSearchParams(params),
})) as unknown as ClerkPaginatedResponse<__experimental_CommerceProductJSON>;

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

startCheckout = async (params: __experimental_CreateCheckoutParams) => {
const json = (
await BaseResource._fetch<__experimental_CommerceCheckoutJSON>({
path: `/me/commerce/checkouts`,
method: 'POST',
body: params as any,
})
)?.response as unknown as __experimental_CommerceCheckoutJSON;

return new __experimental_CommerceCheckout(json);
};
}
2 changes: 2 additions & 0 deletions packages/clerk-js/src/core/modules/commerce/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
export * from './Commerce';
export * from './CommerceBilling';
2 changes: 1 addition & 1 deletion packages/clerk-js/src/core/resources/Base.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,7 +62,7 @@ export abstract class BaseResource {
return !this.id;
}

protected static async _fetch<J extends ClerkResourceJSON | DeletedObjectJSON | null>(
static async _fetch<J extends ClerkResourceJSON | DeletedObjectJSON | null>(
Comment thread
panteliselef marked this conversation as resolved.
requestInit: FapiRequestInit,
opts: BaseFetchOptions = {},
): Promise<FapiResponseJSON<J> | null> {
Expand Down
63 changes: 63 additions & 0 deletions packages/clerk-js/src/core/resources/CommerceCheckout.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
import type {
__experimental_CommerceCheckoutJSON,
__experimental_CommerceCheckoutResource,
__experimental_CommerceTotals,
__experimental_ConfirmCheckoutParams,
} from '@clerk/types';

import { commerceTotalsFromJSON } from '../../utils';
import {
__experimental_CommerceInvoice,
__experimental_CommercePaymentSource,
__experimental_CommercePlan,
__experimental_CommerceSubscription,
BaseResource,
} from './internal';

export class __experimental_CommerceCheckout extends BaseResource implements __experimental_CommerceCheckoutResource {
pathRoot = '/me/commerce/checkouts';

id!: string;
Comment thread
aeliox marked this conversation as resolved.
externalClientSecret!: string;
externalGatewayId!: string;
invoice?: __experimental_CommerceInvoice;
paymentSource?: __experimental_CommercePaymentSource;
plan!: __experimental_CommercePlan;
planPeriod!: string;
status!: string;
subscription?: __experimental_CommerceSubscription;
totals!: __experimental_CommerceTotals;

constructor(data: __experimental_CommerceCheckoutJSON) {
super();
this.fromJSON(data);
}

protected fromJSON(data: __experimental_CommerceCheckoutJSON | null): this {
if (!data) {
return this;
}

this.id = data.id;
this.externalClientSecret = data.external_client_secret;
this.externalGatewayId = data.external_gateway_id;
this.invoice = data.invoice ? new __experimental_CommerceInvoice(data.invoice) : undefined;
this.paymentSource = data.payment_source
? new __experimental_CommercePaymentSource(data.payment_source)
: undefined;
this.plan = new __experimental_CommercePlan(data.plan);
this.planPeriod = data.plan_period;
this.status = data.status;
this.subscription = data.subscription ? new __experimental_CommerceSubscription(data.subscription) : undefined;
this.totals = commerceTotalsFromJSON(data.totals);

return this;
}

confirm = (params?: __experimental_ConfirmCheckoutParams): Promise<this> => {
return this._basePatch({
path: this.path('confirm'),
body: params as any,
});
};
}
30 changes: 30 additions & 0 deletions packages/clerk-js/src/core/resources/CommerceFeature.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
import type { __experimental_CommerceFeatureJSON, __experimental_CommerceFeatureResource } from '@clerk/types';

import { BaseResource } from './internal';

export class __experimental_CommerceFeature extends BaseResource implements __experimental_CommerceFeatureResource {
id!: string;
name!: string;
description!: string;
slug!: string;
avatarUrl!: string;

constructor(data: __experimental_CommerceFeatureJSON) {
super();
this.fromJSON(data);
}

protected fromJSON(data: __experimental_CommerceFeatureJSON | null): this {
if (!data) {
return this;
}

this.id = data.id;
this.name = data.name;
this.description = data.description;
this.slug = data.slug;
this.avatarUrl = data.avatar_url;

return this;
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
1be9179
add pricing table basics
aeliox Jan 16, 2025
b23b7b4
pricing table as customizables / elements
aeliox Jan 29, 2025
3411e5b
add some variants to plan cards
aeliox Jan 30, 2025
6f4aef0
plan annual price toggle
aeliox Jan 31, 2025
8e36198
plan card width, plan sorting
aeliox Jan 31, 2025
fb04388
bump
aeliox Feb 5, 2025
db1b6eb
checkout blade work
aeliox Feb 7, 2025
c0ede3b
checkout happy path
aeliox Feb 14, 2025
01d7259
commerce blade component
aeliox Feb 21, 2025
646cdc6
cancel subscription from plan detail blade, wip
aeliox Feb 26, 2025
92dab17
Conclude merge and update changes
aeliox Feb 26, 2025
e93d3c1
sprinkle in a bunch of __experimental_
aeliox Feb 27, 2025
a2bfdd8
address PR feedback
aeliox Mar 5, 2025
66849fd
wip
alexcarpenter Mar 6, 2025
e979c41
Update pnpm-lock.yaml
alexcarpenter Mar 6, 2025
eac3fff
SegmentedControl
alexcarpenter Mar 6, 2025
87d4597
text alignment
alexcarpenter Mar 6, 2025
b24e790
sandbox tweaks
alexcarpenter Mar 6, 2025
95a2c29
Update template.html
alexcarpenter Mar 6, 2025
4a0fa41
secondary bordered button
alexcarpenter Mar 7, 2025
9b59ae9
remove text-center
alexcarpenter Mar 7, 2025
be4c0ed
disclosure
alexcarpenter Mar 7, 2025
73b2afc
fix
alexcarpenter Mar 7, 2025
b4e088a
feat(clerk-js): `<LineItems />` component (#5302)
alexcarpenter Mar 7, 2025
585942d
animations
alexcarpenter Mar 7, 2025
c305b58
some checkout work, WIP
aeliox Mar 8, 2025
2b288a9
clean up useCheckout state
aeliox Mar 10, 2025
0392107
PR feedback
aeliox Mar 11, 2025
111fee2
Merge branch 'main' into keiran/commerce-pricing-table
aeliox Mar 11, 2025
283cd46
fix payment method line on checkout complete
aeliox Mar 11, 2025
b710814
toggle features
alexcarpenter Mar 11, 2025
e269613
address remaining pieces of feedback
aeliox Mar 11, 2025
2420725
tag missing localization TODOs
aeliox Mar 11, 2025
33aa00c
Merge branch 'main' into keiran/commerce-pricing-table
aeliox Mar 12, 2025
fbde87a
fix tests
aeliox Mar 12, 2025
42feca9
Merge branch 'main' into keiran/commerce-pricing-table
aeliox Mar 12, 2025
cef81aa
add changeset
aeliox Mar 12, 2025
5842e6b
Include stripe dependencies to only Checkout
panteliselef Mar 12, 2025
f008948
bump bundlewatch sizes, pin stripe package versions
aeliox Mar 12, 2025
3d93892
pnpm dedupe
aeliox Mar 12, 2025
2c1c7ee
another bundlewatch bump
aeliox Mar 12, 2025
6c203b6
linter fix
aeliox Mar 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/large-feet-hammer.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
---
'@clerk/localizations': patch
'@clerk/clerk-js': patch
'@clerk/clerk-react': patch
'@clerk/types': patch
---

Introduce experimental billing APIs and components
14 changes: 8 additions & 6 deletions packages/clerk-js/bundlewatch.config.json
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
{
"files": [
{ "path": "./dist/clerk.js", "maxSize": "560kB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "75kB" },
{ "path": "./dist/clerk.headless.js", "maxSize": "48.3KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "89KB" },
{ "path": "./dist/vendors*.js", "maxSize": "25.1KB" },
{ "path": "./dist/clerk.js", "maxSize": "570kB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "76kB" },
{ "path": "./dist/clerk.headless.js", "maxSize": "50KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "92KB" },
{ "path": "./dist/vendors*.js", "maxSize": "26.5KB" },
{ "path": "./dist/coinbase*.js", "maxSize": "35.5KB" },
{ "path": "./dist/createorganization*.js", "maxSize": "5KB" },
{ "path": "./dist/impersonationfab*.js", "maxSize": "5KB" },
Expand All@@ -18,6 +18,8 @@
{ "path": "./dist/userverification*.js", "maxSize": "5KB" },
{ "path": "./dist/onetap*.js", "maxSize": "1KB" },
{ "path": "./dist/waitlist*.js", "maxSize": "1.3KB" },
{ "path": "./dist/keylessPrompt*.js", "maxSize": "5.9KB" }
{ "path": "./dist/keylessPrompt*.js", "maxSize": "5.9KB" },
{ "path": "./dist/pricingTable*.js", "maxSize": "3.5KB" },
{ "path": "./dist/checkout*.js", "maxSize": "8.8KB" }
]
}
2 changes: 2 additions & 0 deletions packages/clerk-js/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,6 +63,8 @@
"@floating-ui/react": "0.25.4",
"@floating-ui/react-dom": "^2.0.2",
"@formkit/auto-animate": "^0.8.1",
"@stripe/react-stripe-js": "3.1.1",
"@stripe/stripe-js": "5.6.0",
"@swc/helpers": "^0.5.13",
"@zxcvbn-ts/core": "3.0.4",
"@zxcvbn-ts/language-common": "3.0.4",
Expand Down
9 changes: 9 additions & 0 deletions packages/clerk-js/rspack.config.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -93,6 +93,15 @@ const common = ({ mode, disableRHC = false }) => {
name: 'signup',
test: module => module.resource && module.resource.includes('/ui/components/SignUp'),
},
checkout: {
minChunks: 1,
name: 'checkout',
test: module =>
module.resource &&
(module.resource.includes('/ui/components/Checkout') ||
// Include `@stripe/react-stripe-js` and `@stripe/stripe-js` in the checkout chunk
module.resource.includes('/node_modules/@stripe')),
},
common: {
minChunks: 1,
name: 'ui-common',
Expand Down
8 changes: 7 additions & 1 deletion packages/clerk-js/sandbox/app.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@ const AVAILABLE_COMPONENTS = [
'organizationProfile',
'organizationSwitcher',
'waitlist',
'pricingTable',
] as const;

const COMPONENT_PROPS_NAMESPACE = 'clerk-js-sandbox';
Expand DownExpand Up@@ -73,6 +74,7 @@ const componentControls: Record<(typeof AVAILABLE_COMPONENTS)[number], Component
organizationProfile: buildComponentControls('organizationProfile'),
organizationSwitcher: buildComponentControls('organizationSwitcher'),
waitlist: buildComponentControls('waitlist'),
pricingTable: buildComponentControls('pricingTable'),
};

declare global {
Expand All@@ -95,7 +97,7 @@ const app = document.getElementById('app') as HTMLDivElement;
function mountIndex(element: HTMLDivElement) {
assertClerkIsLoaded(Clerk);
const user = Clerk.user;
element.innerHTML = `<pre><code>${JSON.stringify({ user }, null, 2)}</code></pre>`;
element.innerHTML = `<pre class="text-left whitespace-pre overflow-x-auto bg-white p-4 border border-gray-100 rounded-md text-sm"><code>${JSON.stringify({ user }, null, 2)}</code></pre>`;
}

function mountOpenSignInButton(element: HTMLDivElement, props) {
Expand DownExpand Up@@ -167,6 +169,9 @@ function addCurrentRouteIndicator(currentRoute: string) {
},
});
},
'/pricing-table': () => {
Clerk.__experimental_mountPricingTable(app, componentControls.pricingTable.getProps() ?? {});
},
'/open-sign-in': () => {
mountOpenSignInButton(app, componentControls.signIn.getProps() ?? {});
},
Expand All@@ -183,6 +188,7 @@ function addCurrentRouteIndicator(currentRoute: string) {
...(componentControls.clerk.getProps() ?? {}),
signInUrl: '/sign-in',
signUpUrl: '/sign-up',
experimental: { commerce: true },
});
renderCurrentRoute();
} else {
Expand Down
10 changes: 9 additions & 1 deletion packages/clerk-js/sandbox/template.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -249,6 +249,14 @@
Keyless
</a>
</li>
<li class="relative">
<a
class="relative isolate flex w-full rounded-md border border-white px-2 py-[0.4375rem] text-sm hover:bg-gray-50 aria-[current]:bg-gray-50"
href="/pricing-table"
>
PricingTable
</a>
</li>
<li class="relative">
<a
class="relative isolate flex w-full rounded-md border border-white px-2 py-[0.4375rem] text-sm hover:bg-gray-50 aria-[current]:bg-gray-50"
Expand All@@ -274,7 +282,7 @@
>
<div
id="app"
class="max-w-full p-12"
class="container w-full p-12"
></div>
</main>

Expand Down
40 changes: 40 additions & 0 deletions packages/clerk-js/src/core/clerk.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,8 @@ import {
import { addClerkPrefix, isAbsoluteUrl, stripScheme } from '@clerk/shared/url';
import { handleValueOrFn, noop } from '@clerk/shared/utils';
import type {
__experimental_CommerceNamespace,
__experimental_PricingTableProps,
__internal_UserVerificationModalProps,
AuthenticateWithCoinbaseWalletParams,
AuthenticateWithGoogleOneTapParams,
Expand DownExpand Up@@ -119,6 +121,7 @@ import {
import { eventBus, events } from './events';
import type { FapiClient, FapiRequestCallback } from './fapiClient';
import { createFapiClient } from './fapiClient';
import { __experimental_Commerce } from './modules/commerce';
import {
BaseResource,
Client,
Expand DownExpand Up@@ -166,6 +169,7 @@ export class Clerk implements ClerkInterface {
version: __PKG_VERSION__,
environment: process.env.NODE_ENV || 'production',
};
private static _commerce: __experimental_CommerceNamespace;

public client: ClientResource | undefined;
public session: SignedInSessionResource | null | undefined;
Expand DownExpand Up@@ -287,6 +291,19 @@ export class Clerk implements ClerkInterface {
return this.#options.standardBrowser || false;
}

get __experimental_commerce(): __experimental_CommerceNamespace {
if (!this.#options.experimental?.commerce) {
throw new Error(
'Clerk: commerce functionality is currently in an experimental state. To enable, pass `experimental.commerce = true`.',
);
}

if (!Clerk._commerce) {
Clerk._commerce = new __experimental_Commerce();
}
return Clerk._commerce;
}

public __internal_getOption<K extends keyof ClerkOptions>(key: K): ClerkOptions[K] {
return this.#options[key];
}
Expand DownExpand Up@@ -886,6 +903,29 @@ export class Clerk implements ClerkInterface {
void this.#componentControls?.ensureMounted().then(controls => controls.unmountComponent({ node }));
};

public __experimental_mountPricingTable = (node: HTMLDivElement, props?: __experimental_PricingTableProps): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls.ensureMounted({ preloadHint: 'PricingTable' }).then(controls =>
controls.mountComponent({
name: 'PricingTable',
appearanceKey: 'pricingTable',
node,
props,
}),
);

this.telemetry?.record(eventPrebuiltComponentMounted('PricingTable', props));
};

public __experimental_unmountPricingTable = (node: HTMLDivElement): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls.ensureMounted().then(controls =>
controls.unmountComponent({
node,
}),
);
};

/**
* `setActive` can be used to set the active session and/or organization.
*/
Expand Down
46 changes: 46 additions & 0 deletions packages/clerk-js/src/core/modules/commerce/Commerce.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
import type {
__experimental_AddPaymentSourceParams,
__experimental_CommerceBillingNamespace,
__experimental_CommerceNamespace,
__experimental_CommercePaymentSourceJSON,
ClerkPaginatedResponse,
} from '@clerk/types';

import { __experimental_CommercePaymentSource, BaseResource } from '../../resources/internal';
import { __experimental_CommerceBilling } from './CommerceBilling';

export class __experimental_Commerce implements __experimental_CommerceNamespace {
private static _billing: __experimental_CommerceBillingNamespace;

get __experimental_billing(): __experimental_CommerceBillingNamespace {
if (!__experimental_Commerce._billing) {
__experimental_Commerce._billing = new __experimental_CommerceBilling();
}
return __experimental_Commerce._billing;
}

addPaymentSource = async (params: __experimental_AddPaymentSourceParams) => {
const json = (
await BaseResource._fetch({
path: `/me/commerce/payment_sources`,
method: 'POST',
body: params as any,
})
)?.response as unknown as __experimental_CommercePaymentSourceJSON;
return new __experimental_CommercePaymentSource(json);
};

getPaymentSources = async () => {
return await BaseResource._fetch({
path: `/me/commerce/payment_sources`,
method: 'GET',
}).then(res => {
const { data: paymentSources, total_count } =
res as unknown as ClerkPaginatedResponse<__experimental_CommercePaymentSourceJSON>;
return {
total_count,
data: paymentSources.map(paymentSource => new __experimental_CommercePaymentSource(paymentSource)),
};
});
};
}
37 changes: 37 additions & 0 deletions packages/clerk-js/src/core/modules/commerce/CommerceBilling.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
import type {
__experimental_CommerceBillingNamespace,
__experimental_CommerceCheckoutJSON,
__experimental_CommercePlanResource,
__experimental_CommerceProductJSON,
__experimental_CreateCheckoutParams,
__experimental_GetPlansParams,
ClerkPaginatedResponse,
} from '@clerk/types';

import { convertPageToOffsetSearchParams } from '../../../utils/convertPageToOffsetSearchParams';
import { __experimental_CommerceCheckout, __experimental_CommercePlan, BaseResource } from '../../resources/internal';

export class __experimental_CommerceBilling implements __experimental_CommerceBillingNamespace {
getPlans = async (params?: __experimental_GetPlansParams): Promise<__experimental_CommercePlanResource[]> => {
const { data: products } = (await BaseResource._fetch({
path: `/commerce/products`,
method: 'GET',
search: convertPageToOffsetSearchParams(params),
})) as unknown as ClerkPaginatedResponse<__experimental_CommerceProductJSON>;

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

startCheckout = async (params: __experimental_CreateCheckoutParams) => {
const json = (
await BaseResource._fetch<__experimental_CommerceCheckoutJSON>({
path: `/me/commerce/checkouts`,
method: 'POST',
body: params as any,
})
)?.response as unknown as __experimental_CommerceCheckoutJSON;

return new __experimental_CommerceCheckout(json);
};
}
2 changes: 2 additions & 0 deletions packages/clerk-js/src/core/modules/commerce/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
export * from './Commerce';
export * from './CommerceBilling';
2 changes: 1 addition & 1 deletion packages/clerk-js/src/core/resources/Base.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,7 +62,7 @@ export abstract class BaseResource {
return !this.id;
}

protected static async _fetch<J extends ClerkResourceJSON | DeletedObjectJSON | null>(
static async _fetch<J extends ClerkResourceJSON | DeletedObjectJSON | null>(
Comment thread
panteliselef marked this conversation as resolved.
requestInit: FapiRequestInit,
opts: BaseFetchOptions = {},
): Promise<FapiResponseJSON<J> | null> {
Expand Down
63 changes: 63 additions & 0 deletions packages/clerk-js/src/core/resources/CommerceCheckout.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
import type {
__experimental_CommerceCheckoutJSON,
__experimental_CommerceCheckoutResource,
__experimental_CommerceTotals,
__experimental_ConfirmCheckoutParams,
} from '@clerk/types';

import { commerceTotalsFromJSON } from '../../utils';
import {
__experimental_CommerceInvoice,
__experimental_CommercePaymentSource,
__experimental_CommercePlan,
__experimental_CommerceSubscription,
BaseResource,
} from './internal';

export class __experimental_CommerceCheckout extends BaseResource implements __experimental_CommerceCheckoutResource {
pathRoot = '/me/commerce/checkouts';

id!: string;
Comment thread
aeliox marked this conversation as resolved.
externalClientSecret!: string;
externalGatewayId!: string;
invoice?: __experimental_CommerceInvoice;
paymentSource?: __experimental_CommercePaymentSource;
plan!: __experimental_CommercePlan;
planPeriod!: string;
status!: string;
subscription?: __experimental_CommerceSubscription;
totals!: __experimental_CommerceTotals;

constructor(data: __experimental_CommerceCheckoutJSON) {
super();
this.fromJSON(data);
}

protected fromJSON(data: __experimental_CommerceCheckoutJSON | null): this {
if (!data) {
return this;
}

this.id = data.id;
this.externalClientSecret = data.external_client_secret;
this.externalGatewayId = data.external_gateway_id;
this.invoice = data.invoice ? new __experimental_CommerceInvoice(data.invoice) : undefined;
this.paymentSource = data.payment_source
? new __experimental_CommercePaymentSource(data.payment_source)
: undefined;
this.plan = new __experimental_CommercePlan(data.plan);
this.planPeriod = data.plan_period;
this.status = data.status;
this.subscription = data.subscription ? new __experimental_CommerceSubscription(data.subscription) : undefined;
this.totals = commerceTotalsFromJSON(data.totals);

return this;
}

confirm = (params?: __experimental_ConfirmCheckoutParams): Promise<this> => {
return this._basePatch({
path: this.path('confirm'),
body: params as any,
});
};
}
30 changes: 30 additions & 0 deletions packages/clerk-js/src/core/resources/CommerceFeature.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
import type { __experimental_CommerceFeatureJSON, __experimental_CommerceFeatureResource } from '@clerk/types';

import { BaseResource } from './internal';

export class __experimental_CommerceFeature extends BaseResource implements __experimental_CommerceFeatureResource {
id!: string;
name!: string;
description!: string;
slug!: string;
avatarUrl!: string;

constructor(data: __experimental_CommerceFeatureJSON) {
super();
this.fromJSON(data);
}

protected fromJSON(data: __experimental_CommerceFeatureJSON | null): this {
if (!data) {
return this;
}

this.id = data.id;
this.name = data.name;
this.description = data.description;
this.slug = data.slug;
this.avatarUrl = data.avatar_url;

return this;
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
1be9179
add pricing table basics
aeliox Jan 16, 2025
b23b7b4
pricing table as customizables / elements
aeliox Jan 29, 2025
3411e5b
add some variants to plan cards
aeliox Jan 30, 2025
6f4aef0
plan annual price toggle
aeliox Jan 31, 2025
8e36198
plan card width, plan sorting
aeliox Jan 31, 2025
fb04388
bump
aeliox Feb 5, 2025
db1b6eb
checkout blade work
aeliox Feb 7, 2025
c0ede3b
checkout happy path
aeliox Feb 14, 2025
01d7259
commerce blade component
aeliox Feb 21, 2025
646cdc6
cancel subscription from plan detail blade, wip
aeliox Feb 26, 2025
92dab17
Conclude merge and update changes
aeliox Feb 26, 2025
e93d3c1
sprinkle in a bunch of __experimental_
aeliox Feb 27, 2025
a2bfdd8
address PR feedback
aeliox Mar 5, 2025
66849fd
wip
alexcarpenter Mar 6, 2025
e979c41
Update pnpm-lock.yaml
alexcarpenter Mar 6, 2025
eac3fff
SegmentedControl
alexcarpenter Mar 6, 2025
87d4597
text alignment
alexcarpenter Mar 6, 2025
b24e790
sandbox tweaks
alexcarpenter Mar 6, 2025
95a2c29
Update template.html
alexcarpenter Mar 6, 2025
4a0fa41
secondary bordered button
alexcarpenter Mar 7, 2025
9b59ae9
remove text-center
alexcarpenter Mar 7, 2025
be4c0ed
disclosure
alexcarpenter Mar 7, 2025
73b2afc
fix
alexcarpenter Mar 7, 2025
b4e088a
feat(clerk-js): `<LineItems />` component (#5302)
alexcarpenter Mar 7, 2025
585942d
animations
alexcarpenter Mar 7, 2025
c305b58
some checkout work, WIP
aeliox Mar 8, 2025
2b288a9
clean up useCheckout state
aeliox Mar 10, 2025
0392107
PR feedback
aeliox Mar 11, 2025
111fee2
Merge branch 'main' into keiran/commerce-pricing-table
aeliox Mar 11, 2025
283cd46
fix payment method line on checkout complete
aeliox Mar 11, 2025
b710814
toggle features
alexcarpenter Mar 11, 2025
e269613
address remaining pieces of feedback
aeliox Mar 11, 2025
2420725
tag missing localization TODOs
aeliox Mar 11, 2025
33aa00c
Merge branch 'main' into keiran/commerce-pricing-table
aeliox Mar 12, 2025
fbde87a
fix tests
aeliox Mar 12, 2025
42feca9
Merge branch 'main' into keiran/commerce-pricing-table
aeliox Mar 12, 2025
cef81aa
add changeset
aeliox Mar 12, 2025
5842e6b
Include stripe dependencies to only Checkout
panteliselef Mar 12, 2025
f008948
bump bundlewatch sizes, pin stripe package versions
aeliox Mar 12, 2025
3d93892
pnpm dedupe
aeliox Mar 12, 2025
2c1c7ee
another bundlewatch bump
aeliox Mar 12, 2025
6c203b6
linter fix
aeliox Mar 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/large-feet-hammer.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
---
'@clerk/localizations': patch
'@clerk/clerk-js': patch
'@clerk/clerk-react': patch
'@clerk/types': patch
---

Introduce experimental billing APIs and components
14 changes: 8 additions & 6 deletions packages/clerk-js/bundlewatch.config.json
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
{
"files": [
{ "path": "./dist/clerk.js", "maxSize": "560kB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "75kB" },
{ "path": "./dist/clerk.headless.js", "maxSize": "48.3KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "89KB" },
{ "path": "./dist/vendors*.js", "maxSize": "25.1KB" },
{ "path": "./dist/clerk.js", "maxSize": "570kB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "76kB" },
{ "path": "./dist/clerk.headless.js", "maxSize": "50KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "92KB" },
{ "path": "./dist/vendors*.js", "maxSize": "26.5KB" },
{ "path": "./dist/coinbase*.js", "maxSize": "35.5KB" },
{ "path": "./dist/createorganization*.js", "maxSize": "5KB" },
{ "path": "./dist/impersonationfab*.js", "maxSize": "5KB" },
Expand All@@ -18,6 +18,8 @@
{ "path": "./dist/userverification*.js", "maxSize": "5KB" },
{ "path": "./dist/onetap*.js", "maxSize": "1KB" },
{ "path": "./dist/waitlist*.js", "maxSize": "1.3KB" },
{ "path": "./dist/keylessPrompt*.js", "maxSize": "5.9KB" }
{ "path": "./dist/keylessPrompt*.js", "maxSize": "5.9KB" },
{ "path": "./dist/pricingTable*.js", "maxSize": "3.5KB" },
{ "path": "./dist/checkout*.js", "maxSize": "8.8KB" }
]
}
2 changes: 2 additions & 0 deletions packages/clerk-js/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,6 +63,8 @@
"@floating-ui/react": "0.25.4",
"@floating-ui/react-dom": "^2.0.2",
"@formkit/auto-animate": "^0.8.1",
"@stripe/react-stripe-js": "3.1.1",
"@stripe/stripe-js": "5.6.0",
"@swc/helpers": "^0.5.13",
"@zxcvbn-ts/core": "3.0.4",
"@zxcvbn-ts/language-common": "3.0.4",
Expand Down
9 changes: 9 additions & 0 deletions packages/clerk-js/rspack.config.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -93,6 +93,15 @@ const common = ({ mode, disableRHC = false }) => {
name: 'signup',
test: module => module.resource && module.resource.includes('/ui/components/SignUp'),
},
checkout: {
minChunks: 1,
name: 'checkout',
test: module =>
module.resource &&
(module.resource.includes('/ui/components/Checkout') ||
// Include `@stripe/react-stripe-js` and `@stripe/stripe-js` in the checkout chunk
module.resource.includes('/node_modules/@stripe')),
},
common: {
minChunks: 1,
name: 'ui-common',
Expand Down
8 changes: 7 additions & 1 deletion packages/clerk-js/sandbox/app.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@ const AVAILABLE_COMPONENTS = [
'organizationProfile',
'organizationSwitcher',
'waitlist',
'pricingTable',
] as const;

const COMPONENT_PROPS_NAMESPACE = 'clerk-js-sandbox';
Expand DownExpand Up@@ -73,6 +74,7 @@ const componentControls: Record<(typeof AVAILABLE_COMPONENTS)[number], Component
organizationProfile: buildComponentControls('organizationProfile'),
organizationSwitcher: buildComponentControls('organizationSwitcher'),
waitlist: buildComponentControls('waitlist'),
pricingTable: buildComponentControls('pricingTable'),
};

declare global {
Expand All@@ -95,7 +97,7 @@ const app = document.getElementById('app') as HTMLDivElement;
function mountIndex(element: HTMLDivElement) {
assertClerkIsLoaded(Clerk);
const user = Clerk.user;
element.innerHTML = `<pre><code>${JSON.stringify({ user }, null, 2)}</code></pre>`;
element.innerHTML = `<pre class="text-left whitespace-pre overflow-x-auto bg-white p-4 border border-gray-100 rounded-md text-sm"><code>${JSON.stringify({ user }, null, 2)}</code></pre>`;
}

function mountOpenSignInButton(element: HTMLDivElement, props) {
Expand DownExpand Up@@ -167,6 +169,9 @@ function addCurrentRouteIndicator(currentRoute: string) {
},
});
},
'/pricing-table': () => {
Clerk.__experimental_mountPricingTable(app, componentControls.pricingTable.getProps() ?? {});
},
'/open-sign-in': () => {
mountOpenSignInButton(app, componentControls.signIn.getProps() ?? {});
},
Expand All@@ -183,6 +188,7 @@ function addCurrentRouteIndicator(currentRoute: string) {
...(componentControls.clerk.getProps() ?? {}),
signInUrl: '/sign-in',
signUpUrl: '/sign-up',
experimental: { commerce: true },
});
renderCurrentRoute();
} else {
Expand Down
10 changes: 9 additions & 1 deletion packages/clerk-js/sandbox/template.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -249,6 +249,14 @@
Keyless
</a>
</li>
<li class="relative">
<a
class="relative isolate flex w-full rounded-md border border-white px-2 py-[0.4375rem] text-sm hover:bg-gray-50 aria-[current]:bg-gray-50"
href="/pricing-table"
>
PricingTable
</a>
</li>
<li class="relative">
<a
class="relative isolate flex w-full rounded-md border border-white px-2 py-[0.4375rem] text-sm hover:bg-gray-50 aria-[current]:bg-gray-50"
Expand All@@ -274,7 +282,7 @@
>
<div
id="app"
class="max-w-full p-12"
class="container w-full p-12"
></div>
</main>

Expand Down
40 changes: 40 additions & 0 deletions packages/clerk-js/src/core/clerk.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,8 @@ import {
import { addClerkPrefix, isAbsoluteUrl, stripScheme } from '@clerk/shared/url';
import { handleValueOrFn, noop } from '@clerk/shared/utils';
import type {
__experimental_CommerceNamespace,
__experimental_PricingTableProps,
__internal_UserVerificationModalProps,
AuthenticateWithCoinbaseWalletParams,
AuthenticateWithGoogleOneTapParams,
Expand DownExpand Up@@ -119,6 +121,7 @@ import {
import { eventBus, events } from './events';
import type { FapiClient, FapiRequestCallback } from './fapiClient';
import { createFapiClient } from './fapiClient';
import { __experimental_Commerce } from './modules/commerce';
import {
BaseResource,
Client,
Expand DownExpand Up@@ -166,6 +169,7 @@ export class Clerk implements ClerkInterface {
version: __PKG_VERSION__,
environment: process.env.NODE_ENV || 'production',
};
private static _commerce: __experimental_CommerceNamespace;

public client: ClientResource | undefined;
public session: SignedInSessionResource | null | undefined;
Expand DownExpand Up@@ -287,6 +291,19 @@ export class Clerk implements ClerkInterface {
return this.#options.standardBrowser || false;
}

get __experimental_commerce(): __experimental_CommerceNamespace {
if (!this.#options.experimental?.commerce) {
throw new Error(
'Clerk: commerce functionality is currently in an experimental state. To enable, pass `experimental.commerce = true`.',
);
}

if (!Clerk._commerce) {
Clerk._commerce = new __experimental_Commerce();
}
return Clerk._commerce;
}

public __internal_getOption<K extends keyof ClerkOptions>(key: K): ClerkOptions[K] {
return this.#options[key];
}
Expand DownExpand Up@@ -886,6 +903,29 @@ export class Clerk implements ClerkInterface {
void this.#componentControls?.ensureMounted().then(controls => controls.unmountComponent({ node }));
};

public __experimental_mountPricingTable = (node: HTMLDivElement, props?: __experimental_PricingTableProps): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls.ensureMounted({ preloadHint: 'PricingTable' }).then(controls =>
controls.mountComponent({
name: 'PricingTable',
appearanceKey: 'pricingTable',
node,
props,
}),
);

this.telemetry?.record(eventPrebuiltComponentMounted('PricingTable', props));
};

public __experimental_unmountPricingTable = (node: HTMLDivElement): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls.ensureMounted().then(controls =>
controls.unmountComponent({
node,
}),
);
};

/**
* `setActive` can be used to set the active session and/or organization.
*/
Expand Down
46 changes: 46 additions & 0 deletions packages/clerk-js/src/core/modules/commerce/Commerce.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
import type {
__experimental_AddPaymentSourceParams,
__experimental_CommerceBillingNamespace,
__experimental_CommerceNamespace,
__experimental_CommercePaymentSourceJSON,
ClerkPaginatedResponse,
} from '@clerk/types';

import { __experimental_CommercePaymentSource, BaseResource } from '../../resources/internal';
import { __experimental_CommerceBilling } from './CommerceBilling';

export class __experimental_Commerce implements __experimental_CommerceNamespace {
private static _billing: __experimental_CommerceBillingNamespace;

get __experimental_billing(): __experimental_CommerceBillingNamespace {
if (!__experimental_Commerce._billing) {
__experimental_Commerce._billing = new __experimental_CommerceBilling();
}
return __experimental_Commerce._billing;
}

addPaymentSource = async (params: __experimental_AddPaymentSourceParams) => {
const json = (
await BaseResource._fetch({
path: `/me/commerce/payment_sources`,
method: 'POST',
body: params as any,
})
)?.response as unknown as __experimental_CommercePaymentSourceJSON;
return new __experimental_CommercePaymentSource(json);
};

getPaymentSources = async () => {
return await BaseResource._fetch({
path: `/me/commerce/payment_sources`,
method: 'GET',
}).then(res => {
const { data: paymentSources, total_count } =
res as unknown as ClerkPaginatedResponse<__experimental_CommercePaymentSourceJSON>;
return {
total_count,
data: paymentSources.map(paymentSource => new __experimental_CommercePaymentSource(paymentSource)),
};
});
};
}
37 changes: 37 additions & 0 deletions packages/clerk-js/src/core/modules/commerce/CommerceBilling.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
import type {
__experimental_CommerceBillingNamespace,
__experimental_CommerceCheckoutJSON,
__experimental_CommercePlanResource,
__experimental_CommerceProductJSON,
__experimental_CreateCheckoutParams,
__experimental_GetPlansParams,
ClerkPaginatedResponse,
} from '@clerk/types';

import { convertPageToOffsetSearchParams } from '../../../utils/convertPageToOffsetSearchParams';
import { __experimental_CommerceCheckout, __experimental_CommercePlan, BaseResource } from '../../resources/internal';

export class __experimental_CommerceBilling implements __experimental_CommerceBillingNamespace {
getPlans = async (params?: __experimental_GetPlansParams): Promise<__experimental_CommercePlanResource[]> => {
const { data: products } = (await BaseResource._fetch({
path: `/commerce/products`,
method: 'GET',
search: convertPageToOffsetSearchParams(params),
})) as unknown as ClerkPaginatedResponse<__experimental_CommerceProductJSON>;

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

startCheckout = async (params: __experimental_CreateCheckoutParams) => {
const json = (
await BaseResource._fetch<__experimental_CommerceCheckoutJSON>({
path: `/me/commerce/checkouts`,
method: 'POST',
body: params as any,
})
)?.response as unknown as __experimental_CommerceCheckoutJSON;

return new __experimental_CommerceCheckout(json);
};
}
2 changes: 2 additions & 0 deletions packages/clerk-js/src/core/modules/commerce/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
export * from './Commerce';
export * from './CommerceBilling';
2 changes: 1 addition & 1 deletion packages/clerk-js/src/core/resources/Base.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,7 +62,7 @@ export abstract class BaseResource {
return !this.id;
}

protected static async _fetch<J extends ClerkResourceJSON | DeletedObjectJSON | null>(
static async _fetch<J extends ClerkResourceJSON | DeletedObjectJSON | null>(
Comment thread
panteliselef marked this conversation as resolved.
requestInit: FapiRequestInit,
opts: BaseFetchOptions = {},
): Promise<FapiResponseJSON<J> | null> {
Expand Down
63 changes: 63 additions & 0 deletions packages/clerk-js/src/core/resources/CommerceCheckout.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
import type {
__experimental_CommerceCheckoutJSON,
__experimental_CommerceCheckoutResource,
__experimental_CommerceTotals,
__experimental_ConfirmCheckoutParams,
} from '@clerk/types';

import { commerceTotalsFromJSON } from '../../utils';
import {
__experimental_CommerceInvoice,
__experimental_CommercePaymentSource,
__experimental_CommercePlan,
__experimental_CommerceSubscription,
BaseResource,
} from './internal';

export class __experimental_CommerceCheckout extends BaseResource implements __experimental_CommerceCheckoutResource {
pathRoot = '/me/commerce/checkouts';

id!: string;
Comment thread
aeliox marked this conversation as resolved.
externalClientSecret!: string;
externalGatewayId!: string;
invoice?: __experimental_CommerceInvoice;
paymentSource?: __experimental_CommercePaymentSource;
plan!: __experimental_CommercePlan;
planPeriod!: string;
status!: string;
subscription?: __experimental_CommerceSubscription;
totals!: __experimental_CommerceTotals;

constructor(data: __experimental_CommerceCheckoutJSON) {
super();
this.fromJSON(data);
}

protected fromJSON(data: __experimental_CommerceCheckoutJSON | null): this {
if (!data) {
return this;
}

this.id = data.id;
this.externalClientSecret = data.external_client_secret;
this.externalGatewayId = data.external_gateway_id;
this.invoice = data.invoice ? new __experimental_CommerceInvoice(data.invoice) : undefined;
this.paymentSource = data.payment_source
? new __experimental_CommercePaymentSource(data.payment_source)
: undefined;
this.plan = new __experimental_CommercePlan(data.plan);
this.planPeriod = data.plan_period;
this.status = data.status;
this.subscription = data.subscription ? new __experimental_CommerceSubscription(data.subscription) : undefined;
this.totals = commerceTotalsFromJSON(data.totals);

return this;
}

confirm = (params?: __experimental_ConfirmCheckoutParams): Promise<this> => {
return this._basePatch({
path: this.path('confirm'),
body: params as any,
});
};
}
30 changes: 30 additions & 0 deletions packages/clerk-js/src/core/resources/CommerceFeature.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
import type { __experimental_CommerceFeatureJSON, __experimental_CommerceFeatureResource } from '@clerk/types';

import { BaseResource } from './internal';

export class __experimental_CommerceFeature extends BaseResource implements __experimental_CommerceFeatureResource {
id!: string;
name!: string;
description!: string;
slug!: string;
avatarUrl!: string;

constructor(data: __experimental_CommerceFeatureJSON) {
super();
this.fromJSON(data);
}

protected fromJSON(data: __experimental_CommerceFeatureJSON | null): this {
if (!data) {
return this;
}

this.id = data.id;
this.name = data.name;
this.description = data.description;
this.slug = data.slug;
this.avatarUrl = data.avatar_url;

return this;
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
1be9179
add pricing table basics
aeliox Jan 16, 2025
b23b7b4
pricing table as customizables / elements
aeliox Jan 29, 2025
3411e5b
add some variants to plan cards
aeliox Jan 30, 2025
6f4aef0
plan annual price toggle
aeliox Jan 31, 2025
8e36198
plan card width, plan sorting
aeliox Jan 31, 2025
fb04388
bump
aeliox Feb 5, 2025
db1b6eb
checkout blade work
aeliox Feb 7, 2025
c0ede3b
checkout happy path
aeliox Feb 14, 2025
01d7259
commerce blade component
aeliox Feb 21, 2025
646cdc6
cancel subscription from plan detail blade, wip
aeliox Feb 26, 2025
92dab17
Conclude merge and update changes
aeliox Feb 26, 2025
e93d3c1
sprinkle in a bunch of __experimental_
aeliox Feb 27, 2025
a2bfdd8
address PR feedback
aeliox Mar 5, 2025
66849fd
wip
alexcarpenter Mar 6, 2025
e979c41
Update pnpm-lock.yaml
alexcarpenter Mar 6, 2025
eac3fff
SegmentedControl
alexcarpenter Mar 6, 2025
87d4597
text alignment
alexcarpenter Mar 6, 2025
b24e790
sandbox tweaks
alexcarpenter Mar 6, 2025
95a2c29
Update template.html
alexcarpenter Mar 6, 2025
4a0fa41
secondary bordered button
alexcarpenter Mar 7, 2025
9b59ae9
remove text-center
alexcarpenter Mar 7, 2025
be4c0ed
disclosure
alexcarpenter Mar 7, 2025
73b2afc
fix
alexcarpenter Mar 7, 2025
b4e088a
feat(clerk-js): `<LineItems />` component (#5302)
alexcarpenter Mar 7, 2025
585942d
animations
alexcarpenter Mar 7, 2025
c305b58
some checkout work, WIP
aeliox Mar 8, 2025
2b288a9
clean up useCheckout state
aeliox Mar 10, 2025
0392107
PR feedback
aeliox Mar 11, 2025
111fee2
Merge branch 'main' into keiran/commerce-pricing-table
aeliox Mar 11, 2025
283cd46
fix payment method line on checkout complete
aeliox Mar 11, 2025
b710814
toggle features
alexcarpenter Mar 11, 2025
e269613
address remaining pieces of feedback
aeliox Mar 11, 2025
2420725
tag missing localization TODOs
aeliox Mar 11, 2025
33aa00c
Merge branch 'main' into keiran/commerce-pricing-table
aeliox Mar 12, 2025
fbde87a
fix tests
aeliox Mar 12, 2025
42feca9
Merge branch 'main' into keiran/commerce-pricing-table
aeliox Mar 12, 2025
cef81aa
add changeset
aeliox Mar 12, 2025
5842e6b
Include stripe dependencies to only Checkout
panteliselef Mar 12, 2025
f008948
bump bundlewatch sizes, pin stripe package versions
aeliox Mar 12, 2025
3d93892
pnpm dedupe
aeliox Mar 12, 2025
2c1c7ee
another bundlewatch bump
aeliox Mar 12, 2025
6c203b6
linter fix
aeliox Mar 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/large-feet-hammer.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
---
'@clerk/localizations': patch
'@clerk/clerk-js': patch
'@clerk/clerk-react': patch
'@clerk/types': patch
---

Introduce experimental billing APIs and components
14 changes: 8 additions & 6 deletions packages/clerk-js/bundlewatch.config.json
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
{
"files": [
{ "path": "./dist/clerk.js", "maxSize": "560kB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "75kB" },
{ "path": "./dist/clerk.headless.js", "maxSize": "48.3KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "89KB" },
{ "path": "./dist/vendors*.js", "maxSize": "25.1KB" },
{ "path": "./dist/clerk.js", "maxSize": "570kB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "76kB" },
{ "path": "./dist/clerk.headless.js", "maxSize": "50KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "92KB" },
{ "path": "./dist/vendors*.js", "maxSize": "26.5KB" },
{ "path": "./dist/coinbase*.js", "maxSize": "35.5KB" },
{ "path": "./dist/createorganization*.js", "maxSize": "5KB" },
{ "path": "./dist/impersonationfab*.js", "maxSize": "5KB" },
Expand All@@ -18,6 +18,8 @@
{ "path": "./dist/userverification*.js", "maxSize": "5KB" },
{ "path": "./dist/onetap*.js", "maxSize": "1KB" },
{ "path": "./dist/waitlist*.js", "maxSize": "1.3KB" },
{ "path": "./dist/keylessPrompt*.js", "maxSize": "5.9KB" }
{ "path": "./dist/keylessPrompt*.js", "maxSize": "5.9KB" },
{ "path": "./dist/pricingTable*.js", "maxSize": "3.5KB" },
{ "path": "./dist/checkout*.js", "maxSize": "8.8KB" }
]
}
2 changes: 2 additions & 0 deletions packages/clerk-js/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,6 +63,8 @@
"@floating-ui/react": "0.25.4",
"@floating-ui/react-dom": "^2.0.2",
"@formkit/auto-animate": "^0.8.1",
"@stripe/react-stripe-js": "3.1.1",
"@stripe/stripe-js": "5.6.0",
"@swc/helpers": "^0.5.13",
"@zxcvbn-ts/core": "3.0.4",
"@zxcvbn-ts/language-common": "3.0.4",
Expand Down
9 changes: 9 additions & 0 deletions packages/clerk-js/rspack.config.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -93,6 +93,15 @@ const common = ({ mode, disableRHC = false }) => {
name: 'signup',
test: module => module.resource && module.resource.includes('/ui/components/SignUp'),
},
checkout: {
minChunks: 1,
name: 'checkout',
test: module =>
module.resource &&
(module.resource.includes('/ui/components/Checkout') ||
// Include `@stripe/react-stripe-js` and `@stripe/stripe-js` in the checkout chunk
module.resource.includes('/node_modules/@stripe')),
},
common: {
minChunks: 1,
name: 'ui-common',
Expand Down
8 changes: 7 additions & 1 deletion packages/clerk-js/sandbox/app.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@ const AVAILABLE_COMPONENTS = [
'organizationProfile',
'organizationSwitcher',
'waitlist',
'pricingTable',
] as const;

const COMPONENT_PROPS_NAMESPACE = 'clerk-js-sandbox';
Expand DownExpand Up@@ -73,6 +74,7 @@ const componentControls: Record<(typeof AVAILABLE_COMPONENTS)[number], Component
organizationProfile: buildComponentControls('organizationProfile'),
organizationSwitcher: buildComponentControls('organizationSwitcher'),
waitlist: buildComponentControls('waitlist'),
pricingTable: buildComponentControls('pricingTable'),
};

declare global {
Expand All@@ -95,7 +97,7 @@ const app = document.getElementById('app') as HTMLDivElement;
function mountIndex(element: HTMLDivElement) {
assertClerkIsLoaded(Clerk);
const user = Clerk.user;
element.innerHTML = `<pre><code>${JSON.stringify({ user }, null, 2)}</code></pre>`;
element.innerHTML = `<pre class="text-left whitespace-pre overflow-x-auto bg-white p-4 border border-gray-100 rounded-md text-sm"><code>${JSON.stringify({ user }, null, 2)}</code></pre>`;
}

function mountOpenSignInButton(element: HTMLDivElement, props) {
Expand DownExpand Up@@ -167,6 +169,9 @@ function addCurrentRouteIndicator(currentRoute: string) {
},
});
},
'/pricing-table': () => {
Clerk.__experimental_mountPricingTable(app, componentControls.pricingTable.getProps() ?? {});
},
'/open-sign-in': () => {
mountOpenSignInButton(app, componentControls.signIn.getProps() ?? {});
},
Expand All@@ -183,6 +188,7 @@ function addCurrentRouteIndicator(currentRoute: string) {
...(componentControls.clerk.getProps() ?? {}),
signInUrl: '/sign-in',
signUpUrl: '/sign-up',
experimental: { commerce: true },
});
renderCurrentRoute();
} else {
Expand Down
10 changes: 9 additions & 1 deletion packages/clerk-js/sandbox/template.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -249,6 +249,14 @@
Keyless
</a>
</li>
<li class="relative">
<a
class="relative isolate flex w-full rounded-md border border-white px-2 py-[0.4375rem] text-sm hover:bg-gray-50 aria-[current]:bg-gray-50"
href="/pricing-table"
>
PricingTable
</a>
</li>
<li class="relative">
<a
class="relative isolate flex w-full rounded-md border border-white px-2 py-[0.4375rem] text-sm hover:bg-gray-50 aria-[current]:bg-gray-50"
Expand All@@ -274,7 +282,7 @@
>
<div
id="app"
class="max-w-full p-12"
class="container w-full p-12"
></div>
</main>

Expand Down
40 changes: 40 additions & 0 deletions packages/clerk-js/src/core/clerk.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,8 @@ import {
import { addClerkPrefix, isAbsoluteUrl, stripScheme } from '@clerk/shared/url';
import { handleValueOrFn, noop } from '@clerk/shared/utils';
import type {
__experimental_CommerceNamespace,
__experimental_PricingTableProps,
__internal_UserVerificationModalProps,
AuthenticateWithCoinbaseWalletParams,
AuthenticateWithGoogleOneTapParams,
Expand DownExpand Up@@ -119,6 +121,7 @@ import {
import { eventBus, events } from './events';
import type { FapiClient, FapiRequestCallback } from './fapiClient';
import { createFapiClient } from './fapiClient';
import { __experimental_Commerce } from './modules/commerce';
import {
BaseResource,
Client,
Expand DownExpand Up@@ -166,6 +169,7 @@ export class Clerk implements ClerkInterface {
version: __PKG_VERSION__,
environment: process.env.NODE_ENV || 'production',
};
private static _commerce: __experimental_CommerceNamespace;

public client: ClientResource | undefined;
public session: SignedInSessionResource | null | undefined;
Expand DownExpand Up@@ -287,6 +291,19 @@ export class Clerk implements ClerkInterface {
return this.#options.standardBrowser || false;
}

get __experimental_commerce(): __experimental_CommerceNamespace {
if (!this.#options.experimental?.commerce) {
throw new Error(
'Clerk: commerce functionality is currently in an experimental state. To enable, pass `experimental.commerce = true`.',
);
}

if (!Clerk._commerce) {
Clerk._commerce = new __experimental_Commerce();
}
return Clerk._commerce;
}

public __internal_getOption<K extends keyof ClerkOptions>(key: K): ClerkOptions[K] {
return this.#options[key];
}
Expand DownExpand Up@@ -886,6 +903,29 @@ export class Clerk implements ClerkInterface {
void this.#componentControls?.ensureMounted().then(controls => controls.unmountComponent({ node }));
};

public __experimental_mountPricingTable = (node: HTMLDivElement, props?: __experimental_PricingTableProps): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls.ensureMounted({ preloadHint: 'PricingTable' }).then(controls =>
controls.mountComponent({
name: 'PricingTable',
appearanceKey: 'pricingTable',
node,
props,
}),
);

this.telemetry?.record(eventPrebuiltComponentMounted('PricingTable', props));
};

public __experimental_unmountPricingTable = (node: HTMLDivElement): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls.ensureMounted().then(controls =>
controls.unmountComponent({
node,
}),
);
};

/**
* `setActive` can be used to set the active session and/or organization.
*/
Expand Down
46 changes: 46 additions & 0 deletions packages/clerk-js/src/core/modules/commerce/Commerce.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
import type {
__experimental_AddPaymentSourceParams,
__experimental_CommerceBillingNamespace,
__experimental_CommerceNamespace,
__experimental_CommercePaymentSourceJSON,
ClerkPaginatedResponse,
} from '@clerk/types';

import { __experimental_CommercePaymentSource, BaseResource } from '../../resources/internal';
import { __experimental_CommerceBilling } from './CommerceBilling';

export class __experimental_Commerce implements __experimental_CommerceNamespace {
private static _billing: __experimental_CommerceBillingNamespace;

get __experimental_billing(): __experimental_CommerceBillingNamespace {
if (!__experimental_Commerce._billing) {
__experimental_Commerce._billing = new __experimental_CommerceBilling();
}
return __experimental_Commerce._billing;
}

addPaymentSource = async (params: __experimental_AddPaymentSourceParams) => {
const json = (
await BaseResource._fetch({
path: `/me/commerce/payment_sources`,
method: 'POST',
body: params as any,
})
)?.response as unknown as __experimental_CommercePaymentSourceJSON;
return new __experimental_CommercePaymentSource(json);
};

getPaymentSources = async () => {
return await BaseResource._fetch({
path: `/me/commerce/payment_sources`,
method: 'GET',
}).then(res => {
const { data: paymentSources, total_count } =
res as unknown as ClerkPaginatedResponse<__experimental_CommercePaymentSourceJSON>;
return {
total_count,
data: paymentSources.map(paymentSource => new __experimental_CommercePaymentSource(paymentSource)),
};
});
};
}
37 changes: 37 additions & 0 deletions packages/clerk-js/src/core/modules/commerce/CommerceBilling.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
import type {
__experimental_CommerceBillingNamespace,
__experimental_CommerceCheckoutJSON,
__experimental_CommercePlanResource,
__experimental_CommerceProductJSON,
__experimental_CreateCheckoutParams,
__experimental_GetPlansParams,
ClerkPaginatedResponse,
} from '@clerk/types';

import { convertPageToOffsetSearchParams } from '../../../utils/convertPageToOffsetSearchParams';
import { __experimental_CommerceCheckout, __experimental_CommercePlan, BaseResource } from '../../resources/internal';

export class __experimental_CommerceBilling implements __experimental_CommerceBillingNamespace {
getPlans = async (params?: __experimental_GetPlansParams): Promise<__experimental_CommercePlanResource[]> => {
const { data: products } = (await BaseResource._fetch({
path: `/commerce/products`,
method: 'GET',
search: convertPageToOffsetSearchParams(params),
})) as unknown as ClerkPaginatedResponse<__experimental_CommerceProductJSON>;

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

startCheckout = async (params: __experimental_CreateCheckoutParams) => {
const json = (
await BaseResource._fetch<__experimental_CommerceCheckoutJSON>({
path: `/me/commerce/checkouts`,
method: 'POST',
body: params as any,
})
)?.response as unknown as __experimental_CommerceCheckoutJSON;

return new __experimental_CommerceCheckout(json);
};
}
2 changes: 2 additions & 0 deletions packages/clerk-js/src/core/modules/commerce/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
export * from './Commerce';
export * from './CommerceBilling';
2 changes: 1 addition & 1 deletion packages/clerk-js/src/core/resources/Base.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,7 +62,7 @@ export abstract class BaseResource {
return !this.id;
}

protected static async _fetch<J extends ClerkResourceJSON | DeletedObjectJSON | null>(
static async _fetch<J extends ClerkResourceJSON | DeletedObjectJSON | null>(
Comment thread
panteliselef marked this conversation as resolved.
requestInit: FapiRequestInit,
opts: BaseFetchOptions = {},
): Promise<FapiResponseJSON<J> | null> {
Expand Down
63 changes: 63 additions & 0 deletions packages/clerk-js/src/core/resources/CommerceCheckout.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
import type {
__experimental_CommerceCheckoutJSON,
__experimental_CommerceCheckoutResource,
__experimental_CommerceTotals,
__experimental_ConfirmCheckoutParams,
} from '@clerk/types';

import { commerceTotalsFromJSON } from '../../utils';
import {
__experimental_CommerceInvoice,
__experimental_CommercePaymentSource,
__experimental_CommercePlan,
__experimental_CommerceSubscription,
BaseResource,
} from './internal';

export class __experimental_CommerceCheckout extends BaseResource implements __experimental_CommerceCheckoutResource {
pathRoot = '/me/commerce/checkouts';

id!: string;
Comment thread
aeliox marked this conversation as resolved.
externalClientSecret!: string;
externalGatewayId!: string;
invoice?: __experimental_CommerceInvoice;
paymentSource?: __experimental_CommercePaymentSource;
plan!: __experimental_CommercePlan;
planPeriod!: string;
status!: string;
subscription?: __experimental_CommerceSubscription;
totals!: __experimental_CommerceTotals;

constructor(data: __experimental_CommerceCheckoutJSON) {
super();
this.fromJSON(data);
}

protected fromJSON(data: __experimental_CommerceCheckoutJSON | null): this {
if (!data) {
return this;
}

this.id = data.id;
this.externalClientSecret = data.external_client_secret;
this.externalGatewayId = data.external_gateway_id;
this.invoice = data.invoice ? new __experimental_CommerceInvoice(data.invoice) : undefined;
this.paymentSource = data.payment_source
? new __experimental_CommercePaymentSource(data.payment_source)
: undefined;
this.plan = new __experimental_CommercePlan(data.plan);
this.planPeriod = data.plan_period;
this.status = data.status;
this.subscription = data.subscription ? new __experimental_CommerceSubscription(data.subscription) : undefined;
this.totals = commerceTotalsFromJSON(data.totals);

return this;
}

confirm = (params?: __experimental_ConfirmCheckoutParams): Promise<this> => {
return this._basePatch({
path: this.path('confirm'),
body: params as any,
});
};
}
30 changes: 30 additions & 0 deletions packages/clerk-js/src/core/resources/CommerceFeature.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
import type { __experimental_CommerceFeatureJSON, __experimental_CommerceFeatureResource } from '@clerk/types';

import { BaseResource } from './internal';

export class __experimental_CommerceFeature extends BaseResource implements __experimental_CommerceFeatureResource {
id!: string;
name!: string;
description!: string;
slug!: string;
avatarUrl!: string;

constructor(data: __experimental_CommerceFeatureJSON) {
super();
this.fromJSON(data);
}

protected fromJSON(data: __experimental_CommerceFeatureJSON | null): this {
if (!data) {
return this;
}

this.id = data.id;
this.name = data.name;
this.description = data.description;
this.slug = data.slug;
this.avatarUrl = data.avatar_url;

return this;
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
1be9179
add pricing table basics
aeliox Jan 16, 2025
b23b7b4
pricing table as customizables / elements
aeliox Jan 29, 2025
3411e5b
add some variants to plan cards
aeliox Jan 30, 2025
6f4aef0
plan annual price toggle
aeliox Jan 31, 2025
8e36198
plan card width, plan sorting
aeliox Jan 31, 2025
fb04388
bump
aeliox Feb 5, 2025
db1b6eb
checkout blade work
aeliox Feb 7, 2025
c0ede3b
checkout happy path
aeliox Feb 14, 2025
01d7259
commerce blade component
aeliox Feb 21, 2025
646cdc6
cancel subscription from plan detail blade, wip
aeliox Feb 26, 2025
92dab17
Conclude merge and update changes
aeliox Feb 26, 2025
e93d3c1
sprinkle in a bunch of __experimental_
aeliox Feb 27, 2025
a2bfdd8
address PR feedback
aeliox Mar 5, 2025
66849fd
wip
alexcarpenter Mar 6, 2025
e979c41
Update pnpm-lock.yaml
alexcarpenter Mar 6, 2025
eac3fff
SegmentedControl
alexcarpenter Mar 6, 2025
87d4597
text alignment
alexcarpenter Mar 6, 2025
b24e790
sandbox tweaks
alexcarpenter Mar 6, 2025
95a2c29
Update template.html
alexcarpenter Mar 6, 2025
4a0fa41
secondary bordered button
alexcarpenter Mar 7, 2025
9b59ae9
remove text-center
alexcarpenter Mar 7, 2025
be4c0ed
disclosure
alexcarpenter Mar 7, 2025
73b2afc
fix
alexcarpenter Mar 7, 2025
b4e088a
feat(clerk-js): `<LineItems />` component (#5302)
alexcarpenter Mar 7, 2025
585942d
animations
alexcarpenter Mar 7, 2025
c305b58
some checkout work, WIP
aeliox Mar 8, 2025
2b288a9
clean up useCheckout state
aeliox Mar 10, 2025
0392107
PR feedback
aeliox Mar 11, 2025
111fee2
Merge branch 'main' into keiran/commerce-pricing-table
aeliox Mar 11, 2025
283cd46
fix payment method line on checkout complete
aeliox Mar 11, 2025
b710814
toggle features
alexcarpenter Mar 11, 2025
e269613
address remaining pieces of feedback
aeliox Mar 11, 2025
2420725
tag missing localization TODOs
aeliox Mar 11, 2025
33aa00c
Merge branch 'main' into keiran/commerce-pricing-table
aeliox Mar 12, 2025
fbde87a
fix tests
aeliox Mar 12, 2025
42feca9
Merge branch 'main' into keiran/commerce-pricing-table
aeliox Mar 12, 2025
cef81aa
add changeset
aeliox Mar 12, 2025
5842e6b
Include stripe dependencies to only Checkout
panteliselef Mar 12, 2025
f008948
bump bundlewatch sizes, pin stripe package versions
aeliox Mar 12, 2025
3d93892
pnpm dedupe
aeliox Mar 12, 2025
2c1c7ee
another bundlewatch bump
aeliox Mar 12, 2025
6c203b6
linter fix
aeliox Mar 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/large-feet-hammer.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
---
'@clerk/localizations': patch
'@clerk/clerk-js': patch
'@clerk/clerk-react': patch
'@clerk/types': patch
---

Introduce experimental billing APIs and components
14 changes: 8 additions & 6 deletions packages/clerk-js/bundlewatch.config.json
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
{
"files": [
{ "path": "./dist/clerk.js", "maxSize": "560kB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "75kB" },
{ "path": "./dist/clerk.headless.js", "maxSize": "48.3KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "89KB" },
{ "path": "./dist/vendors*.js", "maxSize": "25.1KB" },
{ "path": "./dist/clerk.js", "maxSize": "570kB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "76kB" },
{ "path": "./dist/clerk.headless.js", "maxSize": "50KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "92KB" },
{ "path": "./dist/vendors*.js", "maxSize": "26.5KB" },
{ "path": "./dist/coinbase*.js", "maxSize": "35.5KB" },
{ "path": "./dist/createorganization*.js", "maxSize": "5KB" },
{ "path": "./dist/impersonationfab*.js", "maxSize": "5KB" },
Expand All@@ -18,6 +18,8 @@
{ "path": "./dist/userverification*.js", "maxSize": "5KB" },
{ "path": "./dist/onetap*.js", "maxSize": "1KB" },
{ "path": "./dist/waitlist*.js", "maxSize": "1.3KB" },
{ "path": "./dist/keylessPrompt*.js", "maxSize": "5.9KB" }
{ "path": "./dist/keylessPrompt*.js", "maxSize": "5.9KB" },
{ "path": "./dist/pricingTable*.js", "maxSize": "3.5KB" },
{ "path": "./dist/checkout*.js", "maxSize": "8.8KB" }
]
}
2 changes: 2 additions & 0 deletions packages/clerk-js/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,6 +63,8 @@
"@floating-ui/react": "0.25.4",
"@floating-ui/react-dom": "^2.0.2",
"@formkit/auto-animate": "^0.8.1",
"@stripe/react-stripe-js": "3.1.1",
"@stripe/stripe-js": "5.6.0",
"@swc/helpers": "^0.5.13",
"@zxcvbn-ts/core": "3.0.4",
"@zxcvbn-ts/language-common": "3.0.4",
Expand Down
9 changes: 9 additions & 0 deletions packages/clerk-js/rspack.config.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -93,6 +93,15 @@ const common = ({ mode, disableRHC = false }) => {
name: 'signup',
test: module => module.resource && module.resource.includes('/ui/components/SignUp'),
},
checkout: {
minChunks: 1,
name: 'checkout',
test: module =>
module.resource &&
(module.resource.includes('/ui/components/Checkout') ||
// Include `@stripe/react-stripe-js` and `@stripe/stripe-js` in the checkout chunk
module.resource.includes('/node_modules/@stripe')),
},
common: {
minChunks: 1,
name: 'ui-common',
Expand Down
8 changes: 7 additions & 1 deletion packages/clerk-js/sandbox/app.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@ const AVAILABLE_COMPONENTS = [
'organizationProfile',
'organizationSwitcher',
'waitlist',
'pricingTable',
] as const;

const COMPONENT_PROPS_NAMESPACE = 'clerk-js-sandbox';
Expand DownExpand Up@@ -73,6 +74,7 @@ const componentControls: Record<(typeof AVAILABLE_COMPONENTS)[number], Component
organizationProfile: buildComponentControls('organizationProfile'),
organizationSwitcher: buildComponentControls('organizationSwitcher'),
waitlist: buildComponentControls('waitlist'),
pricingTable: buildComponentControls('pricingTable'),
};

declare global {
Expand All@@ -95,7 +97,7 @@ const app = document.getElementById('app') as HTMLDivElement;
function mountIndex(element: HTMLDivElement) {
assertClerkIsLoaded(Clerk);
const user = Clerk.user;
element.innerHTML = `<pre><code>${JSON.stringify({ user }, null, 2)}</code></pre>`;
element.innerHTML = `<pre class="text-left whitespace-pre overflow-x-auto bg-white p-4 border border-gray-100 rounded-md text-sm"><code>${JSON.stringify({ user }, null, 2)}</code></pre>`;
}

function mountOpenSignInButton(element: HTMLDivElement, props) {
Expand DownExpand Up@@ -167,6 +169,9 @@ function addCurrentRouteIndicator(currentRoute: string) {
},
});
},
'/pricing-table': () => {
Clerk.__experimental_mountPricingTable(app, componentControls.pricingTable.getProps() ?? {});
},
'/open-sign-in': () => {
mountOpenSignInButton(app, componentControls.signIn.getProps() ?? {});
},
Expand All@@ -183,6 +188,7 @@ function addCurrentRouteIndicator(currentRoute: string) {
...(componentControls.clerk.getProps() ?? {}),
signInUrl: '/sign-in',
signUpUrl: '/sign-up',
experimental: { commerce: true },
});
renderCurrentRoute();
} else {
Expand Down
10 changes: 9 additions & 1 deletion packages/clerk-js/sandbox/template.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -249,6 +249,14 @@
Keyless
</a>
</li>
<li class="relative">
<a
class="relative isolate flex w-full rounded-md border border-white px-2 py-[0.4375rem] text-sm hover:bg-gray-50 aria-[current]:bg-gray-50"
href="/pricing-table"
>
PricingTable
</a>
</li>
<li class="relative">
<a
class="relative isolate flex w-full rounded-md border border-white px-2 py-[0.4375rem] text-sm hover:bg-gray-50 aria-[current]:bg-gray-50"
Expand All@@ -274,7 +282,7 @@
>
<div
id="app"
class="max-w-full p-12"
class="container w-full p-12"
></div>
</main>

Expand Down
40 changes: 40 additions & 0 deletions packages/clerk-js/src/core/clerk.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,8 @@ import {
import { addClerkPrefix, isAbsoluteUrl, stripScheme } from '@clerk/shared/url';
import { handleValueOrFn, noop } from '@clerk/shared/utils';
import type {
__experimental_CommerceNamespace,
__experimental_PricingTableProps,
__internal_UserVerificationModalProps,
AuthenticateWithCoinbaseWalletParams,
AuthenticateWithGoogleOneTapParams,
Expand DownExpand Up@@ -119,6 +121,7 @@ import {
import { eventBus, events } from './events';
import type { FapiClient, FapiRequestCallback } from './fapiClient';
import { createFapiClient } from './fapiClient';
import { __experimental_Commerce } from './modules/commerce';
import {
BaseResource,
Client,
Expand DownExpand Up@@ -166,6 +169,7 @@ export class Clerk implements ClerkInterface {
version: __PKG_VERSION__,
environment: process.env.NODE_ENV || 'production',
};
private static _commerce: __experimental_CommerceNamespace;

public client: ClientResource | undefined;
public session: SignedInSessionResource | null | undefined;
Expand DownExpand Up@@ -287,6 +291,19 @@ export class Clerk implements ClerkInterface {
return this.#options.standardBrowser || false;
}

get __experimental_commerce(): __experimental_CommerceNamespace {
if (!this.#options.experimental?.commerce) {
throw new Error(
'Clerk: commerce functionality is currently in an experimental state. To enable, pass `experimental.commerce = true`.',
);
}

if (!Clerk._commerce) {
Clerk._commerce = new __experimental_Commerce();
}
return Clerk._commerce;
}

public __internal_getOption<K extends keyof ClerkOptions>(key: K): ClerkOptions[K] {
return this.#options[key];
}
Expand DownExpand Up@@ -886,6 +903,29 @@ export class Clerk implements ClerkInterface {
void this.#componentControls?.ensureMounted().then(controls => controls.unmountComponent({ node }));
};

public __experimental_mountPricingTable = (node: HTMLDivElement, props?: __experimental_PricingTableProps): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls.ensureMounted({ preloadHint: 'PricingTable' }).then(controls =>
controls.mountComponent({
name: 'PricingTable',
appearanceKey: 'pricingTable',
node,
props,
}),
);

this.telemetry?.record(eventPrebuiltComponentMounted('PricingTable', props));
};

public __experimental_unmountPricingTable = (node: HTMLDivElement): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls.ensureMounted().then(controls =>
controls.unmountComponent({
node,
}),
);
};

/**
* `setActive` can be used to set the active session and/or organization.
*/
Expand Down
46 changes: 46 additions & 0 deletions packages/clerk-js/src/core/modules/commerce/Commerce.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
import type {
__experimental_AddPaymentSourceParams,
__experimental_CommerceBillingNamespace,
__experimental_CommerceNamespace,
__experimental_CommercePaymentSourceJSON,
ClerkPaginatedResponse,
} from '@clerk/types';

import { __experimental_CommercePaymentSource, BaseResource } from '../../resources/internal';
import { __experimental_CommerceBilling } from './CommerceBilling';

export class __experimental_Commerce implements __experimental_CommerceNamespace {
private static _billing: __experimental_CommerceBillingNamespace;

get __experimental_billing(): __experimental_CommerceBillingNamespace {
if (!__experimental_Commerce._billing) {
__experimental_Commerce._billing = new __experimental_CommerceBilling();
}
return __experimental_Commerce._billing;
}

addPaymentSource = async (params: __experimental_AddPaymentSourceParams) => {
const json = (
await BaseResource._fetch({
path: `/me/commerce/payment_sources`,
method: 'POST',
body: params as any,
})
)?.response as unknown as __experimental_CommercePaymentSourceJSON;
return new __experimental_CommercePaymentSource(json);
};

getPaymentSources = async () => {
return await BaseResource._fetch({
path: `/me/commerce/payment_sources`,
method: 'GET',
}).then(res => {
const { data: paymentSources, total_count } =
res as unknown as ClerkPaginatedResponse<__experimental_CommercePaymentSourceJSON>;
return {
total_count,
data: paymentSources.map(paymentSource => new __experimental_CommercePaymentSource(paymentSource)),
};
});
};
}
37 changes: 37 additions & 0 deletions packages/clerk-js/src/core/modules/commerce/CommerceBilling.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
import type {
__experimental_CommerceBillingNamespace,
__experimental_CommerceCheckoutJSON,
__experimental_CommercePlanResource,
__experimental_CommerceProductJSON,
__experimental_CreateCheckoutParams,
__experimental_GetPlansParams,
ClerkPaginatedResponse,
} from '@clerk/types';

import { convertPageToOffsetSearchParams } from '../../../utils/convertPageToOffsetSearchParams';
import { __experimental_CommerceCheckout, __experimental_CommercePlan, BaseResource } from '../../resources/internal';

export class __experimental_CommerceBilling implements __experimental_CommerceBillingNamespace {
getPlans = async (params?: __experimental_GetPlansParams): Promise<__experimental_CommercePlanResource[]> => {
const { data: products } = (await BaseResource._fetch({
path: `/commerce/products`,
method: 'GET',
search: convertPageToOffsetSearchParams(params),
})) as unknown as ClerkPaginatedResponse<__experimental_CommerceProductJSON>;

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

startCheckout = async (params: __experimental_CreateCheckoutParams) => {
const json = (
await BaseResource._fetch<__experimental_CommerceCheckoutJSON>({
path: `/me/commerce/checkouts`,
method: 'POST',
body: params as any,
})
)?.response as unknown as __experimental_CommerceCheckoutJSON;

return new __experimental_CommerceCheckout(json);
};
}
2 changes: 2 additions & 0 deletions packages/clerk-js/src/core/modules/commerce/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
export * from './Commerce';
export * from './CommerceBilling';
2 changes: 1 addition & 1 deletion packages/clerk-js/src/core/resources/Base.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,7 +62,7 @@ export abstract class BaseResource {
return !this.id;
}

protected static async _fetch<J extends ClerkResourceJSON | DeletedObjectJSON | null>(
static async _fetch<J extends ClerkResourceJSON | DeletedObjectJSON | null>(
Comment thread
panteliselef marked this conversation as resolved.
requestInit: FapiRequestInit,
opts: BaseFetchOptions = {},
): Promise<FapiResponseJSON<J> | null> {
Expand Down
63 changes: 63 additions & 0 deletions packages/clerk-js/src/core/resources/CommerceCheckout.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
import type {
__experimental_CommerceCheckoutJSON,
__experimental_CommerceCheckoutResource,
__experimental_CommerceTotals,
__experimental_ConfirmCheckoutParams,
} from '@clerk/types';

import { commerceTotalsFromJSON } from '../../utils';
import {
__experimental_CommerceInvoice,
__experimental_CommercePaymentSource,
__experimental_CommercePlan,
__experimental_CommerceSubscription,
BaseResource,
} from './internal';

export class __experimental_CommerceCheckout extends BaseResource implements __experimental_CommerceCheckoutResource {
pathRoot = '/me/commerce/checkouts';

id!: string;
Comment thread
aeliox marked this conversation as resolved.
externalClientSecret!: string;
externalGatewayId!: string;
invoice?: __experimental_CommerceInvoice;
paymentSource?: __experimental_CommercePaymentSource;
plan!: __experimental_CommercePlan;
planPeriod!: string;
status!: string;
subscription?: __experimental_CommerceSubscription;
totals!: __experimental_CommerceTotals;

constructor(data: __experimental_CommerceCheckoutJSON) {
super();
this.fromJSON(data);
}

protected fromJSON(data: __experimental_CommerceCheckoutJSON | null): this {
if (!data) {
return this;
}

this.id = data.id;
this.externalClientSecret = data.external_client_secret;
this.externalGatewayId = data.external_gateway_id;
this.invoice = data.invoice ? new __experimental_CommerceInvoice(data.invoice) : undefined;
this.paymentSource = data.payment_source
? new __experimental_CommercePaymentSource(data.payment_source)
: undefined;
this.plan = new __experimental_CommercePlan(data.plan);
this.planPeriod = data.plan_period;
this.status = data.status;
this.subscription = data.subscription ? new __experimental_CommerceSubscription(data.subscription) : undefined;
this.totals = commerceTotalsFromJSON(data.totals);

return this;
}

confirm = (params?: __experimental_ConfirmCheckoutParams): Promise<this> => {
return this._basePatch({
path: this.path('confirm'),
body: params as any,
});
};
}
30 changes: 30 additions & 0 deletions packages/clerk-js/src/core/resources/CommerceFeature.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
import type { __experimental_CommerceFeatureJSON, __experimental_CommerceFeatureResource } from '@clerk/types';

import { BaseResource } from './internal';

export class __experimental_CommerceFeature extends BaseResource implements __experimental_CommerceFeatureResource {
id!: string;
name!: string;
description!: string;
slug!: string;
avatarUrl!: string;

constructor(data: __experimental_CommerceFeatureJSON) {
super();
this.fromJSON(data);
}

protected fromJSON(data: __experimental_CommerceFeatureJSON | null): this {
if (!data) {
return this;
}

this.id = data.id;
this.name = data.name;
this.description = data.description;
this.slug = data.slug;
this.avatarUrl = data.avatar_url;

return this;
}
}
Loading