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] 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 && (