Skip to content

Commit aae5436

Browse files
feat(ui,backend): Show applied discounts on payments and statements (#9337)
1 parent 73f5daf commit aae5436

5 files changed

Lines changed: 101 additions & 3 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@clerk/ui': patch
3+
'@clerk/backend': patch
4+
---
5+
6+
Show applied discount on billing payment attempts and statements.

‎packages/backend/src/util/billing.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type {
2+
BillingAppliedDiscount,
3+
BillingAppliedDiscountJSON,
24
BillingCredits,
35
BillingCreditsJSON,
46
BillingDiscounts,
@@ -51,6 +53,17 @@ const billingCreditsFromJSON = (credits: BillingCreditsJSON): BillingCredits =>
5153
total: billingMoneyAmountFromJSON(credits.total),
5254
});
5355

56+
const billingAppliedDiscountFromJSON = (discount: BillingAppliedDiscountJSON): BillingAppliedDiscount => ({
57+
amount: billingMoneyAmountFromJSON(discount.amount),
58+
discountId: discount.discount_id,
59+
name: discount.name,
60+
effect: discount.effect,
61+
percentOff: discount.percent_off,
62+
amountOff: discount.amount_off ? billingMoneyAmountFromJSON(discount.amount_off) : undefined,
63+
promoCode: discount.promo_code,
64+
cyclesRemaining: discount.cycles_remaining,
65+
});
66+
5467
const billingDiscountsFromJSON = (discounts: BillingDiscountsJSON): BillingDiscounts => ({
5568
proration: discounts.proration
5669
? {
@@ -60,6 +73,7 @@ const billingDiscountsFromJSON = (discounts: BillingDiscountsJSON): BillingDisco
6073
cyclePassedPercent: discounts.proration.cycle_passed_percent,
6174
}
6275
: null,
76+
discount: discounts.discount ? billingAppliedDiscountFromJSON(discounts.discount) : undefined,
6377
total: billingMoneyAmountFromJSON(discounts.total),
6478
});
6579

‎packages/ui/src/components/PaymentAttempts/PaymentAttemptPage.tsx‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Alert } from '@/ui/elements/Alert';
55
import { Header } from '@/ui/elements/Header';
66
import { LineItems } from '@/ui/elements/LineItems';
77
import { ProfileCard } from '@/ui/elements/ProfileCard';
8-
import { toNegativeAmount } from '@/ui/utils/billing';
8+
import { getDiscountDescription, toNegativeAmount } from '@/ui/utils/billing';
99
import { getPlanSeatLimit, getSeatsPerUnitTotal, summarizeSeatCharges } from '@/ui/utils/billingPlanSeats';
1010
import { formatDate } from '@/ui/utils/formatDate';
1111
import { truncateWithEndVisible } from '@/ui/utils/truncateTextWithEndVisible';
@@ -201,13 +201,14 @@ export const PaymentAttemptPage = () => {
201201
};
202202

