@blocklet/payment-react 1.19.0 → 1.19.2

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 (75) hide show
  1. package/es/components/blockchain/tx.d.ts +1 -1
  2. package/es/components/blockchain/tx.js +9 -11
  3. package/es/components/country-select.d.ts +1 -1
  4. package/es/components/date-range-picker.d.ts +13 -0
  5. package/es/components/date-range-picker.js +279 -0
  6. package/es/components/input.d.ts +5 -2
  7. package/es/components/input.js +6 -2
  8. package/es/components/label.d.ts +7 -0
  9. package/es/components/label.js +50 -0
  10. package/es/components/loading-button.d.ts +1 -1
  11. package/es/history/credit/grants-list.d.ts +14 -0
  12. package/es/history/credit/grants-list.js +215 -0
  13. package/es/history/credit/transactions-list.d.ts +13 -0
  14. package/es/history/credit/transactions-list.js +254 -0
  15. package/es/history/invoice/list.js +21 -1
  16. package/es/index.d.ts +5 -1
  17. package/es/index.js +10 -1
  18. package/es/libs/util.d.ts +2 -0
  19. package/es/libs/util.js +12 -0
  20. package/es/locales/en.js +20 -2
  21. package/es/locales/zh.js +20 -2
  22. package/es/payment/form/address.js +2 -1
  23. package/es/payment/form/index.js +46 -7
  24. package/es/payment/index.js +18 -3
  25. package/es/payment/product-item.d.ts +8 -1
  26. package/es/payment/product-item.js +137 -5
  27. package/es/payment/summary.d.ts +3 -1
  28. package/es/payment/summary.js +9 -0
  29. package/lib/components/blockchain/tx.d.ts +1 -1
  30. package/lib/components/blockchain/tx.js +9 -8
  31. package/lib/components/country-select.d.ts +1 -1
  32. package/lib/components/date-range-picker.d.ts +13 -0
  33. package/lib/components/date-range-picker.js +329 -0
  34. package/lib/components/input.d.ts +5 -2
  35. package/lib/components/input.js +8 -4
  36. package/lib/components/label.d.ts +7 -0
  37. package/lib/components/label.js +62 -0
  38. package/lib/components/loading-button.d.ts +1 -1
  39. package/lib/history/credit/grants-list.d.ts +14 -0
  40. package/lib/history/credit/grants-list.js +277 -0
  41. package/lib/history/credit/transactions-list.d.ts +13 -0
  42. package/lib/history/credit/transactions-list.js +300 -0
  43. package/lib/history/invoice/list.js +24 -0
  44. package/lib/index.d.ts +5 -1
  45. package/lib/index.js +39 -0
  46. package/lib/libs/util.d.ts +2 -0
  47. package/lib/libs/util.js +14 -0
  48. package/lib/locales/en.js +20 -2
  49. package/lib/locales/zh.js +20 -2
  50. package/lib/payment/form/address.js +6 -5
  51. package/lib/payment/form/index.js +49 -9
  52. package/lib/payment/index.js +20 -2
  53. package/lib/payment/product-item.d.ts +8 -1
  54. package/lib/payment/product-item.js +144 -4
  55. package/lib/payment/summary.d.ts +3 -1
  56. package/lib/payment/summary.js +9 -0
  57. package/package.json +3 -3
  58. package/src/components/blockchain/tx.tsx +9 -15
  59. package/src/components/country-select.tsx +2 -2
  60. package/src/components/date-range-picker.tsx +310 -0
  61. package/src/components/input.tsx +14 -3
  62. package/src/components/label.tsx +59 -0
  63. package/src/components/loading-button.tsx +1 -1
  64. package/src/history/credit/grants-list.tsx +276 -0
  65. package/src/history/credit/transactions-list.tsx +316 -0
  66. package/src/history/invoice/list.tsx +18 -1
  67. package/src/index.ts +9 -0
  68. package/src/libs/util.ts +14 -0
  69. package/src/locales/en.tsx +20 -0
  70. package/src/locales/zh.tsx +19 -0
  71. package/src/payment/form/address.tsx +4 -3
  72. package/src/payment/form/index.tsx +112 -53
  73. package/src/payment/index.tsx +17 -1
  74. package/src/payment/product-item.tsx +152 -4
  75. package/src/payment/summary.tsx +13 -2
@@ -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
- {formatQuantityInventory(item.price, item.quantity, locale) ? (
202
+ {quantityInventoryError ? (
112
203
  <Status
113
- label={formatQuantityInventory(item.price, item.quantity, locale)}
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 && (
@@ -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"