payment-kit 1.18.17 → 1.18.19

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 (35) hide show
  1. package/api/src/libs/subscription.ts +116 -0
  2. package/api/src/routes/checkout-sessions.ts +28 -1
  3. package/api/src/routes/customers.ts +5 -1
  4. package/api/src/store/migrations/20250318-donate-invoice.ts +45 -0
  5. package/api/tests/libs/subscription.spec.ts +311 -0
  6. package/blocklet.yml +1 -1
  7. package/package.json +9 -9
  8. package/src/components/currency.tsx +11 -4
  9. package/src/components/customer/link.tsx +54 -14
  10. package/src/components/customer/overdraft-protection.tsx +36 -2
  11. package/src/components/info-card.tsx +55 -7
  12. package/src/components/info-row-group.tsx +122 -0
  13. package/src/components/info-row.tsx +14 -1
  14. package/src/components/payouts/portal/list.tsx +7 -2
  15. package/src/components/subscription/items/index.tsx +1 -1
  16. package/src/components/subscription/metrics.tsx +14 -6
  17. package/src/contexts/info-row.tsx +4 -0
  18. package/src/locales/en.tsx +1 -0
  19. package/src/locales/zh.tsx +1 -0
  20. package/src/pages/admin/billing/invoices/detail.tsx +54 -76
  21. package/src/pages/admin/billing/subscriptions/detail.tsx +34 -71
  22. package/src/pages/admin/customers/customers/detail.tsx +41 -64
  23. package/src/pages/admin/payments/intents/detail.tsx +28 -42
  24. package/src/pages/admin/payments/payouts/detail.tsx +27 -36
  25. package/src/pages/admin/payments/refunds/detail.tsx +27 -41
  26. package/src/pages/admin/products/links/detail.tsx +30 -55
  27. package/src/pages/admin/products/prices/detail.tsx +43 -50
  28. package/src/pages/admin/products/pricing-tables/detail.tsx +23 -25
  29. package/src/pages/admin/products/products/detail.tsx +52 -81
  30. package/src/pages/customer/index.tsx +183 -108
  31. package/src/pages/customer/invoice/detail.tsx +49 -50
  32. package/src/pages/customer/payout/detail.tsx +16 -22
  33. package/src/pages/customer/recharge/account.tsx +92 -34
  34. package/src/pages/customer/recharge/subscription.tsx +6 -0
  35. package/src/pages/customer/subscription/detail.tsx +176 -94
@@ -14,6 +14,7 @@ import {
14
14
  getPrefix,
15
15
  usePaymentContext,
16
16
  PaymentBeneficiaries,
17
+ useMobile,
17
18
  } from '@blocklet/payment-react';
18
19
  import type { TCheckoutSession, TInvoiceExpanded, TPaymentLink } from '@blocklet/payment-types';
19
20
  import { ArrowBackOutlined } from '@mui/icons-material';
@@ -34,6 +35,7 @@ import InvoiceTable from '../../../components/invoice/table';
34
35
  import { goBackOrFallback } from '../../../libs/util';
35
36
  import CustomerRefundList from '../refund/list';
36
37
  import InfoMetric from '../../../components/info-metric';
38
+ import InfoRowGroup from '../../../components/info-row-group';
37
39
 
