@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
@@ -1,7 +1,14 @@
1
+ /* eslint-disable @typescript-eslint/indent */
1
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
- import type { DonationSettings, TLineItemExpanded, TPaymentCurrency } from '@blocklet/payment-types';
3
- import { HelpOutline } from '@mui/icons-material';
4
- import { Box, Divider, Fade, Grow, Stack, Tooltip, Typography, Collapse, IconButton } from '@mui/material';
3
+ import type {
4
+ DonationSettings,
5
+ TLineItemExpanded,
6
+ TPaymentCurrency,
7
+ TCheckoutSession,
8
+ TPaymentMethodExpanded,
9
+ } from '@blocklet/payment-types';
10
+ import { HelpOutline, Close, LocalOffer } from '@mui/icons-material';
11
+ import { Box, Divider, Fade, Grow, Stack, Tooltip, Typography, Collapse, IconButton, Button } from '@mui/material';
5
12
  import type { IconButtonProps } from '@mui/material';
6
13
  import { BN, fromTokenToUnit, fromUnitToToken } from '@ocap/util';
7
14
  import { useRequest, useSetState } from 'ahooks';
@@ -11,7 +18,14 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
11
18
  import { styled } from '@mui/material/styles';
12
19
  import Status from '../components/status';
13
20
  import api from '../libs/api';
14
- import { formatAmount, formatCheckoutHeadlines, getPriceUintAmountByCurrency } from '../libs/util';
21
+ import {
22
+ formatAmount,
23
+ formatCheckoutHeadlines,
24
+ getPriceUintAmountByCurrency,
25
+ formatCouponTerms,
26
+ formatNumber,
27
+ findCurrency,
28
+ } from '../libs/util';
15
29
  import PaymentAmount from './amount';
16
30
  import ProductDonation from './product-donation';
17
31
  import ProductItem from './product-item';
@@ -19,6 +33,7 @@ import Livemode from '../components/livemode';
19
33
  import { usePaymentContext } from '../contexts/payment';
20
34
  import { useMobile } from '../hooks/mobile';
21
35
  import LoadingButton from '../components/loading-button';
36
+ import PromotionCode from '../components/promotion-code';
22
37
 
23
38
  // const shake = keyframes`
24
39
  // 0% {
@@ -71,6 +86,9 @@ type Props = {
71
86
  donationSettings?: DonationSettings; // only include backend part
72
87
  action?: string;
73
88
  completed?: boolean;
89
+ checkoutSession?: TCheckoutSession;
90
+ onPromotionUpdate?: () => void;
91
+ paymentMethods?: TPaymentMethodExpanded[];
74
92
  showFeatures?: boolean;
75
93
  };
76
94
 
