payment-kit 1.18.18 → 1.18.20

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 (36) 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 +8 -8
  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/libs/dayjs.ts +4 -3
  19. package/src/locales/en.tsx +1 -0
  20. package/src/locales/zh.tsx +1 -0
  21. package/src/pages/admin/billing/invoices/detail.tsx +54 -76
  22. package/src/pages/admin/billing/subscriptions/detail.tsx +34 -71
  23. package/src/pages/admin/customers/customers/detail.tsx +41 -64
  24. package/src/pages/admin/payments/intents/detail.tsx +28 -42
  25. package/src/pages/admin/payments/payouts/detail.tsx +27 -36
  26. package/src/pages/admin/payments/refunds/detail.tsx +27 -41
  27. package/src/pages/admin/products/links/detail.tsx +30 -55
  28. package/src/pages/admin/products/prices/detail.tsx +43 -50
  29. package/src/pages/admin/products/pricing-tables/detail.tsx +23 -25
  30. package/src/pages/admin/products/products/detail.tsx +52 -81
  31. package/src/pages/customer/index.tsx +189 -107
  32. package/src/pages/customer/invoice/detail.tsx +49 -50
  33. package/src/pages/customer/payout/detail.tsx +16 -22
  34. package/src/pages/customer/recharge/account.tsx +119 -34
  35. package/src/pages/customer/recharge/subscription.tsx +11 -1
  36. 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 {
@@ -81,12 +83,21 @@ export default function BalanceRechargePage() {
81
83
  const [loading, setLoading] = useState(true);
82
84
  const [error, setError] = useState('');
83
85
  const customInputRef = useRef<HTMLInputElement>(null);
86
+ const [unitCycle, setUnitCycle] = useState<{
87
+ amount: string;
88
+ interval: TimeUnit;
89
+ cycle: number;
90
+ }>({
91
+ amount: '0',
92
+ interval: 'month',
93
+ cycle: 1,
94
+ });
84
95
  const [payerValue, setPayerValue] = useState<{
85
96
  paymentAddress: string;
86
97
  token: string;
87
98
  } | null>(null);
88
99
  const [customAmount, setCustomAmount] = useState(false);
89
- const [presetAmounts] = useState<Array<{ amount: string }>>([{ amount: '10' }, { amount: '50' }, { amount: '100' }]);
100
+ const [presetAmounts, setPresetAmounts] = useState<Array<{ amount: string; multiplier: number; label: string }>>([]);
90
101
  const { session } = useSessionContext();
91
102
  const [currency, setCurrency] = useState<ExtendedPaymentCurrency | null>(null);
92
103
  const [relatedSubscriptions, setRelatedSubscriptions] = useState<Subscription[]>([]);
@@ -105,6 +116,62 @@ export default function BalanceRechargePage() {
105
116
  if (data.currency) {
106
117
  setCurrency(data.currency);
107
118
  setRelatedSubscriptions(data.relatedSubscriptions || []);
119
+
120
+ if (data.recommendedRecharge && data.recommendedRecharge.amount && data.recommendedRecharge.amount !== '0') {
121
+ const baseAmount = data.recommendedRecharge.amount;
122
+ const decimal = data.currency.decimal || 0;
123
+ setUnitCycle({
124
+ amount: parseFloat(formatBNStr(baseAmount, decimal, 6, true)).toString(),
125
+ interval: data.recommendedRecharge.interval as TimeUnit,
126
+ cycle: data.recommendedRecharge.cycle,
127
+ });
128
+ setPresetAmounts([
129
+ {
130
+ amount: Math.ceil(parseFloat(formatBNStr(baseAmount, decimal, 6, true))).toString(),
131
+ multiplier: data.recommendedRecharge.cycle,
132
+ label: t('common.estimatedDuration', {
133
+ duration: formatSmartDuration(1, data.recommendedRecharge.interval as TimeUnit, {
134
+ t,
135
+ }),
136
+ }),
137
+ },
138
+ {
139
+ amount: Math.ceil(
140
+ parseFloat(formatBNStr(new BN(baseAmount).mul(new BN('4')).toString(), decimal, 6, true))
141
+ ).toString(),
142
+ multiplier: data.recommendedRecharge.cycle * 4,
143
+ label: t('common.estimatedDuration', {
144
+ duration: formatSmartDuration(4, data.recommendedRecharge.interval as TimeUnit, {
145
+ t,
146
+ }),
147
+ }),
148
+ },
149
+ {
150
+ amount: Math.ceil(
151
+ parseFloat(formatBNStr(new BN(baseAmount).mul(new BN('8')).toString(), decimal, 6, true))
152
+ ).toString(),
153
+ multiplier: data.recommendedRecharge.cycle * 8,
154
+ label: t('common.estimatedDuration', {
155
+ duration: formatSmartDuration(8, data.recommendedRecharge.interval as TimeUnit, {
156
+ t,
157
+ }),
158
+ }),
159
+ },
160
+ ]);
161
+
162
+ setAmount(
163
+ Math.ceil(
164
+ parseFloat(formatBNStr(new BN(baseAmount).mul(new BN('4')).toString(), decimal, 6, true))
165
+ ).toString()
166
+ );
167
+ } else {
168
+ setPresetAmounts([
169
+ { amount: '10', multiplier: 0, label: '' },
170
+ { amount: '50', multiplier: 0, label: '' },
171
+ { amount: '100', multiplier: 0, label: '' },
172
+ ]);
173
+ }
174
+
108
175
  const supportRecharge = data.currency?.paymentMethod?.type === 'arcblock';
109
176
  if (supportRecharge) {
110
177
  const payerTokenRes = await api.get(`/api/customers/payer-token?currencyId=${currencyId}`);
@@ -261,7 +328,7 @@ export default function BalanceRechargePage() {
261
328
 
262
329
  {currency && (
263
330
  <Box ref={rechargeRef}>
264
- <Stack sx={{ maxWidth: '600px' }}>
331
+ <Stack sx={{ maxWidth: '760px' }}>
265
332
  <Box sx={{ mb: 4 }}>
266
333
  <BalanceCard elevation={0}>
267
334
  <Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
@@ -341,7 +408,7 @@ export default function BalanceRechargePage() {
341
408
 
342
409
  <Paper elevation={0} sx={{ mb: 3, backgroundColor: 'background.default', borderRadius: '16px' }}>
343
410
  <Grid container spacing={2}>
344
- {presetAmounts.map(({ amount: presetAmount }) => (
411
+ {presetAmounts.map(({ amount: presetAmount, label, multiplier }) => (
345
412
  <Grid item xs={6} sm={3} key={presetAmount}>
346
413
  <Card
347
414
  variant="outlined"
@@ -365,7 +432,9 @@ export default function BalanceRechargePage() {
365
432
  }
366
433
  : {}),
367
434
  }}>
368
- <CardActionArea onClick={() => handleSelect(presetAmount)} sx={{ height: '100%', p: 1.5 }}>
435
+ <CardActionArea
436
+ onClick={() => handleSelect(presetAmount)}
437
+ sx={{ height: '100%', p: 1.5, textAlign: 'center' }}>
369
438
  <Typography
370
439
  variant="h6"
371
440
  align="center"
@@ -375,6 +444,11 @@ export default function BalanceRechargePage() {
375
444
  }}>
376
445
  {presetAmount} {currency.symbol}
377
446
  </Typography>
447
+ {multiplier > 0 && label && (
448
+ <Typography variant="caption" align="center" color="text.secondary">
449
+ {label}
450
+ </Typography>
451
+ )}
378
452
  </CardActionArea>
379
453
  </Card>
380
454
  </Grid>
@@ -434,6 +508,19 @@ export default function BalanceRechargePage() {
434
508
  }}
435
509
  inputRef={customInputRef}
436
510
  />
511
+ {amount && Number(amount) > 0 && Number(unitCycle.amount) > 0 && !amountError && (
512
+ <Typography variant="body2" sx={{ color: 'text.lighter', mt: 1 }} fontSize={12}>
513
+ {t('common.estimatedDuration', {
514
+ duration: formatSmartDuration(
515
+ Math.floor(Number(amount) / Number(unitCycle.amount)) < 1
516
+ ? parseFloat((Number(amount) / Number(unitCycle.amount)).toFixed(1))
517
+ : Math.floor(Number(amount) / Number(unitCycle.amount)),
518
+ unitCycle.interval,
519
+ { t }
520
+ ),
521
+ })}
522
+ </Typography>
523
+ )}
437
524
  </Box>
438
525
  )}
439
526
 
@@ -464,39 +551,37 @@ export default function BalanceRechargePage() {
464
551
  {t('customer.recharge.relatedSubscriptions')}
465
552
  </Typography>
466
553
 
467
- <Stack direction="row" sx={{ flexWrap: 'wrap', gap: 2 }}>
554
+ <Grid container spacing={2}>
468
555
  {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>
556
+ <Grid item xs={12} sm={6} md={4} lg={3} key={subscription.id}>
557
+ <Stack
558
+ onClick={() => handleSubscriptionClick(subscription.id)}
559
+ className="base-card"
560
+ sx={{ height: '100%' }}>
561
+ <Stack direction="row" alignItems="center" spacing={1.5} sx={{ mb: 1 }}>
562
+ <Typography
563
+ variant="subtitle1"
564
+ sx={{
565
+ fontWeight: 'medium',
566
+ overflow: 'hidden',
567
+ textOverflow: 'ellipsis',
568
+ whiteSpace: 'nowrap',
569
+ color: 'text.link',
570
+ cursor: 'pointer',
571
+ }}>
572
+ {subscription.description || subscription.id}
573
+ </Typography>
574
+ </Stack>
491
575
 
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>
576
+ {subscription.items && subscription.items[0] && currency && (
577
+ <Typography variant="body1" sx={{ color: 'text.secondary' }}>
578
+ {formatPrice(subscription.items[0].price, currency)}
579
+ </Typography>
580
+ )}
581
+ </Stack>
582
+ </Grid>
498
583
  ))}
499
- </Stack>
584
+ </Grid>
500
585
  </Box>
501
586
  )}
502
587
  <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')}
@@ -473,7 +479,11 @@ export default function RechargePage() {
473
479
  />
474
480
  {amount && Number(amount) > 0 && Number(cycleAmount) > 0 && !amountError && (
475
481
  <Typography variant="body2" sx={{ color: 'text.lighter', mt: '8px !important' }} fontSize={12}>
476
- {formatEstimatedDuration(Math.floor(Number(amount) / Number(cycleAmount)))}
482
+ {formatEstimatedDuration(
483
+ Math.floor(Number(amount) / Number(cycleAmount)) < 1
484
+ ? parseFloat((Number(amount) / Number(cycleAmount)).toFixed(1))
485
+ : Math.floor(Number(amount) / Number(cycleAmount))
486
+ )}
477
487
  </Typography>
478
488
  )}
479
489
  </Box>