diff --git a/packages/clerk-js/src/core/resources/BillingCheckout.ts b/packages/clerk-js/src/core/resources/BillingCheckout.ts index 0dbadd220fa..61d851288c3 100644 --- a/packages/clerk-js/src/core/resources/BillingCheckout.ts +++ b/packages/clerk-js/src/core/resources/BillingCheckout.ts @@ -4,7 +4,7 @@ import { retry } from '@clerk/shared/retry'; import type { BillingCheckoutJSON, BillingCheckoutResource, - BillingCheckoutTotals, + BillingTotals, BillingPayerResource, BillingPaymentMethodResource, BillingSubscriptionPlanPeriod, @@ -34,7 +34,7 @@ export class BillingCheckout extends BaseResource implements BillingCheckoutReso planPeriod!: BillingSubscriptionPlanPeriod; planPeriodStart!: number | undefined; status!: 'needs_confirmation' | 'completed'; - totals!: BillingCheckoutTotals; + totals!: BillingTotals; isImmediatePlanChange!: boolean; freeTrialEndsAt?: Date; payer!: BillingPayerResource; diff --git a/packages/clerk-js/src/core/resources/BillingStatement.ts b/packages/clerk-js/src/core/resources/BillingStatement.ts index 7126987beef..aa66aecd29a 100644 --- a/packages/clerk-js/src/core/resources/BillingStatement.ts +++ b/packages/clerk-js/src/core/resources/BillingStatement.ts @@ -6,7 +6,7 @@ import type { BillingStatementTotals, } from '@clerk/shared/types'; -import { billingTotalsFromJSON } from '../../utils'; +import { billingStatementTotalsFromJSON } from '../../utils'; import { unixEpochToDate } from '../../utils/date'; import { BaseResource, BillingPayment } from './internal'; @@ -30,7 +30,7 @@ export class BillingStatement extends BaseResource implements BillingStatementRe this.id = data.id; this.status = data.status; this.timestamp = unixEpochToDate(data.timestamp); - this.totals = billingTotalsFromJSON(data.totals); + this.totals = billingStatementTotalsFromJSON(data.totals); this.groups = data.groups.map(group => new BillingStatementGroup(group)); return this; } diff --git a/packages/clerk-js/src/core/resources/BillingSubscription.ts b/packages/clerk-js/src/core/resources/BillingSubscription.ts index 131f3d3e9ad..bb6b135ecb0 100644 --- a/packages/clerk-js/src/core/resources/BillingSubscription.ts +++ b/packages/clerk-js/src/core/resources/BillingSubscription.ts @@ -1,6 +1,8 @@ import type { BillingCredits, BillingMoneyAmount, + BillingSubscriptionItemNextPayment, + BillingSubscriptionNextPayment, BillingSubscriptionItemJSON, BillingSubscriptionItemResource, BillingSubscriptionItemSeats, @@ -14,7 +16,12 @@ import type { import { unixEpochToDate } from '@/utils/date'; -import { billingCreditsFromJSON, billingMoneyAmountFromJSON } from '../../utils'; +import { + billingCreditsFromJSON, + billingMoneyAmountFromJSON, + billingSubscriptionItemNextPaymentFromJSON, + billingSubscriptionNextPaymentFromJSON, +} from '../../utils'; import { Billing } from '../modules/billing/namespace'; import { BaseResource, BillingPlan, DeletedObject } from './internal'; @@ -25,10 +32,7 @@ export class BillingSubscription extends BaseResource implements BillingSubscrip createdAt!: Date; pastDueAt!: Date | null; updatedAt!: Date | null; - nextPayment?: { - amount: BillingMoneyAmount; - date: Date; - }; + nextPayment?: BillingSubscriptionNextPayment | null; subscriptionItems!: BillingSubscriptionItemResource[]; eligibleForFreeTrial!: boolean; @@ -49,12 +53,12 @@ export class BillingSubscription extends BaseResource implements BillingSubscrip this.activeAt = unixEpochToDate(data.active_at); this.pastDueAt = data.past_due_at ? unixEpochToDate(data.past_due_at) : null; - if (data.next_payment) { - this.nextPayment = { - amount: billingMoneyAmountFromJSON(data.next_payment.amount), - date: unixEpochToDate(data.next_payment.date), - }; - } + this.nextPayment = + data.next_payment === undefined + ? undefined + : data.next_payment === null + ? null + : billingSubscriptionNextPaymentFromJSON(data.next_payment); this.subscriptionItems = (data.subscription_items || []).map(item => new BillingSubscriptionItem(item)); this.eligibleForFreeTrial = this.withDefault(data.eligible_for_free_trial, false); @@ -80,6 +84,7 @@ export class BillingSubscriptionItem extends BaseResource implements BillingSubs }; seats?: BillingSubscriptionItemSeats; credits?: BillingCredits; + nextPayment?: BillingSubscriptionItemNextPayment | null; isFreeTrial!: boolean; constructor(data: BillingSubscriptionItemJSON) { @@ -111,6 +116,12 @@ export class BillingSubscriptionItem extends BaseResource implements BillingSubs this.seats = data.seats ? { quantity: data.seats.quantity } : undefined; this.credits = data.credits ? billingCreditsFromJSON(data.credits) : undefined; + this.nextPayment = + data.next_payment === undefined + ? undefined + : data.next_payment === null + ? null + : billingSubscriptionItemNextPaymentFromJSON(data.next_payment); this.isFreeTrial = this.withDefault(data.is_free_trial, false); return this; diff --git a/packages/clerk-js/src/utils/__tests__/billing.test.ts b/packages/clerk-js/src/utils/__tests__/billing.test.ts index 5b8afccbc29..1ddb6d77bde 100644 --- a/packages/clerk-js/src/utils/__tests__/billing.test.ts +++ b/packages/clerk-js/src/utils/__tests__/billing.test.ts @@ -1,7 +1,17 @@ -import type { BillingMoneyAmountJSON, BillingPaymentTotalsJSON } from '@clerk/shared/types'; +import type { + BillingMoneyAmountJSON, + BillingPaymentTotalsJSON, + BillingSubscriptionItemNextPaymentJSON, + BillingSubscriptionNextPaymentJSON, + BillingTotalsJSON, +} from '@clerk/shared/types'; import { describe, expect, it } from 'vitest'; -import { billingPaymentTotalsFromJSON } from '../billing'; +import { + billingPaymentTotalsFromJSON, + billingSubscriptionItemNextPaymentFromJSON, + billingSubscriptionNextPaymentFromJSON, +} from '../billing'; const moneyJSON = (amount: number): BillingMoneyAmountJSON => ({ amount, @@ -10,6 +20,55 @@ const moneyJSON = (amount: number): BillingMoneyAmountJSON => ({ currency_symbol: '$', }); +const nextPaymentTotalsJSON = (): BillingTotalsJSON => ({ + subtotal: moneyJSON(5000), + base_fee: moneyJSON(1000), + tax_total: moneyJSON(500), + grand_total: moneyJSON(5500), + credits: { + proration: null, + payer: { + remaining_balance: moneyJSON(2000), + applied_amount: moneyJSON(1000), + }, + total: moneyJSON(1000), + }, + discounts: { + proration: { + amount: moneyJSON(500), + cycle_days_passed: 15, + cycle_days_total: 30, + cycle_passed_percent: 50, + }, + total: moneyJSON(500), + }, + total_due_after_free_trial: null, + credit: null, + past_due: null, + total_due_now: moneyJSON(4500), + per_unit_totals: [ + { + name: 'seats', + block_size: 1, + tiers: [{ quantity: 5, fee_per_block: moneyJSON(1000), total: moneyJSON(5000) }], + }, + ], + totals_due_per_period: { + subtotal: moneyJSON(10000), + base_fee: moneyJSON(1000), + tax_total: moneyJSON(1000), + grand_total: moneyJSON(11000), + per_unit_totals: [ + { + name: 'seats', + block_size: 1, + tiers: [{ quantity: 10, fee_per_block: moneyJSON(1000), total: moneyJSON(10000) }], + }, + ], + }, + total_due_per_period: moneyJSON(11000), +}); + describe('billingPaymentTotalsFromJSON', () => { it('maps subtotal, grand_total, and tax_total', () => { const data: BillingPaymentTotalsJSON = { @@ -68,3 +127,100 @@ describe('billingPaymentTotalsFromJSON', () => { expect(totals.perUnitTotals?.[0].tiers[1].quantity).toBeNull(); }); }); + +describe('billingSubscriptionNextPaymentFromJSON', () => { + it('maps amount, date, and per_unit_totals', () => { + const data: BillingSubscriptionNextPaymentJSON = { + amount: moneyJSON(5000), + date: 1_609_459_200_000, + per_unit_totals: [ + { + name: 'seats', + block_size: 1, + tiers: [{ quantity: 5, fee_per_block: moneyJSON(1000), total: moneyJSON(5000) }], + }, + ], + totals: nextPaymentTotalsJSON(), + }; + + const nextPayment = billingSubscriptionNextPaymentFromJSON(data); + + expect(nextPayment.amount.amount).toBe(5000); + expect(nextPayment.date).toEqual(new Date('2021-01-01T00:00:00.000Z')); + expect(nextPayment.perUnitTotals?.[0]).toMatchObject({ + name: 'seats', + blockSize: 1, + tiers: [{ quantity: 5, feePerBlock: { amount: 1000 }, total: { amount: 5000 } }], + }); + expect(nextPayment.totals).toMatchObject({ + subtotal: { amount: 5000 }, + baseFee: { amount: 1000 }, + taxTotal: { amount: 500 }, + grandTotal: { amount: 5500 }, + totalDueAfterFreeTrial: null, + credit: null, + pastDue: null, + totalDueNow: { amount: 4500 }, + totalDuePerPeriod: { amount: 11000 }, + credits: { + payer: { + remainingBalance: { amount: 2000 }, + appliedAmount: { amount: 1000 }, + }, + total: { amount: 1000 }, + }, + discounts: { + proration: { + amount: { amount: 500 }, + cycleDaysPassed: 15, + cycleDaysTotal: 30, + cyclePassedPercent: 50, + }, + total: { amount: 500 }, + }, + perUnitTotals: [{ name: 'seats', blockSize: 1 }], + totalsDuePerPeriod: { + subtotal: { amount: 10000 }, + baseFee: { amount: 1000 }, + taxTotal: { amount: 1000 }, + grandTotal: { amount: 11000 }, + perUnitTotals: [{ name: 'seats', blockSize: 1 }], + }, + }); + }); +}); + +describe('billingSubscriptionItemNextPaymentFromJSON', () => { + it('maps amount, date, and per_unit_totals', () => { + const data: BillingSubscriptionItemNextPaymentJSON = { + amount: moneyJSON(5000), + date: 1_609_459_200_000, + per_unit_totals: [ + { + name: 'seats', + block_size: 1, + tiers: [{ quantity: 5, fee_per_block: moneyJSON(1000), total: moneyJSON(5000) }], + }, + ], + totals: nextPaymentTotalsJSON(), + }; + + const nextPayment = billingSubscriptionItemNextPaymentFromJSON(data); + + expect(nextPayment.amount.amount).toBe(5000); + expect(nextPayment.date).toEqual(new Date('2021-01-01T00:00:00.000Z')); + expect(nextPayment.perUnitTotals?.[0]).toMatchObject({ + name: 'seats', + blockSize: 1, + tiers: [{ quantity: 5, feePerBlock: { amount: 1000 }, total: { amount: 5000 } }], + }); + expect(nextPayment.totals).toMatchObject({ + subtotal: { amount: 5000 }, + baseFee: { amount: 1000 }, + totalDueNow: { amount: 4500 }, + credits: { total: { amount: 1000 } }, + discounts: { total: { amount: 500 } }, + perUnitTotals: [{ name: 'seats', blockSize: 1 }], + }); + }); +}); diff --git a/packages/clerk-js/src/utils/billing.ts b/packages/clerk-js/src/utils/billing.ts index 5ed5bd727fc..789235f5ce0 100644 --- a/packages/clerk-js/src/utils/billing.ts +++ b/packages/clerk-js/src/utils/billing.ts @@ -1,23 +1,31 @@ import type { - BillingCheckoutDiscounts, - BillingCheckoutDiscountsJSON, BillingCheckoutTotals, BillingCheckoutTotalsJSON, BillingCredits, BillingCreditsJSON, + BillingDiscounts, + BillingDiscountsJSON, BillingMoneyAmount, BillingMoneyAmountJSON, BillingPaymentTotals, BillingPaymentTotalsJSON, - BillingPerPeriodTotals, - BillingPerPeriodTotalsJSON, + BillingPeriodTotals, + BillingPeriodTotalsJSON, BillingPerUnitTotal, BillingPerUnitTotalJSON, BillingPlanUnitPriceJSON, BillingStatementTotals, BillingStatementTotalsJSON, + BillingSubscriptionItemNextPayment, + BillingSubscriptionItemNextPaymentJSON, + BillingSubscriptionNextPayment, + BillingSubscriptionNextPaymentJSON, + BillingTotals, + BillingTotalsJSON, } from '@clerk/shared/types'; +import { unixEpochToDate } from './date'; + export const billingMoneyAmountFromJSON = (data: BillingMoneyAmountJSON): BillingMoneyAmount => { return { amount: data.amount, @@ -80,7 +88,7 @@ export const billingCreditsFromJSON = (data: BillingCreditsJSON): BillingCredits }; }; -const billingCheckoutDiscountsFromJSON = (data: BillingCheckoutDiscountsJSON): BillingCheckoutDiscounts => { +const billingDiscountsFromJSON = (data: BillingDiscountsJSON): BillingDiscounts => { return { proration: data.proration ? { @@ -94,7 +102,7 @@ const billingCheckoutDiscountsFromJSON = (data: BillingCheckoutDiscountsJSON): B }; }; -const billingPerPeriodTotalsFromJSON = (data: BillingPerPeriodTotalsJSON): BillingPerPeriodTotals => { +const billingPeriodTotalsFromJSON = (data: BillingPeriodTotalsJSON): BillingPeriodTotals => { return { subtotal: billingMoneyAmountFromJSON(data.subtotal), baseFee: billingMoneyAmountFromJSON(data.base_fee), @@ -104,7 +112,48 @@ const billingPerPeriodTotalsFromJSON = (data: BillingPerPeriodTotalsJSON): Billi }; }; -export const billingTotalsFromJSON = ( +export const billingTotalsFromJSON = (data: BillingTotalsJSON): BillingTotals => { + return { + subtotal: billingMoneyAmountFromJSON(data.subtotal), + baseFee: data.base_fee ? billingMoneyAmountFromJSON(data.base_fee) : null, + taxTotal: billingMoneyAmountFromJSON(data.tax_total), + grandTotal: billingMoneyAmountFromJSON(data.grand_total), + totalDueAfterFreeTrial: data.total_due_after_free_trial + ? billingMoneyAmountFromJSON(data.total_due_after_free_trial) + : null, + credit: data.credit ? billingMoneyAmountFromJSON(data.credit) : data.credit, + credits: data.credits ? billingCreditsFromJSON(data.credits) : null, + discounts: data.discounts ? billingDiscountsFromJSON(data.discounts) : null, + pastDue: data.past_due ? billingMoneyAmountFromJSON(data.past_due) : data.past_due, + totalDueNow: data.total_due_now ? billingMoneyAmountFromJSON(data.total_due_now) : undefined, + perUnitTotals: data.per_unit_totals ? billingPerUnitTotalsFromJSON(data.per_unit_totals) : undefined, + totalsDuePerPeriod: data.totals_due_per_period + ? billingPeriodTotalsFromJSON(data.totals_due_per_period) + : undefined, + totalDuePerPeriod: data.total_due_per_period ? billingMoneyAmountFromJSON(data.total_due_per_period) : undefined, + }; +}; + +const billingNextPaymentFromJSON = ( + data: BillingSubscriptionNextPaymentJSON | BillingSubscriptionItemNextPaymentJSON, +) => { + return { + amount: billingMoneyAmountFromJSON(data.amount), + date: unixEpochToDate(data.date), + perUnitTotals: data.per_unit_totals ? billingPerUnitTotalsFromJSON(data.per_unit_totals) : undefined, + totals: data.totals ? billingTotalsFromJSON(data.totals) : undefined, + }; +}; + +export const billingSubscriptionNextPaymentFromJSON = ( + data: BillingSubscriptionNextPaymentJSON, +): BillingSubscriptionNextPayment => billingNextPaymentFromJSON(data); + +export const billingSubscriptionItemNextPaymentFromJSON = ( + data: BillingSubscriptionItemNextPaymentJSON, +): BillingSubscriptionItemNextPayment => billingNextPaymentFromJSON(data); + +export const billingStatementTotalsFromJSON = ( data: T, ): T extends { total_due_now: BillingMoneyAmountJSON } ? BillingCheckoutTotals : BillingStatementTotals => { const totals: Partial = { @@ -140,7 +189,7 @@ export const billingTotalsFromJSON = ; +/** + * The `BillingSubscriptionNextPayment` type represents the upcoming payment details for a subscription. + * + * @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 BillingSubscriptionNextPayment { + /** + * The amount of the next payment. + */ + amount: BillingMoneyAmount; + /** + * The date when the next payment is due. + */ + date: Date; + /** + * Per-unit cost breakdown for the next payment (for example, seats). + */ + perUnitTotals?: BillingPerUnitTotal[]; + /** + * Full cost breakdown for the next payment. + */ + totals?: BillingTotals; +} + +/** + * The `BillingSubscriptionItemNextPayment` type represents the upcoming payment details for 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 BillingSubscriptionItemNextPayment { + /** + * The amount of the next payment. + */ + amount: BillingMoneyAmount; + /** + * The date when the next payment is due. + */ + date: Date; + /** + * Per-unit cost breakdown for the next payment (for example, seats). + */ + perUnitTotals?: BillingPerUnitTotal[]; + /** + * Full cost breakdown for the next payment. + */ + totals?: BillingTotals; +} + /** * The `BillingSubscriptionItemResource` type represents an item in a subscription. * @@ -730,6 +778,10 @@ export interface BillingSubscriptionItemResource extends ClerkResource { * The amount charged for the subscription item. */ amount?: BillingMoneyAmount; + /** + * Information about the next payment for this subscription item. + */ + nextPayment?: BillingSubscriptionItemNextPayment | null; /** * The credit from a previous purchase that is being applied to the subscription item. */ @@ -782,16 +834,7 @@ export interface BillingSubscriptionResource extends ClerkResource { /** * Information about the next payment, including the amount and the date it's due. Returns null if there is no upcoming payment. */ - nextPayment?: { - /** - * The amount of the next payment. - */ - amount: BillingMoneyAmount; - /** - * The date when the next payment is due. - */ - date: Date; - }; + nextPayment?: BillingSubscriptionNextPayment | null; /** * The date when the subscription became past due, or `null` if the subscription is not past due. */ @@ -866,7 +909,7 @@ export interface BillingCredits { * * @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 BillingProrationDiscountDetail { +export interface BillingProrationDiscount { amount: BillingMoneyAmount; cycleDaysPassed: number; cycleDaysTotal: number; @@ -878,13 +921,13 @@ export interface BillingProrationDiscountDetail { * * @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 BillingCheckoutDiscounts { +export interface BillingDiscounts { /** * The prorated discount for the part of the billing period that has already passed when adding a seat mid-cycle. * Unlike the proration credit (which refunds the unused remainder of a plan you already paid for), this discount * means you are not charged for the portion of the new seat's cycle that has already elapsed. */ - proration: BillingProrationDiscountDetail | null; + proration: BillingProrationDiscount | null; /** * The total of all discounts applied to the checkout. */ @@ -898,7 +941,7 @@ export interface BillingCheckoutDiscounts { * * @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 BillingPerPeriodTotals { +export interface BillingPeriodTotals { subtotal: BillingMoneyAmount; baseFee: BillingMoneyAmount; taxTotal: BillingMoneyAmount; @@ -910,6 +953,25 @@ export interface BillingPerPeriodTotals { perUnitTotals?: BillingPerUnitTotal[]; } +/** + * @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 BillingTotals { + subtotal: BillingMoneyAmount; + baseFee: BillingMoneyAmount | null; + taxTotal: BillingMoneyAmount; + grandTotal: BillingMoneyAmount; + totalDueAfterFreeTrial?: BillingMoneyAmount | null; + credit?: BillingMoneyAmount | null; + credits: BillingCredits | null; + discounts: BillingDiscounts | null; + pastDue?: BillingMoneyAmount | null; + totalDueNow?: BillingMoneyAmount; + perUnitTotals?: BillingPerUnitTotal[]; + totalsDuePerPeriod?: BillingPeriodTotals; + totalDuePerPeriod?: BillingMoneyAmount; +} + /** * The `BillingCheckoutTotals` type represents the total costs, taxes, and other pricing details for a checkout session. * @@ -963,13 +1025,13 @@ export interface BillingCheckoutTotals { /** * Discounts applied to this checkout such as mid-cycle prorated seat discounts. */ - discounts?: BillingCheckoutDiscounts | null; + discounts?: BillingDiscounts | null; /** * Full renewal period totals after this checkout completes. * Contains the complete breakdown of what the next recurring charge will look like, * including all seats and the base plan fee. */ - totalsDuePerPeriod?: BillingPerPeriodTotals; + totalsDuePerPeriod?: BillingPeriodTotals; } /** @@ -1087,7 +1149,7 @@ export interface BillingCheckoutResource extends ClerkResource { /** * The total costs, taxes, and other pricing details for the checkout. */ - totals: BillingCheckoutTotals; + totals: BillingTotals; /** * A function to confirm and finalize the checkout process, usually after payment information has been provided and validated. [Learn more.](#confirm) */ @@ -1186,7 +1248,7 @@ interface CheckoutFlowProperties { /** * The total costs, taxes, and other pricing details for the checkout. */ - totals: BillingCheckoutTotals; + totals: BillingTotals; /** * Whether the plan change will take effect immediately after checkout. */ diff --git a/packages/shared/src/types/json.ts b/packages/shared/src/types/json.ts index 0be09f55496..876147f846f 100644 --- a/packages/shared/src/types/json.ts +++ b/packages/shared/src/types/json.ts @@ -609,6 +609,10 @@ export interface BillingSubscriptionItemSeatsJSON { * The number of seats available. `null` means unlimited. */ quantity: number | null; + /** + * The per-unit cost breakdown by pricing tier. + */ + tiers?: BillingPerUnitTotalTierJSON[]; } /** @@ -783,6 +787,29 @@ export interface BillingPaymentJSON extends ClerkResourceJSON { totals?: BillingPaymentTotalsJSON | null; } +export interface BillingTotalsJSON { + subtotal: BillingMoneyAmountJSON; + base_fee: BillingMoneyAmountJSON | null; + tax_total: BillingMoneyAmountJSON; + grand_total: BillingMoneyAmountJSON; + total_due_after_free_trial?: BillingMoneyAmountJSON | null; + credit?: BillingMoneyAmountJSON | null; + credits: BillingCreditsJSON | null; + discounts: BillingDiscountsJSON | null; + past_due?: BillingMoneyAmountJSON | null; + total_due_now?: BillingMoneyAmountJSON; + per_unit_totals?: BillingPerUnitTotalJSON[]; + totals_due_per_period?: BillingPeriodTotalsJSON; + total_due_per_period?: BillingMoneyAmountJSON; +} + +export interface BillingSubscriptionItemNextPaymentJSON { + amount: BillingMoneyAmountJSON; + date: number; + per_unit_totals?: BillingPerUnitTotalJSON[]; + totals?: BillingTotalsJSON; +} + /** * @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. */ @@ -812,6 +839,14 @@ export interface BillingSubscriptionItemJSON extends ClerkResourceJSON { canceled_at: number | null; past_due_at: number | null; is_free_trial: boolean; + next_payment?: BillingSubscriptionItemNextPaymentJSON | null; +} + +export interface BillingSubscriptionNextPaymentJSON { + amount: BillingMoneyAmountJSON; + date: number; + per_unit_totals?: BillingPerUnitTotalJSON[]; + totals?: BillingTotalsJSON; } /** @@ -821,12 +856,9 @@ export interface BillingSubscriptionJSON extends ClerkResourceJSON { object: 'commerce_subscription'; id: string; /** - * Describes the details for the next payment cycle. It is `undefined` for subscription items that are cancelled or on the free plan. + * Describes the details for the next payment cycle. It is `undefined` for subscription items that are cancelled or on the free plan, and `null` when there is no upcoming payment. */ - next_payment?: { - amount: BillingMoneyAmountJSON; - date: number; - }; + next_payment?: BillingSubscriptionNextPaymentJSON | null; /** * Due to the free plan subscription item, the top level subscription can either be `active` or `past_due`. */ @@ -880,7 +912,7 @@ export interface BillingCreditsJSON { * Details about a prorated discount applied when adding a seat mid-cycle. The discount covers the part of the * billing period that has already passed, so the payer is only charged for the time remaining in the cycle. */ -export interface BillingProrationDiscountDetailJSON { +export interface BillingProrationDiscountJSON { amount: BillingMoneyAmountJSON; cycle_days_passed: number; cycle_days_total: number; @@ -892,13 +924,13 @@ export interface BillingProrationDiscountDetailJSON { * * @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 BillingCheckoutDiscountsJSON { +export interface BillingDiscountsJSON { /** * The prorated discount for the part of the billing period that has already passed when adding a seat mid-cycle. * Unlike the proration credit (which refunds the unused remainder of a plan you already paid for), this discount * means you are not charged for the portion of the new seat's cycle that has already elapsed. */ - proration: BillingProrationDiscountDetailJSON | null; + proration: BillingProrationDiscountJSON | null; /** * The total of all discounts applied to the checkout. */ @@ -912,7 +944,7 @@ export interface BillingCheckoutDiscountsJSON { * * @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 BillingPerPeriodTotalsJSON { +export interface BillingPeriodTotalsJSON { subtotal: BillingMoneyAmountJSON; base_fee: BillingMoneyAmountJSON; tax_total: BillingMoneyAmountJSON; @@ -958,7 +990,7 @@ export interface BillingCheckoutTotalsJSON { /** * Discounts applied to this checkout such as mid-cycle prorated seat discounts. */ - discounts: BillingCheckoutDiscountsJSON | null; + discounts: BillingDiscountsJSON | null; /** * The expected recurring payment for each future billing period. * Kept for backwards compatibility. Prefer `totals_due_per_period` for the full breakdown. @@ -969,7 +1001,7 @@ export interface BillingCheckoutTotalsJSON { * Contains the complete breakdown of what the next recurring charge will look like, * including all seats and the base plan fee. */ - totals_due_per_period: BillingPerPeriodTotalsJSON; + totals_due_per_period: BillingPeriodTotalsJSON; } /** @@ -994,7 +1026,7 @@ export interface BillingCheckoutJSON extends ClerkResourceJSON { plan_period: BillingSubscriptionPlanPeriod; plan_period_start?: number; status: 'needs_confirmation' | 'completed'; - totals: BillingCheckoutTotalsJSON; + totals: BillingTotalsJSON; is_immediate_plan_change: boolean; free_trial_ends_at?: number; payer: BillingPayerJSON; diff --git a/packages/ui/src/components/Checkout/CheckoutComplete.tsx b/packages/ui/src/components/Checkout/CheckoutComplete.tsx index c329a310c71..21877d2bdb8 100644 --- a/packages/ui/src/components/Checkout/CheckoutComplete.tsx +++ b/packages/ui/src/components/Checkout/CheckoutComplete.tsx @@ -332,7 +332,7 @@ export const CheckoutComplete = () => { localizationKey={ freeTrialEndsAt ? localizationKeys('billing.checkout.title__trialSuccess') - : totals.totalDueNow.amount > 0 + : totals.totalDueNow && totals.totalDueNow.amount > 0 ? localizationKeys('billing.checkout.title__paymentSuccessful') : localizationKeys('billing.checkout.title__subscriptionSuccessful') } @@ -387,7 +387,7 @@ export const CheckoutComplete = () => { }), })} localizationKey={ - totals.totalDueNow.amount > 0 + totals.totalDueNow && totals.totalDueNow.amount > 0 ? localizationKeys('billing.checkout.description__paymentSuccessful') : localizationKeys('billing.checkout.description__subscriptionSuccessful') } @@ -417,10 +417,14 @@ export const CheckoutComplete = () => { })} > - - - - + {totals.totalDueNow ? ( + + + + + ) : null} {freeTrialEndsAt ? ( @@ -431,7 +435,7 @@ export const CheckoutComplete = () => { 0 || freeTrialEndsAt !== null + (totals.totalDueNow && totals.totalDueNow.amount > 0) || freeTrialEndsAt !== null ? localizationKeys('billing.checkout.lineItems.title__paymentMethod') : localizationKeys('billing.checkout.lineItems.title__subscriptionBegins') } @@ -439,7 +443,7 @@ export const CheckoutComplete = () => { 0 || freeTrialEndsAt !== null + (totals.totalDueNow && totals.totalDueNow.amount > 0) || freeTrialEndsAt !== null ? paymentMethod ? paymentMethod.paymentType !== 'card' ? paymentMethod.paymentType diff --git a/packages/ui/src/components/Checkout/CheckoutForm.tsx b/packages/ui/src/components/Checkout/CheckoutForm.tsx index b0c75cae562..9d2b33ad4dd 100644 --- a/packages/ui/src/components/Checkout/CheckoutForm.tsx +++ b/packages/ui/src/components/Checkout/CheckoutForm.tsx @@ -46,7 +46,9 @@ export const CheckoutForm = withCardStateProvider(() => { const showPastDue = !!totals.pastDue?.amount && totals.pastDue.amount > 0; const showProratedDiscount = !!totals.discounts?.proration?.amount && totals.discounts.proration.amount.amount > 0; const showRenewalTotals = - !!totals.totalsDuePerPeriod && totals.totalsDuePerPeriod.grandTotal.amount !== totals.totalDueNow.amount; + !!totals.totalsDuePerPeriod && + totals.totalDueNow && + totals.totalsDuePerPeriod.grandTotal.amount !== totals.totalDueNow.amount; const showDowngradeInfo = !isImmediatePlanChange; const fee = @@ -177,19 +179,23 @@ export const CheckoutForm = withCardStateProvider(() => { text={`${totals.totalDueAfterFreeTrial.currencySymbol}${totals.totalDueAfterFreeTrial.amountFormatted}`} /> - ) : showRenewalTotals ? null : ( + ) : showRenewalTotals ? null : totals.totalDuePerPeriod ? ( - )} + ) : null} - - - - + {totals.totalDueNow ? ( + + + + + ) : null} {showRenewalTotals && ( <> @@ -436,7 +442,7 @@ const useSubmitLabel = () => { return localizationKeys('billing.startFreeTrial'); } - if (totals.totalDueNow.amount > 0) { + if (totals.totalDueNow && totals.totalDueNow.amount > 0) { return localizationKeys('billing.pay', { amount: `${totals.totalDueNow.currencySymbol}${totals.totalDueNow.amountFormatted}`, });