Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
5 changes: 5 additions & 0 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 Down
5 changes: 5 additions & 0 deletions packages/localizations/src/es-CR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,11 @@ export const esCR: LocalizationResource = {
cannotSubscribeMonthly: undefined,
cannotSubscribeUnrecoverable: undefined,
checkout: {
addPromoCode: undefined,
applyPromoCode: undefined,
description__paymentSuccessful: 'Tu nueva suscripción está lista.',
description__subscriptionSuccessful: 'Tu nueva suscripción está lista.',
discount: undefined,
downgradeNotice: undefined,
emailForm: {
subtitle: undefined,
Expand All @@ -116,6 +119,8 @@ export const esCR: LocalizationResource = {
},
pastDueNotice: undefined,
perMonth: undefined,
promoCodePlaceholder: undefined,
removePromoCode: undefined,
title: undefined,
title__paymentSuccessful: '¡Pago exitoso!',
title__subscriptionSuccessful: '¡Éxito!',
Expand Down
Loading
Loading