Skip to content

Commit f2c6a7a

Browse files
committed
feat(js): support new checkout totals shape
Add `discounts` (prorated seat discount + total), `totals_due_per_period` (full recurring renewal breakdown), `total_due_per_period`, and top-level `base_fee` to checkout totals. The top-level totals now reflect only what is actively being purchased, while `totals_due_per_period` conveys the recurring charge. Parse the new fields and surface the prorated discount and renewal totals in the checkout form
1 parent ef43ff4 commit f2c6a7a

9 files changed

Lines changed: 498 additions & 6 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
'@clerk/shared': patch
4+
'@clerk/ui': patch
5+
'@clerk/localizations': patch
6+
'@clerk/msw': patch
7+
---
8+
9+
Update checkout totals to support the new `totals` shape from the API:
10+
11+
- Add `discounts` to `BillingCheckoutTotals` / `BillingCheckoutTotalsJSON`. It exposes a `proration` (a `BillingProrationDiscountDetail` with `amount`, `cycleDaysPassed`, `cycleDaysTotal`, `cyclePassedPercent`) describing the prorated discount applied when adding seats mid-billing-period — i.e. the part of the cycle that has already passed and is not charged — plus a `total` of all discounts.
12+
- Add `totalsDuePerPeriod` / `totals_due_per_period` (`BillingPerPeriodTotals`) with `subtotal`, `baseFee`, `taxTotal`, `grandTotal`, and `perUnitTotals`: the full renewal charge breakdown covering all seats and the base plan fee, as opposed to the top-level totals which now only cover what is actively being purchased in the current checkout.
13+
- Add `totalDuePerPeriod` / `total_due_per_period` (money amount) for backwards compatibility.
14+
- Add `baseFee` / `base_fee` to the top-level totals.
15+
- Parse all new fields in `billingTotalsFromJSON`.
16+
- Render the prorated discount line item in `CheckoutForm` when present.
17+
- Render a renewal subtotal and renewal total section in `CheckoutForm` when `totalsDuePerPeriod` is present.

packages/clerk-js/src/utils/billing.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import type {
2+
BillingCheckoutDiscounts,
3+
BillingCheckoutDiscountsJSON,
24
BillingCheckoutTotals,
35
BillingCheckoutTotalsJSON,
46
BillingCredits,
57
BillingCreditsJSON,
68
BillingMoneyAmount,
79
BillingMoneyAmountJSON,
10+
BillingPerPeriodTotals,
11+
BillingPerPeriodTotalsJSON,
812
BillingPerUnitTotal,
913
BillingPerUnitTotalJSON,
1014
BillingStatementTotals,
@@ -52,6 +56,30 @@ export const billingCreditsFromJSON = (data: BillingCreditsJSON): BillingCredits
5256
};
5357
};
5458

59+
const billingCheckoutDiscountsFromJSON = (data: BillingCheckoutDiscountsJSON): BillingCheckoutDiscounts => {
60+
return {
61+
proration: data.proration
62+
? {
63+
amount: billingMoneyAmountFromJSON(data.proration.amount),
64+
cycleDaysPassed: data.proration.cycle_days_passed,
65+
cycleDaysTotal: data.proration.cycle_days_total,
66+
cyclePassedPercent: data.proration.cycle_passed_percent,
67+
}
68+
: null,
69+
total: billingMoneyAmountFromJSON(data.total),
70+
};
71+
};
72+
73+
const billingPerPeriodTotalsFromJSON = (data: BillingPerPeriodTotalsJSON): BillingPerPeriodTotals => {
74+
return {
75+
subtotal: billingMoneyAmountFromJSON(data.subtotal),
76+
baseFee: billingMoneyAmountFromJSON(data.base_fee),
77+
taxTotal: billingMoneyAmountFromJSON(data.tax_total),
78+
grandTotal: billingMoneyAmountFromJSON(data.grand_total),
79+
perUnitTotals: data.per_unit_totals ? billingPerUnitTotalsFromJSON(data.per_unit_totals) : undefined,
80+
};
81+
};
82+
5583
export const billingTotalsFromJSON = <T extends BillingStatementTotalsJSON | BillingCheckoutTotalsJSON>(
5684
data: T,
5785
): T extends { total_due_now: BillingMoneyAmountJSON } ? BillingCheckoutTotals : BillingStatementTotals => {
@@ -61,6 +89,9 @@ export const billingTotalsFromJSON = <T extends BillingStatementTotalsJSON | Bil
6189
taxTotal: billingMoneyAmountFromJSON(data.tax_total),
6290
};
6391

