@blocklet/payment-react 1.20.10 → 1.20.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/es/components/promotion-code.d.ts +19 -0
  2. package/es/components/promotion-code.js +153 -0
  3. package/es/contexts/payment.d.ts +8 -0
  4. package/es/contexts/payment.js +10 -1
  5. package/es/index.d.ts +2 -1
  6. package/es/index.js +3 -1
  7. package/es/libs/util.d.ts +5 -1
  8. package/es/libs/util.js +23 -0
  9. package/es/locales/en.js +40 -15
  10. package/es/locales/zh.js +29 -0
  11. package/es/payment/form/index.js +7 -1
  12. package/es/payment/index.js +19 -0
  13. package/es/payment/product-item.js +32 -3
  14. package/es/payment/summary.d.ts +5 -2
  15. package/es/payment/summary.js +193 -16
  16. package/lib/components/promotion-code.d.ts +19 -0
  17. package/lib/components/promotion-code.js +155 -0
  18. package/lib/contexts/payment.d.ts +8 -0
  19. package/lib/contexts/payment.js +13 -1
  20. package/lib/index.d.ts +2 -1
  21. package/lib/index.js +8 -0
  22. package/lib/libs/util.d.ts +5 -1
  23. package/lib/libs/util.js +29 -0
  24. package/lib/locales/en.js +40 -15
  25. package/lib/locales/zh.js +29 -0
  26. package/lib/payment/form/index.js +8 -1
  27. package/lib/payment/index.js +23 -0
  28. package/lib/payment/product-item.js +46 -0
  29. package/lib/payment/summary.d.ts +5 -2
  30. package/lib/payment/summary.js +153 -11
  31. package/package.json +9 -9
  32. package/src/components/promotion-code.tsx +184 -0
  33. package/src/contexts/payment.tsx +15 -0
  34. package/src/index.ts +2 -0
  35. package/src/libs/util.ts +35 -0
  36. package/src/locales/en.tsx +40 -15
  37. package/src/locales/zh.tsx +29 -0
  38. package/src/payment/form/index.tsx +10 -1
  39. package/src/payment/index.tsx +22 -0
  40. package/src/payment/product-item.tsx +37 -2
  41. package/src/payment/summary.tsx +201 -16
