@blocklet/payment-react 1.19.0 → 1.19.1
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/blockchain/tx.d.ts +1 -1
- package/es/components/blockchain/tx.js +9 -11
- package/es/components/country-select.d.ts +1 -1
- package/es/components/date-range-picker.d.ts +13 -0
- package/es/components/date-range-picker.js +279 -0
- package/es/components/input.d.ts +5 -2
- package/es/components/input.js +6 -2
- package/es/components/label.d.ts +7 -0
- package/es/components/label.js +49 -0
- package/es/components/loading-button.d.ts +1 -1
- package/es/history/credit/grants-list.d.ts +14 -0
- package/es/history/credit/grants-list.js +215 -0
- package/es/history/credit/transactions-list.d.ts +13 -0
- package/es/history/credit/transactions-list.js +255 -0
- package/es/history/invoice/list.js +21 -1
- package/es/index.d.ts +5 -1
- package/es/index.js +10 -1
- package/es/libs/util.d.ts +2 -0
- package/es/libs/util.js +12 -0
- package/es/locales/en.js +20 -2
- package/es/locales/zh.js +20 -2
- package/es/payment/form/index.js +44 -6
- package/es/payment/index.js +18 -3
- package/es/payment/product-item.d.ts +8 -1
- package/es/payment/product-item.js +137 -5
- package/es/payment/summary.d.ts +3 -1
- package/es/payment/summary.js +9 -0
- package/lib/components/blockchain/tx.d.ts +1 -1
- package/lib/components/blockchain/tx.js +9 -8
- package/lib/components/country-select.d.ts +1 -1
- package/lib/components/date-range-picker.d.ts +13 -0
- package/lib/components/date-range-picker.js +329 -0
- package/lib/components/input.d.ts +5 -2
- package/lib/components/input.js +8 -4
- package/lib/components/label.d.ts +7 -0
- package/lib/components/label.js +60 -0
- package/lib/components/loading-button.d.ts +1 -1
- package/lib/history/credit/grants-list.d.ts +14 -0
- package/lib/history/credit/grants-list.js +277 -0
- package/lib/history/credit/transactions-list.d.ts +13 -0
- package/lib/history/credit/transactions-list.js +301 -0
- package/lib/history/invoice/list.js +24 -0
- package/lib/index.d.ts +5 -1
- package/lib/index.js +39 -0
- package/lib/libs/util.d.ts +2 -0
- package/lib/libs/util.js +14 -0
- package/lib/locales/en.js +20 -2
- package/lib/locales/zh.js +20 -2
- package/lib/payment/form/index.js +45 -6
- package/lib/payment/index.js +20 -2
- package/lib/payment/product-item.d.ts +8 -1
- package/lib/payment/product-item.js +144 -4
- package/lib/payment/summary.d.ts +3 -1
- package/lib/payment/summary.js +9 -0
- package/package.json +3 -3
- package/src/components/blockchain/tx.tsx +9 -15
- package/src/components/country-select.tsx +2 -2
- package/src/components/date-range-picker.tsx +310 -0
- package/src/components/input.tsx +14 -3
- package/src/components/label.tsx +58 -0
- package/src/components/loading-button.tsx +1 -1
- package/src/history/credit/grants-list.tsx +276 -0
- package/src/history/credit/transactions-list.tsx +317 -0
- package/src/history/invoice/list.tsx +18 -1
- package/src/index.ts +9 -0
- package/src/libs/util.ts +14 -0
- package/src/locales/en.tsx +20 -0
- package/src/locales/zh.tsx +19 -0
- package/src/payment/form/address.tsx +2 -2
- package/src/payment/form/index.tsx +110 -52
- package/src/payment/index.tsx +17 -1
- package/src/payment/product-item.tsx +152 -4
- package/src/payment/summary.tsx +13 -2
package/src/locales/en.tsx
CHANGED
|
@@ -30,6 +30,10 @@ export default flat({
|
|
|
30
30
|
remove: 'Remove',
|
|
31
31
|
removed: 'Resource removed',
|
|
32
32
|
confirm: 'Confirm',
|
|
33
|
+
clear: 'Clear',
|
|
34
|
+
selectTimeRange: 'Select time range',
|
|
35
|
+
startDate: 'Start date',
|
|
36
|
+
endDate: 'End date',
|
|
33
37
|
upload: 'Upload',
|
|
34
38
|
change: 'Change',
|
|
35
39
|
cancel: 'Cancel',
|
|
@@ -103,6 +107,7 @@ export default flat({
|
|
|
103
107
|
confirm: 'Confirm',
|
|
104
108
|
cancel: 'Cancel',
|
|
105
109
|
},
|
|
110
|
+
paymentMethod: 'Payment Method',
|
|
106
111
|
},
|
|
107
112
|
payment: {
|
|
108
113
|
checkout: {
|
|
@@ -209,6 +214,11 @@ export default flat({
|
|
|
209
214
|
add: 'Add to order',
|
|
210
215
|
remove: 'Remove from order',
|
|
211
216
|
},
|
|
217
|
+
credit: {
|
|
218
|
+
oneTimeInfo: 'You will receive {amount} {symbol} credits after payment',
|
|
219
|
+
recurringInfo: 'You will receive {amount} {symbol} credits {period}',
|
|
220
|
+
expiresIn: 'credits have a validity period of {duration} {unit}',
|
|
221
|
+
},
|
|
212
222
|
expired: {
|
|
213
223
|
title: 'Expired Link',
|
|
214
224
|
description:
|
|
@@ -241,6 +251,16 @@ export default flat({
|
|
|
241
251
|
amount: 'Amount',
|
|
242
252
|
failed: 'Account changed, please pay manually.',
|
|
243
253
|
balanceLink: 'View Balance',
|
|
254
|
+
credit: {
|
|
255
|
+
title: 'Confirm Credit Payment',
|
|
256
|
+
availableAmount: 'Available Credit: {amount}',
|
|
257
|
+
confirmMessage: 'You will use {amount} credits to subscribe to this service.',
|
|
258
|
+
meteringSubscriptionMessage:
|
|
259
|
+
'This subscription service will deduct credits in real-time based on actual usage. You currently have {available} credits available. Confirm to continue?',
|
|
260
|
+
insufficientTitle: 'Insufficient Credit',
|
|
261
|
+
insufficientMessage:
|
|
262
|
+
'This subscription service will deduct credits in real-time based on actual usage. You currently have insufficient credits. Please top up your credits first.',
|
|
263
|
+
},
|
|
244
264
|
},
|
|
245
265
|
},
|
|
246
266
|
customer: {
|
package/src/locales/zh.tsx
CHANGED
|
@@ -33,6 +33,10 @@ export default flat({
|
|
|
33
33
|
change: '更换',
|
|
34
34
|
confirm: '确认',
|
|
35
35
|
cancel: '取消',
|
|
36
|
+
clear: '清空',
|
|
37
|
+
selectTimeRange: '选择时间范围',
|
|
38
|
+
startDate: '开始日期',
|
|
39
|
+
endDate: '结束日期',
|
|
36
40
|
close: '关闭',
|
|
37
41
|
back: '返回',
|
|
38
42
|
every: '每',
|
|
@@ -103,6 +107,7 @@ export default flat({
|
|
|
103
107
|
confirm: '确认',
|
|
104
108
|
cancel: '取消',
|
|
105
109
|
},
|
|
110
|
+
paymentMethod: '支付方式',
|
|
106
111
|
},
|
|
107
112
|
payment: {
|
|
108
113
|
checkout: {
|
|
@@ -215,6 +220,11 @@ export default flat({
|
|
|
215
220
|
add: '添加到订单',
|
|
216
221
|
remove: '从订单移除',
|
|
217
222
|
},
|
|
223
|
+
credit: {
|
|
224
|
+
oneTimeInfo: '付款完成后您将获得 {amount} {symbol} 额度',
|
|
225
|
+
recurringInfo: '您将{period}获得 {amount} {symbol} 额度',
|
|
226
|
+
expiresIn: '额度有效期为 {duration} {unit}',
|
|
227
|
+
},
|
|
218
228
|
emptyItems: {
|
|
219
229
|
title: '没有任何购买项目',
|
|
220
230
|
description: '可能这个付款链接没有正确配置',
|
|
@@ -236,6 +246,15 @@ export default flat({
|
|
|
236
246
|
amount: '支付金额',
|
|
237
247
|
failed: '账户发生变化,无法自动完成支付,请手动支付。',
|
|
238
248
|
balanceLink: '查看余额',
|
|
249
|
+
credit: {
|
|
250
|
+
title: '确认额度支付',
|
|
251
|
+
availableAmount: '可用额度:{amount}',
|
|
252
|
+
confirmMessage: '您将使用 {amount} 额度来订阅此服务。',
|
|
253
|
+
meteringSubscriptionMessage:
|
|
254
|
+
'此订阅服务将根据实际使用情况实时扣除额度。您当前可用额度为 {available},确认是否继续?',
|
|
255
|
+
insufficientTitle: '额度不足',
|
|
256
|
+
insufficientMessage: '此订阅服务将根据实际使用情况实时扣除额度。您当前可用额度不足,请先充值额度。',
|
|
257
|
+
},
|
|
239
258
|
},
|
|
240
259
|
},
|
|
241
260
|
customer: {
|
|
@@ -79,7 +79,7 @@ export default function AddressForm({ mode, stripe, sx = {}, fieldValidation = {
|
|
|
79
79
|
render={({ field }) => (
|
|
80
80
|
<CountrySelect
|
|
81
81
|
{...field}
|
|
82
|
-
ref={field.ref as unknown as React.RefObject<HTMLDivElement>}
|
|
82
|
+
ref={field.ref as unknown as React.RefObject<HTMLDivElement | null>}
|
|
83
83
|
sx={{
|
|
84
84
|
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
|
85
85
|
borderColor: 'transparent',
|
|
@@ -127,7 +127,7 @@ export default function AddressForm({ mode, stripe, sx = {}, fieldValidation = {
|
|
|
127
127
|
render={({ field }) => (
|
|
128
128
|
<CountrySelect
|
|
129
129
|
{...field}
|
|
130
|
-
ref={field.ref as unknown as React.RefObject<HTMLDivElement>}
|
|
130
|
+
ref={field.ref as unknown as React.RefObject<HTMLDivElement | null>}
|
|
131
131
|
sx={{
|
|
132
132
|
'.MuiOutlinedInput-notchedOutline': {
|
|
133
133
|
borderColor: 'transparent !important',
|
|
@@ -110,9 +110,11 @@ type UserInfo = {
|
|
|
110
110
|
type FastCheckoutInfo = {
|
|
111
111
|
open: boolean;
|
|
112
112
|
loading: boolean;
|
|
113
|
-
sourceType: 'balance' | 'delegation';
|
|
113
|
+
sourceType: 'balance' | 'delegation' | 'credit';
|
|
114
114
|
amount: string;
|
|
115
115
|
payer?: string;
|
|
116
|
+
availableCredit?: string;
|
|
117
|
+
balance?: string;
|
|
116
118
|
};
|
|
117
119
|
|
|
118
120
|
const setUserFormValues = (
|
|
@@ -206,6 +208,9 @@ export default function PaymentForm({
|
|
|
206
208
|
customerLimited?: boolean;
|
|
207
209
|
stripePaying: boolean;
|
|
208
210
|
fastCheckoutInfo: FastCheckoutInfo | null;
|
|
211
|
+
creditInsufficientInfo: {
|
|
212
|
+
open: boolean;
|
|
213
|
+
} | null;
|
|
209
214
|
}>({
|
|
210
215
|
submitting: false,
|
|
211
216
|
paying: false,
|
|
@@ -216,6 +221,7 @@ export default function PaymentForm({
|
|
|
216
221
|
customerLimited: false,
|
|
217
222
|
stripePaying: false,
|
|
218
223
|
fastCheckoutInfo: null,
|
|
224
|
+
creditInsufficientInfo: null,
|
|
219
225
|
});
|
|
220
226
|
|
|
221
227
|
const currencies = flattenPaymentMethods(paymentMethods);
|
|
@@ -418,6 +424,10 @@ export default function PaymentForm({
|
|
|
418
424
|
setState({ fastCheckoutInfo: null });
|
|
419
425
|
};
|
|
420
426
|
|
|
427
|
+
const handleCreditInsufficientClose = () => {
|
|
428
|
+
setState({ creditInsufficientInfo: null });
|
|
429
|
+
};
|
|
430
|
+
|
|
421
431
|
const openConnect = () => {
|
|
422
432
|
try {
|
|
423
433
|
if (!['arcblock', 'ethereum', 'base'].includes(method.type)) {
|
|
@@ -476,7 +486,30 @@ export default function PaymentForm({
|
|
|
476
486
|
});
|
|
477
487
|
|
|
478
488
|
if (['arcblock', 'ethereum', 'base'].includes(method.type)) {
|
|
479
|
-
|
|
489
|
+
// 优先判断 credit 支付
|
|
490
|
+
if (paymentCurrency?.type === 'credit') {
|
|
491
|
+
if (result.data.creditSufficient === true) {
|
|
492
|
+
// 如果是 credit 支付且有足够额度,显示 credit 确认弹窗
|
|
493
|
+
setState({
|
|
494
|
+
fastCheckoutInfo: {
|
|
495
|
+
open: true,
|
|
496
|
+
loading: false,
|
|
497
|
+
sourceType: 'credit',
|
|
498
|
+
amount: result.data.fastPayInfo?.amount || '0',
|
|
499
|
+
payer: result.data.fastPayInfo?.payer,
|
|
500
|
+
availableCredit: result.data.fastPayInfo?.amount || '0',
|
|
501
|
+
balance: result.data.fastPayInfo?.token?.balance || '0',
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
} else {
|
|
505
|
+
// 如果是 credit 支付但额度不足,显示额度不足弹窗
|
|
506
|
+
setState({
|
|
507
|
+
creditInsufficientInfo: {
|
|
508
|
+
open: true,
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
} else if (
|
|
480
513
|
(result.data.balance?.sufficient || result.data.delegation?.sufficient) &&
|
|
481
514
|
!isDonationMode &&
|
|
482
515
|
result.data.fastPayInfo
|
|
@@ -581,73 +614,96 @@ export default function PaymentForm({
|
|
|
581
614
|
}, [state.submitting, state.paying, state.stripePaying, quantityInventoryStatus, payable]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
582
615
|
|
|
583
616
|
const balanceLink = getTokenBalanceLink(method, state.fastCheckoutInfo?.payer || '');
|
|
617
|
+
|
|
584
618
|
const FastCheckoutConfirmDialog = state.fastCheckoutInfo && (
|
|
585
619
|
<ConfirmDialog
|
|
586
620
|
onConfirm={handleFastCheckoutConfirm}
|
|
587
621
|
onCancel={handleFastCheckoutCancel}
|
|
588
|
-
title={
|
|
622
|
+
title={
|
|
623
|
+
state.fastCheckoutInfo.sourceType === 'credit'
|
|
624
|
+
? t('payment.checkout.fastPay.credit.title')
|
|
625
|
+
: t('payment.checkout.fastPay.title')
|
|
626
|
+
}
|
|
589
627
|
message={
|
|
590
|
-
|
|
591
|
-
<Typography>
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
628
|
+
state.fastCheckoutInfo.sourceType === 'credit' ? (
|
|
629
|
+
<Typography>
|
|
630
|
+
{t('payment.checkout.fastPay.credit.meteringSubscriptionMessage', {
|
|
631
|
+
available: `${fromUnitToToken(state.fastCheckoutInfo?.balance || '0', paymentCurrency?.decimal || 18).toString()} ${paymentCurrency?.symbol}`,
|
|
632
|
+
})}
|
|
633
|
+
</Typography>
|
|
634
|
+
) : (
|
|
635
|
+
<Stack>
|
|
636
|
+
<Typography>{t('payment.checkout.fastPay.autoPaymentReason')}</Typography>
|
|
637
|
+
<Divider sx={{ mt: 1.5, mb: 1.5 }} />
|
|
638
|
+
<Stack spacing={1}>
|
|
639
|
+
<Stack
|
|
601
640
|
sx={{
|
|
602
|
-
|
|
603
|
-
|
|
641
|
+
flexDirection: 'row',
|
|
642
|
+
alignItems: 'center',
|
|
643
|
+
justifyContent: 'space-between',
|
|
604
644
|
}}>
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
<Typography
|
|
645
|
+
<Typography
|
|
646
|
+
sx={{
|
|
647
|
+
color: 'text.primary',
|
|
648
|
+
whiteSpace: 'nowrap',
|
|
649
|
+
}}>
|
|
650
|
+
{t('payment.checkout.fastPay.payer')}
|
|
651
|
+
</Typography>
|
|
652
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
|
653
|
+
<DID did={state.fastCheckoutInfo.payer || ''} compact responsive={false} />
|
|
654
|
+
{balanceLink && (
|
|
655
|
+
<Tooltip title={t('payment.checkout.fastPay.balanceLink')} placement="top">
|
|
656
|
+
<OpenInNew
|
|
657
|
+
sx={{
|
|
658
|
+
color: 'text.lighter',
|
|
659
|
+
fontSize: '0.85rem',
|
|
660
|
+
cursor: 'pointer',
|
|
661
|
+
'&:hover': { color: 'text.primary' },
|
|
662
|
+
}}
|
|
663
|
+
onClick={() => {
|
|
664
|
+
window.open(balanceLink, '_blank');
|
|
665
|
+
}}
|
|
666
|
+
/>
|
|
667
|
+
</Tooltip>
|
|
668
|
+
)}
|
|
669
|
+
</Box>
|
|
670
|
+
</Stack>
|
|
671
|
+
<Stack
|
|
633
672
|
sx={{
|
|
634
|
-
|
|
673
|
+
flexDirection: 'row',
|
|
674
|
+
alignItems: 'center',
|
|
675
|
+
justifyContent: 'space-between',
|
|
635
676
|
}}>
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
677
|
+
<Typography
|
|
678
|
+
sx={{
|
|
679
|
+
color: 'text.primary',
|
|
680
|
+
}}>
|
|
681
|
+
{t('payment.checkout.fastPay.amount')}
|
|
682
|
+
</Typography>
|
|
683
|
+
<Typography>
|
|
684
|
+
{fromUnitToToken(state.fastCheckoutInfo.amount, paymentCurrency?.decimal || 18).toString()}{' '}
|
|
685
|
+
{paymentCurrency?.symbol}
|
|
686
|
+
</Typography>
|
|
687
|
+
</Stack>
|
|
642
688
|
</Stack>
|
|
643
689
|
</Stack>
|
|
644
|
-
|
|
690
|
+
)
|
|
645
691
|
}
|
|
646
692
|
loading={state.fastCheckoutInfo.loading}
|
|
647
693
|
color="primary"
|
|
648
694
|
/>
|
|
649
695
|
);
|
|
650
696
|
|
|
697
|
+
const CreditInsufficientDialog = state.creditInsufficientInfo && (
|
|
698
|
+
<ConfirmDialog
|
|
699
|
+
onConfirm={handleCreditInsufficientClose}
|
|
700
|
+
onCancel={handleCreditInsufficientClose}
|
|
701
|
+
title={t('payment.checkout.fastPay.credit.insufficientTitle')}
|
|
702
|
+
message={<Typography>{t('payment.checkout.fastPay.credit.insufficientMessage')}</Typography>}
|
|
703
|
+
confirm={t('common.confirm')}
|
|
704
|
+
/>
|
|
705
|
+
);
|
|
706
|
+
|
|
651
707
|
if (onlyShowBtn) {
|
|
652
708
|
return (
|
|
653
709
|
<>
|
|
@@ -700,6 +756,7 @@ export default function PaymentForm({
|
|
|
700
756
|
/>
|
|
701
757
|
)}
|
|
702
758
|
{FastCheckoutConfirmDialog}
|
|
759
|
+
{CreditInsufficientDialog}
|
|
703
760
|
</>
|
|
704
761
|
);
|
|
705
762
|
}
|
|
@@ -890,6 +947,7 @@ export default function PaymentForm({
|
|
|
890
947
|
/>
|
|
891
948
|
)}
|
|
892
949
|
{FastCheckoutConfirmDialog}
|
|
950
|
+
{CreditInsufficientDialog}
|
|
893
951
|
</>
|
|
894
952
|
);
|
|
895
953
|
}
|
package/src/payment/index.tsx
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
getStatementDescriptor,
|
|
31
31
|
isMobileSafari,
|
|
32
32
|
isValidCountry,
|
|
33
|
+
showStaking,
|
|
33
34
|
} from '../libs/util';
|
|
34
35
|
import type { CheckoutCallbacks, CheckoutContext, CheckoutFormData } from '../types';
|
|
35
36
|
import PaymentError from './error';
|
|
@@ -208,6 +209,19 @@ function PaymentInner({
|
|
|
208
209
|
}
|
|
209
210
|
};
|
|
210
211
|
|
|
212
|
+
const onQuantityChange = async (itemId: string, quantity: number) => {
|
|
213
|
+
try {
|
|
214
|
+
const { data } = await api.put(`/api/checkout-sessions/${state.checkoutSession.id}/adjust-quantity`, {
|
|
215
|
+
itemId,
|
|
216
|
+
quantity,
|
|
217
|
+
});
|
|
218
|
+
setState({ checkoutSession: data });
|
|
219
|
+
} catch (err) {
|
|
220
|
+
console.error(err);
|
|
221
|
+
Toast.error(formatError(err));
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
211
225
|
const onCancelCrossSell = async () => {
|
|
212
226
|
try {
|
|
213
227
|
const { data } = await api.delete(`/api/checkout-sessions/${state.checkoutSession.id}/cross-sell`);
|
|
@@ -263,10 +277,11 @@ function PaymentInner({
|
|
|
263
277
|
// @ts-ignore
|
|
264
278
|
state.checkoutSession.subscription_data?.min_stake_amount || 0
|
|
265
279
|
)}
|
|
266
|
-
showStaking={method
|
|
280
|
+
showStaking={showStaking(method, currency, !!state.checkoutSession.subscription_data?.no_stake)}
|
|
267
281
|
currency={currency}
|
|
268
282
|
onUpsell={onUpsell}
|
|
269
283
|
onDownsell={onDownsell}
|
|
284
|
+
onQuantityChange={onQuantityChange}
|
|
270
285
|
onApplyCrossSell={onApplyCrossSell}
|
|
271
286
|
onCancelCrossSell={onCancelCrossSell}
|
|
272
287
|
onChangeAmount={onChangeAmount}
|
|
@@ -274,6 +289,7 @@ function PaymentInner({
|
|
|
274
289
|
crossSellBehavior={state.checkoutSession.cross_sell_behavior}
|
|
275
290
|
donationSettings={paymentLink?.donation_settings}
|
|
276
291
|
action={action}
|
|
292
|
+
completed={completed}
|
|
277
293
|
/>
|
|
278
294
|
{mode === 'standalone' && !isMobile && (
|
|
279
295
|
<CheckoutFooter className="cko-footer" sx={{ color: 'text.lighter' }} />
|
|
@@ -1,11 +1,13 @@
|
|
|
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 } from '@mui/material';
|
|
3
|
+
import { Box, Stack, Typography, IconButton, TextField, Alert } from '@mui/material';
|
|
4
|
+
import { Add, Remove } from '@mui/icons-material';
|
|
4
5
|
|
|
5
|
-
import React, { useMemo } from 'react';
|
|
6
|
+
import React, { useMemo, useState } from 'react';
|
|
6
7
|
import Status from '../components/status';
|
|
7
8
|
import Switch from '../components/switch-button';
|
|
8
9
|
import {
|
|
10
|
+
findCurrency,
|
|
9
11
|
formatLineItemPricing,
|
|
10
12
|
formatPrice,
|
|
11
13
|
formatQuantityInventory,
|
|
@@ -14,6 +16,7 @@ import {
|
|
|
14
16
|
} from '../libs/util';
|
|
15
17
|
import ProductCard from './product-card';
|
|
16
18
|
import dayjs from '../libs/dayjs';
|
|
19
|
+
import { usePaymentContext } from '../contexts/payment';
|
|
17
20
|
|
|
18
21
|
type Props = {
|
|
19
22
|
item: TLineItemExpanded;
|
|
@@ -25,6 +28,14 @@ type Props = {
|
|
|
25
28
|
onDownsell: Function;
|
|
26
29
|
mode?: 'normal' | 'cross-sell';
|
|
27
30
|
children?: React.ReactNode;
|
|
31
|
+
// 数量调整相关
|
|
32
|
+
adjustableQuantity?: {
|
|
33
|
+
enabled: boolean;
|
|
34
|
+
minimum?: number;
|
|
35
|
+
maximum?: number;
|
|
36
|
+
};
|
|
37
|
+
onQuantityChange?: (itemId: string, quantity: number) => void;
|
|
38
|
+
completed?: boolean;
|
|
28
39
|
};
|
|
29
40
|
|
|
30
41
|
export default function ProductItem({
|
|
@@ -37,12 +48,90 @@ export default function ProductItem({
|
|
|
37
48
|
children = null,
|
|
38
49
|
onUpsell,
|
|
39
50
|
onDownsell,
|
|
51
|
+
completed = false,
|
|
52
|
+
adjustableQuantity = { enabled: false },
|
|
53
|
+
onQuantityChange = () => {},
|
|
40
54
|
}: Props) {
|
|
41
55
|
const { t, locale } = useLocaleContext();
|
|
56
|
+
const { settings } = usePaymentContext();
|
|
42
57
|
const pricing = formatLineItemPricing(item, currency, { trialEnd, trialInDays }, locale);
|
|
43
58
|
const saving = formatUpsellSaving(items, currency);
|
|
44
59
|
const metered = item.price?.recurring?.usage_type === 'metered' ? t('common.metered') : '';
|
|
45
60
|
const canUpsell = mode === 'normal' && items.length === 1;
|
|
61
|
+
|
|
62
|
+
const isCreditProduct = item.price.product?.type === 'credit' && item.price.metadata?.credit_config?.credit_amount;
|
|
63
|
+
const creditAmount = isCreditProduct ? Number(item.price.metadata.credit_config.credit_amount) : 0;
|
|
64
|
+
const creditCurrency = isCreditProduct
|
|
65
|
+
? findCurrency(settings.paymentMethods, item.price.metadata?.credit_config?.currency_id ?? '')
|
|
66
|
+
: null;
|
|
67
|
+
const validDuration = item.price.metadata?.credit_config?.valid_duration_value;
|
|
68
|
+
const validDurationUnit = item.price.metadata?.credit_config?.valid_duration_unit || 'days';
|
|
69
|
+
|
|
70
|
+
const [localQuantity, setLocalQuantity] = useState(item.quantity);
|
|
71
|
+
const canAdjustQuantity = adjustableQuantity.enabled && mode === 'normal';
|
|
72
|
+
const minQuantity = Math.max(adjustableQuantity.minimum || 1, 1);
|
|
73
|
+
const quantityAvailable = Math.min(item.price.quantity_limit_per_checkout, item.price.quantity_available);
|
|
74
|
+
const maxQuantity = Math.min(adjustableQuantity.maximum || 999, quantityAvailable || 999);
|
|
75
|
+
|
|
76
|
+
const handleQuantityChange = (newQuantity: number) => {
|
|
77
|
+
if (newQuantity >= minQuantity && newQuantity <= maxQuantity) {
|
|
78
|
+
setLocalQuantity(newQuantity);
|
|
79
|
+
if (formatQuantityInventory(item.price, newQuantity, locale)) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
onQuantityChange(item.price_id, newQuantity);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const handleQuantityIncrease = () => {
|
|
87
|
+
if (localQuantity < maxQuantity) {
|
|
88
|
+
handleQuantityChange(localQuantity + 1);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const handleQuantityDecrease = () => {
|
|
93
|
+
if (localQuantity > minQuantity) {
|
|
94
|
+
handleQuantityChange(localQuantity - 1);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const handleQuantityInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
99
|
+
const value = parseInt(event.target.value, 10);
|
|
100
|
+
if (!Number.isNaN(value)) {
|
|
101
|
+
handleQuantityChange(value);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Credit 信息格式化
|
|
106
|
+
const formatCreditInfo = () => {
|
|
107
|
+
if (!isCreditProduct) return null;
|
|
108
|
+
|
|
109
|
+
const isRecurring = item.price.type === 'recurring';
|
|
110
|
+
const totalCredit = creditAmount * localQuantity;
|
|
111
|
+
|
|
112
|
+
let message = '';
|
|
113
|
+
if (isRecurring) {
|
|
114
|
+
message = t('payment.checkout.credit.recurringInfo', {
|
|
115
|
+
amount: totalCredit,
|
|
116
|
+
period: formatRecurring(item.price.recurring!, true, 'per', locale),
|
|
117
|
+
});
|
|
118
|
+
} else {
|
|
119
|
+
message = t('payment.checkout.credit.oneTimeInfo', {
|
|
120
|
+
amount: totalCredit,
|
|
121
|
+
symbol: creditCurrency?.symbol || 'Credits',
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (validDuration && validDuration > 0) {
|
|
126
|
+
message += `,${t('payment.checkout.credit.expiresIn', {
|
|
127
|
+
duration: validDuration,
|
|
128
|
+
unit: t(`common.${validDurationUnit}`),
|
|
129
|
+
})}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return message;
|
|
133
|
+
};
|
|
134
|
+
|
|
46
135
|
const primaryText = useMemo(() => {
|
|
47
136
|
const price = item.upsell_price || item.price || {};
|
|
48
137
|
const isRecurring = price?.type === 'recurring' && price?.recurring;
|
|
@@ -53,6 +142,8 @@ export default function ProductItem({
|
|
|
53
142
|
return pricing.primary;
|
|
54
143
|
}, [trialInDays, trialEnd, pricing, item, locale]);
|
|
55
144
|
|
|
145
|
+
const quantityInventoryError = formatQuantityInventory(item.price, localQuantity, locale);
|
|
146
|
+
|
|
56
147
|
return (
|
|
57
148
|
<Stack
|
|
58
149
|
direction="column"
|
|
@@ -108,9 +199,9 @@ export default function ProductItem({
|
|
|
108
199
|
)}
|
|
109
200
|
</Stack>
|
|
110
201
|
</Stack>
|
|
111
|
-
{
|
|
202
|
+
{quantityInventoryError ? (
|
|
112
203
|
<Status
|
|
113
|
-
label={
|
|
204
|
+
label={quantityInventoryError}
|
|
114
205
|
variant="outlined"
|
|
115
206
|
sx={{
|
|
116
207
|
mt: 1,
|
|
@@ -120,6 +211,63 @@ export default function ProductItem({
|
|
|
120
211
|
}}
|
|
121
212
|
/>
|
|
122
213
|
) : null}
|
|
214
|
+
|
|
215
|
+
{/* 数量调整器 */}
|
|
216
|
+
{canAdjustQuantity && !completed && (
|
|
217
|
+
<Box sx={{ mt: 1, p: 1 }}>
|
|
218
|
+
<Stack
|
|
219
|
+
direction="row"
|
|
220
|
+
spacing={1}
|
|
221
|
+
sx={{
|
|
222
|
+
alignItems: 'center',
|
|
223
|
+
}}>
|
|
224
|
+
<Typography
|
|
225
|
+
variant="body2"
|
|
226
|
+
sx={{
|
|
227
|
+
color: 'text.secondary',
|
|
228
|
+
minWidth: 'fit-content',
|
|
229
|
+
}}>
|
|
230
|
+
{t('common.quantity')}:
|
|
231
|
+
</Typography>
|
|
232
|
+
<IconButton
|
|
233
|
+
size="small"
|
|
234
|
+
onClick={handleQuantityDecrease}
|
|
235
|
+
disabled={localQuantity <= minQuantity}
|
|
236
|
+
sx={{ minWidth: 32, width: 32, height: 32 }}>
|
|
237
|
+
<Remove fontSize="small" />
|
|
238
|
+
</IconButton>
|
|
239
|
+
<TextField
|
|
240
|
+
size="small"
|
|
241
|
+
value={localQuantity}
|
|
242
|
+
onChange={handleQuantityInputChange}
|
|
243
|
+
sx={{ width: 60 }}
|
|
244
|
+
type="number"
|
|
245
|
+
slotProps={{
|
|
246
|
+
htmlInput: {
|
|
247
|
+
min: minQuantity,
|
|
248
|
+
max: maxQuantity,
|
|
249
|
+
style: { textAlign: 'center', padding: '4px' },
|
|
250
|
+
},
|
|
251
|
+
}}
|
|
252
|
+
/>
|
|
253
|
+
<IconButton
|
|
254
|
+
size="small"
|
|
255
|
+
onClick={handleQuantityIncrease}
|
|
256
|
+
disabled={localQuantity >= maxQuantity}
|
|
257
|
+
sx={{ minWidth: 32, width: 32, height: 32 }}>
|
|
258
|
+
<Add fontSize="small" />
|
|
259
|
+
</IconButton>
|
|
260
|
+
</Stack>
|
|
261
|
+
</Box>
|
|
262
|
+
)}
|
|
263
|
+
|
|
264
|
+
{/* Credit 信息展示 */}
|
|
265
|
+
{isCreditProduct && (
|
|
266
|
+
<Alert severity="info" sx={{ mt: 1, fontSize: '0.875rem' }} icon={false}>
|
|
267
|
+
{formatCreditInfo()}
|
|
268
|
+
</Alert>
|
|
269
|
+
)}
|
|
270
|
+
|
|
123
271
|
{children}
|
|
124
272
|
</Stack>
|
|
125
273
|
{canUpsell && !item.upsell_price_id && item.price.upsell?.upsells_to && (
|
package/src/payment/summary.tsx
CHANGED
|
@@ -62,6 +62,7 @@ type Props = {
|
|
|
62
62
|
showStaking?: boolean;
|
|
63
63
|
onUpsell?: Function;
|
|
64
64
|
onDownsell?: Function;
|
|
65
|
+
onQuantityChange?: Function;
|
|
65
66
|
onChangeAmount?: Function;
|
|
66
67
|
onApplyCrossSell?: Function;
|
|
67
68
|
onCancelCrossSell?: Function;
|
|
@@ -69,6 +70,7 @@ type Props = {
|
|
|
69
70
|
crossSellBehavior?: string;
|
|
70
71
|
donationSettings?: DonationSettings; // only include backend part
|
|
71
72
|
action?: string;
|
|
73
|
+
completed?: boolean;
|
|
72
74
|
};
|
|
73
75
|
|
|
74
76
|
async function fetchCrossSell(id: string) {
|
|
@@ -118,7 +120,6 @@ function getStakingSetup(items: TLineItemExpanded[], currency: TPaymentCurrency,
|
|
|
118
120
|
|
|
119
121
|
return '0';
|
|
120
122
|
}
|
|
121
|
-
|
|
122
123
|
export default function PaymentSummary({
|
|
123
124
|
items,
|
|
124
125
|
currency,
|
|
@@ -126,6 +127,7 @@ export default function PaymentSummary({
|
|
|
126
127
|
billingThreshold,
|
|
127
128
|
onUpsell = noop,
|
|
128
129
|
onDownsell = noop,
|
|
130
|
+
onQuantityChange = noop,
|
|
129
131
|
onApplyCrossSell = noop,
|
|
130
132
|
onCancelCrossSell = noop,
|
|
131
133
|
onChangeAmount = noop,
|
|
@@ -135,6 +137,7 @@ export default function PaymentSummary({
|
|
|
135
137
|
donationSettings = undefined,
|
|
136
138
|
action = '',
|
|
137
139
|
trialEnd = 0,
|
|
140
|
+
completed = false,
|
|
138
141
|
...rest
|
|
139
142
|
}: Props) {
|
|
140
143
|
const { t, locale } = useLocaleContext();
|
|
@@ -167,6 +170,11 @@ export default function PaymentSummary({
|
|
|
167
170
|
runAsync();
|
|
168
171
|
};
|
|
169
172
|
|
|
173
|
+
const handleQuantityChange = async (itemId: string, quantity: number) => {
|
|
174
|
+
await onQuantityChange!(itemId, quantity);
|
|
175
|
+
runAsync();
|
|
176
|
+
};
|
|
177
|
+
|
|
170
178
|
const handleDownsell = async (from: string) => {
|
|
171
179
|
await onDownsell!(from);
|
|
172
180
|
runAsync();
|
|
@@ -222,7 +230,10 @@ export default function PaymentSummary({
|
|
|
222
230
|
trialEnd={trialEnd}
|
|
223
231
|
currency={currency}
|
|
224
232
|
onUpsell={handleUpsell}
|
|
225
|
-
onDownsell={handleDownsell}
|
|
233
|
+
onDownsell={handleDownsell}
|
|
234
|
+
adjustableQuantity={x.adjustable_quantity}
|
|
235
|
+
completed={completed}
|
|
236
|
+
onQuantityChange={handleQuantityChange}>
|
|
226
237
|
{x.cross_sell && (
|
|
227
238
|
<Stack
|
|
228
239
|
direction="row"
|