92+
if ('base_fee' in data && data.base_fee) {
93+
totals.baseFee = billingMoneyAmountFromJSON(data.base_fee);
94+
}
6495
if ('past_due' in data) {
6596
totals.pastDue = data.past_due ? billingMoneyAmountFromJSON(data.past_due) : null;
6697
}
@@ -84,5 +115,17 @@ export const billingTotalsFromJSON = <T extends BillingStatementTotalsJSON | Bil
84115
: null;
85116
}
86117

118+
if ('discounts' in data) {
119+
totals.discounts = data.discounts ? billingCheckoutDiscountsFromJSON(data.discounts) : null;
120+
}
121+
122+
if ('total_due_per_period' in data && data.total_due_per_period) {
123+
totals.totalDuePerPeriod = billingMoneyAmountFromJSON(data.total_due_per_period);
124+
}
125+
126+
if ('totals_due_per_period' in data && data.totals_due_per_period) {
127+
totals.totalsDuePerPeriod = billingPerPeriodTotalsFromJSON(data.totals_due_per_period);
128+
}
129+
87130
return totals as T extends { total_due_now: BillingMoneyAmountJSON } ? BillingCheckoutTotals : BillingStatementTotals;
88131
};

packages/localizations/src/en-US.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export const enUS: LocalizationResource = {
119119
accountCredit: 'Account credit',
120120
creditRemainder: 'Credit for the remainder of your current subscription.',
121121
payerCreditRemainder: 'Credit from account balance.',
122+
proratedDiscount: 'Prorated discount',
122123
defaultFreePlanActive: "You're currently on the Free plan",
123124
free: 'Free',
124125
getStarted: 'Get started',
@@ -187,6 +188,8 @@ export const enUS: LocalizationResource = {
187188
trialStartedOn: 'Trial started on',
188189
},
189190
subtotal: 'Subtotal',
191+
subtotalRenewal: 'Subtotal per period',
192+
totalDuePerPeriod: 'Total per period',
190193
switchPlan: 'Switch to this plan',
191194
switchToAnnual: 'Switch to annual',
192195
switchToAnnualWithAnnualPrice: 'Switch to annual {{currency}}{{price}} / year',

packages/msw/BillingService.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import type {
33
BillingCheckoutTotalsJSON,
44
BillingInitializedPaymentMethodJSON,
55
BillingMoneyAmountJSON,
6+
BillingPayerJSON,
67
BillingPaymentJSON,
78
BillingPaymentMethodJSON,
8-
BillingPayerJSON,
99
BillingPlanJSON,
1010
BillingStatementJSON,
1111
BillingSubscriptionItemJSON,
@@ -169,9 +169,9 @@ export class BillingService {
169169
planPeriod: BillingSubscriptionPlanPeriod,
170170
): BillingMoneyAmountJSON {
171171
if (planPeriod === 'annual') {
172-
return plan.annual_fee ?? plan.fee;
172+
return plan.annual_fee ?? plan.fee ?? this.createMoney(0);
173173
}
174-
return plan.fee;
174+
return plan.fee ?? this.createMoney(0);
175175
}
176176

177177
static createSubscriptionItem(
@@ -338,12 +338,22 @@ export class BillingService {
338338
const totals: BillingCheckoutTotalsWithOptionalAccountCredit = {
339339
grand_total: amount,
340340
subtotal: amount,
341+
base_fee: amount,
341342
tax_total: tax,
342343
total_due_now: amount,
343344
credit: null,
345+
credits: null,
344346
past_due: null,
345347
total_due_after_free_trial: amount,
346348
account_credit: null,
349+
discounts: null,
350+
total_due_per_period: amount,
351+
totals_due_per_period: {
352+
subtotal: amount,
353+
base_fee: amount,
354+
tax_total: tax,
355+
grand_total: amount,
356+
},
347357
};
348358
return totals;
349359
}

packages/shared/src/types/billing.ts

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -814,16 +814,72 @@ export interface BillingCredits {
814814
total: BillingMoneyAmount;
815815
}
816816

817+
/**
818+
* Details about a prorated discount applied when adding a seat mid-cycle. The discount covers the part of the
819+
* billing period that has already passed, so the payer is only charged for the time remaining in the cycle.
820+
*
821+
* @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.
822+
*/
823+
export interface BillingProrationDiscountDetail {
824+
amount: BillingMoneyAmount;
825+
cycleDaysPassed: number;
826+
cycleDaysTotal: number;
827+
cyclePassedPercent: number;
828+
}
829+
830+
/**
831+
* Discounts applied to the checkout, such as prorated discounts for mid-cycle seat additions.
832+
*
833+
* @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.
834+
*/
835+
export interface BillingCheckoutDiscounts {
836+
/**
837+
* The prorated discount for the part of the billing period that has already passed when adding a seat mid-cycle.
838+
* Unlike the proration credit (which refunds the unused remainder of a plan you already paid for), this discount
839+
* means you are not charged for the portion of the new seat's cycle that has already elapsed.
840+
*/
841+
proration: BillingProrationDiscountDetail | null;
842+
/**
843+
* The total of all discounts applied to the checkout.
844+
*/
845+
total: BillingMoneyAmount;
846+
}
847+
848+
/**
849+
* Per-period renewal totals, describing what the subscription renewal charge will look like after the current checkout.
850+
* Unlike the top-level checkout totals (which only reflect the items actively being purchased),
851+
* this object contains the full renewal breakdown including all seats and the base plan fee.
852+
*
853+
* @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.
854+
*/
855+
export interface BillingPerPeriodTotals {
856+
subtotal: BillingMoneyAmount;
857+
baseFee: BillingMoneyAmount;
858+
taxTotal: BillingMoneyAmount;
859+
grandTotal: BillingMoneyAmount;
860+
/**
861+
* Per-unit cost breakdown for the renewal period, covering all units purchased to date
862+
* (not just the ones being added in this checkout).
863+
*/
864+
perUnitTotals?: BillingPerUnitTotal[];
865+
}
866+
817867
/**
818868
* The `BillingCheckoutTotals` type represents the total costs, taxes, and other pricing details for a checkout session.
819869
*
820870
* @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.
821871
*/
822872
export interface BillingCheckoutTotals {
823873
/**
824-
* The price of the items or Plan before taxes, credits, or discounts are applied.
874+
* The price of items actively being purchased in this checkout, before taxes and discounts.
875+
* When only adding seats mid-cycle, this reflects just the new seats and excludes the base plan fee and
876+
* seats that were already paid for.
825877
*/
826878
subtotal: BillingMoneyAmount;
879+
/**
880+
* The base plan fee portion of the totals, before per-unit charges and adjustments.
881+
*/
882+
baseFee?: BillingMoneyAmount;
827883
/**
828884
* The total amount for the checkout, including taxes and after credits/discounts are applied. This is the final amount due.
829885
*/
@@ -833,7 +889,8 @@ export interface BillingCheckoutTotals {
833889
*/
834890
taxTotal: BillingMoneyAmount;
835891
/**
836-
* Per-unit cost breakdown for this checkout (for example, seats).
892+
* Per-unit cost breakdown for items actively being purchased in this checkout (for example, seats being added).
893+
* When only adding seats mid-cycle, this only covers the seats being added, not seats already paid for.
837894
*/
838895
perUnitTotals?: BillingPerUnitTotal[];
839896
/**
@@ -853,6 +910,21 @@ export interface BillingCheckoutTotals {
853910
* The amount that becomes due after a free trial ends.
854911
*/
855912
totalDueAfterFreeTrial: BillingMoneyAmount | null;
913+
/**
914+
* Discounts applied to this checkout such as mid-cycle prorated seat discounts.
915+
*/
916+
discounts?: BillingCheckoutDiscounts | null;
917+
/**
918+
* The expected recurring payment for each future billing period.
919+
* Kept for backwards compatibility; prefer `totalsDuePerPeriod` for the full breakdown.
920+
*/
921+
totalDuePerPeriod?: BillingMoneyAmount;
922+
/**
923+
* Full renewal period totals after this checkout completes.
924+
* Contains the complete breakdown of what the next recurring charge will look like,
925+
* including all seats and the base plan fee.
926+
*/
927+
totalsDuePerPeriod?: BillingPerPeriodTotals;
856928
}
857929

858930
/**

packages/shared/src/types/json.ts

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -849,23 +849,101 @@ export interface BillingCreditsJSON {
849849
total: BillingMoneyAmountJSON;
850850
}
851851

852+
/**
853+
* Details about a prorated discount applied when adding a seat mid-cycle. The discount covers the part of the
854+
* billing period that has already passed, so the payer is only charged for the time remaining in the cycle.
855+
*/
856+
export interface BillingProrationDiscountDetailJSON {
857+
amount: BillingMoneyAmountJSON;
858+
cycle_days_passed: number;
859+
cycle_days_total: number;
860+
cycle_passed_percent: number;
861+
}
862+
863+
/**
864+
* Discounts applied to the checkout, such as prorated discounts for mid-cycle seat additions.
865+
*
866+
* @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.
867+
*/
868+
export interface BillingCheckoutDiscountsJSON {
869+
/**
870+
* The prorated discount for the part of the billing period that has already passed when adding a seat mid-cycle.
871+
* Unlike the proration credit (which refunds the unused remainder of a plan you already paid for), this discount
872+
* means you are not charged for the portion of the new seat's cycle that has already elapsed.
873+
*/
874+
proration: BillingProrationDiscountDetailJSON | null;
875+
/**
876+
* The total of all discounts applied to the checkout.
877+
*/
878+
total: BillingMoneyAmountJSON;
879+
}
880+
881+
/**
882+
* Per-period renewal totals, describing what the subscription renewal charge will look like after the current checkout.
883+
* Unlike the top-level checkout totals (which only reflect the items actively being purchased),
884+
* this object contains the full renewal breakdown including all seats and the base plan fee.
885+
*
886+
* @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.
887+
*/
888+
export interface BillingPerPeriodTotalsJSON {
889+
subtotal: BillingMoneyAmountJSON;
890+
base_fee: BillingMoneyAmountJSON;
891+
tax_total: BillingMoneyAmountJSON;
892+
grand_total: BillingMoneyAmountJSON;
893+
/**
894+
* Per-unit cost breakdown for the renewal period, covering all units purchased to date
895+
* (not just the ones being added in this checkout).
896+
*/
897+
per_unit_totals?: BillingPerUnitTotalJSON[];
898+
}
899+
852900
/**
853901
* @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.
854902
*/
855903
export interface BillingCheckoutTotalsJSON {
856904
grand_total: BillingMoneyAmountJSON;
905+
/**
906+
* The price of items actively being purchased in this checkout, before taxes and discounts.
907+
* When only adding seats mid-cycle, this reflects just the new seats and excludes the base plan fee and
908+
* seats that were already paid for.
909+
*/
857910
subtotal: BillingMoneyAmountJSON;
911+
/**
912+
* The base plan fee portion of the totals, before per-unit charges and adjustments.
913+
*/
914+
base_fee: BillingMoneyAmountJSON;
858915
tax_total: BillingMoneyAmountJSON;
859916
/**
860-
* Per-unit cost breakdown for this checkout (for example, seats).
917+
* Per-unit cost breakdown for items actively being purchased in this checkout (for example, seats being added).
918+
* When only adding seats mid-cycle, this only covers the seats being added, not seats already paid for.
919+
* Omitted when the checkout is not seat-based.
861920
*/
862921
per_unit_totals?: BillingPerUnitTotalJSON[];
863922
total_due_now: BillingMoneyAmountJSON;
923+
/**
924+
* Legacy credit field. Kept for backwards compatibility; prefer the unified `credits` breakdown.
925+
*/
864926
credit: BillingMoneyAmountJSON | null;
865927
credits: BillingCreditsJSON | null;
866928
account_credit: BillingMoneyAmountJSON | null;
867929
past_due: BillingMoneyAmountJSON | null;
868930
total_due_after_free_trial: BillingMoneyAmountJSON | null;
931+
/**
932+
* Discounts applied to this checkout such as mid-cycle prorated seat discounts.
933+
* The key is always present; the value is `null` when no discounts apply.
934+
*/
935+
discounts: BillingCheckoutDiscountsJSON | null;
936+
/**
937+
* The expected recurring payment for each future billing period.
938+
* Kept for backwards compatibility. Prefer `totals_due_per_period` for the full breakdown.
939+
*/
940+
total_due_per_period: BillingMoneyAmountJSON;
941+
/**
942+
* Full renewal period totals after this checkout completes.
943+
* Contains the complete breakdown of what the next recurring charge will look like,
944+
* including all seats and the base plan fee.
945+
*/
946+
totals_due_per_period: BillingPerPeriodTotalsJSON;
869947
}
870948

871949
/**

packages/shared/src/types/localization.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,13 +214,16 @@ export type __internal_LocalizationResource = {
214214
viewPayment: LocalizationValue;
215215
availableFeatures: LocalizationValue;
216216
subtotal: LocalizationValue;
217+
subtotalRenewal: LocalizationValue;
217218
credit: LocalizationValue;
218219
prorationCredit: LocalizationValue;
219220
accountCredit: LocalizationValue;
220221
creditRemainder: LocalizationValue;
221222
payerCreditRemainder: LocalizationValue;
223+
proratedDiscount: LocalizationValue;
222224
totalDue: LocalizationValue;
223225
totalDueToday: LocalizationValue;
226+
totalDuePerPeriod: LocalizationValue;
224227
pastDue: LocalizationValue;
225228
pay: LocalizationValue<'amount'>;
226229
cancelSubscriptionTitle: LocalizationValue<'plan'>;

0 commit comments

Comments
 (0)