Skip to content
4 changes: 2 additions & 2 deletions packages/clerk-js/src/core/modules/billing/namespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ export class Billing implements BillingNamespace {
}

getPlans = async (params?: GetPlansParams): Promise<ClerkPaginatedResponse<BillingPlanResource>> => {
const { for: forParam, ...safeParams } = params || {};
const searchParams = { ...safeParams, payer_type: forParam === 'organization' ? 'org' : 'user' };
const { for: forParam, org_id, min_seats, ...safeParams } = params || {};
const searchParams = { ...safeParams, payer_type: forParam === 'organization' ? 'org' : 'user', org_id, min_seats };
return await BaseResource._fetch({
path: `${Billing.#pathRoot}/plans`,
method: 'GET',
Expand Down
19 changes: 15 additions & 4 deletions packages/clerk-js/src/core/modules/checkout/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,16 @@ type CheckoutKey = string & { readonly __tag: 'CheckoutKey' };
/**
* Generate cache key for checkout instance
*/
function cacheKey(options: { userId: string; orgId?: string; planId: string; planPeriod: string }): CheckoutKey {
const { userId, orgId, planId, planPeriod } = options;
return `${userId}-${orgId || 'user'}-${planId}-${planPeriod}` as CheckoutKey;
function cacheKey(options: {
userId: string;
orgId?: string;
planId: string;
planPeriod: string;
seatsQuantity?: number;
priceId?: string;
}): CheckoutKey {
const { userId, orgId, planId, planPeriod, seatsQuantity, priceId } = options;
return `${userId}-${orgId || 'user'}-${planId}-${planPeriod}-${seatsQuantity}-${priceId}` as CheckoutKey;
}

/**
Expand All @@ -26,7 +33,7 @@ const CheckoutSignalCache = new Map<
* Create a checkout instance with the given options
*/
function createCheckoutInstance(clerk: Clerk, options: __experimental_CheckoutOptions): CheckoutSignalValue {
const { for: forOrganization, planId, planPeriod } = options;
const { for: forOrganization, planId, planPeriod, seatsQuantity, priceId } = options;

if (clerk.user === null) {
throw new Error('Clerk: User is not authenticated');
Expand All @@ -43,6 +50,8 @@ function createCheckoutInstance(clerk: Clerk, options: __experimental_CheckoutOp
orgId: forOrganization === 'organization' ? clerk.organization?.id : undefined,
planId,
planPeriod,
seatsQuantity,
priceId,
});

const checkoutInstance = CheckoutSignalCache.get(checkoutKey);
Expand All @@ -56,6 +65,8 @@ function createCheckoutInstance(clerk: Clerk, options: __experimental_CheckoutOp
...(forOrganization === 'organization' ? { orgId: clerk.organization?.id } : {}),
planId,
planPeriod,
seatsQuantity,
priceId,
});

CheckoutSignalCache.set(checkoutKey, { resource: checkout, signals });
Expand Down
20 changes: 10 additions & 10 deletions packages/clerk-js/src/core/resources/BillingPlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import type {
BillingMoneyAmount,
BillingPayerResourceType,
BillingPlanJSON,
BillingPlanPrice,
BillingPlanResource,
BillingPlanUnitPrice,
} from '@clerk/shared/types';

import { billingMoneyAmountFromJSON } from '@/utils/billing';
import { billingMoneyAmountFromJSON, billingUnitPriceFromJSON } from '@/utils/billing';

import { BaseResource, Feature } from './internal';

Expand All @@ -26,6 +27,7 @@ export class BillingPlan extends BaseResource implements BillingPlanResource {
avatarUrl: string | null = null;
features!: Feature[];
unitPrices?: BillingPlanUnitPrice[];
availablePrices?: BillingPlanPrice[];
freeTrialDays!: number | null;
freeTrialEnabled!: boolean;

Expand Down Expand Up @@ -55,15 +57,13 @@ export class BillingPlan extends BaseResource implements BillingPlanResource {
this.freeTrialDays = this.withDefault(data.free_trial_days, null);
this.freeTrialEnabled = this.withDefault(data.free_trial_enabled, false);
this.features = (data.features || []).map(feature => new Feature(feature));
this.unitPrices = data.unit_prices?.map(unitPrice => ({
name: unitPrice.name,
blockSize: unitPrice.block_size,
tiers: unitPrice.tiers.map(tier => ({
id: tier.id,
startsAtBlock: tier.starts_at_block,
endsAfterBlock: tier.ends_after_block,
feePerBlock: billingMoneyAmountFromJSON(tier.fee_per_block),
})),
this.unitPrices = data.unit_prices?.map(billingUnitPriceFromJSON);
this.availablePrices = data.available_prices?.map(price => ({
id: price.id,
fee: price.fee ? billingMoneyAmountFromJSON(price.fee) : null,
annualMonthlyFee: price.annual_monthly_fee ? billingMoneyAmountFromJSON(price.annual_monthly_fee) : null,
isDefault: price.is_default,
unitPrices: data.unit_prices?.map(billingUnitPriceFromJSON),
}));

return this;
Expand Down
2 changes: 2 additions & 0 deletions packages/clerk-js/src/core/resources/BillingSubscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export class BillingSubscriptionItem extends BaseResource implements BillingSubs
id!: string;
plan!: BillingPlan;
planPeriod!: BillingSubscriptionPlanPeriod;
priceId!: string;
status!: BillingSubscriptionStatus;
createdAt!: Date;
periodStart!: Date;
Expand Down Expand Up @@ -94,6 +95,7 @@ export class BillingSubscriptionItem extends BaseResource implements BillingSubs
this.id = data.id;
this.plan = new BillingPlan(data.plan);
this.planPeriod = data.plan_period;
this.priceId = data.price_id;
this.status = data.status;

this.createdAt = unixEpochToDate(data.created_at);
Expand Down
12 changes: 12 additions & 0 deletions packages/clerk-js/src/utils/billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
BillingMoneyAmountJSON,
BillingPerUnitTotal,
BillingPerUnitTotalJSON,
BillingPlanUnitPriceJSON,
BillingStatementTotals,
BillingStatementTotalsJSON,
} from '@clerk/shared/types';
Expand All @@ -32,6 +33,17 @@ const billingPerUnitTotalsFromJSON = (data: BillingPerUnitTotalJSON[]): BillingP
}));
};

export const billingUnitPriceFromJSON = (unitPrice: BillingPlanUnitPriceJSON) => ({
name: unitPrice.name,
blockSize: unitPrice.block_size,
tiers: unitPrice.tiers.map(tier => ({
id: tier.id,
startsAtBlock: tier.starts_at_block,
endsAfterBlock: tier.ends_after_block,
feePerBlock: billingMoneyAmountFromJSON(tier.fee_per_block),
})),
});

export const billingCreditsFromJSON = (data: BillingCreditsJSON): BillingCredits => {
return {
proration: data.proration
Expand Down
4 changes: 4 additions & 0 deletions packages/localizations/src/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1204,6 +1204,10 @@ export const enUS: LocalizationResource = {
form_username_invalid_length: 'Your username must be between {{min_length}} and {{max_length}} characters long.',
form_username_needs_non_number_char: 'Your username must contain at least one non-numeric character.',
identification_deletion_failed: undefined,
insufficient_seats_change_plan:
'Your organization does not have enough seats to invite the desired number of members. Please change to a plan that supports the number of members you are attempting to invite.',
insufficient_seats_contact_support:
'Your organization does not have enough seats to invite the desired number of members. Please contact support.',
not_allowed_access: undefined,
organization_domain_blocked: undefined,
organization_domain_common: undefined,
Expand Down
4 changes: 4 additions & 0 deletions packages/react/src/components/CheckoutButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export const CheckoutButton = withClerk(
const {
planId,
planPeriod,
seatsQuantity,
priceId,
for: _for,
onSubscriptionComplete,
newSubscriptionRedirectUrl,
Expand Down Expand Up @@ -84,6 +86,8 @@ export const CheckoutButton = withClerk(
return clerk.__internal_openCheckout({
planId,
planPeriod,
seatsQuantity,
priceId,
for: _for,
onSubscriptionComplete,
newSubscriptionRedirectUrl,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ describe('CheckoutButton', () => {
const props = {
planId: 'test_plan',
planPeriod: 'month' as const,
seatsQuantity: 7,
onSubscriptionComplete: vi.fn(),
newSubscriptionRedirectUrl: '/success',
checkoutProps: {
Expand All @@ -121,6 +122,7 @@ describe('CheckoutButton', () => {
onSubscriptionComplete: props.onSubscriptionComplete,
newSubscriptionRedirectUrl: props.newSubscriptionRedirectUrl,
planPeriod: props.planPeriod,
seatsQuantity: props.seatsQuantity,
}),
);
});
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/errors/clerkApiError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export class ClerkAPIError<Meta extends ClerkAPIErrorMeta = any> implements Cler
zxcvbn: json.meta?.zxcvbn,
plan: json.meta?.plan,
isPlanUpgradePossible: json.meta?.is_plan_upgrade_possible,
seatsQuantityToAdd: json.meta?.seats_quantity_to_add,
seatsQuantity: json.meta?.seats_quantity,
} as unknown as Meta,
};
this.code = parsedError.code;
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/errors/parseError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export function errorToJSON(error: ClerkAPIError | null): ClerkAPIErrorJSON {
zxcvbn: error?.meta?.zxcvbn,
plan: error?.meta?.plan,
is_plan_upgrade_possible: error?.meta?.isPlanUpgradePossible,
seats_quantity_to_add: error?.meta?.seatsQuantityToAdd,
seats_quantity: error?.meta?.seatsQuantity,
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ describe('PaymentElement Localization', () => {
grandTotal: { amount: 1000, amountFormatted: '$10.00', currency: 'usd', currencySymbol: '$' },
taxTotal: { amount: 0, amountFormatted: '$0.00', currency: 'usd', currencySymbol: '$' },
totalDueNow: { amount: 1000, amountFormatted: '$10.00', currency: 'usd', currencySymbol: '$' },
totalDuePerPeriod: { amount: 1000, amountFormatted: '$10.00', currency: 'usd', currencySymbol: '$' },
totalDueAfterFreeTrial: null,
credit: { amount: 0, amountFormatted: '$0.00', currency: 'usd', currencySymbol: '$' },
credits: {
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/react/contexts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ export type UseCheckoutOptions = {
* The ID of the Subscription Plan to check out (e.g. `cplan_xxx`).
*/
planId: string;
seatsQuantity?: number;
priceId?: string;
};

const [CheckoutContext, useCheckoutContext] = createContextAndHook<UseCheckoutOptions>('CheckoutContext');
Expand Down
6 changes: 3 additions & 3 deletions packages/shared/src/react/hooks/useCheckout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type UseCheckoutParams = Parameters<typeof __experimental_CheckoutProvider>[0];
*/
export const useCheckout = (options?: UseCheckoutParams): CheckoutSignalValue => {
const contextOptions = useCheckoutContext();
const { for: forOrganization, planId, planPeriod } = options || contextOptions;
const { for: forOrganization, planId, planPeriod, seatsQuantity, priceId } = options || contextOptions;
const organization = useOrganizationBase();
const { isLoaded, user } = useUser();
const clerk = useClerkInstanceContext();
Expand All @@ -33,8 +33,8 @@ export const useCheckout = (options?: UseCheckoutParams): CheckoutSignalValue =>
}

const signal = useCallback(() => {
return clerk.__experimental_checkout({ planId, planPeriod, for: forOrganization });
}, [user?.id, organization?.id, planId, planPeriod, forOrganization]);
return clerk.__experimental_checkout({ planId, planPeriod, for: forOrganization, seatsQuantity, priceId });
}, [user?.id, organization?.id, planId, planPeriod, forOrganization, seatsQuantity, priceId]);

const subscribe = useCallback(
(callback: () => void) => {
Expand Down
14 changes: 14 additions & 0 deletions packages/shared/src/types/billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ export type GetPlansParams = ClerkPaginationParams<{
* The type of payer for the Plans.
*/
for?: ForPayerType;
org_id?: string;
min_seats?: number;
}>;

/**
Expand Down Expand Up @@ -210,6 +212,7 @@ export interface BillingPlanResource extends ClerkResource {
* Per-unit pricing tiers for this Plan (for example, seats).
*/
unitPrices?: BillingPlanUnitPrice[];
availablePrices?: BillingPlanPrice[];
/**
* The number of days of the free trial for the Plan. `null` if the Plan does not have a free trial.
*/
Expand Down Expand Up @@ -276,6 +279,14 @@ export interface BillingPlanUnitPrice {
tiers: BillingPlanUnitPriceTier[];
}

export interface BillingPlanPrice {
id: string;
fee: BillingMoneyAmount | null;
annualMonthlyFee: BillingMoneyAmount | null;
isDefault: boolean;
unitPrices?: BillingPlanUnitPrice[];
}

/**
* The `BillingPerUnitTotalTier` type represents the cost breakdown for a single tier in checkout totals.
*
Expand Down Expand Up @@ -656,6 +667,7 @@ export interface BillingSubscriptionItemResource extends ClerkResource {
* The billing period for the subscription item.
*/
planPeriod: BillingSubscriptionPlanPeriod;
priceId: string;
/**
* The status of the subscription item.
*/
Expand Down Expand Up @@ -893,6 +905,8 @@ export type CreateCheckoutParams = WithOptionalOrgType<{
* The billing period for the Plan.
*/
planPeriod: BillingSubscriptionPlanPeriod;
seatsQuantity?: number;
priceId?: string;
}>;

/**
Expand Down
6 changes: 6 additions & 0 deletions packages/shared/src/types/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ export type __experimental_CheckoutOptions = {
for?: ForPayerType;
planPeriod: BillingSubscriptionPlanPeriod;
planId: string;
seatsQuantity?: number;
priceId?: string;
};

export type CheckoutErrors = {
Expand Down Expand Up @@ -2205,6 +2207,8 @@ export type __internal_CheckoutProps = {
appearance?: ClerkAppearanceTheme;
planId?: string;
planPeriod?: BillingSubscriptionPlanPeriod;
seatsQuantity?: number;
priceId?: string;
for?: ForPayerType;
onSubscriptionComplete?: () => void;
portalId?: string;
Expand All @@ -2225,6 +2229,8 @@ export type __experimental_CheckoutButtonProps = {
planId: string;
planPeriod?: BillingSubscriptionPlanPeriod;
for?: ForPayerType;
seatsQuantity?: number;
priceId?: string;
onSubscriptionComplete?: () => void;
checkoutProps?: {
appearance?: ClerkAppearanceTheme;
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/src/types/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export interface ClerkAPIErrorJSON {
name: string;
};
is_plan_upgrade_possible?: boolean;
seats_quantity_to_add?: number;
seats_quantity?: number;
};
}

Expand Down Expand Up @@ -63,6 +65,8 @@ export interface ClerkAPIError {
name: string;
};
isPlanUpgradePossible?: boolean;
seatsQuantityToAdd?: number;
seatsQuantity?: number;
};
}

Expand Down
10 changes: 10 additions & 0 deletions packages/shared/src/types/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,14 @@ export interface BillingPerUnitTotalJSON {
tiers: BillingPerUnitTotalTierJSON[];
}

export interface BillingPriceJSON extends ClerkResourceJSON {
object: 'commerce_price';
fee: BillingMoneyAmountJSON | null;
annual_monthly_fee: BillingMoneyAmountJSON | null;
is_default: boolean;
unit_prices?: BillingPlanUnitPriceJSON[];
}

/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change. It is advised to [pin](https://clerk.com/docs/pinning) the SDK version and the clerk-js version to avoid breaking changes.
*/
Expand All @@ -688,6 +696,7 @@ export interface BillingPlanJSON extends ClerkResourceJSON {
* Per-unit pricing tiers for this plan (for example, seats).
*/
unit_prices?: BillingPlanUnitPriceJSON[];
available_prices?: BillingPriceJSON[];
}

/**
Expand Down Expand Up @@ -774,6 +783,7 @@ export interface BillingSubscriptionItemJSON extends ClerkResourceJSON {
credits?: BillingCreditsJSON;
plan: BillingPlanJSON;
plan_period: BillingSubscriptionPlanPeriod;
price_id: string;
status: BillingSubscriptionStatus;
created_at: number;
period_start: number;
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/types/localization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1695,4 +1695,6 @@ type UnstableErrors = WithParamName<{
organization_membership_quota_exceeded: LocalizationValue;
organization_not_found_or_unauthorized: LocalizationValue;
organization_not_found_or_unauthorized_with_create_organization_disabled: LocalizationValue;
insufficient_seats_contact_support: LocalizationValue;
insufficient_seats_change_plan: LocalizationValue;
}>;
Loading
Loading