38
40
  const fetchData = (
39
41
  id: string
@@ -43,11 +45,10 @@ const fetchData = (
43
45
  return api.get(`/api/invoices/${id}`).then((res) => res.data);
44
46
  };
45
47
 
46
- const InfoDirection = 'column';
47
- const InfoAlignItems = 'flex-start';
48
48
  export default function CustomerInvoiceDetail() {
49
49
  const { t, locale } = useLocaleContext();
50
50
  const [searchParams] = useSearchParams();
51
+ const { isMobile } = useMobile();
51
52
  const { connect } = usePaymentContext();
52
53
  const params = useParams<{ id: string }>();
53
54
  const [state, setState] = useSetState({
@@ -138,7 +139,16 @@ export default function CustomerInvoiceDetail() {
138
139
  </Typography>
139
140
  </Stack>
140
141
  <Stack direction="row" spacing={1} justifyContent="flex-end" alignItems="center">
141
- {['open', 'paid', 'uncollectible'].includes(data.status) && <Download data={data} />}
142
+ {['open', 'paid', 'uncollectible'].includes(data.status) && !isDonation && <Download data={data} />}
143
+ {data?.paymentLink?.donation_settings?.reference && (
144
+ <Button
145
+ variant="outlined"
146
+ onClick={() => {
147
+ window.open(data?.paymentLink?.donation_settings?.reference, '_blank');
148
+ }}>
149
+ {t('customer.payout.viewReference')}
150
+ </Button>
151
+ )}
142
152
  {['open', 'uncollectible'].includes(data.status) && !hidePayButton && (
143
153
  <Button
144
154
  variant="outlined"
@@ -240,51 +250,50 @@ export default function CustomerInvoiceDetail() {
240
250
  </Stack>
241
251
  </Box>
242
252
  <Divider />
243
- <Box className="section">
253
+ <Box className="section" sx={{ containerType: 'inline-size' }}>
244
254
  <Typography variant="h3" mb={3} className="section-header">
245
255
  {t('payment.customer.invoice.details')}
246
256
  </Typography>
247
- <Stack
248
- className="invoice-summary-wrapper"
257
+ <InfoRowGroup
249
258
  sx={{
250
259
  display: 'grid',
251
260
  gridTemplateColumns: {
252
261
  xs: 'repeat(1, 1fr)',
253
- sm: 'repeat(1, 1fr)',
254
- md: 'repeat(2, 1fr)',
255
- lg: 'repeat(3, 1fr)',
262
+ lg: 'repeat(2, 1fr)',
256
263
  },
257
- gap: {
258
- xs: 0,
259
- md: 2,
264
+ '@container (min-width: 980px)': {
265
+ gridTemplateColumns: 'repeat(2, 1fr)',
266
+ },
267
+ '.info-row-wrapper': {
268
+ gap: 1,
269
+ flexDirection: {
270
+ xs: 'column',
271
+ lg: 'row',
272
+ },
273
+ alignItems: {
274
+ xs: 'flex-start',
275
+ lg: 'center',
276
+ },
277
+ '@container (min-width: 980px)': {
278
+ flexDirection: 'row',
279
+ alignItems: 'center',
280
+ },
281
+ },
282
+ '.currency-name': {
283
+ color: 'text.secondary',
260
284
  },
261
285
  }}>
262
286
  <InfoRow
263
- label={t('admin.invoice.description')}
264
- value={getInvoiceDescriptionAndReason(data, locale)?.description}
265
- direction={InfoDirection}
266
- alignItems={InfoAlignItems}
267
- />
268
- <InfoRow
269
- label={t('admin.invoice.from')}
270
- value={data.statement_descriptor || blocklet.appName}
271
- direction={InfoDirection}
272
- alignItems={InfoAlignItems}
287
+ label={t('admin.invoice.billTo')}
288
+ value={<CustomerLink customer={data.customer} linked={false} size={isMobile ? 'default' : 'small'} />}
273
289
  />
274
-
290
+ <InfoRow label={t('admin.invoice.from')} value={data.statement_descriptor || blocklet.appName} />
275
291
  <InfoRow
276
- label={t('admin.invoice.billTo')}
277
- value={<CustomerLink customer={data.customer} linked={false} />}
278
- direction={InfoDirection}
279
- alignItems={InfoAlignItems}
292
+ label={t('admin.invoice.description')}
293
+ value={getInvoiceDescriptionAndReason(data, locale)?.description}
280
294
  />
281
295
  {data.status_transitions?.paid_at && (
282
- <InfoRow
283
- label={t('admin.invoice.paidAt')}
284
- value={formatTime(data.status_transitions.paid_at * 1000)}
285
- direction={InfoDirection}
286
- alignItems={InfoAlignItems}
287
- />
296
+ <InfoRow label={t('admin.invoice.paidAt')} value={formatTime(data.status_transitions.paid_at * 1000)} />
288
297
  )}
289
298
  <InfoRow
290
299
  label={t('admin.paymentCurrency.name')}
@@ -294,15 +303,17 @@ export default function CustomerInvoiceDetail() {
294
303
  name={`${data.paymentCurrency.symbol} (${data.paymentMethod.name})`}
295
304
  />
296
305
  }
297
- direction={InfoDirection}
298
- alignItems={InfoAlignItems}
299
306
  />
307
+ {(isStake || isSlashStake) && (
308
+ <InfoRow
309
+ label={isSlashStake ? t('common.slashStakeAmount') : t('common.stakeAmount')}
310
+ value={`${formatAmount(data.total, data.paymentCurrency.decimal)} ${data.paymentCurrency.symbol}`}
311
+ />
312
+ )}
300
313
  {(!!data.paymentIntent?.payment_details?.ethereum || !!data.paymentIntent?.payment_details?.base) && (
301
314
  <InfoRow
302
315
  label={t('common.txGas')}
303
316
  value={<TxGas details={data.paymentIntent.payment_details as any} method={data.paymentMethod} />}
304
- direction={InfoDirection}
305
- alignItems={InfoAlignItems}
306
317
  />
307
318
  )}
308
319
  {paymentDetails && (
@@ -311,16 +322,6 @@ export default function CustomerInvoiceDetail() {
311
322
  isStake ? t('common.stakeTxHash') : t(`common.${paymentDetails?.arcblock?.type || 'transfer'}TxHash`)
312
323
  }
313
324
  value={<TxLink details={paymentDetails} method={data.paymentMethod} mode="customer" />}
314
- direction={InfoDirection}
315
- alignItems={InfoAlignItems}
316
- />
317
- )}
318
- {(isStake || isSlashStake) && (
319
- <InfoRow
320
- label={isSlashStake ? t('common.slashStakeAmount') : t('common.stakeAmount')}
321
- value={`${formatAmount(data.total, data.paymentCurrency.decimal)} ${data.paymentCurrency.symbol}`}
322
- direction={InfoDirection}
323
- alignItems={InfoAlignItems}
324
325
  />
325
326
  )}
326
327
  {data?.relatedInvoice && (
@@ -338,11 +339,9 @@ export default function CustomerInvoiceDetail() {
338
339
  {data.relatedInvoice?.number}
339
340
  </Typography>
340
341
  }
341
- direction={InfoDirection}
342
- alignItems={InfoAlignItems}
343
342
  />
344
343
  )}
345
- </Stack>
344
+ </InfoRowGroup>
346
345
  </Box>
347
346
  {isDonation && !isEmpty(data.paymentIntent?.beneficiaries) && (
348
347
  <>
@@ -25,6 +25,7 @@ import SectionHeader from '../../../components/section/header';
25
25
  import { getCustomerProfileUrl, goBackOrFallback } from '../../../libs/util';
26
26
  import CustomerLink from '../../../components/customer/link';
27
27
  import InfoCard from '../../../components/info-card';
28
+ import InfoRowGroup from '../../../components/info-row-group';
28
29
 
29
30
  const fetchData = (
30
31
  id: string
@@ -37,9 +38,6 @@ const fetchData = (
37
38
  return api.get(`/api/payouts/${id}`).then((res) => res.data);
38
39
  };
39
40
 
40
- const InfoDirection = 'column';
41
- const InfoAlignItems = 'flex-start';
42
-
43
41
  export default function PayoutDetail() {
44
42
  const { t } = useLocaleContext();
45
43
  const { isMobile } = useMobile();
@@ -237,41 +235,37 @@ export default function PayoutDetail() {
237
235
  <Box flex={1} className="payment-link-column-1" sx={{ gap: 2.5, display: 'flex', flexDirection: 'column' }}>
238
236
  <Box className="section">
239
237
  <SectionHeader title={t('common.detail')} />
240
- <Stack
238
+ <InfoRowGroup
241
239
  sx={{
242
240
  display: 'grid',
243
241
  gridTemplateColumns: {
244
242
  xs: 'repeat(1, 1fr)',
245
- sm: 'repeat(1, 1fr)',
246
- md: 'repeat(2, 1fr)',
247
- lg: 'repeat(3, 1fr)',
243
+ lg: 'repeat(2, 1fr)',
248
244
  },
249
- gap: {
250
- xs: 0,
251
- md: 2,
245
+ '.info-row-wrapper': {
246
+ gap: 1,
247
+ flexDirection: {
248
+ xs: 'column',
249
+ lg: 'row',
250
+ },
251
+ alignItems: {
252
+ xs: 'flex-start',
253
+ lg: 'center',
254
+ },
252
255
  },
253
256
  }}>
254
- <InfoRow
255
- label={t('common.createdAt')}
256
- value={formatTime(data.created_at)}
257
- direction={InfoDirection}
258
- alignItems={InfoAlignItems}
259
- />
257
+ <InfoRow label={t('common.createdAt')} value={formatTime(data.created_at)} />
260
258
  {paymentIntent && paymentIntent.payment_details && (
261
259
  <InfoRow
262
260
  label={t('customer.payout.payTxHash')}
263
261
  value={<TxLink details={paymentIntent.payment_details} method={data.paymentMethod} mode="customer" />}
264
- direction={InfoDirection}
265
- alignItems={InfoAlignItems}
266
262
  />
267
263
  )}
268
264
  <InfoRow
269
265
  label={t('customer.payout.receiver')}
270
- value={<CustomerLink customer={data.customer} linked={false} />}
271
- direction={InfoDirection}
272
- alignItems={InfoAlignItems}
266
+ value={<CustomerLink customer={data.customer} linked={false} size={isMobile ? 'default' : 'small'} />}
273
267
  />
274
- </Stack>
268
+ </InfoRowGroup>
275
269
  </Box>
276
270
  </Box>
277
271
  </Stack>
@@ -31,9 +31,11 @@ import type { TPaymentCurrency, TPaymentMethod } from '@blocklet/payment-types';
31
31
  import { joinURL } from 'ufo';
32
32
  import { AccountBalanceWalletOutlined, ArrowBackOutlined, ArrowForwardOutlined } from '@mui/icons-material';
33
33
  import Empty from '@arcblock/ux/lib/Empty';
34
+ import { BN } from '@ocap/util';
34
35
  import RechargeList from '../../../components/invoice/recharge';
35
36
  import { getTokenBalanceLink, goBackOrFallback } from '../../../libs/util';
36
37
  import { useSessionContext } from '../../../contexts/session';
38
+ import { formatSmartDuration, TimeUnit } from '../../../libs/dayjs';
37
39
 
38
40
  // 扩展PaymentCurrency类型以包含paymentMethod
39
41
  interface ExtendedPaymentCurrency extends TPaymentCurrency {
@@ -86,7 +88,7 @@ export default function BalanceRechargePage() {
86
88
  token: string;
87
89
  } | null>(null);
88
90
  const [customAmount, setCustomAmount] = useState(false);
89
- const [presetAmounts] = useState<Array<{ amount: string }>>([{ amount: '10' }, { amount: '50' }, { amount: '100' }]);
91
+ const [presetAmounts, setPresetAmounts] = useState<Array<{ amount: string; multiplier: number; label: string }>>([]);
90
92
  const { session } = useSessionContext();
91
93
  const [currency, setCurrency] = useState<ExtendedPaymentCurrency | null>(null);
92
94
  const [relatedSubscriptions, setRelatedSubscriptions] = useState<Subscription[]>([]);
@@ -105,6 +107,57 @@ export default function BalanceRechargePage() {
105
107
  if (data.currency) {
106
108
  setCurrency(data.currency);
107
109
  setRelatedSubscriptions(data.relatedSubscriptions || []);
110
+
111
+ if (data.recommendedRecharge && data.recommendedRecharge.amount) {
112
+ const baseAmount = data.recommendedRecharge.amount;
113
+ const decimal = data.currency.decimal || 0;
114
+ setPresetAmounts([
115
+ {
116
+ amount: Math.ceil(parseFloat(formatBNStr(baseAmount, decimal, 6, true))).toString(),
117
+ multiplier: data.recommendedRecharge.cycle,
118
+ label: t('common.estimatedDuration', {
119
+ duration: formatSmartDuration(1, data.recommendedRecharge.interval as TimeUnit, {
120
+ t,
121
+ }),
122
+ }),
123
+ },
124
+ {
125
+ amount: Math.ceil(
126
+ parseFloat(formatBNStr(new BN(baseAmount).mul(new BN('4')).toString(), decimal, 6, true))
127
+ ).toString(),
128
+ multiplier: data.recommendedRecharge.cycle * 4,
129
+ label: t('common.estimatedDuration', {
130
+ duration: formatSmartDuration(4, data.recommendedRecharge.interval as TimeUnit, {
131
+ t,
132
+ }),
133
+ }),
134
+ },
135
+ {
136
+ amount: Math.ceil(
137
+ parseFloat(formatBNStr(new BN(baseAmount).mul(new BN('8')).toString(), decimal, 6, true))
138
+ ).toString(),
139
+ multiplier: data.recommendedRecharge.cycle * 8,
140
+ label: t('common.estimatedDuration', {
141
+ duration: formatSmartDuration(8, data.recommendedRecharge.interval as TimeUnit, {
142
+ t,
143
+ }),
144
+ }),
145
+ },
146
+ ]);
147
+
148
+ setAmount(
149
+ Math.ceil(
150
+ parseFloat(formatBNStr(new BN(baseAmount).mul(new BN('4')).toString(), decimal, 6, true))
151
+ ).toString()
152
+ );
153
+ } else {
154
+ setPresetAmounts([
155
+ { amount: '10', multiplier: 0, label: '' },
156
+ { amount: '50', multiplier: 0, label: '' },
157
+ { amount: '100', multiplier: 0, label: '' },
158
+ ]);
159
+ }
160
+
108
161
  const supportRecharge = data.currency?.paymentMethod?.type === 'arcblock';
109
162
  if (supportRecharge) {
110
163
  const payerTokenRes = await api.get(`/api/customers/payer-token?currencyId=${currencyId}`);
@@ -261,7 +314,7 @@ export default function BalanceRechargePage() {
261
314
 
262
315
  {currency && (
263
316
  <Box ref={rechargeRef}>
264
- <Stack sx={{ maxWidth: '600px' }}>
317
+ <Stack sx={{ maxWidth: '760px' }}>
265
318
  <Box sx={{ mb: 4 }}>
266
319
  <BalanceCard elevation={0}>
267
320
  <Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
@@ -341,7 +394,7 @@ export default function BalanceRechargePage() {
341
394
 
342
395
  <Paper elevation={0} sx={{ mb: 3, backgroundColor: 'background.default', borderRadius: '16px' }}>
343
396
  <Grid container spacing={2}>
344
- {presetAmounts.map(({ amount: presetAmount }) => (
397
+ {presetAmounts.map(({ amount: presetAmount, label, multiplier }) => (
345
398
  <Grid item xs={6} sm={3} key={presetAmount}>
346
399
  <Card
347
400
  variant="outlined"
@@ -365,7 +418,9 @@ export default function BalanceRechargePage() {
365
418
  }
366
419
  : {}),
367
420
  }}>
368
- <CardActionArea onClick={() => handleSelect(presetAmount)} sx={{ height: '100%', p: 1.5 }}>
421
+ <CardActionArea
422
+ onClick={() => handleSelect(presetAmount)}
423
+ sx={{ height: '100%', p: 1.5, textAlign: 'center' }}>
369
424
  <Typography
370
425
  variant="h6"
371
426
  align="center"
@@ -375,6 +430,11 @@ export default function BalanceRechargePage() {
375
430
  }}>
376
431
  {presetAmount} {currency.symbol}
377
432
  </Typography>
433
+ {multiplier > 0 && label && (
434
+ <Typography variant="caption" align="center" color="text.secondary">
435
+ {label}
436
+ </Typography>
437
+ )}
378
438
  </CardActionArea>
379
439
  </Card>
380
440
  </Grid>
@@ -464,39 +524,37 @@ export default function BalanceRechargePage() {
464
524
  {t('customer.recharge.relatedSubscriptions')}
465
525
  </Typography>
466
526
 
467
- <Stack direction="row" sx={{ flexWrap: 'wrap', gap: 2 }}>
527
+ <Grid container spacing={2}>
468
528
  {relatedSubscriptions.map((subscription) => (
469
- <Stack
470
- key={subscription.id}
471
- onClick={() => handleSubscriptionClick(subscription.id)}
472
- className="base-card"
473
- sx={{
474
- minWidth: '220px',
475
- flex: 1,
476
- }}>
477
- <Stack direction="row" alignItems="center" spacing={1.5} sx={{ mb: 1 }}>
478
- <Typography
479
- variant="subtitle1"
480
- sx={{
481
- fontWeight: 'medium',
482
- overflow: 'hidden',
483
- textOverflow: 'ellipsis',
484
- whiteSpace: 'nowrap',
485
- color: 'text.link',
486
- cursor: 'pointer',
487
- }}>
488
- {subscription.description || subscription.id}
489
- </Typography>
490
- </Stack>
529
+ <Grid item xs={12} sm={6} md={4} lg={3} key={subscription.id}>
530
+ <Stack
531
+ onClick={() => handleSubscriptionClick(subscription.id)}
532
+ className="base-card"
533
+ sx={{ height: '100%' }}>
534
+ <Stack direction="row" alignItems="center" spacing={1.5} sx={{ mb: 1 }}>
535
+ <Typography
536
+ variant="subtitle1"
537
+ sx={{
538
+ fontWeight: 'medium',
539
+ overflow: 'hidden',
540
+ textOverflow: 'ellipsis',
541
+ whiteSpace: 'nowrap',
542
+ color: 'text.link',
543
+ cursor: 'pointer',
544
+ }}>
545
+ {subscription.description || subscription.id}
546
+ </Typography>
547
+ </Stack>
491
548
 
492
- {subscription.items && subscription.items[0] && currency && (
493
- <Typography variant="body1" sx={{ color: 'text.secondary' }}>
494
- {formatPrice(subscription.items[0].price, currency)}
495
- </Typography>
496
- )}
497
- </Stack>
549
+ {subscription.items && subscription.items[0] && currency && (
550
+ <Typography variant="body1" sx={{ color: 'text.secondary' }}>
551
+ {formatPrice(subscription.items[0].price, currency)}
552
+ </Typography>
553
+ )}
554
+ </Stack>
555
+ </Grid>
498
556
  ))}
499
- </Stack>
557
+ </Grid>
500
558
  </Box>
501
559
  )}
502
560
  <Divider sx={{ mb: 3 }} />
@@ -294,6 +294,12 @@ export default function RechargePage() {
294
294
  display: 'grid',
295
295
  gridTemplateColumns: { xs: 'repeat(1, 1fr)', sm: 'repeat(1, 1fr)', md: 'repeat(1, 1fr)' },
296
296
  gap: { xs: 0, md: 2 },
297
+ '.currency-name': {
298
+ color: 'text.secondary',
299
+ },
300
+ '.info-row-label': {
301
+ fontWeight: 500,
302
+ },
297
303
  }}>
298
304
  <InfoRow
299
305
  label={t('common.customer')}