@@ -0,0 +1,184 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import { TextField, Button, Alert, Box, InputAdornment } from '@mui/material';
3
+ import { Add } from '@mui/icons-material';
4
+ import { useState } from 'react';
5
+ import LoadingButton from './loading-button';
6
+ import api from '../libs/api';
7
+ import { usePaymentContext } from '../contexts/payment';
8
+
9
+ export interface AppliedPromoCode {
10
+ id: string;
11
+ code: string;
12
+ discount_amount?: string;
13
+ }
14
+
15
+ interface PromotionCodeProps {
16
+ checkoutSessionId: string;
17
+ initialAppliedCodes?: AppliedPromoCode[];
18
+ disabled?: boolean;
19
+ className?: string;
20
+ placeholder?: string;
21
+ currencyId: string;
22
+ onUpdate?: (data: { appliedCodes: AppliedPromoCode[]; discountAmount: string }) => void;
23
+ }
24
+
25
+ export default function PromotionCode({
26
+ checkoutSessionId,
27
+ initialAppliedCodes = [],
28
+ disabled = false,
29
+ className = '',
30
+ placeholder = '',
31
+ onUpdate = undefined,
32
+ currencyId,
33
+ }: PromotionCodeProps) {
34
+ const { t } = useLocaleContext();
35
+ const [showInput, setShowInput] = useState(false);
36
+ const [code, setCode] = useState('');
37
+ const [error, setError] = useState('');
38
+ const [applying, setApplying] = useState(false);
39
+ const [appliedCodes, setAppliedCodes] = useState<AppliedPromoCode[]>(initialAppliedCodes);
40
+ const { session, paymentState } = usePaymentContext();
41
+
42
+ const handleLoginCheck = () => {
43
+ if (!session.user) {
44
+ session?.login(() => {
45
+ handleApply();
46
+ });
47
+ } else {
48
+ handleApply();
49
+ }
50
+ };
51
+
52
+ const handleApply = async () => {
53
+ if (!code.trim()) return;
54
+
55
+ // Prevent applying promotion during payment process
56
+ if (paymentState.paying || paymentState.stripePaying) {
57
+ return;
58
+ }
59
+
60
+ setApplying(true);
61
+ setError('');
62
+
63
+ try {
64
+ const response = await api.post(`/api/checkout-sessions/${checkoutSessionId}/apply-promotion`, {
65
+ promotion_code: code.trim(),
66
+ currency_id: currencyId,
67
+ });
68
+
69
+ const discounts = response.data.discounts || [];
70
+ const appliedDiscount = discounts[0];
71
+
72
+ if (appliedDiscount) {
73
+ const newCode: AppliedPromoCode = {
74
+ id: appliedDiscount.promotion_code || appliedDiscount.coupon,
75
+ code: code.trim(),
76
+ discount_amount: appliedDiscount.discount_amount,
77
+ };
78
+
79
+ setAppliedCodes([newCode]);
80
+ setCode('');
81
+ setShowInput(false);
82
+
83
+ onUpdate?.({
84
+ appliedCodes: [newCode],
85
+ discountAmount: appliedDiscount.discount_amount,
86
+ });
87
+ }
88
+ } catch (err: any) {
89
+ const errorMessage = err.response?.data?.error || err.message;
90
+ setError(errorMessage);
91
+ } finally {
92
+ setApplying(false);
93
+ }
94
+ };
95
+
96
+ const handleKeyPress = (event: React.KeyboardEvent) => {
97
+ if (event.key === 'Enter' && !applying && code.trim()) {
98
+ handleLoginCheck();
99
+ }
100
+ };
101
+
102
+ const isPaymentInProgress = paymentState.paying || paymentState.stripePaying;
103
+
104
+ return (
105
+ <Box className={className}>
106
+ {/* Input field or add button - only show if no codes applied */}
107
+ {appliedCodes.length === 0 &&
108
+ !disabled &&
109
+ !isPaymentInProgress &&
110
+ (showInput ? (
111
+ <Box
112
+ onBlur={() => {
113
+ if (!code.trim()) {
114
+ setShowInput(false);
115
+ }
116
+ }}>
117
+ <TextField
118
+ fullWidth
119
+ value={code}
120
+ onChange={(e) => setCode(e.target.value)}
121
+ onKeyPress={handleKeyPress}
122
+ placeholder={placeholder || t('payment.checkout.promotion.placeholder')}
123
+ variant="outlined"
124
+ size="small"
125
+ disabled={applying}
126
+ autoFocus
127
+ slotProps={{
128
+ input: {
129
+ endAdornment: (
130
+ <InputAdornment position="end">
131
+ <LoadingButton
132
+ size="small"
133
+ onClick={handleLoginCheck}
134
+ loading={applying}
135
+ disabled={!code.trim()}
136
+ variant="text"
137
+ sx={{
138
+ color: 'primary.main',
139
+ fontSize: 'small',
140
+ }}>
141
+ {t('payment.checkout.promotion.apply')}
142
+ </LoadingButton>
143
+ </InputAdornment>
144
+ ),
145
+ },
146
+ }}
147
+ sx={{
148
+ '& .MuiOutlinedInput-root': {
149
+ pr: 1,
150
+ },
151
+ }}
152
+ />
153
+
154
+ {error && (
155
+ <Alert
156
+ severity="error"
157
+ sx={{
158
+ my: 1,
159
+ }}>
160
+ {error}
161
+ </Alert>
162
+ )}
163
+ </Box>
164
+ ) : (
165
+ <Button
166
+ onClick={() => setShowInput(true)}
167
+ startIcon={<Add fontSize="small" />}
168
+ variant="text"
169
+ sx={{
170
+ fontWeight: 'normal',
171
+ textTransform: 'none',
172
+ justifyContent: 'flex-start',
173
+ p: 0,
174
+ '&:hover': {
175
+ backgroundColor: 'transparent',
176
+ textDecoration: 'underline',
177
+ },
178
+ }}>
179
+ {t('payment.checkout.promotion.add_code')}
180
+ </Button>
181
+ ))}
182
+ </Box>
183
+ );
184
+ }
@@ -29,6 +29,11 @@ export type PaymentContextType = {
29
29
  api: Axios;
30
30
  payable: boolean;
31
31
  setPayable: (status: boolean) => void;
32
+ paymentState: {
33
+ paying: boolean;
34
+ stripePaying: boolean;
35
+ };
36
+ setPaymentState: (state: Partial<{ paying: boolean; stripePaying: boolean }>) => void;
32
37
  };