203203
function PaymentAttemptBody({ paymentAttempt }: { paymentAttempt: BillingPaymentResource | undefined }) {
204-
const { $ } = useLocalizations();
204+
const { $, t } = useLocalizations();
205205

206206
if (!paymentAttempt) {
207207
return null;
208208
}
209209

210210
const { subscriptionItem } = paymentAttempt;
211+
const catalogDiscount = paymentAttempt.totals?.discounts?.discount;
211212

212213
const fee =
213214
subscriptionItem.planPeriod === 'month'
@@ -285,6 +286,21 @@ function PaymentAttemptBody({ paymentAttempt }: { paymentAttempt: BillingPayment
285286
<LineItems.Description text={$(toNegativeAmount(paymentAttempt.totals.discounts.proration.amount))} />
286287
</LineItems.Group>
287288
)}
289+
{catalogDiscount && catalogDiscount.amount.amount > 0 && (
290+
<LineItems.Group variant='tertiary'>
291+
<LineItems.Title
292+
title={catalogDiscount.name}
293+
description={getDiscountDescription(
294+
catalogDiscount,
295+
catalogDiscount.cyclesRemaining,
296+
subscriptionItem.planPeriod,
297+
{ $, t },
298+
)}
299+
badge={catalogDiscount.promoCode ? <Badge>{catalogDiscount.promoCode}</Badge> : null}
300+
/>
301+
<LineItems.Description text={$(toNegativeAmount(catalogDiscount.amount))} />
302+
</LineItems.Group>
303+
)}
288304
{subscriptionItem.credits &&
289305
subscriptionItem.credits.proration &&
290306
subscriptionItem.credits.proration.amount.amount > 0 && (

‎packages/ui/src/components/Statements/StatementPage.tsx‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { __internal_useStatementQuery } from '@clerk/shared/react/index';
33
import { Alert } from '@/ui/elements/Alert';
44
import { Header } from '@/ui/elements/Header';
55
import { ProfileCard } from '@/ui/elements/ProfileCard';
6+
import { getDiscountDescription } from '@/ui/utils/billing';
67
import { formatDate } from '@/ui/utils/formatDate';
78

89
import { useSubscriberTypeContext, useSubscriberTypeLocalizationRoot } from '../../contexts/components';
@@ -146,6 +147,17 @@ export const StatementPage = () => {
146147
value={`(${$(item.totals.discounts.proration.amount)})`}
147148
/>
148149
) : null}
150+
{item.totals?.discounts?.discount && item.totals.discounts.discount.amount.amount > 0 ? (
151+
<Statement.SectionContentDetailsListItem
152+
label={`${item.totals.discounts.discount.name} ${getDiscountDescription(
153+
item.totals.discounts.discount,
154+
item.totals.discounts.discount.cyclesRemaining,
155+
item.subscriptionItem.planPeriod,
156+
{ $, t },
157+
)}`}
158+
value={`(${$(item.totals.discounts.discount.amount)})`}
159+
/>
160+
) : null}
149161
{item.subscriptionItem.credits &&
150162
item.subscriptionItem.credits.proration &&
151163
item.subscriptionItem.credits.proration.amount.amount > 0 ? (

‎packages/ui/src/utils/billing.ts‎

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,54 @@
1-
import type { BillingMoneyAmount } from '@clerk/shared/types/billing';
1+
import type { BillingMoneyAmount, BillingSubscriptionPlanPeriod } from '@clerk/shared/types/billing';
2+
3+
import type { useLocalizations } from '../localization';
4+
import { localizationKeys } from '../localization';
5+
6+
type Discount = {
7+
effect?: 'percentage' | 'fixed_amount';
8+
percentOff?: number;
9+
amountOff?: BillingMoneyAmount;
10+
};
11+
12+
type Localizations = Pick<ReturnType<typeof useLocalizations>, '$' | 't'>;
13+
14+
export function getDiscountDescription(
15+
discount: Discount,
16+
cycles: number | null,
17+
planPeriod: BillingSubscriptionPlanPeriod,
18+
{ $, t }: Localizations,
19+
) {
20+
const amount =
21+
discount.effect === 'percentage' && discount.percentOff !== undefined
22+
? `${discount.percentOff}%`
23+
: discount.amountOff
24+
? $(discount.amountOff)
25+
: '';
26+
27+
if (cycles === null) {
28+
return t(localizationKeys('billing.discountAmount', { amount }));
29+
}
30+
31+
const period = getBillingPeriodLabel(planPeriod, cycles, t);
32+
return t(localizationKeys('billing.discountDuration', { amount, cycles, period }));
33+
}
34+
35+
export function getBillingPeriodLabel(
36+
planPeriod: BillingSubscriptionPlanPeriod,
37+
cycles: number,
38+
t: Localizations['t'],
39+
) {
40+
return t(
41+
localizationKeys(
42+
planPeriod === 'annual'
43+
? cycles === 1
44+
? 'billing.year'
45+
: 'billing.years'
46+
: cycles === 1
47+
? 'billing.month'
48+
: 'billing.months',
49+
),
50+
).toLocaleLowerCase();
51+
}
252

353
/**
454
* Given a BillingMoneyAmount, convert positive values to negative. If the amount is already negative, leave it alone.

0 commit comments

Comments
 (0)