payment-kit 1.18.15 → 1.18.16

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.
@@ -17,7 +17,7 @@ import {
17
17
  } from '@blocklet/payment-react';
18
18
  import type { TCheckoutSession, TInvoiceExpanded, TPaymentLink } from '@blocklet/payment-types';
19
19
  import { ArrowBackOutlined } from '@mui/icons-material';
20
- import { Alert, Box, Button, CircularProgress, Divider, Stack, Typography } from '@mui/material';
20
+ import { Alert, Box, Button, CircularProgress, Divider, Stack, Tooltip, Typography } from '@mui/material';
21
21
  import { styled } from '@mui/system';
22
22
  import { useRequest, useSetState } from 'ahooks';
23
23
  import { useEffect } from 'react';
@@ -204,9 +204,31 @@ export default function CustomerInvoiceDetail() {
204
204
  }}>
205
205
  <InfoMetric
206
206
  label={t('common.status')}
207
- value={<Status label={data.status} color={getInvoiceStatusColor(data.status)} />}
207
+ value={
208
+ <Tooltip
209
+ title={data.status === 'void' ? t('payment.customer.invoice.noPaymentRequired') : ''}
210
+ arrow
211
+ placement="top">
212
+ <span>
213
+ <Status label={data.status} color={getInvoiceStatusColor(data.status)} />
214
+ </span>
215
+ </Tooltip>
216
+ }
208
217
  divider
209
218
  />
219
+ {data.subscription && (
220
+ <InfoMetric
221
+ label={t('admin.subscription.name')}
222
+ value={
223
+ <Link to={`/customer/subscription/${data.subscription.id}`}>
224
+ <Typography variant="body1" color="text.link">
225
+ {data.subscription.description || data.subscription.id}
226
+ </Typography>
227
+ </Link>
228
+ }
229
+ divider
230
+ />
231
+ )}
210
232
  <InfoMetric label={t('common.createdAt')} value={formatTime(data.created_at)} divider />