@@ -139,23 +157,74 @@ export default function PaymentSummary({
139
157
  action = '',
140
158
  trialEnd = 0,
141
159
  completed = false,
160
+ checkoutSession = undefined,
161
+ paymentMethods = [],
162
+ onPromotionUpdate = noop,
142
163
  showFeatures = false,
143
164
  ...rest
144
165
  }: Props) {
145
166
  const { t, locale } = useLocaleContext();
146
167
  const { isMobile } = useMobile();
147
- const settings = usePaymentContext();
168
+ const { paymentState, ...settings } = usePaymentContext();
148
169
  const [state, setState] = useSetState({ loading: false, shake: false, expanded: items?.length < 3 });
149
170
  const { data, runAsync } = useRequest(() =>
150
171
  checkoutSessionId ? fetchCrossSell(checkoutSessionId) : Promise.resolve(null)
151
172
  );
152
- const headlines = formatCheckoutHeadlines(items, currency, { trialEnd, trialInDays }, locale);
153
- const staking = showStaking ? getStakingSetup(items, currency, billingThreshold) : '0';
173
+
174
+ const sessionDiscounts = (checkoutSession as any)?.discounts || [];
175
+ const allowPromotionCodes = !!checkoutSession?.allow_promotion_codes;
176
+ const hasDiscounts = sessionDiscounts?.length > 0;
177
+
178
+ const discountCurrency =
179
+ paymentMethods && checkoutSession
180
+ ? (findCurrency(
181
+ paymentMethods as TPaymentMethodExpanded[],
182
+ hasDiscounts ? checkoutSession?.currency_id || currency.id : (currency.id as string)
183
+ ) as TPaymentCurrency) || settings.settings?.baseCurrency
184
+ : currency;
185
+
186
+ const headlines = formatCheckoutHeadlines(items, discountCurrency, { trialEnd, trialInDays }, locale);
187
+ const staking = showStaking ? getStakingSetup(items, discountCurrency, billingThreshold) : '0';
188
+
189
+ const getAppliedPromotionCodes = () => {
190
+ if (!sessionDiscounts?.length) return [];
191
+
192
+ return sessionDiscounts.map((discount: any) => ({
193
+ id: discount.promotion_code || discount.coupon,
194
+ code: discount.verification_data?.code || 'APPLIED',
195
+ discount_amount: discount.discount_amount,
196
+ }));
197
+ };
198
+
199
+ const handlePromotionUpdate = () => {
200
+ onPromotionUpdate?.();
201
+ };
202
+
203
+ const handleRemovePromotion = async (sessionId: string) => {
204
+ // Prevent removing promotion during payment process
205
+ if (paymentState.paying || paymentState.stripePaying) {
206
+ return;
207
+ }
208
+ try {
209
+ await api.delete(`/api/checkout-sessions/${sessionId}/remove-promotion`);
210
+ onPromotionUpdate?.();
211
+ } catch (err: any) {
212
+ console.error('Failed to remove promotion code:', err);
213
+ }
214
+ };
215
+
216
+ const discountAmount = new BN(checkoutSession?.total_details?.amount_discount || '0');
217
+
218
+ const subtotalAmount = fromUnitToToken(
219
+ new BN(fromTokenToUnit(headlines.actualAmount, discountCurrency?.decimal)).add(new BN(staking)).toString(),
220
+ discountCurrency?.decimal
221
+ );
154
222
 
155
223
  const totalAmount = fromUnitToToken(
156
- new BN(fromTokenToUnit(headlines.actualAmount, currency?.decimal)).add(new BN(staking)).toString(),
157
- currency?.decimal
224
+ new BN(fromTokenToUnit(subtotalAmount, discountCurrency?.decimal)).sub(discountAmount).toString(),
225
+ discountCurrency?.decimal
158
226
  );
227
+
159
228
  useBus(
160
229
  'error.REQUIRE_CROSS_SELL',
161
230
  () => {
@@ -217,20 +286,20 @@ export default function PaymentSummary({
217
286
  {items.map((x: TLineItemExpanded) =>
218
287
  x.price.custom_unit_amount && onChangeAmount && donationSettings ? (
219
288
  <ProductDonation
220
- key={`${x.price_id}-${currency.id}`}
289
+ key={`${x.price_id}-${discountCurrency.id}`}
221
290
  item={x}
222
291
  settings={donationSettings}
223
292
  onChange={onChangeAmount}
224
- currency={currency}
293
+ currency={discountCurrency}
225
294
  />
226
295
  ) : (
227
296
  <ProductItem
228
- key={`${x.price_id}-${currency.id}`}
297
+ key={`${x.price_id}-${discountCurrency.id}`}
229
298
  item={x}
230
299
  items={items}
231
300
  trialInDays={trialInDays}
232
301
  trialEnd={trialEnd}
233
- currency={currency}
302
+ currency={discountCurrency}
234
303
  onUpsell={handleUpsell}
235
304
  onDownsell={handleDownsell}
236
305
  adjustableQuantity={x.adjustable_quantity}
@@ -272,7 +341,7 @@ export default function PaymentSummary({
272
341
  item={{ quantity: 1, price: data, price_id: data.id, cross_sell: true } as TLineItemExpanded}
273
342
  items={items}
274
343
  trialInDays={trialInDays}
275
- currency={currency}
344
+ currency={discountCurrency}
276
345
  trialEnd={trialEnd}
277
346
  onUpsell={noop}
278
347
  onDownsell={noop}>
@@ -393,11 +462,127 @@ export default function PaymentSummary({
393
462
  </Tooltip>
394
463
  </Stack>
395
464
  <Typography>
396
- {formatAmount(staking, currency.decimal)} {currency.symbol}
465
+ {formatAmount(staking, discountCurrency.decimal)} {discountCurrency.symbol}
397
466
  </Typography>
398
467
  </Stack>
399
468
  </>
400
469
  )}
470
+ {(allowPromotionCodes || hasDiscounts) && (
471
+ <Stack
472
+ direction="row"
473
+ spacing={1}
474
+ sx={{
475
+ justifyContent: 'space-between',
476
+ alignItems: 'center',
477
+ ...(staking > 0 && {
478
+ borderTop: '1px solid',
479
+ borderColor: 'divider',
480
+ pt: 1,
481
+ mt: 1,
482
+ }),
483
+ }}>
484
+ <Typography className="base-label">{t('common.subtotal')}</Typography>
485
+ <Typography>
486
+ {formatNumber(subtotalAmount)} {discountCurrency.symbol}
487
+ </Typography>
488
+ </Stack>
489
+ )}
490
+ {/* Promotion Code Section - only show add button if no discounts applied */}
491
+ {allowPromotionCodes && !hasDiscounts && (
492
+ <Box sx={{ mt: 1 }}>
493
+ <PromotionCode
494
+ checkoutSessionId={checkoutSession.id}
495
+ initialAppliedCodes={getAppliedPromotionCodes()}
496
+ disabled={completed}
497
+ onUpdate={handlePromotionUpdate}
498
+ currencyId={currency.id}
499
+ />
500
+ </Box>
501
+ )}
502
+
503
+ {/* Promotion Code Details */}
504
+ {hasDiscounts && (
505
+ <Box
506
+ sx={{
507
+ py: 1.5,
508
+ }}>
509
+ {sessionDiscounts.map((discount: any) => {
510
+ const promotionCodeInfo = discount.promotion_code_details;
511
+ const couponInfo = discount.coupon_details;
512
+ const discountDescription = couponInfo ? formatCouponTerms(couponInfo, discountCurrency, locale) : '';
513
+ const notSupported = discountDescription === t('payment.checkout.coupon.noDiscount');
514
+
515
+ return (
516
+ <Stack key={discount.promotion_code || discount.coupon || `discount-${discount.discount_amount}`}>
517
+ <Stack
518
+ direction="row"
519
+ spacing={1}
520
+ sx={{
521
+ justifyContent: 'space-between',
522
+ alignItems: 'center',
523
+ }}>
524
+ <Stack
525
+ direction="row"
526
+ spacing={1}
527
+ sx={{
528
+ alignItems: 'center',
529
+ backgroundColor: 'grey.100',
530
+ width: 'fit-content',
531
+ px: 1,
532
+ py: 0.5,
533
+ borderRadius: 1,
534
+ }}>
535
+ <Typography
536
+ sx={{
537
+ fontWeight: 'medium',
538
+ fontSize: 'small',
539
+ display: 'flex',
540
+ alignItems: 'center',
541
+ gap: 0.5,
542
+ }}>
543
+ <LocalOffer sx={{ color: 'warning.main', fontSize: 'small' }} />
544
+ {promotionCodeInfo?.code || discount.verification_data?.code || t('payment.checkout.discount')}
545
+ </Typography>
546
+ {!completed && (
547
+ <Button
548
+ size="small"
549
+ disabled={paymentState.paying || paymentState.stripePaying}
550
+ onClick={() => handleRemovePromotion(checkoutSessionId)}
551
+ sx={{
552
+ minWidth: 'auto',
553
+ width: 16,
554
+ height: 16,
555
+ color: 'text.secondary',
556
+ '&.Mui-disabled': {
557
+ color: 'text.disabled',
558
+ },
559
+ }}>
560
+ <Close sx={{ fontSize: 14 }} />
561
+ </Button>
562
+ )}
563
+ </Stack>
564
+ <Typography sx={{ color: 'text.secondary' }}>
565
+ -{formatAmount(discount.discount_amount || '0', discountCurrency.decimal)}{' '}
566
+ {discountCurrency.symbol}
567
+ </Typography>
568
+ </Stack>
569
+ {/* Show discount description */}
570
+ {discountDescription && (
571
+ <Typography
572
+ sx={{
573
+ fontSize: 'small',
574
+ color: notSupported ? 'error.main' : 'text.secondary',
575
+ mt: 0.5,
576
+ }}>
577
+ {discountDescription}
578
+ </Typography>
579
+ )}
580
+ </Stack>
581
+ );
582
+ })}
583
+ </Box>
584
+ )}
585
+
401
586
  <Stack
402
587
  sx={{
403
588
  display: 'flex',
@@ -407,7 +592,7 @@ export default function PaymentSummary({
407
592
  width: '100%',
408
593
  }}>
409
594
  <Box className="base-label">{t('common.total')} </Box>
410
- <PaymentAmount amount={`${totalAmount} ${currency.symbol}`} sx={{ fontSize: '16px' }} />
595
+ <PaymentAmount amount={`${totalAmount} ${discountCurrency.symbol}`} sx={{ fontSize: '16px' }} />
411
596
  </Stack>
412
597
  {headlines.then && headlines.showThen && (
413
598
  <Typography