Skip to content
8 changes: 8 additions & 0 deletions .changeset/brown-guests-roll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@clerk/localizations': minor
'@clerk/clerk-js': minor
'@clerk/shared': minor
'@clerk/ui': minor
---

Add support for applying promo codes at checkout
2 changes: 1 addition & 1 deletion integration/tests/pricing-table.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ testAgainstRunningApps({})('pricing table @billing', ({ app }) => {

await expect(matchLineItem(u.po.checkout.root, 'Total Due after')).toBeHidden();
await expect(matchLineItem(u.po.checkout.root, 'Total due today', '$999.00')).toBeVisible();
expect(await countLineItems(u.po.checkout.root)).toBe(3);
expect(await countLineItems(u.po.checkout.root)).toBe(4);

await u.po.checkout.root.getByRole('button', { name: /^pay\s\$/i }).waitFor({ state: 'visible' });
await u.po.checkout.clickPayOrSubscribe();
Expand Down
4 changes: 2 additions & 2 deletions packages/clerk-js/bundlewatch.config.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"files": [
{ "path": "./dist/clerk.js", "maxSize": "549KB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "75KB" },
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "117KB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "77KB" },
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "119KB" },
{ "path": "./dist/clerk.no-rhc.js", "maxSize": "316KB" },
{ "path": "./dist/clerk.native.js", "maxSize": "76KB" },
{ "path": "./dist/vendors*.js", "maxSize": "7KB" },
Expand Down
14 changes: 14 additions & 0 deletions packages/clerk-js/src/core/modules/billing/namespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type {
GetPlansParams,
GetStatementsParams,
GetSubscriptionParams,
UpdateCheckoutParams,
} from '@clerk/shared/types';

import { convertPageToOffsetSearchParams } from '../../../utils/convertPageToOffsetSearchParams';
Expand Down Expand Up @@ -149,6 +150,19 @@ export class Billing implements BillingNamespace {
return new BillingCheckout(json);
};

updateCheckout = async (params: UpdateCheckoutParams) => {
const { id, orgId, ...rest } = params;
const json = (
await BaseResource._fetch<BillingCheckoutJSON>({
path: Billing.path(`/checkouts/${id}`, { orgId }),
method: 'PATCH',
body: rest as any,
})
)?.response as unknown as BillingCheckoutJSON;

return new BillingCheckout(json);
};

getCreditBalance = async (params: GetCreditBalanceParams): Promise<BillingCreditBalanceResource> => {
return await BaseResource._fetch({
path: Billing.path('/credits', { orgId: params.orgId }),
Expand Down
42 changes: 37 additions & 5 deletions packages/clerk-js/src/core/resources/BillingCheckout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
CheckoutSignalValue,
ConfirmCheckoutParams,
CreateCheckoutParams,
UpdateCheckoutParams,
} from '@clerk/shared/types';
import { computed, endBatch, signal, startBatch } from 'alien-signals';

Expand Down Expand Up @@ -117,7 +118,7 @@ export const createSignals = () => {
return { resourceSignal, errorSignal, fetchSignal, computedSignal };
};

type CheckoutTask = 'start' | 'confirm' | 'finalize';
type CheckoutTask = 'start' | 'update' | 'confirm' | 'finalize';

export class CheckoutFlow implements CheckoutFlowResourceNonStrict {
private resource = new BillingCheckout(null);
Expand Down Expand Up @@ -197,6 +198,24 @@ export class CheckoutFlow implements CheckoutFlowResourceNonStrict {
});
}

async update(params: Pick<UpdateCheckoutParams, 'promoCode'>): Promise<{ error: ClerkError | null }> {
if (!this.resource.id) {
throw new Error('Clerk: `start()` must be called before `update()`');
}
return this.runAsyncCheckoutTask(
'update',
async () => {
this.resource = (await BillingCheckout.clerk.billing?.updateCheckout({
id: this.resource.id,
orgId: this.resource.payer.organizationId || undefined,
...params,
})) as BillingCheckout;
},
undefined,
false,
);
}

async finalize(params?: CheckoutFlowFinalizeParams): Promise<{ error: ClerkError | null }> {
const { navigate } = params || {};
return this.runAsyncCheckoutTask('finalize', async () => {
Expand All @@ -208,13 +227,23 @@ export class CheckoutFlow implements CheckoutFlowResourceNonStrict {
});
}

private runAsyncCheckoutTask<T>(operationType: CheckoutTask, task: () => Promise<T>, beforeTask?: () => void) {
private runAsyncCheckoutTask<T>(
operationType: CheckoutTask,
task: () => Promise<T>,
beforeTask?: () => void,
updateErrorSignal = true,
) {
// Noops during transitive state
if (typeof BillingCheckout.clerk.user === 'undefined') {
console.warn('Clerk: Checkout operations cannot be performed during transitive state');
return { error: null };
}
return createRunAsyncCheckoutTask(this, this.signals, this.pendingOperations)(operationType, task, beforeTask);
return createRunAsyncCheckoutTask(this, this.signals, this.pendingOperations)(
operationType,
task,
beforeTask,
updateErrorSignal,
);
}
}

Expand All @@ -226,8 +255,9 @@ function createRunAsyncCheckoutTask(
operationType: CheckoutTask,
task: () => Promise<T>,
beforeTask?: () => void,
updateErrorSignal?: boolean,
) => Promise<{ error: ClerkError | null }> {
return async (operationType, task, beforeTask?: () => void) => {
return async (operationType, task, beforeTask?: () => void, updateErrorSignal = true) => {
if (pendingOperations.get(operationType)) {
// Wait for the existing operation to complete and return its result
// If it fails, all callers should receive the same error
Expand All @@ -246,7 +276,9 @@ function createRunAsyncCheckoutTask(
signals.resourceSignal({ resource: resource });
return { error: null };
} catch (err) {
signals.errorSignal({ error: err });
if (updateErrorSignal) {
signals.errorSignal({ error: err });
}
return { error: err };
} finally {
pendingOperations.delete(operationType);
Expand Down
5 changes: 5 additions & 0 deletions packages/localizations/src/ar-SA.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,11 @@ export const arSA: LocalizationResource = {
cannotSubscribeMonthly: undefined,
cannotSubscribeUnrecoverable: undefined,
checkout: {
addPromoCode: undefined,
applyPromoCode: undefined,
description__paymentSuccessful: undefined,
description__subscriptionSuccessful: undefined,
discount: undefined,
downgradeNotice: undefined,
emailForm: {
subtitle: undefined,
Expand All @@ -116,6 +119,8 @@ export const arSA: LocalizationResource = {
},
pastDueNotice: undefined,
perMonth: undefined,
promoCodePlaceholder: undefined,
removePromoCode: undefined,
title: undefined,
title__paymentSuccessful: undefined,
title__subscriptionSuccessful: undefined,
Expand Down
5 changes: 5 additions & 0 deletions packages/localizations/src/be-BY.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,11 @@ export const beBY: LocalizationResource = {
cannotSubscribeMonthly: undefined,
cannotSubscribeUnrecoverable: undefined,
checkout: {
addPromoCode: undefined,
applyPromoCode: undefined,
description__paymentSuccessful: undefined,
description__subscriptionSuccessful: undefined,
discount: undefined,
downgradeNotice: undefined,
emailForm: {
subtitle: undefined,
Expand All @@ -116,6 +119,8 @@ export const beBY: LocalizationResource = {
},
pastDueNotice: undefined,
perMonth: undefined,
promoCodePlaceholder: undefined,
removePromoCode: undefined,
title: undefined,
title__paymentSuccessful: undefined,
title__subscriptionSuccessful: undefined,
Expand Down
5 changes: 5 additions & 0 deletions packages/localizations/src/bg-BG.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,11 @@ export const bgBG: LocalizationResource = {
cannotSubscribeMonthly: undefined,
cannotSubscribeUnrecoverable: undefined,
checkout: {
addPromoCode: undefined,
applyPromoCode: undefined,
description__paymentSuccessful: undefined,
description__subscriptionSuccessful: undefined,
discount: undefined,
downgradeNotice: undefined,
emailForm: {
subtitle: undefined,
Expand All @@ -117,6 +120,8 @@ export const bgBG: LocalizationResource = {
},
pastDueNotice: undefined,
perMonth: undefined,
promoCodePlaceholder: undefined,
removePromoCode: undefined,
title: undefined,
title__paymentSuccessful: undefined,
title__subscriptionSuccessful: undefined,
Expand Down
5 changes: 5 additions & 0 deletions packages/localizations/src/bn-IN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,11 @@ export const bnIN: LocalizationResource = {
cannotSubscribeUnrecoverable:
'আপনি এই প্ল্যানে সাবস্ক্রাইব করতে পারবেন না। আপনার বিদ্যমান সাবস্ক্রিপশন এই প্ল্যানের চেয়ে বেশি ব্যয়বহুল।',
checkout: {
addPromoCode: undefined,
applyPromoCode: undefined,
description__paymentSuccessful: 'আপনার পেমেন্ট সফল হয়েছে।',
description__subscriptionSuccessful: 'আপনার নতুন সাবস্ক্রিপশন সম্পূর্ণ প্রস্তুত।',
discount: undefined,
downgradeNotice:
'বিলিং চক্রের শেষ পর্যন্ত আপনি আপনার বর্তমান সাবস্ক্রিপশন এবং এর বৈশিষ্ট্যগুলি রাখবেন, তারপরে আপনাকে এই সাবস্ক্রিপশনে স্যুইচ করা হবে।',
emailForm: {
Expand All @@ -122,6 +125,8 @@ export const bnIN: LocalizationResource = {
},
pastDueNotice: 'আপনার পূর্ববর্তী সাবস্ক্রিপশন বকেয়া ছিল, কোনো পেমেন্ট ছাড়াই।',
perMonth: 'প্রতি মাসে',
promoCodePlaceholder: undefined,
removePromoCode: undefined,
title: 'চেকআউট',
title__paymentSuccessful: 'পেমেন্ট সফল হয়েছে!',
title__subscriptionSuccessful: 'সফল!',
Expand Down
5 changes: 5 additions & 0 deletions packages/localizations/src/ca-ES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,11 @@ export const caES: LocalizationResource = {
cannotSubscribeUnrecoverable:
"No pots subscriure't a aquest pla. La teva subscripció actual és més cara que aquest pla.",
checkout: {
addPromoCode: undefined,
applyPromoCode: undefined,
description__paymentSuccessful: "El teu pagament s'ha realitzat correctament.",
description__subscriptionSuccessful: 'La teva nova subscripció està a punt.',
discount: undefined,
downgradeNotice:
'Mantindràs la teva subscripció actual i les seves funcions fins al final del cicle de facturació; després es canviarà a aquesta subscripció.',
emailForm: {
Expand All @@ -123,6 +126,8 @@ export const caES: LocalizationResource = {
},
pastDueNotice: 'La teva subscripció anterior tenia un pagament pendent.',
perMonth: 'al mes',
promoCodePlaceholder: undefined,
removePromoCode: undefined,
title: 'Pagament',
title__paymentSuccessful: 'Pagament realitzat amb èxit!',
title__subscriptionSuccessful: 'Tot a punt!',
Expand Down
5 changes: 5 additions & 0 deletions packages/localizations/src/cs-CZ.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,11 @@ export const csCZ: LocalizationResource = {
'Nelze se přihlásit k tomuto plánu s měsíční platbou. Abyste se k němu přihlásili, musíte zvolit roční platbu.',
cannotSubscribeUnrecoverable: undefined,
checkout: {
addPromoCode: undefined,
applyPromoCode: undefined,
description__paymentSuccessful: 'Vaše platba byla úspěšná.',
description__subscriptionSuccessful: 'Vaše nové předplatné je nastaveno.',
discount: undefined,
downgradeNotice:
'Současné předplatné a jeho funkce si ponecháte do konce fakturačního cyklu, poté budete převedeni na toto předplatné.',
emailForm: {
Expand All @@ -120,6 +123,8 @@ export const csCZ: LocalizationResource = {
},
pastDueNotice: 'Vaše předchozí předplatné bylo po splatnosti, bez platby.',
perMonth: 'měsíčně',
promoCodePlaceholder: undefined,
removePromoCode: undefined,
title: 'Pokladna',
title__paymentSuccessful: 'Platba byla úspěšná!',
title__subscriptionSuccessful: 'Úspěch!',
Expand Down
5 changes: 5 additions & 0 deletions packages/localizations/src/da-DK.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,11 @@ export const daDK: LocalizationResource = {
cannotSubscribeMonthly: undefined,
cannotSubscribeUnrecoverable: undefined,
checkout: {
addPromoCode: undefined,
applyPromoCode: undefined,
description__paymentSuccessful: undefined,
description__subscriptionSuccessful: undefined,
discount: undefined,
downgradeNotice: undefined,
emailForm: {
subtitle: undefined,
Expand All @@ -116,6 +119,8 @@ export const daDK: LocalizationResource = {
},
pastDueNotice: undefined,
perMonth: undefined,
promoCodePlaceholder: undefined,
removePromoCode: undefined,
title: undefined,
title__paymentSuccessful: undefined,
title__subscriptionSuccessful: undefined,
Expand Down
5 changes: 5 additions & 0 deletions packages/localizations/src/de-DE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,11 @@ export const deDE: LocalizationResource = {
cannotSubscribeUnrecoverable:
'Sie können diesen Plan nicht abonnieren. Ihr vorhandenes Abonnement ist teurer als dieser Plan.',
checkout: {
addPromoCode: undefined,
applyPromoCode: undefined,
description__paymentSuccessful: 'Ihre Bezahlung war erfolgreich.',
description__subscriptionSuccessful: 'Ihr Abonnement wurde erfolgreich aktiviert.',
discount: undefined,
downgradeNotice:
'Sie behalten Ihr aktuelles Abonnement bis zum Ende des Abrechnungszeitraums. So lange können Sie weiterhin alle Funktionen nutzen, danach werden Sie auf dieses Abonnement umgestellt.',
emailForm: {
Expand All @@ -122,6 +125,8 @@ export const deDE: LocalizationResource = {
},
pastDueNotice: 'Ihr vorheriges Abonnement war überfällig, ohne Zahlung.',
perMonth: 'pro Monat',
promoCodePlaceholder: undefined,
removePromoCode: undefined,
title: 'Bezahlung',
title__paymentSuccessful: 'Zahlung erfolgreich!',
title__subscriptionSuccessful: 'Geschafft!',
Expand Down
5 changes: 5 additions & 0 deletions packages/localizations/src/el-GR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,11 @@ export const elGR: LocalizationResource = {
cannotSubscribeMonthly: 'Δεν μπορείτε να εγγραφείτε μηνιαίως σε αυτό το πλάνο',
cannotSubscribeUnrecoverable: 'Δεν μπορείτε να εγγραφείτε σε αυτό το πλάνο αυτήν τη στιγμή',
checkout: {
addPromoCode: undefined,
applyPromoCode: undefined,
description__paymentSuccessful: 'Η πληρωμή σας ολοκληρώθηκε επιτυχώς',
description__subscriptionSuccessful: 'Η συνδρομή σας ξεκίνησε επιτυχώς',
discount: undefined,
downgradeNotice: 'Θα υποβαθμιστείτε στο τέλος της τρέχουσας περιόδου χρέωσης',
emailForm: {
subtitle: 'Εισάγετε τη διεύθυνση email σας για να συνεχίσετε',
Expand All @@ -116,6 +119,8 @@ export const elGR: LocalizationResource = {
},
pastDueNotice: 'Η συνδρομή σας είναι ληξιπρόθεσμη. Παρακαλώ ενημερώστε τη μέθοδο πληρωμής σας.',
perMonth: 'ανά μήνα',
promoCodePlaceholder: undefined,
removePromoCode: undefined,
title: 'Ολοκλήρωση πληρωμής',
title__paymentSuccessful: 'Επιτυχής πληρωμή',
title__subscriptionSuccessful: 'Επιτυχής συνδρομή',
Expand Down
5 changes: 5 additions & 0 deletions packages/localizations/src/en-GB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,11 @@ export const enGB: LocalizationResource = {
cannotSubscribeMonthly: undefined,
cannotSubscribeUnrecoverable: undefined,
checkout: {
addPromoCode: undefined,
applyPromoCode: undefined,
description__paymentSuccessful: undefined,
description__subscriptionSuccessful: undefined,
discount: undefined,
downgradeNotice: undefined,
emailForm: {
subtitle: undefined,
Expand All @@ -116,6 +119,8 @@ export const enGB: LocalizationResource = {
},
pastDueNotice: undefined,
perMonth: undefined,
promoCodePlaceholder: undefined,
removePromoCode: undefined,
title: undefined,
title__paymentSuccessful: undefined,
title__subscriptionSuccessful: undefined,
Expand Down
9 changes: 7 additions & 2 deletions packages/localizations/src/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,11 @@ export const enUS: LocalizationResource = {
cannotSubscribeUnrecoverable:
'You cannot subscribe to this plan. Your existing subscription is more expensive than this plan.',
checkout: {
addPromoCode: 'Add promo code',
applyPromoCode: 'Apply',
description__paymentSuccessful: 'Your payment was successful.',
description__subscriptionSuccessful: 'Your new subscription is all set.',
discount: 'Discount',
downgradeNotice:
'You will keep your current subscription and its features until the end of the billing cycle, then you will be switched to this subscription.',
emailForm: {
Expand All @@ -111,6 +114,8 @@ export const enUS: LocalizationResource = {
},
pastDueNotice: 'Your previous subscription was past due, with no payment.',
perMonth: 'per month',
promoCodePlaceholder: 'Enter promo code',
removePromoCode: 'Remove promo code',
title: 'Checkout',
title__paymentSuccessful: 'Payment was successful!',
title__subscriptionSuccessful: 'Success!',
Expand All @@ -121,9 +126,9 @@ export const enUS: LocalizationResource = {
credit: 'Credit',
creditRemainder: 'Credit for the remainder of your current subscription.',
defaultFreePlanActive: "You're currently on the Free plan",
discountAmount: '({{amount}} off)',
discountAmount: '{{amount}} off',
discountCyclesRemaining: '{{cycles}} {{period}} remaining',
discountDuration: '({{amount}} off first {{cycles}} {{period}})',
discountDuration: '{{amount}} off first {{cycles}} {{period}}',
free: 'Free',
getStarted: 'Get started',
highlightedPlanBadge: 'Popular',
Expand Down
Loading
Loading