@blocklet/payment-react 1.18.56 → 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.
Files changed (251) hide show
  1. package/es/checkout/donate.d.ts +1 -15
  2. package/es/checkout/donate.js +301 -189
  3. package/es/checkout/form.d.ts +1 -15
  4. package/es/checkout/form.js +5 -13
  5. package/es/checkout/table.js +3 -3
  6. package/es/components/blockchain/gas.d.ts +1 -5
  7. package/es/components/blockchain/gas.js +10 -2
  8. package/es/components/blockchain/tx.d.ts +1 -8
  9. package/es/components/blockchain/tx.js +29 -10
  10. package/es/components/confirm.d.ts +1 -10
  11. package/es/components/confirm.js +4 -10
  12. package/es/components/country-select.d.ts +3 -2
  13. package/es/components/country-select.js +375 -352
  14. package/es/components/date-range-picker.d.ts +13 -0
  15. package/es/components/date-range-picker.js +279 -0
  16. package/es/components/input.d.ts +14 -20
  17. package/es/components/input.js +51 -44
  18. package/es/components/label.d.ts +7 -0
  19. package/es/components/label.js +49 -0
  20. package/es/components/lazy-loader.js +1 -2
  21. package/es/components/link.d.ts +2 -9
  22. package/es/components/link.js +9 -6
  23. package/es/components/livemode.d.ts +2 -8
  24. package/es/components/livemode.js +1 -5
  25. package/es/components/loading-button.d.ts +6 -1
  26. package/es/components/loading-button.js +56 -66
  27. package/es/components/over-due-invoice-payment.d.ts +0 -18
  28. package/es/components/over-due-invoice-payment.js +138 -95
  29. package/es/components/payment-beneficiaries.d.ts +2 -7
  30. package/es/components/payment-beneficiaries.js +86 -40
  31. package/es/components/pricing-item.d.ts +0 -5
  32. package/es/components/pricing-item.js +1 -4
  33. package/es/components/pricing-table.d.ts +2 -10
  34. package/es/components/pricing-table.js +8 -7
  35. package/es/components/resume-subscription.d.ts +0 -10
  36. package/es/components/resume-subscription.js +42 -21
  37. package/es/components/truncated-text.d.ts +2 -9
  38. package/es/components/truncated-text.js +0 -5
  39. package/es/contexts/donate.d.ts +0 -7
  40. package/es/contexts/donate.js +10 -8
  41. package/es/contexts/payment.d.ts +1 -4
  42. package/es/contexts/payment.js +7 -2
  43. package/es/history/credit/grants-list.d.ts +14 -0
  44. package/es/history/credit/grants-list.js +215 -0
  45. package/es/history/credit/transactions-list.d.ts +13 -0
  46. package/es/history/credit/transactions-list.js +255 -0
  47. package/es/history/invoice/list.d.ts +2 -18
  48. package/es/history/invoice/list.js +172 -74
  49. package/es/history/payment/list.js +115 -38
  50. package/es/hooks/keyboard.d.ts +1 -1
  51. package/es/hooks/keyboard.js +2 -4
  52. package/es/index.d.ts +5 -1
  53. package/es/index.js +10 -1
  54. package/es/libs/cached-request.js +2 -4
  55. package/es/libs/phone-validator.js +1 -2
  56. package/es/libs/util.d.ts +2 -0
  57. package/es/libs/util.js +14 -4
  58. package/es/libs/validator.js +2 -4
  59. package/es/locales/en.js +20 -2
  60. package/es/locales/zh.js +20 -2
  61. package/es/payment/amount.d.ts +2 -7
  62. package/es/payment/amount.js +1 -5
  63. package/es/payment/donation-form.d.ts +2 -10
  64. package/es/payment/donation-form.js +196 -160
  65. package/es/payment/error.d.ts +2 -8
  66. package/es/payment/error.js +40 -20
  67. package/es/payment/footer.d.ts +2 -3
  68. package/es/payment/footer.js +19 -6
  69. package/es/payment/form/addon.js +14 -4
  70. package/es/payment/form/address.d.ts +2 -9
  71. package/es/payment/form/address.js +3 -6
  72. package/es/payment/form/currency.js +45 -25
  73. package/es/payment/form/index.d.ts +2 -8
  74. package/es/payment/form/index.js +151 -71
  75. package/es/payment/form/phone.js +2 -4
  76. package/es/payment/form/stripe/form.d.ts +2 -8
  77. package/es/payment/form/stripe/form.js +1 -3
  78. package/es/payment/header.js +38 -16
  79. package/es/payment/index.d.ts +2 -9
  80. package/es/payment/index.js +23 -17
  81. package/es/payment/product-card.d.ts +2 -11
  82. package/es/payment/product-card.js +84 -50
  83. package/es/payment/product-donation.js +175 -114
  84. package/es/payment/product-item.d.ts +9 -9
  85. package/es/payment/product-item.js +320 -145
  86. package/es/payment/product-skeleton.js +2 -2
  87. package/es/payment/skeleton/donation.js +27 -7
  88. package/es/payment/skeleton/overview.js +22 -2
  89. package/es/payment/skeleton/payment.js +33 -5
  90. package/es/payment/success.d.ts +2 -9
  91. package/es/payment/success.js +41 -14
  92. package/es/payment/summary.d.ts +4 -17
  93. package/es/payment/summary.js +193 -111
  94. package/es/theme/index.d.ts +0 -5
  95. package/es/theme/index.js +2 -5
  96. package/es/theme/typography.d.ts +2 -2
  97. package/lib/checkout/donate.d.ts +1 -15
  98. package/lib/checkout/donate.js +75 -54
  99. package/lib/checkout/form.d.ts +1 -15
  100. package/lib/checkout/form.js +7 -15
  101. package/lib/checkout/table.js +4 -4
  102. package/lib/components/blockchain/gas.d.ts +1 -5
  103. package/lib/components/blockchain/gas.js +3 -2
  104. package/lib/components/blockchain/tx.d.ts +1 -8
  105. package/lib/components/blockchain/tx.js +15 -10
  106. package/lib/components/confirm.d.ts +1 -10
  107. package/lib/components/confirm.js +5 -11
  108. package/lib/components/country-select.d.ts +3 -2
  109. package/lib/components/country-select.js +23 -22
  110. package/lib/components/date-range-picker.d.ts +13 -0
  111. package/lib/components/date-range-picker.js +329 -0
  112. package/lib/components/input.d.ts +14 -20
  113. package/lib/components/input.js +28 -27
  114. package/lib/components/label.d.ts +7 -0
  115. package/lib/components/label.js +60 -0
  116. package/lib/components/lazy-loader.js +1 -1
  117. package/lib/components/link.d.ts +2 -9
  118. package/lib/components/link.js +3 -8
  119. package/lib/components/livemode.d.ts +2 -8
  120. package/lib/components/livemode.js +3 -7
  121. package/lib/components/loading-button.d.ts +6 -1
  122. package/lib/components/loading-button.js +9 -17
  123. package/lib/components/over-due-invoice-payment.d.ts +0 -18
  124. package/lib/components/over-due-invoice-payment.js +31 -33
  125. package/lib/components/payment-beneficiaries.d.ts +2 -7
  126. package/lib/components/payment-beneficiaries.js +12 -11
  127. package/lib/components/pricing-item.d.ts +0 -5
  128. package/lib/components/pricing-item.js +2 -5
  129. package/lib/components/pricing-table.d.ts +2 -10
  130. package/lib/components/pricing-table.js +5 -11
  131. package/lib/components/resume-subscription.d.ts +0 -10
  132. package/lib/components/resume-subscription.js +16 -16
  133. package/lib/components/table.js +1 -1
  134. package/lib/components/truncated-text.d.ts +2 -9
  135. package/lib/components/truncated-text.js +1 -6
  136. package/lib/contexts/donate.d.ts +0 -7
  137. package/lib/contexts/donate.js +4 -7
  138. package/lib/contexts/payment.d.ts +1 -4
  139. package/lib/contexts/payment.js +4 -7
  140. package/lib/history/credit/grants-list.d.ts +14 -0
  141. package/lib/history/credit/grants-list.js +277 -0
  142. package/lib/history/credit/transactions-list.d.ts +13 -0
  143. package/lib/history/credit/transactions-list.js +301 -0
  144. package/lib/history/invoice/list.d.ts +2 -18
  145. package/lib/history/invoice/list.js +73 -37
  146. package/lib/history/payment/list.js +30 -16
  147. package/lib/hooks/keyboard.d.ts +1 -1
  148. package/lib/hooks/mobile.js +1 -1
  149. package/lib/hooks/subscription.js +1 -1
  150. package/lib/index.d.ts +5 -1
  151. package/lib/index.js +41 -2
  152. package/lib/libs/api.js +1 -1
  153. package/lib/libs/dayjs.js +1 -1
  154. package/lib/libs/phone-validator.js +0 -2
  155. package/lib/libs/theme.js +1 -1
  156. package/lib/libs/util.d.ts +2 -0
  157. package/lib/libs/util.js +15 -1
  158. package/lib/libs/validator.js +1 -1
  159. package/lib/locales/en.js +21 -3
  160. package/lib/locales/index.js +1 -1
  161. package/lib/locales/zh.js +21 -3
  162. package/lib/payment/amount.d.ts +2 -7
  163. package/lib/payment/amount.js +2 -6
  164. package/lib/payment/donation-form.d.ts +2 -10
  165. package/lib/payment/donation-form.js +33 -38
  166. package/lib/payment/error.d.ts +2 -8
  167. package/lib/payment/error.js +11 -13
  168. package/lib/payment/footer.d.ts +2 -3
  169. package/lib/payment/footer.js +5 -5
  170. package/lib/payment/form/addon.js +5 -3
  171. package/lib/payment/form/address.d.ts +2 -9
  172. package/lib/payment/form/address.js +5 -8
  173. package/lib/payment/form/currency.js +3 -3
  174. package/lib/payment/form/index.d.ts +2 -8
  175. package/lib/payment/form/index.js +64 -21
  176. package/lib/payment/form/phone.js +1 -1
  177. package/lib/payment/form/stripe/form.d.ts +2 -8
  178. package/lib/payment/form/stripe/form.js +3 -6
  179. package/lib/payment/header.js +8 -4
  180. package/lib/payment/index.d.ts +2 -9
  181. package/lib/payment/index.js +27 -18
  182. package/lib/payment/product-card.d.ts +2 -11
  183. package/lib/payment/product-card.js +13 -20
  184. package/lib/payment/product-donation.js +71 -66
  185. package/lib/payment/product-item.d.ts +9 -9
  186. package/lib/payment/product-item.js +168 -29
  187. package/lib/payment/product-skeleton.js +2 -2
  188. package/lib/payment/skeleton/donation.js +8 -4
  189. package/lib/payment/skeleton/overview.js +6 -2
  190. package/lib/payment/skeleton/payment.js +9 -3
  191. package/lib/payment/success.d.ts +2 -9
  192. package/lib/payment/success.js +12 -15
  193. package/lib/payment/summary.d.ts +4 -17
  194. package/lib/payment/summary.js +53 -45
  195. package/lib/theme/index.d.ts +0 -5
  196. package/lib/theme/index.js +2 -5
  197. package/lib/theme/typography.d.ts +2 -2
  198. package/package.json +40 -40
  199. package/src/checkout/donate.tsx +103 -35
  200. package/src/checkout/form.tsx +5 -14
  201. package/src/checkout/table.tsx +3 -3
  202. package/src/components/blockchain/gas.tsx +5 -3
  203. package/src/components/blockchain/tx.tsx +19 -11
  204. package/src/components/confirm.tsx +4 -11
  205. package/src/components/country-select.tsx +391 -378
  206. package/src/components/date-range-picker.tsx +310 -0
  207. package/src/components/input.tsx +61 -46
  208. package/src/components/label.tsx +58 -0
  209. package/src/components/link.tsx +9 -7
  210. package/src/components/livemode.tsx +2 -6
  211. package/src/components/loading-button.tsx +63 -76
  212. package/src/components/over-due-invoice-payment.tsx +43 -28
  213. package/src/components/payment-beneficiaries.tsx +33 -14
  214. package/src/components/pricing-item.tsx +1 -4
  215. package/src/components/pricing-table.tsx +8 -8
  216. package/src/components/resume-subscription.tsx +20 -14
  217. package/src/components/table.tsx +2 -2
  218. package/src/components/truncated-text.tsx +0 -6
  219. package/src/contexts/donate.tsx +6 -7
  220. package/src/contexts/payment.tsx +7 -3
  221. package/src/history/credit/grants-list.tsx +276 -0
  222. package/src/history/credit/transactions-list.tsx +317 -0
  223. package/src/history/invoice/list.tsx +92 -36
  224. package/src/history/payment/list.tsx +53 -16
  225. package/src/hooks/keyboard.ts +1 -1
  226. package/src/index.ts +9 -0
  227. package/src/libs/util.ts +14 -0
  228. package/src/locales/en.tsx +20 -0
  229. package/src/locales/zh.tsx +19 -0
  230. package/src/payment/amount.tsx +1 -6
  231. package/src/payment/donation-form.tsx +47 -29
  232. package/src/payment/error.tsx +16 -8
  233. package/src/payment/footer.tsx +11 -3
  234. package/src/payment/form/addon.tsx +6 -1
  235. package/src/payment/form/address.tsx +3 -7
  236. package/src/payment/form/currency.tsx +12 -2
  237. package/src/payment/form/index.tsx +121 -45
  238. package/src/payment/form/stripe/form.tsx +1 -5
  239. package/src/payment/header.tsx +14 -2
  240. package/src/payment/index.tsx +27 -22
  241. package/src/payment/product-card.tsx +41 -18
  242. package/src/payment/product-donation.tsx +85 -47
  243. package/src/payment/product-item.tsx +198 -28
  244. package/src/payment/product-skeleton.tsx +3 -2
  245. package/src/payment/skeleton/donation.tsx +12 -2
  246. package/src/payment/skeleton/overview.tsx +12 -2
  247. package/src/payment/skeleton/payment.tsx +16 -3
  248. package/src/payment/success.tsx +26 -15
  249. package/src/payment/summary.tsx +87 -44
  250. package/src/theme/index.tsx +5 -8
  251. package/src/theme/typography.ts +2 -2
