@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.
- package/es/components/promotion-code.d.ts +19 -0
- package/es/components/promotion-code.js +153 -0
- package/es/contexts/payment.d.ts +8 -0
- package/es/contexts/payment.js +10 -1
- package/es/index.d.ts +2 -1
- package/es/index.js +3 -1
- package/es/libs/util.d.ts +5 -1
- package/es/libs/util.js +23 -0
- package/es/locales/en.js +40 -15
- package/es/locales/zh.js +29 -0
- package/es/payment/form/index.js +7 -1
- package/es/payment/index.js +19 -0
- package/es/payment/product-item.js +32 -3
- package/es/payment/summary.d.ts +5 -2
- package/es/payment/summary.js +193 -16
- package/lib/components/promotion-code.d.ts +19 -0
- package/lib/components/promotion-code.js +155 -0
- package/lib/contexts/payment.d.ts +8 -0
- package/lib/contexts/payment.js +13 -1
- package/lib/index.d.ts +2 -1
- package/lib/index.js +8 -0
- package/lib/libs/util.d.ts +5 -1
- package/lib/libs/util.js +29 -0
- package/lib/locales/en.js +40 -15
- package/lib/locales/zh.js +29 -0
- package/lib/payment/form/index.js +8 -1
- package/lib/payment/index.js +23 -0
- package/lib/payment/product-item.js +46 -0
- package/lib/payment/summary.d.ts +5 -2
- package/lib/payment/summary.js +153 -11
- package/package.json +9 -9
- package/src/components/promotion-code.tsx +184 -0
- package/src/contexts/payment.tsx +15 -0
- package/src/index.ts +2 -0
- package/src/libs/util.ts +35 -0
- package/src/locales/en.tsx +40 -15
- package/src/locales/zh.tsx +29 -0
- package/src/payment/form/index.tsx +10 -1
- package/src/payment/index.tsx +22 -0
- package/src/payment/product-item.tsx +37 -2
- package/src/payment/summary.tsx +201 -16
package/src/payment/summary.tsx
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/indent */
|
|
1
2
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
|
|
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 {
|
|
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
|
-
|
|
153
|
-
const
|
|
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(
|
|
157
|
-
|
|
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}-${
|
|
289
|
+
key={`${x.price_id}-${discountCurrency.id}`}
|
|
221
290
|
item={x}
|
|
222
291
|
settings={donationSettings}
|
|
223
292
|
onChange={onChangeAmount}
|
|
224
|
-
currency={
|
|
293
|
+
currency={discountCurrency}
|
|
225
294
|
/>
|
|
226
295
|
) : (
|
|
227
296
|
<ProductItem
|
|
228
|
-
key={`${x.price_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={
|
|
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={
|
|
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,
|
|
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} ${
|
|
595
|
+
<PaymentAmount amount={`${totalAmount} ${discountCurrency.symbol}`} sx={{ fontSize: '16px' }} />
|
|
411
596
|
</Stack>
|
|
412
597
|
{headlines.then && headlines.showThen && (
|
|
413
598
|
<Typography
|