33
38
 
34
39
  export type PaymentContextProps = {
@@ -188,6 +193,14 @@ function PaymentProvider({
188
193
 
189
194
  const prefix = getPrefix();
190
195
  const [payable, setPayable] = useState(true);
196
+ const [paymentState, setPaymentState] = useState({
197
+ paying: false,
198
+ stripePaying: false,
199
+ });
200
+
201
+ const updatePaymentState = (state: Partial<{ paying: boolean; stripePaying: boolean }>) => {
202
+ setPaymentState((prev) => ({ ...prev, ...state }));
203
+ };
191
204
 
192
205
  if (error) {
193
206
  return <Alert severity="error">{error.message}</Alert>;
@@ -212,6 +225,8 @@ function PaymentProvider({
212
225
  api,
213
226
  payable,
214
227
  setPayable,
228
+ paymentState,
229
+ setPaymentState: updatePaymentState,
215
230
  }}>
216
231
  {children}
217
232
  </Provider>
package/src/index.ts CHANGED
@@ -39,6 +39,7 @@ import DateRangePicker from './components/date-range-picker';
39
39
  import AutoTopupModal from './components/auto-topup/modal';
40
40
  import AutoTopup from './components/auto-topup';
41
41
  import Collapse from './components/collapse';
42
+ import PromotionCode from './components/promotion-code';
42
43
 
43
44
  export { PaymentThemeProvider } from './theme';
44
45
 
@@ -102,4 +103,5 @@ export {
102
103
  AutoTopupModal,
103
104
  AutoTopup,
104
105
  Collapse,
106
+ PromotionCode,
105
107
  };
package/src/libs/util.ts CHANGED
@@ -5,6 +5,7 @@ import type {
5
5
  PaymentDetails,
6
6
  PriceCurrency,
7
7
  PriceRecurring,
8
+ TCoupon,
8
9
  TInvoiceExpanded,
9
10
  TLineItemExpanded,
10
11
  TPaymentCurrency,
@@ -31,6 +32,40 @@ import type { ActionProps, PricingRenderProps } from '../types';
31
32
 
32
33
  export const PAYMENT_KIT_DID = 'z2qaCNvKMv5GjouKdcDWexv6WqtHbpNPQDnAk';
33
34
 
35
+ /**
36
+ * Format coupon discount terms for display
37
+ */
38
+ export const formatCouponTerms = (coupon: TCoupon, currency: TPaymentCurrency, locale: string = 'en'): string => {
39
+ let couponOff = '';
40
+
41
+ if (coupon.percent_off && coupon.percent_off > 0) {
42
+ couponOff = t('payment.checkout.coupon.percentage', locale, { percent: coupon.percent_off });
43
+ }
44
+
45
+ if (coupon.amount_off && coupon.amount_off !== '0') {
46
+ const { symbol } = currency;
47
+ couponOff =
48
+ coupon.currency_id === currency.id
49
+ ? coupon.amount_off || ''
50
+ : coupon.currency_options?.[currency.id]?.amount_off || '';
51
+ if (couponOff) {
52
+ couponOff = t('payment.checkout.coupon.fixedAmount', locale, {
53
+ amount: formatAmount(couponOff, currency.decimal),
54
+ symbol,
55
+ });
56
+ }
57
+ }
58
+
59
+ if (!couponOff) {
60
+ return t('payment.checkout.coupon.noDiscount');
61
+ }
62
+
63
+ return t(`payment.checkout.coupon.terms.${coupon.duration}`, locale, {
64
+ couponOff,
65
+ months: coupon.duration_in_months || 0,
66
+ });
67
+ };
68
+
34
69
  export const isPaymentKitMounted = () => {
35
70
  return (window.blocklet?.componentMountPoints || []).some((x: any) => x.did === PAYMENT_KIT_DID);
36
71
  };
@@ -104,8 +104,8 @@ export default flat({
104
104
  know: 'I know',
105
105
  relatedSubscription: 'Subscription',
106
106
  connect: {
107
- defaultScan: 'Use following methods to complete this action',
108
- scan: 'Use following methods to complete this {action}',
107
+ defaultScan: 'Use the following methods to complete this action',
108
+ scan: 'Use the following methods to complete this {action}',
109
109
  confirm: 'Confirm',
110
110
  cancel: 'Cancel',
111
111
  },
@@ -156,14 +156,14 @@ export default flat({
156
156
  },
157
157
  inactive: 'Donation feature is not enabled',
158
158
  enable: 'Enable Donation',
159
- enableSuccess: 'Enable Success',
160
- configPrompt: 'Donation feature is enabled, you can configure donation options in Payment Kit',
159
+ enableSuccess: 'Successfully enabled',
160
+ configPrompt: 'The donation feature is enabled. You can configure donation options in Payment Kit',
161
161
  configNow: 'Configure Now',
162
162
  later: 'Configure Later',
163
163
  configTip: 'Configure donation settings in Payment Kit',
164
164
  },
165
165
  cardPay: '{action} with bank card',
166
- empty: 'No thing to pay',
166
+ empty: 'Nothing to pay',
167
167
  per: 'per',
168
168
  pay: 'Pay {payee}',
169
169
  try1: 'Try {name}',
@@ -177,8 +177,8 @@ export default flat({
177
177
  least: 'continue with at least',
178
178
  completed: {
179
179
  payment: 'Thanks for your purchase',
180
- subscription: 'Thanks for your subscribing',
181
- setup: 'Thanks for your subscribing',
180
+ subscription: 'Thanks for subscribing',
181
+ setup: 'Thanks for subscribing',
182
182
  donate: 'Thanks for your tip',
183
183
  tip: 'A payment to {payee} has been completed. You can view the details of this payment in your account.',
184
184
  },
@@ -188,7 +188,7 @@ export default flat({
188
188
  delivered: 'Installation completed',
189
189
  failed: 'Processing failed',
190
190
  failedMsg:
191
- 'An exception occurred during installation. We will automatically process a refund for you. We apologize for the inconvenience caused. Thank you for your understanding!',
191
+ 'An exception occurred during installation. We will automatically process a refund for you. We apologize for the inconvenience. Thank you for your understanding!',
192
192
  },
193
193
  confirm: {
194
194
  withStake:
@@ -241,14 +241,39 @@ export default flat({
241
241
  },
242
242
  emptyItems: {
243
243
  title: 'Nothing to show here',
244
- description: 'Seems this checkoutSession is not configured properly',
244
+ description: 'It seems this checkout session is not configured properly',
245
245
  },
246
246
  orderSummary: 'Order Summary',
247
247
  paymentDetails: 'Payment Details',
248
248
  productListTotal: 'Includes {total} items',
249
+ promotion: {
250
+ add_code: 'Add promotion code',
251
+ enter_code: 'Enter promotion code',
252
+ placeholder: 'Enter code',
253
+ apply: 'Apply',
254
+ applied: 'Applied promotion codes',
255
+ dialog: {
256
+ title: 'Add promotion code',
257
+ },
258
+ error: {
259
+ unknown: 'Unknown error',
260
+ network: 'Network error occurred',
261
+ removal: 'Failed to remove code',
262
+ },
263
+ },
264
+ coupon: {
265
+ noDiscount: 'No discount',
266
+ percentage: '{percent}% off',
267
+ fixedAmount: '{amount} {symbol} off',
268
+ terms: {
269
+ forever: '{couponOff} forever',
270
+ once: '{couponOff} once',
271
+ repeating: "{couponOff} for {months} month{months > 1 ? 's' : ''}",
272
+ },
273
+ },
249
274
  connectModal: {
250
275
  title: '{action}',
251
- scan: 'Use following methods to complete this payment',
276
+ scan: 'Use the following methods to complete this payment',
252
277
  confirm: 'Confirm',
253
278
  cancel: 'Cancel',
254
279
  },
@@ -337,7 +362,7 @@ export default flat({
337
362
  summary: 'Summary',
338
363
  products: 'Products',
339
364
  update: 'Update Information',
340
- empty: 'Seems you do not have any subscriptions or payments here',
365
+ empty: 'It seems you do not have any subscriptions or payments here',
341
366
  cancel: {
342
367
  button: 'Unsubscribe',
343
368
  title: 'Cancel your subscription',
@@ -350,7 +375,7 @@ export default flat({
350
375
  tip: 'We would love your feedback, it will help us improve our service',
351
376
  too_expensive: 'The service is too expensive',
352
377
  missing_features: 'Features are missing for this service',
353
- switched_service: 'I have switched to alternative service',
378
+ switched_service: 'I have switched to an alternative service',
354
379
  unused: 'I no longer use this service',
355
380
  customer_service: 'The customer service is poor',
356
381
  too_complex: 'The service is too complex to use',
@@ -361,7 +386,7 @@ export default flat({
361
386
  pastDue: {
362
387
  button: 'Pay',
363
388
  invoices: 'Past Due Invoices',
364
- warning: 'Past due invoices need to be paid immediately, otherwise you can not make new purchases anymore.',
389
+ warning: 'Past due invoices need to be paid immediately, otherwise you cannot make new purchases anymore.',
365
390
  alert: {
366
391
  title: 'You have unpaid invoices',
367
392
  customMessage: 'Please pay immediately, otherwise new purchases or subscriptions will be prohibited.',
@@ -419,7 +444,7 @@ export default flat({
419
444
  amountApplied: 'Applied Credit',
420
445
  pay: 'Pay this invoice',
421
446
  paySuccess: 'You have successfully paid the invoice',
422
- payError: 'Failed to paid the invoice',
447
+ payError: 'Failed to pay the invoice',
423
448
  renew: 'Renew the subscription',
424
449
  renewSuccess: 'You have successfully renewed the subscription',
425
450
  renewError: 'Failed to renew the subscription',
@@ -447,7 +472,7 @@ export default flat({
447
472
  viewAll: 'View all',
448
473
  empty: 'There are no subscriptions here',
449
474
  changePayment: 'Change payment method',
450
- trialLeft: 'Trail Left',
475
+ trialLeft: 'Trial Left',
451
476
  owner: 'Subscription Owner',
452
477
  },
453
478
  overdue: {
@@ -229,6 +229,35 @@ export default flat({
229
229
  add: '添加到订单',
230
230
  remove: '从订单移除',
231
231
  },
232
+ promotion: {
233
+ add_code: '添加促销码',
234
+ enter_code: '输入促销码',
235
+ apply: '应用',
236
+ applied: '已应用的促销码',
237
+ placeholder: '输入促销码',
238
+ duration_once: '1次优惠 {amount} {symbol}',
239
+ duration_repeating: '{months}个月优惠 {amount} {symbol}',
240
+ duration_forever: '永久优惠 {amount} {symbol}',
241
+ dialog: {
242
+ title: '添加促销码',
243
+ },
244
+ error: {
245
+ invalid: '无效的促销码',
246
+ expired: '促销码已过期',
247
+ used: '促销码已被使用',
248
+ not_applicable: '促销码不适用于此订单',
249
+ },
250
+ },
251
+ coupon: {
252
+ noDiscount: '无优惠',
253
+ percentage: '{percent}%',
254
+ fixedAmount: '{amount} {symbol}',
255
+ terms: {
256
+ forever: '永久享 {couponOff} 折扣',
257
+ once: '单次享 {couponOff} 折扣',
258
+ repeating: '{months} 个月内享 {couponOff} 折扣',
259
+ },
260
+ },
232
261
  credit: {
233
262
  oneTimeInfo: '付款完成后您将获得 {amount} {symbol} 额度',
234
263
  recurringInfo: '您将{period}获得 {amount} {symbol} 额度',
@@ -176,7 +176,7 @@ export default function PaymentForm({
176
176
  // const theme = useTheme();
177
177
  const { t, locale } = useLocaleContext();
178
178
  const { isMobile } = useMobile();
179
- const { session, connect, payable } = usePaymentContext();
179
+ const { session, connect, payable, setPaymentState } = usePaymentContext();
180
180
  const subscription = useSubscription('events');
181
181
  const formErrorPosition = 'bottom';
182
182
  const {
@@ -244,6 +244,15 @@ export default function PaymentForm({
244
244
  }
245
245
  }, [subscription]); // eslint-disable-line react-hooks/exhaustive-deps
246
246
 
247
+ // Sync payment states to PaymentContext
248
+ useEffect(() => {
249
+ setPaymentState({
250
+ paying: state.submitting || state.paying,
251
+ stripePaying: state.stripePaying,
252
+ });
253
+ // eslint-disable-next-line react-hooks/exhaustive-deps
254
+ }, [state.submitting, state.paying, state.stripePaying]);
255
+
247
256
  const mergeUserInfo = (
248
257
  customerInfo: UserInfo | (TCustomer & { fullName?: string }),
249
258
  userInfo?: UserInfo
@@ -177,6 +177,15 @@ function PaymentInner({
177
177
  if (onChange) {
178
178
  onChange(methods.getValues());
179
179
  }
180
+ if ((state.checkoutSession as any)?.discounts?.length) {
181
+ api
182
+ .post(`/api/checkout-sessions/${state.checkoutSession.id}/recalculate-promotion`, {
183
+ currency_id: currencyId,
184
+ })
185
+ .then(() => {
186
+ onPromotionUpdate();
187
+ });
188
+ }
180
189
  }, [currencyId]); // eslint-disable-line
181
190
 
182
191
  const onUpsell = async (from: string, to: string) => {
@@ -245,6 +254,16 @@ function PaymentInner({
245
254
  }
246
255
  };
247
256
 
257
+ const onPromotionUpdate = async () => {
258
+ try {
259
+ const { data } = await api.get(`/api/checkout-sessions/retrieve/${state.checkoutSession.id}`);
260
+ setState({ checkoutSession: data.checkoutSession });
261
+ } catch (err) {
262
+ console.error(err);
263
+ Toast.error(formatError(err));
264
+ }
265
+ };
266
+
248
267
  const handlePaid = (result: any) => {
249
268
  setState({ checkoutSession: result.checkoutSession });
250
269
  onPaid(result);
@@ -292,6 +311,9 @@ function PaymentInner({
292
311
  donationSettings={paymentLink?.donation_settings}
293
312
  action={action}
294
313
  completed={completed}
314
+ checkoutSession={state.checkoutSession}
315
+ onPromotionUpdate={onPromotionUpdate}
316
+ paymentMethods={paymentMethods as TPaymentMethodExpanded[]}
295
317
  showFeatures={showFeatures}
296
318
  />
297
319
  {mode === 'standalone' && !isMobile && (
@@ -1,7 +1,7 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
2
  import type { PriceRecurring, TLineItemExpanded, TPaymentCurrency } from '@blocklet/payment-types';
3
- import { Box, Stack, Typography, IconButton, TextField, Alert } from '@mui/material';
4
- import { Add, Remove } from '@mui/icons-material';
3
+ import { Box, Stack, Typography, IconButton, TextField, Alert, Chip } from '@mui/material';
4
+ import { Add, Remove, LocalOffer } from '@mui/icons-material';
5
5
 
6
6
  import React, { useMemo, useState } from 'react';
7
7
  import Status from '../components/status';
@@ -14,6 +14,7 @@ import {
14
14
  formatQuantityInventory,
15
15
  formatRecurring,
16
16
  formatUpsellSaving,
17
+ formatAmount,
17
18
  } from '../libs/util';
18
19
  import ProductCard from './product-card';
19
20
  import dayjs from '../libs/dayjs';
@@ -212,6 +213,40 @@ export default function ProductItem({
212
213
  )}
213
214
  </Stack>
214
215
  </Stack>
216
+
217
+ {/* Display discount information for this item */}
218
+ {item.discount_amounts && item.discount_amounts.length > 0 && (
219
+ <Stack direction="row" spacing={1} sx={{ mt: 1, alignItems: 'center' }}>
220
+ {item.discount_amounts.map((discountAmount: any) => (
221
+ <Chip
222
+ key={discountAmount.promotion_code}
223
+ icon={<LocalOffer sx={{ fontSize: '0.8rem !important' }} />}
224
+ label={
225
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
226
+ <Typography component="span" sx={{ fontSize: '0.75rem', fontWeight: 'medium' }}>
227
+ {discountAmount.promotion_code?.code || 'DISCOUNT'}
228
+ </Typography>
229
+ <Typography component="span" sx={{ fontSize: '0.75rem' }}>
230
+ (-{formatAmount(discountAmount.amount || '0', currency.decimal)} {currency.symbol})
231
+ </Typography>
232
+ </Box>
233
+ }
234
+ size="small"
235
+ variant="filled"
236
+ sx={{
237
+ height: 20,
238
+ '& .MuiChip-icon': {
239
+ color: 'warning.main',
240
+ },
241
+ '& .MuiChip-label': {
242
+ px: 1,
243
+ },
244
+ }}
245
+ />
246
+ ))}
247
+ </Stack>
248
+ )}
249
+
215
250
  {showFeatures && features.length > 0 && (
216
251
  <Box
217
252
  sx={{