@@ -265,17 +265,24 @@ export default function ProductDonation({
265
265
  return (
266
266
  <Box
267
267
  ref={containerRef}
268
- display="flex"
269
- flexDirection="column"
270
- alignItems="flex-start"
271
- gap={1.5}
272
268
  onKeyDown={handleKeyDown}
273
269
  tabIndex={0}
274
- sx={{ outline: 'none' }}>
270
+ sx={{
271
+ display: 'flex',
272
+ flexDirection: 'column',
273
+ alignItems: 'flex-start',
274
+ gap: 1.5,
275
+ outline: 'none',
276
+ }}>
275
277
  {supportPreset && (
276
278
  <Grid container spacing={2}>
277
279
  {presets.map((amount) => (
278
- <Grid item xs={6} sm={3} key={amount}>
280
+ <Grid
281
+ size={{
282
+ xs: 6,
283
+ sm: 3,
284
+ }}
285
+ key={amount}>
279
286
  <Card
280
287
  variant="outlined"
281
288
  className="tab-navigable-card"
@@ -302,19 +309,30 @@ export default function ProductDonation({
302
309
  aria-selected={formatAmount(state.selected) === formatAmount(amount) && !state.custom}>
303
310
  <Stack
304
311
  direction="row"
305
- sx={{ py: 1.5, px: 1.5 }}
306
312
  spacing={0.5}
307
- alignItems="center"
308
- justifyContent="center">
313
+ sx={{
314
+ alignItems: 'center',
315
+ justifyContent: 'center',
316
+ py: 1.5,
317
+ px: 1.5,
318
+ }}>
309
319
  <Avatar src={currency?.logo} sx={{ width: 16, height: 16, mr: 0.5 }} alt={currency?.symbol} />
310
320
  <Typography
311
321
  component="strong"
312
- lineHeight={1}
313
322
  variant="h3"
314
- sx={{ fontVariantNumeric: 'tabular-nums', fontWeight: 400 }}>
323
+ sx={{
324
+ lineHeight: 1,
325
+ fontVariantNumeric: 'tabular-nums',
326
+ fontWeight: 400,
327
+ }}>
315
328
  {amount}
316
329
  </Typography>
317
- <Typography lineHeight={1} fontSize={14} color="text.secondary">
330
+ <Typography
331
+ sx={{
332
+ lineHeight: 1,
333
+ fontSize: 14,
334
+ color: 'text.secondary',
335
+ }}>
318
336
  {currency?.symbol}
319
337
  </Typography>
320
338
  </Stack>
@@ -323,7 +341,12 @@ export default function ProductDonation({
323
341
  </Grid>
324
342
  ))}
325
343
  {supportCustom && (
326
- <Grid item xs={6} sm={3} key="custom">
344
+ <Grid
345
+ size={{
346
+ xs: 6,
347
+ sm: 3,
348
+ }}
349
+ key="custom">
327
350
  <Card
328
351
  variant="outlined"
329
352
  className="tab-navigable-card"
@@ -344,11 +367,19 @@ export default function ProductDonation({
344
367
  <CardActionArea onClick={handleCustomSelect} tabIndex={0} aria-selected={state.custom}>
345
368
  <Stack
346
369
  direction="row"
347
- sx={{ py: 1.5, px: 1.5 }}
348
370
  spacing={0.5}
349
- alignItems="center"
350
- justifyContent="center">
351
- <Typography variant="h3" lineHeight={1} sx={{ fontWeight: 400 }}>
371
+ sx={{
372
+ alignItems: 'center',
373
+ justifyContent: 'center',
374
+ py: 1.5,
375
+ px: 1.5,
376
+ }}>
377
+ <Typography
378
+ variant="h3"
379
+ sx={{
380
+ lineHeight: 1,
381
+ fontWeight: 400,
382
+ }}>
352
383
  {t('common.custom')}
353
384
  </Typography>
354
385
  </Stack>
@@ -368,36 +399,6 @@ export default function ProductDonation({
368
399
  error={!!state.error}
369
400
  helperText={state.error}
370
401
  inputRef={customInputRef}
371
- InputProps={{
372
- endAdornment: (
373
- <Stack direction="row" spacing={0.5} alignItems="center" sx={{ ml: 1 }}>
374
- <IconButton
375
- size="small"
376
- onClick={handleCustomSelect}
377
- disabled={state.animating}
378
- sx={{
379
- mr: 0.5,
380
- opacity: state.animating ? 0.5 : 1,
381
- transition: 'all 0.2s ease',
382
- '&:hover': {
383
- transform: 'scale(1.2)',
384
- transition: 'transform 0.3s ease',
385
- },
386
- }}
387
- aria-label={t('common.random')}>
388
- <AutoAwesomeIcon fontSize="small" />
389
- </IconButton>
390
- <Avatar src={currency?.logo} sx={{ width: 16, height: 16 }} alt={currency?.symbol} />
391
- <Typography>{currency?.symbol}</Typography>
392
- </Stack>
393
- ),
394
- autoComplete: 'off',
395
- sx: {
396
- '& input': {
397
- transition: 'all 0.25s ease',
398
- },
399
- },
400
- }}
401
402
  sx={{
402
403
  mt: defaultPreset !== '0' ? 0 : 1,
403
404
  '& .MuiInputBase-root': {
@@ -414,7 +415,44 @@ export default function ProductDonation({
414
415
  WebkitAppearance: 'none',
415
416
  margin: 0,
416
417
  },
418
+ '& input': {
419
+ transition: 'all 0.25s ease',
420
+ },
421
+ }}
422
+ slotProps={{
423
+ input: {
424
+ endAdornment: (
425
+ <Stack
426
+ direction="row"
427
+ spacing={0.5}
428
+ sx={{
429
+ alignItems: 'center',
430
+ ml: 1,
431
+ }}>
432
+ <IconButton
433
+ size="small"
434
+ onClick={handleCustomSelect}
435
+ disabled={state.animating}
436
+ sx={{
437
+ mr: 0.5,
438
+ opacity: state.animating ? 0.5 : 1,
439
+ transition: 'all 0.2s ease',
440
+ '&:hover': {
441
+ transform: 'scale(1.2)',
442
+ transition: 'transform 0.3s ease',
443
+ },
444
+ }}
445
+ aria-label={t('common.random')}>
446
+ <AutoAwesomeIcon fontSize="small" />
447
+ </IconButton>
448
+ <Avatar src={currency?.logo} sx={{ width: 16, height: 16 }} alt={currency?.symbol} />
449
+ <Typography>{currency?.symbol}</Typography>
450
+ </Stack>
451
+ ),
452
+ autoComplete: 'off',
453
+ },
417
454
  }}
455
+ autoComplete="off"
418
456
  />
419
457
  )}
420
458
  </Box>
@@ -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,12 +28,14 @@ type Props = {
25
28
  onDownsell: Function;
26
29
  mode?: 'normal' | 'cross-sell';
27
30
  children?: React.ReactNode;
28
- };
29
-
30
- ProductItem.defaultProps = {
31
- mode: 'normal',
32
- children: null,
33
- trialEnd: 0,
31
+ // 数量调整相关
32
+ adjustableQuantity?: {
33
+ enabled: boolean;
34
+ minimum?: number;
35
+ maximum?: number;
36
+ };
37
+ onQuantityChange?: (itemId: string, quantity: number) => void;
38
+ completed?: boolean;
34
39
  };
35
40
 
36
41
  export default function ProductItem({
@@ -39,16 +44,94 @@ export default function ProductItem({
39
44
  trialInDays,
40
45
  trialEnd = 0,
41
46
  currency,
42
- mode,
43
- children,
47
+ mode = 'normal',
48
+ children = null,
44
49
  onUpsell,
45
50
  onDownsell,
51
+ completed = false,
52
+ adjustableQuantity = { enabled: false },
53
+ onQuantityChange = () => {},
46
54
  }: Props) {
47
55
  const { t, locale } = useLocaleContext();
56
+ const { settings } = usePaymentContext();
48
57
  const pricing = formatLineItemPricing(item, currency, { trialEnd, trialInDays }, locale);
49
58
  const saving = formatUpsellSaving(items, currency);
50
59
  const metered = item.price?.recurring?.usage_type === 'metered' ? t('common.metered') : '';
51
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
+
52
135
  const primaryText = useMemo(() => {
53
136
  const price = item.upsell_price || item.price || {};
54
137
  const isRecurring = price?.type === 'recurring' && price?.recurring;
@@ -59,29 +142,55 @@ export default function ProductItem({
59
142
  return pricing.primary;
60
143
  }, [trialInDays, trialEnd, pricing, item, locale]);
61
144
 
145
+ const quantityInventoryError = formatQuantityInventory(item.price, localQuantity, locale);
146
+
62
147
  return (
63
- <Stack direction="column" alignItems="flex-start" spacing={1} sx={{ width: '100%' }} className="product-item">
64
- <Stack direction="column" alignItems="flex-start" sx={{ width: '100%' }} className="product-item-content">
148
+ <Stack
149
+ direction="column"
150
+ spacing={1}
151
+ className="product-item"
152
+ sx={{
153
+ alignItems: 'flex-start',
154
+ width: '100%',
155
+ }}>
156
+ <Stack
157
+ direction="column"
158
+ className="product-item-content"
159
+ sx={{
160
+ alignItems: 'flex-start',
161
+ width: '100%',
162
+ }}>
65
163
  <Stack
66
164
  direction="row"
67
- alignItems="center"
68
- flexWrap="wrap"
69
165
  spacing={0.5}
70
- justifyContent="space-between"
71
- sx={{ width: '100%' }}>
166
+ sx={{
167
+ alignItems: 'center',
168
+ flexWrap: 'wrap',
169
+ justifyContent: 'space-between',
170
+ width: '100%',
171
+ }}>
72
172
  <ProductCard
73
173
  logo={item.price.product?.images[0]}
74
174
  name={item.price.product?.name}
75
175
  // description={item.price.product?.description}
76
176
  extra={
77
- <Box display="flex" alignItems="center">
177
+ <Box
178
+ sx={{
179
+ display: 'flex',
180
+ alignItems: 'center',
181
+ }}>
78
182
  {item.price.type === 'recurring' && item.price.recurring
79
183
  ? [pricing.quantity, t('common.billed', { rule: `${formatRecurring(item.upsell_price?.recurring || item.price.recurring, true, 'per', locale)} ${metered}` })].filter(Boolean).join(', ') // prettier-ignore
80
184
  : pricing.quantity}
81
185
  </Box>
82
186
  }
83
187
  />
84
- <Stack direction="column" alignItems="flex-end" flex={1}>
188
+ <Stack
189
+ direction="column"
190
+ sx={{
191
+ alignItems: 'flex-end',
192
+ flex: 1,
193
+ }}>
85
194
  <Typography sx={{ color: 'text.primary', fontWeight: 500, whiteSpace: 'nowrap' }} gutterBottom>
86
195
  {primaryText}
87
196
  </Typography>
@@ -90,9 +199,9 @@ export default function ProductItem({
90
199
  )}
91
200
  </Stack>
92
201
  </Stack>
93
- {formatQuantityInventory(item.price, item.quantity, locale) ? (
202
+ {quantityInventoryError ? (
94
203
  <Status
95
- label={formatQuantityInventory(item.price, item.quantity, locale)}
204
+ label={quantityInventoryError}
96
205
  variant="outlined"
97
206
  sx={{
98
207
  mt: 1,
@@ -102,15 +211,74 @@ export default function ProductItem({
102
211
  }}
103
212
  />
104
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
+
105
271
  {children}
106
272
  </Stack>
107
273
  {canUpsell && !item.upsell_price_id && item.price.upsell?.upsells_to && (
108
274
  <Stack
109
275
  direction="row"
110
- alignItems="center"
111
- justifyContent="space-between"
112
- sx={{ width: '100%' }}
113
- className="product-item-upsell">
276
+ className="product-item-upsell"
277
+ sx={{
278
+ alignItems: 'center',
279
+ justifyContent: 'space-between',
280
+ width: '100%',
281
+ }}>
114
282
  <Typography
115
283
  component="label"
116
284
  htmlFor="upsell-switch"
@@ -148,10 +316,12 @@ export default function ProductItem({
148
316
  {canUpsell && item.upsell_price_id && (
149
317
  <Stack
150
318
  direction="row"
151
- alignItems="center"
152
- justifyContent="space-between"
153
- sx={{ width: '100%' }}
154
- className="product-item-upsell">
319
+ className="product-item-upsell"
320
+ sx={{
321
+ alignItems: 'center',
322
+ justifyContent: 'space-between',
323
+ width: '100%',
324
+ }}>
155
325
  <Typography
156
326
  component="label"
157
327
  htmlFor="upsell-switch"
@@ -5,16 +5,17 @@ export default function ProductSkeleton({ count }: { count: number }) {
5
5
  <Fade in>
6
6
  <Stack
7
7
  direction="column"
8
- alignItems="center"
9
- padding={4}
10
8
  spacing={1}
11
9
  sx={{
10
+ alignItems: 'center',
11
+ padding: 4,
12
12
  width: 320,
13
13
  border: '1px solid',
14
14
  borderColor: 'grey.100',
15
15
  borderRadius: 1,
16
16
  transition: 'border-color 0.3s ease 0s, box-shadow 0.3s ease 0s',
17
17
  boxShadow: '0 4px 8px rgba(0, 0, 0, 20%)',
18
+
18
19
  '&:hover': {
19
20
  borderColor: '#ddd',
20
21
  boxShadow: '0 8px 16px rgba(0, 0, 0, 20%)',
@@ -18,9 +18,19 @@ export default function DonationSkeleton() {
18
18
  },
19
19
  }}
20
20
  />
21
- <Stack direction="row" justifyContent="space-between" spacing={2}>
21
+ <Stack
22
+ direction="row"
23
+ spacing={2}
24
+ sx={{
25
+ justifyContent: 'space-between',
26
+ }}>
22
27
  <Skeleton variant="text" sx={{ fontSize: '1.5rem', width: '40%' }} />
23
- <Box display="flex" alignItems="center" gap={2}>
28
+ <Box
29
+ sx={{
30
+ display: 'flex',
31
+ alignItems: 'center',
32
+ gap: 2,
33
+ }}>
24
34
  <Box>
25
35
  <Skeleton height={60} width={80} />
26
36
  </Box>
@@ -4,11 +4,21 @@ export default function OverviewSkeleton() {
4
4
  return (
5
5
  <Fade in>
6
6
  <Stack direction="column">
7
- <Stack direction="row" alignItems="center" spacing={2}>
7
+ <Stack
8
+ direction="row"
9
+ spacing={2}
10
+ sx={{
11
+ alignItems: 'center',
12
+ }}>
8
13
  <Skeleton variant="text" sx={{ fontSize: '1.75rem', width: '40%' }} />
9
14
  </Stack>
10
15
  <Skeleton sx={{ mt: 2 }} variant="rounded" height={100} />
11
- <Typography mt={2} component="div" variant="h4">
16
+ <Typography
17
+ component="div"
18
+ variant="h4"
19
+ sx={{
20
+ mt: 2,
21
+ }}>
12
22
  <Skeleton />
13
23
  </Typography>
14
24
  <Typography component="div" variant="h2">
@@ -6,8 +6,16 @@ export default function PaymentSkeleton() {
6
6
  <Stack direction="column">
7
7
  <Skeleton variant="text" sx={{ fontSize: '1.75rem', width: '40%' }} />
8
8
  <Skeleton sx={{ mt: 2 }} variant="rounded" height={68} />
9
- <Box mt={1}>
10
- <Typography component="div" variant="h4" mb={-1}>
9
+ <Box
10
+ sx={{
11
+ mt: 1,
12
+ }}>
13
+ <Typography
14
+ component="div"
15
+ variant="h4"
16
+ sx={{
17
+ mb: -1,
18
+ }}>
11
19
  <Skeleton />
12
20
  </Typography>
13
21
  <Typography component="div">
@@ -15,7 +23,12 @@ export default function PaymentSkeleton() {
15
23
  </Typography>
16
24
  </Box>
17
25
  <Box>
18
- <Typography component="div" variant="h4" mb={-1}>
26
+ <Typography
27
+ component="div"
28
+ variant="h4"
29
+ sx={{
30
+ mb: -1,
31
+ }}>
19
32
  <Skeleton />
20
33
  </Typography>
21
34
  <Typography component="div">
@@ -20,9 +20,9 @@ export default function PaymentSuccess({
20
20
  message,
21
21
  action,
22
22
  payee,
23
- invoiceId,
24
- subscriptionId,
25
- subscriptions,
23
+ invoiceId = '',
24
+ subscriptionId = '',
25
+ subscriptions = [],
26
26
  }: Props) {
27
27
  const { t } = useLocaleContext();
28
28
  const { prefix } = usePaymentContext();
@@ -88,7 +88,11 @@ export default function PaymentSuccess({
88
88
  }
89
89
  } else if (invoiceId) {
90
90
  next = (
91
- <Typography textAlign="center" sx={{ mt: 2 }}>
91
+ <Typography
92
+ sx={{
93
+ textAlign: 'center',
94
+ mt: 2,
95
+ }}>
92
96
  <Link href={joinURL(prefix, `/customer/invoice/${invoiceId}`)}>
93
97
  {t('payment.checkout.next.invoice', { payee })}
94
98
  </Link>
@@ -100,9 +104,11 @@ export default function PaymentSuccess({
100
104
  <Grow in>
101
105
  <Stack
102
106
  direction="column"
103
- alignItems="center"
104
- justifyContent={mode === 'standalone' ? 'center' : 'flex-start'}
105
- sx={{ height: mode === 'standalone' ? 'fit-content' : 300 }}>
107
+ sx={{
108
+ alignItems: 'center',
109
+ justifyContent: mode === 'standalone' ? 'center' : 'flex-start',
110
+ height: mode === 'standalone' ? 'fit-content' : 300,
111
+ }}>
106
112
  <Div>
107
113
  <div className="check-icon">
108
114
  <span className="icon-line line-tip" />
@@ -111,10 +117,21 @@ export default function PaymentSuccess({
111
117
  <div className="icon-fix" />
112
118
  </div>
113
119
  </Div>
114
- <Typography variant="h5" color="text.primary" mb={3}>
120
+ <Typography
121
+ variant="h5"
122
+ sx={{
123
+ color: 'text.primary',
124
+ mb: 3,
125
+ }}>
115
126
  {message}
116
127
  </Typography>
117
- <Typography variant="body1" color="text.secondary" textAlign="center" sx={{ fontSize: '14px' }}>
128
+ <Typography
129
+ variant="body1"
130
+ sx={{
131
+ color: 'text.secondary',
132
+ textAlign: 'center',
133
+ fontSize: '14px',
134
+ }}>
118
135
  {t('payment.checkout.completed.tip', { payee })}
119
136
  </Typography>
120
137
  {next}
@@ -123,12 +140,6 @@ export default function PaymentSuccess({
123
140
  );
124
141
  }
125
142
 
126
- PaymentSuccess.defaultProps = {
127
- invoiceId: '',
128
- subscriptionId: '',
129
- subscriptions: [],
130
- };
131
-
132
143
  const Div = styled('div')`
133
144
  width: 80px;
134
145
  height: 115px;