211
233
  {data.period_start > 0 && data.period_end > 0 && (
212
234
  <InfoMetric
@@ -301,20 +323,6 @@ export default function CustomerInvoiceDetail() {
301
323
  alignItems={InfoAlignItems}
302
324
  />
303
325
  )}
304
- {data.subscription && (
305
- <InfoRow
306
- label={t('admin.subscription.name')}
307
- value={
308
- <Link to={`/customer/subscription/${data.subscription.id}`}>
309
- <Typography variant="body1" color="text.link">
310
- {data.subscription.description || data.subscription.id}
311
- </Typography>
312
- </Link>
313
- }
314
- direction={InfoDirection}
315
- alignItems={InfoAlignItems}
316
- />
317
- )}
318
326
  {data?.relatedInvoice && (
319
327
  <InfoRow
320
328
  label={t('customer.invoice.relatedInvoice')}
@@ -6,6 +6,7 @@ import {
6
6
  getPrefix,
7
7
  getQueryParams,
8
8
  usePaymentContext,
9
+ OverdueInvoicePayment,
9
10
  } from '@blocklet/payment-react';
10
11
  import type { TCustomerExpanded } from '@blocklet/payment-types';
11
12
  import { ArrowBackOutlined } from '@mui/icons-material';
@@ -29,9 +30,10 @@ const fetchData = (): Promise<TCustomerExpanded> => {
29
30
  export default function CustomerInvoicePastDue() {
30
31
  const { t } = useLocaleContext();
31
32
  const { events } = useSessionContext();
32
- const { connect } = usePaymentContext();
33
+ const { connect, session } = usePaymentContext();
33
34
  const [params] = useSearchParams();
34
- const [alertVisible, setAlertVisible] = useState(false);
35
+ const [hasUnpaidInvoices, setHasUnpaidInvoices] = useState(false);
36
+ const [showOverduePayment, setShowOverduePayment] = useState(false);
35
37
 
36
38
  const { loading, error, data, runAsync } = useRequest(fetchData);
37
39
 
@@ -60,27 +62,31 @@ export default function CustomerInvoicePastDue() {
60
62
  const subscriptionId = params.get('subscription') || '';
61
63
  const currencyId = params.get('currency') || '';
62
64
  const handleBatchPay = () => {
63
- connect.open({
64
- containerEl: undefined as unknown as Element,
65
- saveConnect: false,
66
- action: 'collect-batch',
67
- prefix: joinURL(getPrefix(), '/api/did'),
68
- extraParams: { subscriptionId, currencyId },
69
- onSuccess: () => {
70
- connect.close();
71
- },
72
- onClose: () => {
73
- connect.close();
74
- },
75
- onError: (err: any) => {
76
- Toast.error(formatError(err));
77
- },
78
- });
65
+ if (subscriptionId && currencyId) {
66
+ connect.open({
67
+ containerEl: undefined as unknown as Element,
68
+ saveConnect: false,
69
+ action: 'collect-batch',
70
+ prefix: joinURL(getPrefix(), '/api/did'),
71
+ extraParams: { subscriptionId, currencyId },
72
+ onSuccess: () => {
73
+ connect.close();
74
+ },
75
+ onClose: () => {
76
+ connect.close();
77
+ },
78
+ onError: (err: any) => {
79
+ Toast.error(formatError(err));
80
+ },
81
+ });
82
+ } else {
83
+ setShowOverduePayment(true);
84
+ }
79
85
  };
80
86
 
81
87
  const onTableDataChange = (tableData: any, prevData: any) => {
82
88
  if (isEmpty(tableData) || tableData?.count === 0) {
83
- setAlertVisible(false);
89
+ setHasUnpaidInvoices(false);
84
90
  if (prevData?.count > 0) {
85
91
  // paid all invoices
86
92
  const referer = getQueryParams(window.location.href)?.referer;
@@ -92,7 +98,7 @@ export default function CustomerInvoicePastDue() {
92
98
  }
93
99
  return;
94
100
  }
95
- setAlertVisible(true);
101
+ setHasUnpaidInvoices(true);
96
102
  };
97
103
 
98
104
  return (
@@ -110,12 +116,28 @@ export default function CustomerInvoicePastDue() {
110
116
  </Stack>
111
117
  </Stack>
112
118
  <Root direction="column" spacing={3}>
113
- {alertVisible && <Alert severity="error">{t('payment.customer.pastDue.warning')}</Alert>}
119
+ {hasUnpaidInvoices && <Alert severity="error">{t('payment.customer.pastDue.warning')}</Alert>}
120
+ {showOverduePayment && (
121
+ <OverdueInvoicePayment
122
+ customerId={session.user.did}
123
+ onPaid={() => {
124
+ setShowOverduePayment(false);
125
+ }}
126
+ successToast={false}
127
+ dialogProps={{
128
+ open: showOverduePayment,
129
+ onClose: () => setShowOverduePayment(false),
130
+ }}
131
+ detailLinkOptions={{
132
+ enabled: false,
133
+ }}
134
+ />
135
+ )}
114
136
 
115
137
  <Box className="section">
116
138
  <SectionHeader title={t('payment.customer.pastDue.invoices')} mb={0}>
117
- {subscriptionId && currencyId && (
118
- <Button size="small" variant="contained" color="info" onClick={handleBatchPay}>
139
+ {hasUnpaidInvoices && (
140
+ <Button size="small" variant="contained" color="primary" onClick={handleBatchPay}>
119
141
  {t('admin.subscription.batchPay.button')}
120
142
  </Button>
121
143
  )}
@@ -0,0 +1,515 @@
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { useParams, useNavigate } from 'react-router-dom';
3
+ import Toast from '@arcblock/ux/lib/Toast';
4
+ import {
5
+ Box,
6
+ Typography,
7
+ TextField,
8
+ Button,
9
+ CircularProgress,
10
+ Alert,
11
+ Stack,
12
+ Divider,
13
+ Card,
14
+ CardActionArea,
15
+ Grid,
16
+ Paper,
17
+ Avatar,
18
+ } from '@mui/material';
19
+ import { styled } from '@mui/system';
20
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
21
+ import {
22
+ usePaymentContext,
23
+ getPrefix,
24
+ formatError,
25
+ formatAmountPrecisionLimit,
26
+ api,
27
+ formatBNStr,
28
+ formatPrice,
29
+ } from '@blocklet/payment-react';
30
+ import type { TPaymentCurrency, TPaymentMethod } from '@blocklet/payment-types';
31
+ import { joinURL } from 'ufo';
32
+ import { ArrowBackOutlined, ArrowForwardOutlined } from '@mui/icons-material';
33
+ import Empty from '@arcblock/ux/lib/Empty';
34
+ import RechargeList from '../../../components/invoice/recharge';
35
+ import { getTokenBalanceLink, goBackOrFallback } from '../../../libs/util';
36
+ import { useSessionContext } from '../../../contexts/session';
37
+
38
+ // 扩展PaymentCurrency类型以包含paymentMethod
39
+ interface ExtendedPaymentCurrency extends TPaymentCurrency {
40
+ paymentMethod?: TPaymentMethod;
41
+ }
42
+
43
+ interface Subscription {
44
+ id: string;
45
+ description?: string;
46
+ status: string;
47
+ paymentMethod?: {
48
+ logo?: string;
49
+ name?: string;
50
+ };
51
+ paymentCurrency?: {
52
+ symbol: string;
53
+ decimal: number;
54
+ };
55
+ items?: Array<{
56
+ price: any;
57
+ }>;
58
+ }
59
+
60
+ const Root = styled(Stack)(({ theme }) => ({
61
+ marginBottom: theme.spacing(3),
62
+ gap: theme.spacing(3),
63
+ flexDirection: 'column',
64
+ maxWidth: '800px',
65
+ }));
66
+
67
+ const BalanceCard = styled(Card)(({ theme }) => ({
68
+ padding: theme.spacing(2),
69
+ backgroundColor: theme.palette.background.paper,
70
+ border: `1px solid ${theme.palette.divider}`,
71
+ borderRadius: theme.shape.borderRadius,
72
+ marginBottom: theme.spacing(2),
73
+ }));
74
+
75
+ const SubscriptionScroll = styled(Box)(() => ({
76
+ display: 'flex',
77
+ overflowX: 'auto',
78
+ padding: '8px 2px',
79
+ scrollbarWidth: 'none',
80
+ '&::-webkit-scrollbar': {
81
+ display: 'none',
82
+ },
83
+ '-ms-overflow-style': 'none',
84
+ gap: '16px',
85
+ }));
86
+
87
+ export default function BalanceRechargePage() {
88
+ const { t, locale } = useLocaleContext();
89
+ const { currencyId } = useParams<{ currencyId: string }>();
90
+ const navigate = useNavigate();
91
+ const { connect } = usePaymentContext();
92
+ const [amount, setAmount] = useState('50');
93
+ const [amountError, setAmountError] = useState('');
94
+ const [loading, setLoading] = useState(true);
95
+ const [error, setError] = useState('');
96
+ const customInputRef = useRef<HTMLInputElement>(null);
97
+ const [payerValue, setPayerValue] = useState<{
98
+ paymentAddress: string;
99
+ token: string;
100
+ } | null>(null);
101
+ const [customAmount, setCustomAmount] = useState(false);
102
+ const [presetAmounts] = useState<Array<{ amount: string }>>([{ amount: '10' }, { amount: '50' }, { amount: '100' }]);
103
+ const { session } = useSessionContext();
104
+ const [currency, setCurrency] = useState<ExtendedPaymentCurrency | null>(null);
105
+ const [relatedSubscriptions, setRelatedSubscriptions] = useState<Subscription[]>([]);
106
+
107
+ const fetchData = async () => {
108
+ try {
109
+ setLoading(true);
110
+ if (!currencyId) {
111
+ // 如果没有 currencyId,重定向回客户页面
112
+ navigate('/customer');
113
+ return;
114
+ }
115
+
116
+ const { data } = await api.get(`/api/customers/recharge?currencyId=${currencyId}`);
117
+
118
+ if (data.currency) {
119
+ setCurrency(data.currency);
120
+ setRelatedSubscriptions(data.relatedSubscriptions || []);
121
+ const supportRecharge = data.currency?.paymentMethod?.type === 'arcblock';
122
+ if (supportRecharge) {
123
+ const payerTokenRes = await api.get(`/api/customers/payer-token?currencyId=${currencyId}`);
124
+ if (payerTokenRes?.data) {
125
+ setPayerValue(payerTokenRes.data);
126
+ }
127
+ }
128
+ } else {
129
+ setError(t('customer.balance.currency.notFound'));
130
+ }
131
+ } catch (err) {
132
+ setError(formatError(err) || t('common.fetchError'));
133
+ console.error(err);
134
+ } finally {
135
+ setLoading(false);
136
+ }
137
+ };
138
+
139
+ const rechargeRef = useRef<HTMLDivElement>(null);
140
+
141
+ useEffect(() => {
142
+ fetchData();
143
+ }, [currencyId]);
144
+
145
+ useEffect(() => {
146
+ if (rechargeRef.current && currency) {
147
+ setTimeout(() => {
148
+ if (rechargeRef.current) {
149
+ const rechargePosition = rechargeRef.current.getBoundingClientRect();
150
+ const absoluteTop = window.scrollY + rechargePosition.top;
151
+ const scrollToPosition = absoluteTop + 20;
152
+
153
+ window.scrollTo({
154
+ top: scrollToPosition,
155
+ behavior: 'smooth',
156
+ });
157
+ }
158
+ }, 200);
159
+ }
160
+ }, [currency]);
161
+
162
+ const handleRecharge = () => {
163
+ if (!currency) return;
164
+
165
+ if (Number.isNaN(Number(amount))) {
166
+ return;
167
+ }
168
+
169
+ connect.open({
170
+ containerEl: undefined as unknown as Element,
171
+ saveConnect: false,
172
+ action: 'recharge-account',
173
+ prefix: joinURL(getPrefix(), '/api/did'),
174
+ extraParams: {
175
+ customerDid: session?.user?.did,
176
+ currencyId: currency.id,
177
+ amount: Number(amount),
178
+ },
179
+ onSuccess: () => {
180
+ connect.close();
181
+ Toast.success(t('customer.recharge.success'));
182
+ fetchData();
183
+ },
184
+ onClose: () => {
185
+ connect.close();
186
+ },
187
+ onError: (err: any) => {
188
+ Toast.error(formatError(err));
189
+ },
190
+ });
191
+ };
192
+
193
+ const handleAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
194
+ const { value } = e.target;
195
+ if (!currency) return;
196
+ if (!/^\d*\.?\d*$/.test(value)) return;
197
+ // 不允许以小数点开头
198
+ if (value.startsWith('.')) return;
199
+ const precision = currency.decimal;
200
+ const errorMessage = formatAmountPrecisionLimit(value, locale, precision || 6);
201
+ setAmountError(errorMessage || '');
202
+ setAmount(value);
203
+ };
204
+
205
+ const handleSelect = (selectedAmount: string) => {
206
+ setAmount(selectedAmount);
207
+ setCustomAmount(false);
208
+ };
209
+
210
+ const handleCustomSelect = () => {
211
+ setCustomAmount(true);
212
+ setAmount('');
213
+ setTimeout(() => {
214
+ customInputRef.current?.focus();
215
+ }, 0);
216
+ };
217
+
218
+ const handleSubscriptionClick = (subscriptionId: string) => {
219
+ navigate(`/customer/subscription/${subscriptionId}`);
220
+ };
221
+
222
+ if (loading) {
223
+ return (
224
+ <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '50vh' }}>
225
+ <CircularProgress />
226
+ </Box>
227
+ );
228
+ }
229
+
230
+ if (currency?.paymentMethod?.type !== 'arcblock') {
231
+ return (
232
+ <Box>
233
+ <Button startIcon={<ArrowBackOutlined />} variant="outlined" onClick={() => goBackOrFallback('/customer')}>
234
+ {t('common.previous')}
235
+ </Button>
236
+ <Empty>{t('customer.recharge.unsupported')}</Empty>
237
+ </Box>
238
+ );
239
+ }
240
+
241
+ if (error) {
242
+ return (
243
+ <Box>
244
+ <Button startIcon={<ArrowBackOutlined />} variant="outlined" onClick={() => goBackOrFallback('/customer')}>
245
+ {t('common.previous')}
246
+ </Button>
247
+ <Alert severity="error" sx={{ mt: 2 }}>
248
+ {error}
249
+ </Alert>
250
+ </Box>
251
+ );
252
+ }
253
+
254
+ const currentBalance = formatBNStr(payerValue?.token || '0', currency?.decimal || 0, 6, false);
255
+ const balanceLink = currency?.paymentMethod
256
+ ? getTokenBalanceLink(currency.paymentMethod, payerValue?.paymentAddress || '')
257
+ : undefined;
258
+
259
+ return (
260
+ <Root>
261
+ <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ position: 'relative' }}>
262
+ <Button
263
+ startIcon={<ArrowBackOutlined />}
264
+ variant="text"
265
+ color="inherit"
266
+ onClick={() => goBackOrFallback('/customer')}
267
+ sx={{ color: 'text.secondary' }}>
268
+ {t('common.previous')}
269
+ </Button>
270
+ </Stack>
271
+
272
+ <Typography variant="h2" gutterBottom>
273
+ {currency?.symbol} {t('customer.recharge.title')}
274
+ </Typography>
275
+
276
+ {currency && (
277
+ <Box>
278
+ {relatedSubscriptions.length > 0 && (
279
+ <Box sx={{ mb: 3 }}>
280
+ <Typography variant="h6" gutterBottom>
281
+ {t('customer.recharge.relatedSubscriptions')}
282
+ </Typography>
283
+
284
+ <SubscriptionScroll>
285
+ {relatedSubscriptions.map((subscription) => (
286
+ <Stack
287
+ key={subscription.id}
288
+ onClick={() => handleSubscriptionClick(subscription.id)}
289
+ className="base-card"
290
+ sx={{
291
+ minWidth: '220px',
292
+ maxWidth: '220px',
293
+ }}>
294
+ <Stack direction="row" alignItems="center" spacing={1.5} sx={{ mb: 1 }}>
295
+ <Typography
296
+ variant="subtitle1"
297
+ sx={{
298
+ fontWeight: 'medium',
299
+ overflow: 'hidden',
300
+ textOverflow: 'ellipsis',
301
+ whiteSpace: 'nowrap',
302
+ color: 'text.link',
303
+ cursor: 'pointer',
304
+ }}>
305
+ {subscription.description || subscription.id}
306
+ </Typography>
307
+ </Stack>
308
+
309
+ {subscription.items && subscription.items[0] && currency && (
310
+ <Typography variant="body1" sx={{ color: 'text.secondary' }}>
311
+ {formatPrice(subscription.items[0].price, currency)}
312
+ </Typography>
313
+ )}
314
+ </Stack>
315
+ ))}
316
+ </SubscriptionScroll>
317
+ </Box>
318
+ )}
319
+
320
+ <Box sx={{ mb: 4 }}>
321
+ <BalanceCard elevation={0}>
322
+ <Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
323
+ <Stack spacing={0.5}>
324
+ <Typography variant="body2" color="text.secondary">
325
+ {t('customer.recharge.receiveAddress')}
326
+ </Typography>
327
+ <Typography variant="body1" sx={{ wordBreak: 'break-all', fontFamily: 'monospace' }}>
328
+ {payerValue?.paymentAddress || t('customer.balance.addressNotFound')}
329
+ </Typography>
330
+ </Stack>
331
+ {currency.logo && <Avatar src={currency.logo} alt={currency.name} sx={{ width: 40, height: 40 }} />}
332
+ </Stack>
333
+ </BalanceCard>
334
+
335
+ <BalanceCard
336
+ elevation={0}
337
+ onClick={() => balanceLink && window.open(balanceLink, '_blank')}
338
+ sx={{
339
+ background: 'var(--tags-tag-orange-bg, #B7FEE3)',
340
+ color: 'var(--tags-tag-orange-text, #007C52)',
341
+ borderRadius: 'var(--radius-m, 8px)',
342
+ border: 'none',
343
+ transition: 'all 0.2s ease-in-out',
344
+ cursor: balanceLink ? 'pointer' : 'default',
345
+ position: 'relative',
346
+ '&:hover': balanceLink
347
+ ? {
348
+ transform: 'translateY(-4px)',
349
+ boxShadow: '0 8px 16px rgba(0, 124, 82, 0.1)',
350
+ '& .arrow-icon': {
351
+ opacity: 1,
352
+ transform: 'translateX(0)',
353
+ },
354
+ }
355
+ : undefined,
356
+ }}>
357
+ <Stack direction="row" alignItems="center" justifyContent="space-between">
358
+ <Stack>
359
+ <Typography variant="h6" color="inherit">
360
+ {t('admin.customer.summary.balance')}
361
+ </Typography>
362
+ <Typography variant="h3" sx={{ fontWeight: 'bold', mt: 1 }}>
363
+ {currentBalance} {currency.symbol}
364
+ </Typography>
365
+ </Stack>
366
+ {balanceLink && (
367
+ <ArrowForwardOutlined
368
+ className="arrow-icon"
369
+ sx={{
370
+ opacity: 0,
371
+ transform: 'translateX(-10px)',
372
+ transition: 'all 0.2s ease-in-out',
373
+ color: 'inherit',
374
+ fontSize: 30,
375
+ }}
376
+ />
377
+ )}
378
+ </Stack>
379
+ </BalanceCard>
380
+ </Box>
381
+
382
+ <Paper
383
+ ref={rechargeRef}
384
+ elevation={0}
385
+ sx={{ mb: 3, backgroundColor: 'background.default', borderRadius: '16px' }}>
386
+ <Grid container spacing={2}>
387
+ {presetAmounts.map(({ amount: presetAmount }) => (
388
+ <Grid item xs={6} sm={3} key={presetAmount}>
389
+ <Card
390
+ variant="outlined"
391
+ sx={{
392
+ height: '100%',
393
+ display: 'flex',
394
+ flexDirection: 'column',
395
+ justifyContent: 'center',
396
+ transition: 'all 0.2s',
397
+ cursor: 'pointer',
398
+ borderRadius: '12px',
399
+ '&:hover': {
400
+ transform: 'translateY(-4px)',
401
+ boxShadow: 3,
402
+ },
403
+ ...(amount === presetAmount && !customAmount
404
+ ? {
405
+ borderColor: 'primary.main',
406
+ borderWidth: 2,
407
+ backgroundColor: 'primary.lighter',
408
+ }
409
+ : {}),
410
+ }}>
411
+ <CardActionArea onClick={() => handleSelect(presetAmount)} sx={{ height: '100%', p: 1.5 }}>
412
+ <Typography
413
+ variant="h6"
414
+ align="center"
415
+ sx={{
416
+ fontWeight: 600,
417
+ color: amount === presetAmount && !customAmount ? 'primary.main' : 'text.primary',
418
+ }}>
419
+ {presetAmount} {currency.symbol}
420
+ </Typography>
421
+ </CardActionArea>
422
+ </Card>
423
+ </Grid>
424
+ ))}
425
+ <Grid item xs={6} sm={3}>
426
+ <Card
427
+ variant="outlined"
428
+ sx={{
429
+ height: '100%',
430
+ display: 'flex',
431
+ flexDirection: 'column',
432
+ justifyContent: 'center',
433
+ transition: 'all 0.2s',
434
+ cursor: 'pointer',
435
+ borderRadius: '12px',
436
+ '&:hover': {
437
+ transform: 'translateY(-4px)',
438
+ boxShadow: '0 4px 10px rgba(0,0,0,0.1)',
439
+ },
440
+ ...(customAmount
441
+ ? {
442
+ borderColor: 'primary.main',
443
+ borderWidth: 2,
444
+ backgroundColor: 'primary.lighter',
445
+ }
446
+ : {}),
447
+ }}>
448
+ <CardActionArea onClick={handleCustomSelect} sx={{ height: '100%', p: 1 }}>
449
+ <Stack direction="row" spacing={1} justifyContent="center" alignItems="center">
450
+ <Typography
451
+ variant="h6"
452
+ align="center"
453
+ sx={{ fontWeight: 600, color: customAmount ? 'primary.main' : 'text.primary' }}>
454
+ {t('customer.recharge.custom') || t('common.custom')}
455
+ </Typography>
456
+ </Stack>
457
+ </CardActionArea>
458
+ </Card>
459
+ </Grid>
460
+ </Grid>
461
+ </Paper>
462
+
463
+ {customAmount && (
464
+ <Box sx={{ mb: 3 }}>
465
+ <TextField
466
+ fullWidth
467
+ label={t('customer.recharge.amount')}
468
+ variant="outlined"
469
+ type="text"
470
+ value={amount}
471
+ error={!!amountError}
472
+ helperText={amountError}
473
+ onChange={handleAmountChange}
474
+ InputProps={{
475
+ endAdornment: <Typography>{currency.symbol}</Typography>,
476
+ autoComplete: 'off',
477
+ }}
478
+ inputRef={customInputRef}
479
+ />
480
+ </Box>
481
+ )}
482
+
483
+ <Button
484
+ fullWidth
485
+ size="large"
486
+ variant="contained"
487
+ color="primary"
488
+ onClick={handleRecharge}
489
+ disabled={!amount || parseFloat(amount) <= 0 || !!amountError}
490
+ sx={{
491
+ mb: 4,
492
+ py: 1.5,
493
+ borderRadius: '8px',
494
+ boxShadow: '0 4px 10px rgba(0,0,0,0.1)',
495
+ '&:hover': {
496
+ boxShadow: '0 6px 15px rgba(0,0,0,0.15)',
497
+ transform: 'translateY(-2px)',
498
+ },
499
+ transition: 'all 0.2s',
500
+ }}>
501
+ {t('customer.recharge.submit')}
502
+ </Button>
503
+
504
+ <Divider sx={{ mb: 3 }} />
505
+
506
+ <Typography variant="h4" gutterBottom>
507
+ {t('customer.recharge.history')}
508
+ </Typography>
509
+
510
+ <RechargeList currency_id={currencyId} />
511
+ </Box>
512
+ )}
513
+ </Root>
514
+ );
515
+ }