diff --git a/.changeset/cute-ideas-appear.md b/.changeset/cute-ideas-appear.md new file mode 100644 index 00000000000..9b1d6379498 --- /dev/null +++ b/.changeset/cute-ideas-appear.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': minor +'@clerk/shared': minor +--- + +Add support for parsing seat-based billing fields from FAPI. diff --git a/packages/clerk-js/src/core/resources/BillingPlan.ts b/packages/clerk-js/src/core/resources/BillingPlan.ts index 6afbdbfe3b9..977e454cbd7 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 3b80c7dbe66..f463c82646b 100644 --- a/packages/clerk-js/src/core/resources/BillingSubscription.ts +++ b/packages/clerk-js/src/core/resources/BillingSubscription.ts @@ -2,6 +2,7 @@ import type { BillingMoneyAmount, BillingSubscriptionItemJSON, BillingSubscriptionItemResource, + BillingSubscriptionItemSeats, BillingSubscriptionJSON, BillingSubscriptionPlanPeriod, BillingSubscriptionResource, @@ -75,6 +76,7 @@ export class BillingSubscriptionItem extends BaseResource implements BillingSubs credit?: { amount: BillingMoneyAmount; }; + seats?: BillingSubscriptionItemSeats; isFreeTrial!: boolean; constructor(data: BillingSubscriptionItemJSON) { @@ -102,6 +104,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.isFreeTrial = this.withDefault(data.is_free_trial, false); return this; diff --git a/packages/clerk-js/src/utils/billing.ts b/packages/clerk-js/src/utils/billing.ts index a72868a859d..d795c2841f0 100644 --- a/packages/clerk-js/src/utils/billing.ts +++ b/packages/clerk-js/src/utils/billing.ts @@ -3,6 +3,8 @@ import type { BillingCheckoutTotalsJSON, BillingMoneyAmount, BillingMoneyAmountJSON, + BillingPerUnitTotal, + BillingPerUnitTotalJSON, BillingStatementTotals, BillingStatementTotalsJSON, } from '@clerk/shared/types'; @@ -16,6 +18,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 billingTotalsFromJSON = ( data: T, ): T extends { total_due_now: BillingMoneyAmountJSON } ? BillingCheckoutTotals : BillingStatementTotals => { @@ -31,6 +45,9 @@ export const billingTotalsFromJSON = @@ -708,6 +813,10 @@ export interface BillingCheckoutTotals { * The amount of tax included in the checkout. */ taxTotal: BillingMoneyAmount; + /** + * Per-unit cost breakdown for this checkout (for example, seats). + */ + perUnitTotals?: BillingPerUnitTotal[]; /** * The amount that needs to be immediately paid to complete the checkout. */ diff --git a/packages/shared/src/types/json.ts b/packages/shared/src/types/json.ts index 5ce69d083d4..c56fd4ce9c4 100644 --- a/packages/shared/src/types/json.ts +++ b/packages/shared/src/types/json.ts @@ -596,6 +596,68 @@ export interface FeatureJSON extends ClerkResourceJSON { avatar_url: string | null; } +/** + * @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 BillingSubscriptionItemSeatsJSON { + /** + * The number of seats available. `null` means unlimited. + */ + quantity: number | null; +} + +/** + * @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. + * + * Represents a single pricing tier for a unit type on a plan. + */ +export interface BillingPlanUnitPriceTierJSON { + id: string; + object: 'commerce_unit_price'; + starts_at_block: number; + /** + * `null` means unlimited. + */ + ends_after_block: number | null; + fee_per_block: BillingMoneyAmountJSON; +} + +/** + * @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. + * + * Represents unit pricing for a specific unit type (for example, seats) on a plan. + */ +export interface BillingPlanUnitPriceJSON { + name: string; + block_size: number; + tiers: BillingPlanUnitPriceTierJSON[]; +} + +/** + * @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. + * + * Represents the cost breakdown for a single tier in checkout totals. + */ +export interface BillingPerUnitTotalTierJSON { + /** + * `null` means unlimited. + */ + quantity: number | null; + fee_per_block: BillingMoneyAmountJSON; + total: BillingMoneyAmountJSON; +} + +/** + * @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. + * + * Represents the per-unit cost breakdown in checkout totals. + */ +export interface BillingPerUnitTotalJSON { + name: string; + block_size: number; + tiers: BillingPerUnitTotalTierJSON[]; +} + /** * @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. */ @@ -617,6 +679,10 @@ export interface BillingPlanJSON extends ClerkResourceJSON { features?: FeatureJSON[]; free_trial_days?: number | null; free_trial_enabled?: boolean; + /** + * Per-unit pricing tiers for this plan (for example, seats). + */ + unit_prices?: BillingPlanUnitPriceJSON[]; } /** @@ -695,6 +761,11 @@ export interface BillingSubscriptionItemJSON extends ClerkResourceJSON { credit?: { amount: BillingMoneyAmountJSON; }; + /** + * Seat entitlement details for this subscription item. Only set for organization subscription items with + * seat-based billing. + */ + seats?: BillingSubscriptionItemSeatsJSON; plan: BillingPlanJSON; plan_period: BillingSubscriptionPlanPeriod; status: BillingSubscriptionStatus; @@ -751,6 +822,10 @@ export interface BillingCheckoutTotalsJSON { grand_total: BillingMoneyAmountJSON; subtotal: BillingMoneyAmountJSON; tax_total: BillingMoneyAmountJSON; + /** + * Per-unit cost breakdown for this checkout (for example, seats). + */ + per_unit_totals?: BillingPerUnitTotalJSON[]; total_due_now: BillingMoneyAmountJSON; credit: BillingMoneyAmountJSON | null; past_due: BillingMoneyAmountJSON | null;