diff --git a/.changeset/shy-loops-type.md b/.changeset/shy-loops-type.md
new file mode 100644
index 00000000000..b55584f9cd2
--- /dev/null
+++ b/.changeset/shy-loops-type.md
@@ -0,0 +1,7 @@
+---
+'@clerk/localizations': minor
+'@clerk/clerk-js': minor
+'@clerk/shared': minor
+---
+
+Add support for account credits in checkout.
diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json
index c96f7bd1fb8..9ed455d99ca 100644
--- a/packages/clerk-js/bundlewatch.config.json
+++ b/packages/clerk-js/bundlewatch.config.json
@@ -24,7 +24,7 @@
{ "path": "./dist/keylessPrompt*.js", "maxSize": "6.5KB" },
{ "path": "./dist/enableOrganizationsPrompt*.js", "maxSize": "6.5KB" },
{ "path": "./dist/pricingTable*.js", "maxSize": "4.02KB" },
- { "path": "./dist/checkout*.js", "maxSize": "8.82KB" },
+ { "path": "./dist/checkout*.js", "maxSize": "10KB" },
{ "path": "./dist/up-billing-page*.js", "maxSize": "3.0KB" },
{ "path": "./dist/op-billing-page*.js", "maxSize": "3.0KB" },
{ "path": "./dist/up-plans-page*.js", "maxSize": "1.0KB" },
diff --git a/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx b/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx
index 7d7c7a4f2b1..cad7532cc74 100644
--- a/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx
+++ b/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx
@@ -35,7 +35,8 @@ export const CheckoutForm = withCardStateProvider(() => {
return null;
}
- const showCredits = !!totals.credit?.amount && totals.credit.amount > 0;
+ const showProratedCredit = !!totals.credits?.proration?.amount && totals.credits.proration.amount.amount > 0;
+ const showAccountCredits = !!totals.credits?.payer?.appliedAmount && totals.credits.payer.appliedAmount.amount > 0;
const showPastDue = !!totals.pastDue?.amount && totals.pastDue.amount > 0;
const showDowngradeInfo = !isImmediatePlanChange;
@@ -80,10 +81,20 @@ export const CheckoutForm = withCardStateProvider(() => {
- {showCredits && (
+ {showProratedCredit && (
-
+
+
+ )}
+ {showAccountCredits && (
+
+
+
)}
{showPastDue && (
diff --git a/packages/clerk-js/src/ui/components/Checkout/__tests__/Checkout.test.tsx b/packages/clerk-js/src/ui/components/Checkout/__tests__/Checkout.test.tsx
index 74402763745..8414f0c9ea8 100644
--- a/packages/clerk-js/src/ui/components/Checkout/__tests__/Checkout.test.tsx
+++ b/packages/clerk-js/src/ui/components/Checkout/__tests__/Checkout.test.tsx
@@ -309,6 +309,113 @@ describe('Checkout', () => {
});
});
+ it('renders credit details', async () => {
+ const { wrapper, fixtures } = await createFixtures(f => {
+ f.withUser({ email_addresses: ['test@clerk.com'] });
+ f.withBilling();
+ });
+
+ fixtures.clerk.user?.getPaymentMethods.mockResolvedValue({
+ data: [],
+ total_count: 0,
+ });
+
+ fixtures.clerk.billing.startCheckout.mockResolvedValue({
+ id: 'chk_credits_1',
+ status: 'needs_confirmation',
+ externalClientSecret: 'cs_test_credits_1',
+ externalGatewayId: 'gw_test',
+ totals: {
+ subtotal: { amount: 2500, amountFormatted: '25.00', currency: 'USD', currencySymbol: '$' },
+ grandTotal: { amount: 1000, amountFormatted: '10.00', currency: 'USD', currencySymbol: '$' },
+ taxTotal: { amount: 0, amountFormatted: '0.00', currency: 'USD', currencySymbol: '$' },
+ credit: { amount: 0, amountFormatted: '0.00', currency: 'USD', currencySymbol: '$' },
+ credits: {
+ proration: {
+ amount: { amount: 500, amountFormatted: '5.00', currency: 'USD', currencySymbol: '$' },
+ cycleDaysRemaining: 15,
+ cycleDaysTotal: 30,
+ cycleRemainingPercent: 50,
+ },
+ payer: {
+ remainingBalance: { amount: 2000, amountFormatted: '20.00', currency: 'USD', currencySymbol: '$' },
+ appliedAmount: { amount: 1000, amountFormatted: '10.00', currency: 'USD', currencySymbol: '$' },
+ },
+ total: { amount: 1500, amountFormatted: '15.00', currency: 'USD', currencySymbol: '$' },
+ },
+ pastDue: { amount: 0, amountFormatted: '0.00', currency: 'USD', currencySymbol: '$' },
+ totalDueNow: { amount: 1000, amountFormatted: '10.00', currency: 'USD', currencySymbol: '$' },
+ },
+ isImmediatePlanChange: true,
+ planPeriod: 'month',
+ plan: {
+ id: 'plan_credits',
+ name: 'Pro',
+ description: 'Pro plan',
+ features: [],
+ fee: {
+ amount: 2500,
+ amountFormatted: '25.00',
+ currency: 'USD',
+ currencySymbol: '$',
+ },
+ annualFee: {
+ amount: 30000,
+ amountFormatted: '300.00',
+ currency: 'USD',
+ currencySymbol: '$',
+ },
+ annualMonthlyFee: {
+ amount: 2500,
+ amountFormatted: '25.00',
+ currency: 'USD',
+ currencySymbol: '$',
+ },
+ slug: 'pro',
+ avatarUrl: '',
+ publiclyVisible: true,
+ isDefault: true,
+ isRecurring: true,
+ hasBaseFee: false,
+ forPayerType: 'user',
+ freeTrialDays: 7,
+ freeTrialEnabled: true,
+ },
+ paymentMethod: undefined,
+ confirm: vi.fn(),
+ freeTrialEndsAt: null,
+ needsPaymentMethod: false,
+ } as any);
+
+ const { getByRole, getByText } = render(
+ {}}
+ >
+
+ ,
+ { wrapper },
+ );
+
+ await waitFor(() => {
+ expect(getByRole('heading', { name: 'Checkout' })).toBeVisible();
+ });
+
+ const prorationCreditRow = getByText('Credit for the remainder of your current subscription.').closest(
+ '.cl-lineItemsGroup',
+ );
+ const accountCreditRow = getByText('Credit from account balance.').closest('.cl-lineItemsGroup');
+
+ expect(prorationCreditRow).toBeInTheDocument();
+ expect(accountCreditRow).toBeInTheDocument();
+
+ expect(prorationCreditRow).toHaveTextContent('- $5.00');
+ expect(accountCreditRow).toHaveTextContent('- $10.00');
+ });
+
it('renders free trial details during confirmation stage', async () => {
const { wrapper, fixtures } = await createFixtures(f => {
f.withUser({ email_addresses: ['test@clerk.com'] });
diff --git a/packages/clerk-js/src/utils/billing.ts b/packages/clerk-js/src/utils/billing.ts
index a72868a859d..8c42f1a86fa 100644
--- a/packages/clerk-js/src/utils/billing.ts
+++ b/packages/clerk-js/src/utils/billing.ts
@@ -1,6 +1,8 @@
import type {
BillingCheckoutTotals,
BillingCheckoutTotalsJSON,
+ BillingCredits,
+ BillingCreditsJSON,
BillingMoneyAmount,
BillingMoneyAmountJSON,
BillingStatementTotals,
@@ -16,6 +18,26 @@ export const billingMoneyAmountFromJSON = (data: BillingMoneyAmountJSON): Billin
};
};
+const billingCreditsFromJSON = (data: BillingCreditsJSON): BillingCredits => {
+ return {
+ proration: data.proration
+ ? {
+ amount: billingMoneyAmountFromJSON(data.proration.amount),
+ cycleDaysRemaining: data.proration.cycle_days_remaining,
+ cycleDaysTotal: data.proration.cycle_days_total,
+ cycleRemainingPercent: data.proration.cycle_remaining_percent,
+ }
+ : null,
+ payer: data.payer
+ ? {
+ remainingBalance: billingMoneyAmountFromJSON(data.payer.remaining_balance),
+ appliedAmount: billingMoneyAmountFromJSON(data.payer.applied_amount),
+ }
+ : null,
+ total: billingMoneyAmountFromJSON(data.total),
+ };
+};
+
export const billingTotalsFromJSON = (
data: T,
): T extends { total_due_now: BillingMoneyAmountJSON } ? BillingCheckoutTotals : BillingStatementTotals => {
@@ -31,7 +53,9 @@ export const billingTotalsFromJSON = {
totalDueNow: { amount: 1000, amountFormatted: '$10.00', currency: 'usd', currencySymbol: '$' },
totalDueAfterFreeTrial: null,
credit: { amount: 0, amountFormatted: '$0.00', currency: 'usd', currencySymbol: '$' },
+ credits: {
+ proration: null,
+ payer: null,
+ total: { amount: 0, amountFormatted: '$0.00', currency: 'usd', currencySymbol: '$' },
+ },
pastDue: { amount: 0, amountFormatted: '$0.00', currency: 'usd', currencySymbol: '$' },
},
status: 'needs_confirmation' as const,
diff --git a/packages/shared/src/types/billing.ts b/packages/shared/src/types/billing.ts
index 26f11f7e4ea..a7ba45ae532 100644
--- a/packages/shared/src/types/billing.ts
+++ b/packages/shared/src/types/billing.ts
@@ -645,6 +645,24 @@ export interface BillingMoneyAmount {
currencySymbol: string;
}
+export interface BillingProrationCreditDetail {
+ amount: BillingMoneyAmount;
+ cycleDaysRemaining: number;
+ cycleDaysTotal: number;
+ cycleRemainingPercent: number;
+}
+
+export interface BillingPayerCredit {
+ remainingBalance: BillingMoneyAmount;
+ appliedAmount: BillingMoneyAmount;
+}
+
+export interface BillingCredits {
+ proration: BillingProrationCreditDetail | null;
+ payer: BillingPayerCredit | null;
+ total: BillingMoneyAmount;
+}
+
/**
* The `BillingCheckoutTotals` type represents the total costs, taxes, and other pricing details for a checkout session.
*
@@ -671,6 +689,7 @@ export interface BillingCheckoutTotals {
* Any credits (like account balance or promo credits) that are being applied to the checkout.
*/
credit: BillingMoneyAmount | null;
+ credits: BillingCredits | null;
/**
* Any outstanding amount from previous unpaid invoices that is being collected as part of the checkout.
*/
diff --git a/packages/shared/src/types/json.ts b/packages/shared/src/types/json.ts
index e24e426cca2..783395c5fa9 100644
--- a/packages/shared/src/types/json.ts
+++ b/packages/shared/src/types/json.ts
@@ -730,6 +730,7 @@ export interface BillingSubscriptionItemJSON extends ClerkResourceJSON {
credit?: {
amount: BillingMoneyAmountJSON;
};
+ credits?: BillingCreditsJSON;
plan: BillingPlanJSON;
plan_period: BillingSubscriptionPlanPeriod;
status: BillingSubscriptionStatus;
@@ -779,6 +780,33 @@ export interface BillingMoneyAmountJSON {
currency_symbol: string;
}
+/**
+ * Contains proration credit details including billing cycle information.
+ */
+export interface BillingProrationCreditDetailJSON {
+ amount: BillingMoneyAmountJSON;
+ cycle_days_remaining: number;
+ cycle_days_total: number;
+ cycle_remaining_percent: number;
+}
+
+/**
+ * Contains payer credit details including the available balance and the amount applied to this checkout.
+ */
+export interface BillingPayerCreditJSON {
+ remaining_balance: BillingMoneyAmountJSON;
+ applied_amount: BillingMoneyAmountJSON;
+}
+
+/**
+ * Unified credits breakdown for checkout totals. Can be used instead of `credit` field.
+ */
+export interface BillingCreditsJSON {
+ proration: BillingProrationCreditDetailJSON | null;
+ payer: BillingPayerCreditJSON | null;
+ 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.
*/
@@ -788,6 +816,8 @@ export interface BillingCheckoutTotalsJSON {
tax_total: BillingMoneyAmountJSON;
total_due_now: BillingMoneyAmountJSON;
credit: BillingMoneyAmountJSON | null;
+ credits: BillingCreditsJSON | null;
+ account_credit: BillingMoneyAmountJSON | null;
past_due: BillingMoneyAmountJSON | null;
total_due_after_free_trial: BillingMoneyAmountJSON | null;
}
diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts
index 7c3b5ae0fc0..b7eb936734a 100644
--- a/packages/shared/src/types/localization.ts
+++ b/packages/shared/src/types/localization.ts
@@ -207,6 +207,7 @@ export type __internal_LocalizationResource = {
subtotal: LocalizationValue;
credit: LocalizationValue;
creditRemainder: LocalizationValue;
+ payerCreditRemainder: LocalizationValue;
totalDue: LocalizationValue;
totalDueToday: LocalizationValue;
pastDue: LocalizationValue;