From 136e9f33e1a8b31ccbc7bda605c77d6ba9300591 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Thu, 5 Feb 2026 12:46:58 -0600 Subject: [PATCH 1/5] feat(clerk-js,localizations,shared,ui): Add support for account credits in checkout --- .changeset/shy-loops-type.md | 8 + .../scenarios/checkout-account-credit.ts | 313 ++++++++++++++++++ packages/clerk-js/sandbox/scenarios/index.ts | 1 + packages/clerk-js/src/utils/billing.ts | 4 +- packages/localizations/src/en-US.ts | 1 + packages/shared/src/types/billing.ts | 1 + packages/shared/src/types/json.ts | 1 + packages/shared/src/types/localization.ts | 1 + .../src/components/Checkout/CheckoutForm.tsx | 13 +- 9 files changed, 340 insertions(+), 3 deletions(-) create mode 100644 .changeset/shy-loops-type.md create mode 100644 packages/clerk-js/sandbox/scenarios/checkout-account-credit.ts diff --git a/.changeset/shy-loops-type.md b/.changeset/shy-loops-type.md new file mode 100644 index 00000000000..60027067651 --- /dev/null +++ b/.changeset/shy-loops-type.md @@ -0,0 +1,8 @@ +--- +'@clerk/localizations': minor +'@clerk/clerk-js': minor +'@clerk/shared': minor +'@clerk/ui': minor +--- + +Add support for account credits in checkout. diff --git a/packages/clerk-js/sandbox/scenarios/checkout-account-credit.ts b/packages/clerk-js/sandbox/scenarios/checkout-account-credit.ts new file mode 100644 index 00000000000..c10155d28c2 --- /dev/null +++ b/packages/clerk-js/sandbox/scenarios/checkout-account-credit.ts @@ -0,0 +1,313 @@ +import { + clerkHandlers, + http, + HttpResponse, + EnvironmentService, + SessionService, + setClerkState, + type MockScenario, + UserService, +} from '@clerk/msw'; + +export function CheckoutAccountCredit(): 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: { + account_credit: 100, + }, + }, + }); + }); + + const paymentMethodsHandler = http.get('https://*.clerk.accounts.dev/v1/me/billing/payment_methods', () => { + return HttpResponse.json({ + response: { + data: { + account_credit: 100, + }, + }, + }); + }); + + 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: 1, + amount_formatted: '25.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: 1, + amount_formatted: '10.00', + currency: 'string', + currency_symbol: '$', + }, + past_due: null, + credit: { + amount: 1, + amount_formatted: '5.00', + currency: 'string', + currency_symbol: '$', + }, + account_credit: { + amount: 1, + amount_formatted: '10.00', + currency: 'string', + 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 account credit', + handlers: [checkoutAccountCreditHandler, subscriptionHandler, paymentMethodsHandler, ...clerkHandlers], + initialState: { session, user }, + name: 'checkout-account-credit', + }; +} diff --git a/packages/clerk-js/sandbox/scenarios/index.ts b/packages/clerk-js/sandbox/scenarios/index.ts index 988c7ecf0f9..73ddfca0ce6 100644 --- a/packages/clerk-js/sandbox/scenarios/index.ts +++ b/packages/clerk-js/sandbox/scenarios/index.ts @@ -1 +1,2 @@ export { UserButtonSignedIn } from './user-button-signed-in'; +export { CheckoutAccountCredit } from './checkout-account-credit'; diff --git a/packages/clerk-js/src/utils/billing.ts b/packages/clerk-js/src/utils/billing.ts index a72868a859d..303db1b91cd 100644 --- a/packages/clerk-js/src/utils/billing.ts +++ b/packages/clerk-js/src/utils/billing.ts @@ -31,7 +31,9 @@ export const billingTotalsFromJSON = { return null; } - const showCredits = !!totals.credit?.amount && totals.credit.amount > 0; + const showProratedCredit = !!totals.credit?.amount && totals.credit.amount > 0; + const showAccountCredits = !!totals.accountCredit?.amount && totals.accountCredit.amount > 0; const showPastDue = !!totals.pastDue?.amount && totals.pastDue.amount > 0; const showDowngradeInfo = !isImmediatePlanChange; @@ -80,12 +81,20 @@ export const CheckoutForm = withCardStateProvider(() => { - {showCredits && ( + {showProratedCredit && ( )} + {showAccountCredits && ( + + + + + )} {showPastDue && ( From 40ff41c98e143ebb014a67efbab3ed2a86aebb56 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:20:54 -0600 Subject: [PATCH 2/5] fix(clerk-js,localizations,shared,ui): Use new credits field --- .../scenarios/checkout-account-credit.ts | 45 ++++++++++++++----- packages/clerk-js/src/utils/billing.ts | 26 ++++++++++- packages/localizations/src/en-US.ts | 2 +- packages/shared/src/types/billing.ts | 20 ++++++++- packages/shared/src/types/json.ts | 29 ++++++++++++ packages/shared/src/types/localization.ts | 2 +- .../src/components/Checkout/CheckoutForm.tsx | 9 ++-- 7 files changed, 113 insertions(+), 20 deletions(-) diff --git a/packages/clerk-js/sandbox/scenarios/checkout-account-credit.ts b/packages/clerk-js/sandbox/scenarios/checkout-account-credit.ts index c10155d28c2..816efdb6afe 100644 --- a/packages/clerk-js/sandbox/scenarios/checkout-account-credit.ts +++ b/packages/clerk-js/sandbox/scenarios/checkout-account-credit.ts @@ -22,9 +22,7 @@ export function CheckoutAccountCredit(): MockScenario { const subscriptionHandler = http.get('https://*.clerk.accounts.dev/v1/me/billing/subscription', () => { return HttpResponse.json({ response: { - data: { - account_credit: 100, - }, + data: {}, }, }); }); @@ -32,9 +30,7 @@ export function CheckoutAccountCredit(): MockScenario { const paymentMethodsHandler = http.get('https://*.clerk.accounts.dev/v1/me/billing/payment_methods', () => { return HttpResponse.json({ response: { - data: { - account_credit: 100, - }, + data: {}, }, }); }); @@ -161,11 +157,38 @@ export function CheckoutAccountCredit(): MockScenario { currency: 'string', currency_symbol: '$', }, - account_credit: { - amount: 1, - amount_formatted: '10.00', - currency: 'string', - currency_symbol: '$', + credits: { + proration: { + amount: { + amount: 1, + amount_formatted: '5.00', + currency: 'string', + currency_symbol: '$', + }, + cycle_days_remaining: 1, + cycle_days_total: 1, + cycle_remaining_percent: 1, + }, + payer_credit: { + remaining_balance: { + amount: 1, + amount_formatted: '100.00', + currency: 'string', + currency_symbol: '$', + }, + applied_amount: { + amount: 1, + amount_formatted: '10.00', + currency: 'string', + currency_symbol: '$', + }, + }, + total: { + amount: 1, + amount_formatted: '15.00', + currency: 'string', + currency_symbol: '$', + }, }, }, subscription_item: { diff --git a/packages/clerk-js/src/utils/billing.ts b/packages/clerk-js/src/utils/billing.ts index 303db1b91cd..3f623bcffac 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, + payerCredit: data.payer_credit + ? { + remainingBalance: billingMoneyAmountFromJSON(data.payer_credit.remaining_balance), + appliedAmount: billingMoneyAmountFromJSON(data.payer_credit.applied_amount), + } + : null, + total: billingMoneyAmountFromJSON(data.total), + }; +}; + export const billingTotalsFromJSON = ( data: T, ): T extends { total_due_now: BillingMoneyAmountJSON } ? BillingCheckoutTotals : BillingStatementTotals => { @@ -31,8 +53,8 @@ export const billingTotalsFromJSON = { return null; } - const showProratedCredit = !!totals.credit?.amount && totals.credit.amount > 0; - const showAccountCredits = !!totals.accountCredit?.amount && totals.accountCredit.amount > 0; + const showProratedCredit = !!totals.credits?.proration?.amount && totals.credits.proration.amount.amount > 0; + const showAccountCredits = + !!totals.credits?.payerCredit?.appliedAmount && totals.credits.payerCredit.appliedAmount.amount > 0; const showPastDue = !!totals.pastDue?.amount && totals.pastDue.amount > 0; const showDowngradeInfo = !isImmediatePlanChange; @@ -89,9 +90,9 @@ export const CheckoutForm = withCardStateProvider(() => { )} {showAccountCredits && ( - + )} From 4f968e2c6b83deaaf1ac52cd1c365b6e1921050c Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:08:20 -0600 Subject: [PATCH 3/5] fix(clerk-js,shared,ui): Rename payerCredit to payer --- .../clerk-js/sandbox/scenarios/checkout-account-credit.ts | 2 +- packages/clerk-js/src/utils/billing.ts | 6 +++--- packages/shared/src/types/billing.ts | 2 +- packages/shared/src/types/json.ts | 2 +- packages/ui/src/components/Checkout/CheckoutForm.tsx | 5 ++--- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/clerk-js/sandbox/scenarios/checkout-account-credit.ts b/packages/clerk-js/sandbox/scenarios/checkout-account-credit.ts index 816efdb6afe..bd2c70fa797 100644 --- a/packages/clerk-js/sandbox/scenarios/checkout-account-credit.ts +++ b/packages/clerk-js/sandbox/scenarios/checkout-account-credit.ts @@ -169,7 +169,7 @@ export function CheckoutAccountCredit(): MockScenario { cycle_days_total: 1, cycle_remaining_percent: 1, }, - payer_credit: { + payer: { remaining_balance: { amount: 1, amount_formatted: '100.00', diff --git a/packages/clerk-js/src/utils/billing.ts b/packages/clerk-js/src/utils/billing.ts index 3f623bcffac..8c42f1a86fa 100644 --- a/packages/clerk-js/src/utils/billing.ts +++ b/packages/clerk-js/src/utils/billing.ts @@ -28,10 +28,10 @@ const billingCreditsFromJSON = (data: BillingCreditsJSON): BillingCredits => { cycleRemainingPercent: data.proration.cycle_remaining_percent, } : null, - payerCredit: data.payer_credit + payer: data.payer ? { - remainingBalance: billingMoneyAmountFromJSON(data.payer_credit.remaining_balance), - appliedAmount: billingMoneyAmountFromJSON(data.payer_credit.applied_amount), + remainingBalance: billingMoneyAmountFromJSON(data.payer.remaining_balance), + appliedAmount: billingMoneyAmountFromJSON(data.payer.applied_amount), } : null, total: billingMoneyAmountFromJSON(data.total), diff --git a/packages/shared/src/types/billing.ts b/packages/shared/src/types/billing.ts index 61ad24acbaf..05e5eea58dd 100644 --- a/packages/shared/src/types/billing.ts +++ b/packages/shared/src/types/billing.ts @@ -704,7 +704,7 @@ export interface BillingPayerCredit { export interface BillingCredits { proration: BillingProrationCreditDetail | null; - payerCredit: BillingPayerCredit | null; + payer: BillingPayerCredit | null; total: BillingMoneyAmount; } diff --git a/packages/shared/src/types/json.ts b/packages/shared/src/types/json.ts index cf747dd6659..9b3ca7d6ee3 100644 --- a/packages/shared/src/types/json.ts +++ b/packages/shared/src/types/json.ts @@ -769,7 +769,7 @@ export interface BillingPayerCreditJSON { */ export interface BillingCreditsJSON { proration: BillingProrationCreditDetailJSON | null; - payer_credit: BillingPayerCreditJSON | null; + payer: BillingPayerCreditJSON | null; total: BillingMoneyAmountJSON; } diff --git a/packages/ui/src/components/Checkout/CheckoutForm.tsx b/packages/ui/src/components/Checkout/CheckoutForm.tsx index cba02b50347..275577fb92b 100644 --- a/packages/ui/src/components/Checkout/CheckoutForm.tsx +++ b/packages/ui/src/components/Checkout/CheckoutForm.tsx @@ -36,8 +36,7 @@ export const CheckoutForm = withCardStateProvider(() => { } const showProratedCredit = !!totals.credits?.proration?.amount && totals.credits.proration.amount.amount > 0; - const showAccountCredits = - !!totals.credits?.payerCredit?.appliedAmount && totals.credits.payerCredit.appliedAmount.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; @@ -92,7 +91,7 @@ export const CheckoutForm = withCardStateProvider(() => { )} From 644618eeca3c11facb9a2c928fc6cc250f48a0e3 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:22:46 -0600 Subject: [PATCH 4/5] fix(clerk-js): Render proration credit from credits --- packages/ui/src/components/Checkout/CheckoutForm.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/Checkout/CheckoutForm.tsx b/packages/ui/src/components/Checkout/CheckoutForm.tsx index 275577fb92b..7fc80d0979f 100644 --- a/packages/ui/src/components/Checkout/CheckoutForm.tsx +++ b/packages/ui/src/components/Checkout/CheckoutForm.tsx @@ -84,7 +84,9 @@ export const CheckoutForm = withCardStateProvider(() => { {showProratedCredit && ( - + )} {showAccountCredits && ( From f258cdbf8109f9761bdf1a3a92420f400139fb80 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:43:09 -0600 Subject: [PATCH 5/5] test(react): Fix test data --- packages/shared/src/react/__tests__/payment-element.test.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/shared/src/react/__tests__/payment-element.test.tsx b/packages/shared/src/react/__tests__/payment-element.test.tsx index 8499e757f00..3efd256e3ec 100644 --- a/packages/shared/src/react/__tests__/payment-element.test.tsx +++ b/packages/shared/src/react/__tests__/payment-element.test.tsx @@ -129,6 +129,11 @@ describe('PaymentElement Localization', () => { 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,