diff --git a/.changeset/cute-ideas-appear.md b/.changeset/cute-ideas-appear.md new file mode 100644 index 00000000000..07a6e4ad8db --- /dev/null +++ b/.changeset/cute-ideas-appear.md @@ -0,0 +1,8 @@ +--- +'@clerk/localizations': minor +'@clerk/clerk-js': minor +'@clerk/shared': minor +'@clerk/ui': minor +--- + +Add support for seat-based billing plans in Clerk Billing. \ No newline at end of file diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 407eea6964c..5b44126e81d 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -1,7 +1,7 @@ { "files": [ { "path": "./dist/clerk.js", "maxSize": "543KB" }, - { "path": "./dist/clerk.browser.js", "maxSize": "67KB" }, + { "path": "./dist/clerk.browser.js", "maxSize": "68KB" }, { "path": "./dist/clerk.legacy.browser.js", "maxSize": "110KB" }, { "path": "./dist/clerk.no-rhc.js", "maxSize": "309KB" }, { "path": "./dist/clerk.native.js", "maxSize": "68KB" }, diff --git a/packages/clerk-js/sandbox/scenarios/checkout-seats.ts b/packages/clerk-js/sandbox/scenarios/checkout-seats.ts new file mode 100644 index 00000000000..5e553c07cdb --- /dev/null +++ b/packages/clerk-js/sandbox/scenarios/checkout-seats.ts @@ -0,0 +1,341 @@ +import { + clerkHandlers, + http, + HttpResponse, + EnvironmentService, + SessionService, + setClerkState, + type MockScenario, + UserService, +} from '@clerk/msw'; + +export function CheckoutSeats(): MockScenario { + const user = UserService.create(); + const session = SessionService.create(user); + + setClerkState({ + environment: EnvironmentService.MULTI_SESSION, + session, + user, + }); + + const subscriptionHandler = http.get('https://*.clerk.accounts.dev/v1/me/billing/subscription', () => { + return HttpResponse.json({ + response: { + data: {}, + }, + }); + }); + + const paymentMethodsHandler = http.get('https://*.clerk.accounts.dev/v1/me/billing/payment_methods', () => { + return HttpResponse.json({ + response: { + data: {}, + }, + }); + }); + + const checkoutAccountCreditHandler = http.post('https://*.clerk.accounts.dev/v1/me/billing/checkouts', () => { + return HttpResponse.json({ + response: { + object: 'commerce_checkout', + id: 'string', + plan: { + object: 'commerce_plan', + id: 'string', + name: 'Pro', + fee: { + amount: 0, + amount_formatted: '25.00', + currency: 'string', + currency_symbol: '$', + }, + annual_monthly_fee: { + amount: 0, + amount_formatted: 'string', + currency: 'string', + currency_symbol: 'string', + }, + annual_fee: { + amount: 0, + amount_formatted: 'string', + currency: 'string', + currency_symbol: 'string', + }, + description: null, + is_default: true, + is_recurring: true, + publicly_visible: true, + has_base_fee: true, + for_payer_type: 'string', + slug: 'string', + avatar_url: null, + free_trial_enabled: true, + free_trial_days: null, + features: [ + { + object: 'feature', + id: 'string', + name: 'string', + description: null, + slug: 'string', + avatar_url: null, + }, + ], + }, + plan_period: 'month', + payer: { + object: 'commerce_payer', + id: 'string', + instance_id: 'string', + user_id: null, + first_name: null, + last_name: null, + email: null, + organization_id: null, + organization_name: null, + image_url: 'https://example.com', + created_at: 1, + updated_at: 1, + }, + payment_method: { + object: 'commerce_payment_method', + id: 'string', + payer_id: 'string', + payment_type: 'card', + is_default: true, + gateway: 'string', + gateway_external_id: 'string', + gateway_external_account_id: null, + last4: null, + status: 'active', + wallet_type: null, + card_type: null, + expiry_year: null, + expiry_month: null, + created_at: 1, + updated_at: 1, + is_removable: true, + }, + external_gateway_id: 'string', + status: 'needs_confirmation', + totals: { + subtotal: { + amount: 4500, + amount_formatted: '45.00', + currency: 'string', + currency_symbol: '$', + }, + tax_total: { + amount: 1, + amount_formatted: 'string', + currency: 'string', + currency_symbol: 'string', + }, + grand_total: { + amount: 1, + amount_formatted: 'string', + currency: 'string', + currency_symbol: 'string', + }, + total_due_after_free_trial: { + amount: 1, + amount_formatted: 'string', + currency: 'string', + currency_symbol: 'string', + }, + total_due_now: { + amount: 4500, + amount_formatted: '45.00', + currency: 'string', + currency_symbol: '$', + }, + past_due: null, + credit: { + amount: 1, + amount_formatted: '5.00', + currency: 'string', + currency_symbol: '$', + }, + per_unit_totals: [ + { + name: 'seats', + block_size: 1, + tiers: [ + { + quantity: 10, + fee_per_block: { + amount: 0, + amount_formatted: '0.00', + currency: 'USD', + currency_symbol: '$', + }, + total: { + amount: 0, + amount_formatted: '0.00', + currency: 'USD', + currency_symbol: '$', + }, + }, + { + quantity: 2, + fee_per_block: { + amount: 1000, + amount_formatted: '10.00', + currency: 'USD', + currency_symbol: '$', + }, + total: { + amount: 2000, + amount_formatted: '20.00', + currency: 'USD', + currency_symbol: '$', + }, + }, + ], + }, + ], + }, + subscription_item: { + object: 'commerce_subscription_item', + id: 'string', + instance_id: 'string', + status: 'active', + credit: { + amount: { + amount: 1, + amount_formatted: 'string', + currency: 'string', + currency_symbol: 'string', + }, + cycle_days_remaining: 1, + cycle_days_total: 1, + cycle_remaining_percent: 1, + }, + plan_id: 'string', + price_id: 'string', + plan: { + object: 'commerce_plan', + id: 'string', + name: 'string', + fee: { + amount: 0, + amount_formatted: 'string', + currency: 'string', + currency_symbol: 'string', + }, + annual_monthly_fee: { + amount: 0, + amount_formatted: 'string', + currency: 'string', + currency_symbol: 'string', + }, + annual_fee: { + amount: 0, + amount_formatted: 'string', + currency: 'string', + currency_symbol: 'string', + }, + description: null, + is_default: true, + is_recurring: true, + publicly_visible: true, + has_base_fee: true, + for_payer_type: 'string', + slug: 'string', + avatar_url: null, + free_trial_enabled: true, + free_trial_days: null, + features: [ + { + object: 'feature', + id: 'string', + name: 'string', + description: null, + slug: 'string', + avatar_url: null, + }, + ], + }, + plan_period: 'month', + payment_method_id: 'string', + payment_method: { + object: 'commerce_payment_method', + id: 'string', + payer_id: 'string', + payment_type: 'card', + is_default: true, + gateway: 'string', + gateway_external_id: 'string', + gateway_external_account_id: null, + last4: null, + status: 'active', + wallet_type: null, + card_type: null, + expiry_year: null, + expiry_month: null, + created_at: 1, + updated_at: 1, + is_removable: true, + }, + lifetime_paid: { + amount: 0, + amount_formatted: 'string', + currency: 'string', + currency_symbol: 'string', + }, + amount: { + amount: 0, + amount_formatted: 'string', + currency: 'string', + currency_symbol: 'string', + }, + next_payment: { + amount: { + amount: 0, + amount_formatted: 'string', + currency: 'string', + currency_symbol: 'string', + }, + date: 1, + }, + payer_id: 'string', + payer: { + object: 'commerce_payer', + id: 'string', + instance_id: 'string', + user_id: null, + first_name: null, + last_name: null, + email: null, + organization_id: null, + organization_name: null, + image_url: 'https://example.com', + created_at: 1, + updated_at: 1, + }, + is_free_trial: true, + period_start: 1, + period_end: null, + proration_date: 'string', + canceled_at: null, + past_due_at: null, + ended_at: null, + created_at: 1, + updated_at: 1, + }, + plan_period_start: 1, + is_immediate_plan_change: true, + free_trial_ends_at: 1, + needs_payment_method: true, + }, + }); + }); + + return { + description: 'Checkout with seats', + handlers: [checkoutAccountCreditHandler, subscriptionHandler, paymentMethodsHandler, ...clerkHandlers], + initialState: { session, user }, + name: 'checkout-seats', + }; +} diff --git a/packages/clerk-js/sandbox/scenarios/index.ts b/packages/clerk-js/sandbox/scenarios/index.ts index 8a974a68d49..eb8717b5deb 100644 --- a/packages/clerk-js/sandbox/scenarios/index.ts +++ b/packages/clerk-js/sandbox/scenarios/index.ts @@ -1,3 +1,6 @@ export { UserButtonSignedIn } from './user-button-signed-in'; export { CheckoutAccountCredit } from './checkout-account-credit'; +export { CheckoutSeats } from './checkout-seats'; +export { OrgProfileSeatLimit } from './org-profile-seat-limit'; +export { PricingTableSBB } from './pricing-table-sbb'; export { AnnualOnlyPlans } from './annual-only-plans'; diff --git a/packages/clerk-js/sandbox/scenarios/org-profile-seat-limit.ts b/packages/clerk-js/sandbox/scenarios/org-profile-seat-limit.ts new file mode 100644 index 00000000000..355fcb19db5 --- /dev/null +++ b/packages/clerk-js/sandbox/scenarios/org-profile-seat-limit.ts @@ -0,0 +1,74 @@ +import { + BillingService, + clerkHandlers, + EnvironmentService, + SessionService, + setClerkState, + type MockScenario, + UserService, + OrganizationService, +} from '@clerk/msw'; + +export function OrgProfileSeatLimit(): MockScenario { + const organization = OrganizationService.create({ maxAllowedMemberships: 10 }); + const user = UserService.create(); + user.organizationMemberships = [ + { + object: 'organization_membership', + id: 'orgmem_3004mVaZrB4yD63C9KuwTMWNKbj', + public_metadata: {}, + role: 'org:owner', + role_name: 'Owner', + permissions: [ + 'org:applications:create', + 'org:applications:manage', + 'org:applications:delete', + 'org:billing:read', + 'org:billing:manage', + 'org:config:read', + 'org:config:manage', + 'org:global:read', + 'org:global:manage', + 'org:instances:create', + 'org:instances:manage', + 'org:instances:delete', + 'org:restrictions:read', + 'org:restrictions:manage', + 'org:secrets:manage', + 'org:users:imp', + 'org:sys_profile:manage', + 'org:sys_profile:delete', + 'org:sys_billing:read', + 'org:sys_billing:manage', + 'org:sys_domains:read', + 'org:sys_domains:manage', + 'org:sys_memberships:read', + 'org:sys_memberships:manage', + ], + created_at: 1752751315275, + updated_at: 1752751315275, + organization, + }, + ]; + const session = SessionService.create(user); + const plans = BillingService.createDefaultPlans(); + const subscription = BillingService.createSubscription(plans[1]); + + setClerkState({ + environment: EnvironmentService.MULTI_SESSION, + session, + user, + organization, + billing: { + plans, + subscription, + }, + }); + + return { + description: 'OrganizationProfile with a seat limit', + handlers: clerkHandlers, + initialState: { session, user, organization }, + name: 'org-profile-seat-limit', + }; +} diff --git a/packages/clerk-js/sandbox/scenarios/pricing-table-sbb.ts b/packages/clerk-js/sandbox/scenarios/pricing-table-sbb.ts new file mode 100644 index 00000000000..e77ed48ec34 --- /dev/null +++ b/packages/clerk-js/sandbox/scenarios/pricing-table-sbb.ts @@ -0,0 +1,371 @@ +import { + clerkHandlers, + http, + HttpResponse, + EnvironmentService, + SessionService, + setClerkState, + type MockScenario, + UserService, +} from '@clerk/msw'; +import type { BillingPlanJSON } from '@clerk/shared/types'; + +export function PricingTableSBB(): MockScenario { + const user = UserService.create(); + const session = SessionService.create(user); + const money = (amount: number) => ({ + amount, + amount_formatted: (amount / 100).toFixed(2), + currency: 'USD', + currency_symbol: '$', + }); + const mockFeatures = [ + { + object: 'feature' as const, + id: 'feature_custom_domains', + name: 'Custom domains', + description: 'Connect and manage branded domains.', + slug: 'custom-domains', + avatar_url: null, + }, + { + object: 'feature' as const, + id: 'feature_saml_sso', + name: 'SAML SSO', + description: 'Single sign-on with enterprise identity providers.', + slug: 'saml-sso', + avatar_url: null, + }, + { + object: 'feature' as const, + id: 'feature_audit_logs', + name: 'Audit logs', + description: 'Track account activity and security events.', + slug: 'audit-logs', + avatar_url: null, + }, + { + object: 'feature' as const, + id: 'feature_priority_support', + name: 'Priority support', + description: 'Faster response times from the support team.', + slug: 'priority-support', + avatar_url: null, + }, + { + object: 'feature' as const, + id: 'feature_rate_limit_boost', + name: 'Rate limit boost', + description: 'Higher API request thresholds for production traffic.', + slug: 'rate-limit-boost', + avatar_url: null, + }, + ]; + + setClerkState({ + environment: EnvironmentService.MULTI_SESSION, + session, + user, + }); + + const subscriptionHandler = http.get('https://*.clerk.accounts.dev/v1/me/billing/subscription', () => { + return HttpResponse.json({ + response: { + data: {}, + }, + }); + }); + + const paymentMethodsHandler = http.get('https://*.clerk.accounts.dev/v1/me/billing/payment_methods', () => { + return HttpResponse.json({ + response: { + data: {}, + }, + }); + }); + + const plansHandler = http.get('https://*.clerk.accounts.dev/v1/billing/plans', () => { + return HttpResponse.json({ + data: [ + { + object: 'commerce_plan', + id: 'plan_a_sbb', + name: 'Plan A', + fee: money(12989), + annual_fee: null, + annual_monthly_fee: null, + description: null, + is_default: false, + is_recurring: true, + has_base_fee: true, + for_payer_type: 'org', + publicly_visible: true, + slug: 'plan-a-sbb', + avatar_url: null, + features: mockFeatures, + free_trial_enabled: false, + free_trial_days: null, + unit_prices: [ + { + name: 'seats', + block_size: 1, + tiers: [ + { + id: 'tier_plan_a_seats_1', + object: 'commerce_unit_price', + starts_at_block: 1, + ends_after_block: 5, + fee_per_block: money(0), + }, + ], + }, + ], + }, + { + object: 'commerce_plan', + id: 'plan_b_sbb', + name: 'Plan B', + fee: money(12989), + annual_fee: null, + annual_monthly_fee: null, + description: null, + is_default: false, + is_recurring: true, + has_base_fee: true, + for_payer_type: 'org', + publicly_visible: true, + slug: 'plan-b-sbb', + avatar_url: null, + features: mockFeatures, + free_trial_enabled: false, + free_trial_days: null, + unit_prices: [ + { + name: 'seats', + block_size: 1, + tiers: [ + { + id: 'tier_plan_b_seats_1', + object: 'commerce_unit_price', + starts_at_block: 1, + ends_after_block: null, + fee_per_block: money(1200), + }, + ], + }, + ], + }, + { + object: 'commerce_plan', + id: 'plan_c_sbb', + name: 'Plan C', + fee: money(0), + annual_fee: null, + annual_monthly_fee: null, + description: null, + is_default: false, + is_recurring: true, + has_base_fee: false, + for_payer_type: 'org', + publicly_visible: true, + slug: 'plan-c-sbb', + avatar_url: null, + features: mockFeatures, + free_trial_enabled: false, + free_trial_days: null, + unit_prices: [ + { + name: 'seats', + block_size: 1, + tiers: [ + { + id: 'tier_plan_c_seats_1', + object: 'commerce_unit_price', + starts_at_block: 1, + ends_after_block: null, + fee_per_block: money(1200), + }, + ], + }, + ], + }, + { + object: 'commerce_plan', + id: 'plan_d_sbb', + name: 'Plan D', + fee: money(12989), + annual_fee: null, + annual_monthly_fee: null, + description: null, + is_default: false, + is_recurring: true, + has_base_fee: true, + for_payer_type: 'org', + publicly_visible: true, + slug: 'plan-d-sbb', + avatar_url: null, + features: mockFeatures, + free_trial_enabled: false, + free_trial_days: null, + unit_prices: [ + { + name: 'seats', + block_size: 1, + tiers: [ + { + id: 'tier_plan_d_seats_1', + object: 'commerce_unit_price', + starts_at_block: 1, + ends_after_block: 5, + fee_per_block: money(0), + }, + { + id: 'tier_plan_d_seats_2', + object: 'commerce_unit_price', + starts_at_block: 6, + ends_after_block: null, + fee_per_block: money(1200), + }, + ], + }, + ], + }, + { + object: 'commerce_plan', + id: 'plan_e_sbb', + name: 'Plan E', + fee: money(12989), + annual_fee: null, + annual_monthly_fee: null, + description: null, + is_default: false, + is_recurring: true, + has_base_fee: true, + for_payer_type: 'org', + publicly_visible: true, + slug: 'plan-e-sbb', + avatar_url: null, + features: mockFeatures, + free_trial_enabled: false, + free_trial_days: null, + }, + { + object: 'commerce_plan', + id: 'plan_f_sbb', + name: 'Plan F', + fee: money(0), + annual_fee: null, + annual_monthly_fee: null, + description: null, + is_default: true, + is_recurring: true, + has_base_fee: false, + for_payer_type: 'org', + publicly_visible: true, + slug: 'plan-f-sbb', + avatar_url: null, + features: mockFeatures, + free_trial_enabled: false, + free_trial_days: null, + unit_prices: [ + { + name: 'seats', + block_size: 1, + tiers: [ + { + id: 'tier_plan_f_seats_1', + object: 'commerce_unit_price', + starts_at_block: 1, + ends_after_block: 5, + fee_per_block: money(0), + }, + { + id: 'tier_plan_f_seats_2', + object: 'commerce_unit_price', + starts_at_block: 6, + ends_after_block: null, + fee_per_block: money(1200), + }, + ], + }, + ], + }, + { + object: 'commerce_plan', + id: 'plan_g_sbb', + name: 'Plan G', + fee: money(0), + annual_fee: null, + annual_monthly_fee: null, + description: null, + is_default: false, + is_recurring: true, + has_base_fee: false, + for_payer_type: 'org', + publicly_visible: true, + slug: 'plan-g-sbb', + avatar_url: null, + features: mockFeatures, + free_trial_enabled: false, + free_trial_days: null, + unit_prices: [ + { + name: 'seats', + block_size: 1, + tiers: [ + { + id: 'tier_plan_g_seats_1', + object: 'commerce_unit_price', + starts_at_block: 1, + ends_after_block: null, + fee_per_block: money(0), + }, + ], + }, + ], + }, + { + object: 'commerce_plan', + id: 'plan_h_sbb', + name: 'Plan H', + fee: money(12989), + annual_fee: money(10000), + annual_monthly_fee: money(833), + description: null, + is_default: false, + is_recurring: true, + has_base_fee: true, + for_payer_type: 'org', + publicly_visible: true, + slug: 'plan-h-sbb', + avatar_url: null, + features: mockFeatures, + free_trial_enabled: false, + free_trial_days: null, + unit_prices: [ + { + name: 'seats', + block_size: 1, + tiers: [ + { + id: 'tier_plan_h_seats_1', + object: 'commerce_unit_price', + starts_at_block: 1, + ends_after_block: null, + fee_per_block: money(0), + }, + ], + }, + ], + }, + ] as BillingPlanJSON[], + }); + }); + + return { + description: 'PricingTable with seat-based billing plans', + handlers: [plansHandler, subscriptionHandler, paymentMethodsHandler, ...clerkHandlers], + initialState: { session, user }, + name: 'pricing-table-sbb', + }; +} diff --git a/packages/clerk-js/src/core/resources/BillingPlan.ts b/packages/clerk-js/src/core/resources/BillingPlan.ts index f9159db563d..fb5ab1b0ad6 100644 --- a/packages/clerk-js/src/core/resources/BillingPlan.ts +++ b/packages/clerk-js/src/core/resources/BillingPlan.ts @@ -3,6 +3,7 @@ import type { BillingPayerResourceType, BillingPlanJSON, BillingPlanResource, + BillingPlanUnitPrice, } from '@clerk/shared/types'; import { billingMoneyAmountFromJSON } from '@/utils/billing'; @@ -24,6 +25,7 @@ export class BillingPlan extends BaseResource implements BillingPlanResource { slug!: string; avatarUrl: string | null = null; features!: Feature[]; + unitPrices?: BillingPlanUnitPrice[]; freeTrialDays!: number | null; freeTrialEnabled!: boolean; @@ -53,6 +55,16 @@ 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), + })), + })); return this; } diff --git a/packages/clerk-js/src/core/resources/BillingSubscription.ts b/packages/clerk-js/src/core/resources/BillingSubscription.ts index ddcd083fdc5..239194daacd 100644 --- a/packages/clerk-js/src/core/resources/BillingSubscription.ts +++ b/packages/clerk-js/src/core/resources/BillingSubscription.ts @@ -3,6 +3,7 @@ import type { BillingMoneyAmount, BillingSubscriptionItemJSON, BillingSubscriptionItemResource, + BillingSubscriptionItemSeats, BillingSubscriptionJSON, BillingSubscriptionPlanPeriod, BillingSubscriptionResource, @@ -76,6 +77,7 @@ export class BillingSubscriptionItem extends BaseResource implements BillingSubs credit?: { amount: BillingMoneyAmount; }; + seats?: BillingSubscriptionItemSeats; credits?: BillingCredits; isFreeTrial!: boolean; @@ -104,6 +106,7 @@ export class BillingSubscriptionItem extends BaseResource implements BillingSubs this.amount = data.amount ? billingMoneyAmountFromJSON(data.amount) : undefined; this.credit = data.credit && data.credit.amount ? { amount: billingMoneyAmountFromJSON(data.credit.amount) } : undefined; + this.seats = data.seats ? { quantity: data.seats.quantity } : undefined; this.credits = data.credits ? billingCreditsFromJSON(data.credits) : undefined; diff --git a/packages/clerk-js/src/utils/billing.ts b/packages/clerk-js/src/utils/billing.ts index acdeb376016..77b28782197 100644 --- a/packages/clerk-js/src/utils/billing.ts +++ b/packages/clerk-js/src/utils/billing.ts @@ -5,6 +5,8 @@ import type { BillingCreditsJSON, BillingMoneyAmount, BillingMoneyAmountJSON, + BillingPerUnitTotal, + BillingPerUnitTotalJSON, BillingStatementTotals, BillingStatementTotalsJSON, } from '@clerk/shared/types'; @@ -18,6 +20,18 @@ export const billingMoneyAmountFromJSON = (data: BillingMoneyAmountJSON): Billin }; }; +const billingPerUnitTotalsFromJSON = (data: BillingPerUnitTotalJSON[]): BillingPerUnitTotal[] => { + return data.map(unitTotal => ({ + name: unitTotal.name, + blockSize: unitTotal.block_size, + tiers: unitTotal.tiers.map(tier => ({ + quantity: tier.quantity, + feePerBlock: billingMoneyAmountFromJSON(tier.fee_per_block), + total: billingMoneyAmountFromJSON(tier.total), + })), + })); +}; + export const billingCreditsFromJSON = (data: BillingCreditsJSON): BillingCredits => { return { proration: data.proration @@ -53,6 +67,10 @@ export const billingTotalsFromJSON = m.organization?.id === orgId); if (membership) { return createNoStoreResponse({ - data: [SessionService.serialize(membership)], - total_count: 1, + response: { + data: [SessionService.serialize(membership)], + total_count: 1, + }, }); } } diff --git a/packages/shared/src/types/billing.ts b/packages/shared/src/types/billing.ts index 89e1ea5092d..786887fd2b6 100644 --- a/packages/shared/src/types/billing.ts +++ b/packages/shared/src/types/billing.ts @@ -206,6 +206,10 @@ export interface BillingPlanResource extends ClerkResource { * The Features the Plan offers. */ features: FeatureResource[]; + /** + * Per-unit pricing tiers for this Plan (for example, seats). + */ + unitPrices?: BillingPlanUnitPrice[]; /** * The number of days of the free trial for the Plan. `null` if the Plan does not have a free trial. */ @@ -216,6 +220,102 @@ export interface BillingPlanResource extends ClerkResource { freeTrialEnabled: boolean; } +/** + * The `BillingSubscriptionItemSeats` type represents seat entitlements attached to a subscription item. + * + * @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. + */ +export interface BillingSubscriptionItemSeats { + /** + * The seat limit active while the parent subscription item was active. `null` means unlimited. + */ + quantity: number | null; +} + +/** + * The `BillingPlanUnitPriceTier` type represents a single pricing tier for a unit type on a plan. + * + * @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. + */ +export interface BillingPlanUnitPriceTier { + /** + * The unique identifier of the unit price tier. + */ + id: string; + /** + * The first block number this tier applies to. + */ + startsAtBlock: number; + /** + * The final block this tier applies to. `null` means unlimited. + */ + endsAfterBlock: number | null; + /** + * The fee charged for each block in this tier. + */ + feePerBlock: BillingMoneyAmount; +} + +/** + * The `BillingPlanUnitPrice` type represents unit pricing for a specific unit type (for example, seats) on a plan. + * + * @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. + */ +export interface BillingPlanUnitPrice { + /** + * The unit name, for example `seats`. + */ + name: string; + /** + * Number of units represented by one billable block. + */ + blockSize: number; + /** + * Tiers that define how each block range is priced. + */ + tiers: BillingPlanUnitPriceTier[]; +} + +/** + * The `BillingPerUnitTotalTier` type represents the cost breakdown for a single tier in checkout totals. + * + * @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. + */ +export interface BillingPerUnitTotalTier { + /** + * The quantity billed within this tier. `null` means unlimited. + */ + quantity: number | null; + /** + * The fee charged per block for this tier. + */ + feePerBlock: BillingMoneyAmount; + /** + * The total billed amount for this tier. + */ + total: BillingMoneyAmount; +} + +/** + * The `BillingPerUnitTotal` type represents the per-unit cost breakdown in checkout totals. + * + * @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. + */ +export interface BillingPerUnitTotal { + /** + * The unit name, for example `seats`. + */ + name: string; + /** + * Number of units represented by one billable block. + */ + blockSize: number; + /** + * Detailed tier breakdown for this unit total. + */ + tiers: BillingPerUnitTotalTier[]; +} + /** * The `FeatureResource` type represents a Feature of a Plan. * @@ -594,6 +694,11 @@ export interface BillingSubscriptionItemResource extends ClerkResource { amount: BillingMoneyAmount; }; credits?: BillingCredits; + /** + * Seat entitlement details for this subscription item. Only set for organization subscription items with + * seat-based billing. + */ + seats?: BillingSubscriptionItemSeats; /** * A function to cancel the subscription item. Accepts the following parameters: *