payment-kit 1.18.15 → 1.18.17
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.
- package/api/src/index.ts +2 -0
- package/api/src/libs/invoice.ts +5 -3
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +32 -14
- package/api/src/libs/session.ts +9 -1
- package/api/src/libs/util.ts +12 -4
- package/api/src/routes/checkout-sessions.ts +286 -120
- package/api/src/routes/connect/change-payment.ts +9 -1
- package/api/src/routes/connect/change-plan.ts +9 -1
- package/api/src/routes/connect/collect-batch.ts +7 -5
- package/api/src/routes/connect/pay.ts +1 -1
- package/api/src/routes/connect/recharge-account.ts +124 -0
- package/api/src/routes/connect/setup.ts +8 -1
- package/api/src/routes/connect/shared.ts +175 -54
- package/api/src/routes/connect/subscribe.ts +11 -1
- package/api/src/routes/customers.ts +150 -7
- package/api/src/routes/donations.ts +1 -1
- package/api/src/routes/invoices.ts +47 -1
- package/api/src/routes/subscriptions.ts +0 -3
- package/blocklet.yml +2 -1
- package/package.json +16 -16
- package/src/app.tsx +11 -3
- package/src/components/info-card.tsx +6 -2
- package/src/components/info-row.tsx +1 -0
- package/src/components/invoice/recharge.tsx +85 -56
- package/src/components/invoice/table.tsx +7 -1
- package/src/components/subscription/portal/actions.tsx +1 -1
- package/src/components/subscription/portal/list.tsx +6 -0
- package/src/locales/en.tsx +9 -0
- package/src/locales/zh.tsx +9 -0
- package/src/pages/admin/payments/payouts/detail.tsx +16 -5
- package/src/pages/customer/index.tsx +226 -284
- package/src/pages/customer/invoice/detail.tsx +24 -16
- package/src/pages/customer/invoice/past-due.tsx +46 -23
- package/src/pages/customer/payout/detail.tsx +16 -5
- package/src/pages/customer/recharge/account.tsx +513 -0
- package/src/pages/customer/{recharge.tsx → recharge/subscription.tsx} +22 -19
- package/src/pages/customer/subscription/embed.tsx +16 -1
|
@@ -0,0 +1,513 @@
|
|
|
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 { AccountBalanceWalletOutlined, 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
|
+
}));
|
|
65
|
+
|
|
66
|
+
const BalanceCard = styled(Card)(({ theme }) => ({
|
|
67
|
+
padding: theme.spacing(2),
|
|
68
|
+
backgroundColor: theme.palette.background.paper,
|
|
69
|
+
border: `1px solid ${theme.palette.divider}`,
|
|
70
|
+
borderRadius: theme.shape.borderRadius,
|
|
71
|
+
marginBottom: theme.spacing(2),
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
export default function BalanceRechargePage() {
|
|
75
|
+
const { t, locale } = useLocaleContext();
|
|
76
|
+
const { currencyId } = useParams<{ currencyId: string }>();
|
|
77
|
+
const navigate = useNavigate();
|
|
78
|
+
const { connect } = usePaymentContext();
|
|
79
|
+
const [amount, setAmount] = useState('50');
|
|
80
|
+
const [amountError, setAmountError] = useState('');
|
|
81
|
+
const [loading, setLoading] = useState(true);
|
|
82
|
+
const [error, setError] = useState('');
|
|
83
|
+
const customInputRef = useRef<HTMLInputElement>(null);
|
|
84
|
+
const [payerValue, setPayerValue] = useState<{
|
|
85
|
+
paymentAddress: string;
|
|
86
|
+
token: string;
|
|
87
|
+
} | null>(null);
|
|
88
|
+
const [customAmount, setCustomAmount] = useState(false);
|
|
89
|
+
const [presetAmounts] = useState<Array<{ amount: string }>>([{ amount: '10' }, { amount: '50' }, { amount: '100' }]);
|
|
90
|
+
const { session } = useSessionContext();
|
|
91
|
+
const [currency, setCurrency] = useState<ExtendedPaymentCurrency | null>(null);
|
|
92
|
+
const [relatedSubscriptions, setRelatedSubscriptions] = useState<Subscription[]>([]);
|
|
93
|
+
|
|
94
|
+
const fetchData = async () => {
|
|
95
|
+
try {
|
|
96
|
+
setLoading(true);
|
|
97
|
+
if (!currencyId) {
|
|
98
|
+
// 如果没有 currencyId,重定向回客户页面
|
|
99
|
+
navigate('/customer');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const { data } = await api.get(`/api/customers/recharge?currencyId=${currencyId}`);
|
|
104
|
+
|
|
105
|
+
if (data.currency) {
|
|
106
|
+
setCurrency(data.currency);
|
|
107
|
+
setRelatedSubscriptions(data.relatedSubscriptions || []);
|
|
108
|
+
const supportRecharge = data.currency?.paymentMethod?.type === 'arcblock';
|
|
109
|
+
if (supportRecharge) {
|
|
110
|
+
const payerTokenRes = await api.get(`/api/customers/payer-token?currencyId=${currencyId}`);
|
|
111
|
+
if (payerTokenRes?.data) {
|
|
112
|
+
setPayerValue(payerTokenRes.data);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
setError(t('customer.balance.currency.notFound'));
|
|
117
|
+
}
|
|
118
|
+
} catch (err) {
|
|
119
|
+
setError(formatError(err) || t('common.fetchError'));
|
|
120
|
+
console.error(err);
|
|
121
|
+
} finally {
|
|
122
|
+
setLoading(false);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const rechargeRef = useRef<HTMLDivElement>(null);
|
|
127
|
+
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
fetchData();
|
|
130
|
+
}, [currencyId]);
|
|
131
|
+
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (rechargeRef.current && currency) {
|
|
134
|
+
setTimeout(() => {
|
|
135
|
+
if (rechargeRef.current) {
|
|
136
|
+
const rechargePosition = rechargeRef.current.getBoundingClientRect();
|
|
137
|
+
const absoluteTop = window.scrollY + rechargePosition.top;
|
|
138
|
+
const scrollToPosition = absoluteTop - 120;
|
|
139
|
+
window.scrollTo({
|
|
140
|
+
top: scrollToPosition,
|
|
141
|
+
behavior: 'smooth',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}, 200);
|
|
145
|
+
}
|
|
146
|
+
}, [currency, loading]);
|
|
147
|
+
|
|
148
|
+
const handleRecharge = () => {
|
|
149
|
+
if (!currency) return;
|
|
150
|
+
|
|
151
|
+
if (Number.isNaN(Number(amount))) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
connect.open({
|
|
156
|
+
containerEl: undefined as unknown as Element,
|
|
157
|
+
saveConnect: false,
|
|
158
|
+
action: 'recharge-account',
|
|
159
|
+
prefix: joinURL(getPrefix(), '/api/did'),
|
|
160
|
+
extraParams: {
|
|
161
|
+
customerDid: session?.user?.did,
|
|
162
|
+
currencyId: currency.id,
|
|
163
|
+
amount: Number(amount),
|
|
164
|
+
},
|
|
165
|
+
onSuccess: () => {
|
|
166
|
+
connect.close();
|
|
167
|
+
Toast.success(t('customer.recharge.success'));
|
|
168
|
+
fetchData();
|
|
169
|
+
},
|
|
170
|
+
onClose: () => {
|
|
171
|
+
connect.close();
|
|
172
|
+
},
|
|
173
|
+
onError: (err: any) => {
|
|
174
|
+
Toast.error(formatError(err));
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const handleAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
180
|
+
const { value } = e.target;
|
|
181
|
+
if (!currency) return;
|
|
182
|
+
if (!/^\d*\.?\d*$/.test(value)) return;
|
|
183
|
+
// 不允许以小数点开头
|
|
184
|
+
if (value.startsWith('.')) return;
|
|
185
|
+
const precision = currency.decimal;
|
|
186
|
+
const errorMessage = formatAmountPrecisionLimit(value, locale, precision || 6);
|
|
187
|
+
setAmountError(errorMessage || '');
|
|
188
|
+
setAmount(value);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const handleSelect = (selectedAmount: string) => {
|
|
192
|
+
setAmount(selectedAmount);
|
|
193
|
+
setCustomAmount(false);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const handleCustomSelect = () => {
|
|
197
|
+
setCustomAmount(true);
|
|
198
|
+
setAmount('');
|
|
199
|
+
setTimeout(() => {
|
|
200
|
+
customInputRef.current?.focus();
|
|
201
|
+
}, 0);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const handleSubscriptionClick = (subscriptionId: string) => {
|
|
205
|
+
navigate(`/customer/subscription/${subscriptionId}`);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
if (loading) {
|
|
209
|
+
return (
|
|
210
|
+
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '50vh' }}>
|
|
211
|
+
<CircularProgress />
|
|
212
|
+
</Box>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (currency?.paymentMethod?.type !== 'arcblock') {
|
|
217
|
+
return (
|
|
218
|
+
<Box>
|
|
219
|
+
<Button startIcon={<ArrowBackOutlined />} variant="outlined" onClick={() => goBackOrFallback('/customer')}>
|
|
220
|
+
{t('common.previous')}
|
|
221
|
+
</Button>
|
|
222
|
+
<Empty>{t('customer.recharge.unsupported')}</Empty>
|
|
223
|
+
</Box>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (error) {
|
|
228
|
+
return (
|
|
229
|
+
<Box>
|
|
230
|
+
<Button startIcon={<ArrowBackOutlined />} variant="outlined" onClick={() => goBackOrFallback('/customer')}>
|
|
231
|
+
{t('common.previous')}
|
|
232
|
+
</Button>
|
|
233
|
+
<Alert severity="error" sx={{ mt: 2 }}>
|
|
234
|
+
{error}
|
|
235
|
+
</Alert>
|
|
236
|
+
</Box>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const currentBalance = formatBNStr(payerValue?.token || '0', currency?.decimal || 0, 6, false);
|
|
241
|
+
const balanceLink = currency?.paymentMethod
|
|
242
|
+
? getTokenBalanceLink(currency.paymentMethod, payerValue?.paymentAddress || '')
|
|
243
|
+
: undefined;
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<Root>
|
|
247
|
+
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ position: 'relative' }}>
|
|
248
|
+
<Button
|
|
249
|
+
startIcon={<ArrowBackOutlined />}
|
|
250
|
+
variant="text"
|
|
251
|
+
color="inherit"
|
|
252
|
+
onClick={() => goBackOrFallback('/customer')}
|
|
253
|
+
sx={{ color: 'text.secondary' }}>
|
|
254
|
+
{t('common.previous')}
|
|
255
|
+
</Button>
|
|
256
|
+
</Stack>
|
|
257
|
+
|
|
258
|
+
<Typography variant="h2" gutterBottom>
|
|
259
|
+
{currency?.symbol} {t('customer.recharge.title')}
|
|
260
|
+
</Typography>
|
|
261
|
+
|
|
262
|
+
{currency && (
|
|
263
|
+
<Box ref={rechargeRef}>
|
|
264
|
+
<Stack sx={{ maxWidth: '600px' }}>
|
|
265
|
+
<Box sx={{ mb: 4 }}>
|
|
266
|
+
<BalanceCard elevation={0}>
|
|
267
|
+
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
|
|
268
|
+
<Stack spacing={0.5}>
|
|
269
|
+
<Typography variant="body2" color="text.secondary">
|
|
270
|
+
{t('customer.recharge.receiveAddress')}
|
|
271
|
+
</Typography>
|
|
272
|
+
<Typography variant="body1" sx={{ wordBreak: 'break-all', fontFamily: 'monospace' }}>
|
|
273
|
+
{payerValue?.paymentAddress || t('customer.balance.addressNotFound')}
|
|
274
|
+
</Typography>
|
|
275
|
+
</Stack>
|
|
276
|
+
{currency.logo && (
|
|
277
|
+
<Avatar
|
|
278
|
+
src={currency.logo}
|
|
279
|
+
alt={currency.name}
|
|
280
|
+
sx={{
|
|
281
|
+
width: {
|
|
282
|
+
xs: 30,
|
|
283
|
+
sm: 40,
|
|
284
|
+
},
|
|
285
|
+
height: {
|
|
286
|
+
xs: 30,
|
|
287
|
+
sm: 40,
|
|
288
|
+
},
|
|
289
|
+
}}
|
|
290
|
+
/>
|
|
291
|
+
)}
|
|
292
|
+
</Stack>
|
|
293
|
+
</BalanceCard>
|
|
294
|
+
|
|
295
|
+
<BalanceCard
|
|
296
|
+
elevation={0}
|
|
297
|
+
onClick={() => balanceLink && window.open(balanceLink, '_blank')}
|
|
298
|
+
sx={{
|
|
299
|
+
color: 'text.primary',
|
|
300
|
+
borderRadius: 'var(--radius-m, 8px)',
|
|
301
|
+
transition: 'all 0.2s ease-in-out',
|
|
302
|
+
cursor: balanceLink ? 'pointer' : 'default',
|
|
303
|
+
position: 'relative',
|
|
304
|
+
'&:hover': balanceLink
|
|
305
|
+
? {
|
|
306
|
+
'& .arrow-icon': {
|
|
307
|
+
opacity: 1,
|
|
308
|
+
transform: 'translateX(0)',
|
|
309
|
+
},
|
|
310
|
+
}
|
|
311
|
+
: undefined,
|
|
312
|
+
}}>
|
|
313
|
+
<Stack direction="row" alignItems="center" justifyContent="space-between">
|
|
314
|
+
<Stack>
|
|
315
|
+
<Typography
|
|
316
|
+
variant="body1"
|
|
317
|
+
color="inherit"
|
|
318
|
+
sx={{ display: 'flex', alignItems: 'center', gap: 1, color: 'text.secondary' }}>
|
|
319
|
+
<AccountBalanceWalletOutlined color="success" fontSize="small" />
|
|
320
|
+
{t('admin.customer.summary.balance')}
|
|
321
|
+
</Typography>
|
|
322
|
+
<Typography variant="h3" sx={{ fontWeight: 'bold', mt: 1, color: 'text.primary' }}>
|
|
323
|
+
{currentBalance} {currency.symbol}
|
|
324
|
+
</Typography>
|
|
325
|
+
</Stack>
|
|
326
|
+
{balanceLink && (
|
|
327
|
+
<ArrowForwardOutlined
|
|
328
|
+
className="arrow-icon"
|
|
329
|
+
sx={{
|
|
330
|
+
opacity: 0,
|
|
331
|
+
transform: 'translateX(-10px)',
|
|
332
|
+
transition: 'all 0.2s ease-in-out',
|
|
333
|
+
color: 'inherit',
|
|
334
|
+
fontSize: 30,
|
|
335
|
+
}}
|
|
336
|
+
/>
|
|
337
|
+
)}
|
|
338
|
+
</Stack>
|
|
339
|
+
</BalanceCard>
|
|
340
|
+
</Box>
|
|
341
|
+
|
|
342
|
+
<Paper elevation={0} sx={{ mb: 3, backgroundColor: 'background.default', borderRadius: '16px' }}>
|
|
343
|
+
<Grid container spacing={2}>
|
|
344
|
+
{presetAmounts.map(({ amount: presetAmount }) => (
|
|
345
|
+
<Grid item xs={6} sm={3} key={presetAmount}>
|
|
346
|
+
<Card
|
|
347
|
+
variant="outlined"
|
|
348
|
+
sx={{
|
|
349
|
+
height: '100%',
|
|
350
|
+
display: 'flex',
|
|
351
|
+
flexDirection: 'column',
|
|
352
|
+
justifyContent: 'center',
|
|
353
|
+
transition: 'all 0.2s',
|
|
354
|
+
cursor: 'pointer',
|
|
355
|
+
borderRadius: '12px',
|
|
356
|
+
'&:hover': {
|
|
357
|
+
transform: 'translateY(-4px)',
|
|
358
|
+
boxShadow: 3,
|
|
359
|
+
},
|
|
360
|
+
...(amount === presetAmount && !customAmount
|
|
361
|
+
? {
|
|
362
|
+
borderColor: 'primary.main',
|
|
363
|
+
borderWidth: 2,
|
|
364
|
+
backgroundColor: 'primary.lighter',
|
|
365
|
+
}
|
|
366
|
+
: {}),
|
|
367
|
+
}}>
|
|
368
|
+
<CardActionArea onClick={() => handleSelect(presetAmount)} sx={{ height: '100%', p: 1.5 }}>
|
|
369
|
+
<Typography
|
|
370
|
+
variant="h6"
|
|
371
|
+
align="center"
|
|
372
|
+
sx={{
|
|
373
|
+
fontWeight: 600,
|
|
374
|
+
color: amount === presetAmount && !customAmount ? 'primary.main' : 'text.primary',
|
|
375
|
+
}}>
|
|
376
|
+
{presetAmount} {currency.symbol}
|
|
377
|
+
</Typography>
|
|
378
|
+
</CardActionArea>
|
|
379
|
+
</Card>
|
|
380
|
+
</Grid>
|
|
381
|
+
))}
|
|
382
|
+
<Grid item xs={6} sm={3}>
|
|
383
|
+
<Card
|
|
384
|
+
variant="outlined"
|
|
385
|
+
sx={{
|
|
386
|
+
height: '100%',
|
|
387
|
+
display: 'flex',
|
|
388
|
+
flexDirection: 'column',
|
|
389
|
+
justifyContent: 'center',
|
|
390
|
+
transition: 'all 0.2s',
|
|
391
|
+
cursor: 'pointer',
|
|
392
|
+
borderRadius: '12px',
|
|
393
|
+
'&:hover': {
|
|
394
|
+
transform: 'translateY(-4px)',
|
|
395
|
+
boxShadow: '0 4px 10px rgba(0,0,0,0.1)',
|
|
396
|
+
},
|
|
397
|
+
...(customAmount
|
|
398
|
+
? {
|
|
399
|
+
borderColor: 'primary.main',
|
|
400
|
+
borderWidth: 2,
|
|
401
|
+
backgroundColor: 'primary.lighter',
|
|
402
|
+
}
|
|
403
|
+
: {}),
|
|
404
|
+
}}>
|
|
405
|
+
<CardActionArea onClick={handleCustomSelect} sx={{ height: '100%', p: 1 }}>
|
|
406
|
+
<Stack direction="row" spacing={1} justifyContent="center" alignItems="center">
|
|
407
|
+
<Typography
|
|
408
|
+
variant="h6"
|
|
409
|
+
align="center"
|
|
410
|
+
sx={{ fontWeight: 600, color: customAmount ? 'primary.main' : 'text.primary' }}>
|
|
411
|
+
{t('customer.recharge.custom') || t('common.custom')}
|
|
412
|
+
</Typography>
|
|
413
|
+
</Stack>
|
|
414
|
+
</CardActionArea>
|
|
415
|
+
</Card>
|
|
416
|
+
</Grid>
|
|
417
|
+
</Grid>
|
|
418
|
+
</Paper>
|
|
419
|
+
|
|
420
|
+
{customAmount && (
|
|
421
|
+
<Box sx={{ mb: 3 }}>
|
|
422
|
+
<TextField
|
|
423
|
+
fullWidth
|
|
424
|
+
label={t('customer.recharge.amount')}
|
|
425
|
+
variant="outlined"
|
|
426
|
+
type="text"
|
|
427
|
+
value={amount}
|
|
428
|
+
error={!!amountError}
|
|
429
|
+
helperText={amountError}
|
|
430
|
+
onChange={handleAmountChange}
|
|
431
|
+
InputProps={{
|
|
432
|
+
endAdornment: <Typography>{currency.symbol}</Typography>,
|
|
433
|
+
autoComplete: 'off',
|
|
434
|
+
}}
|
|
435
|
+
inputRef={customInputRef}
|
|
436
|
+
/>
|
|
437
|
+
</Box>
|
|
438
|
+
)}
|
|
439
|
+
|
|
440
|
+
<Button
|
|
441
|
+
fullWidth
|
|
442
|
+
size="large"
|
|
443
|
+
variant="contained"
|
|
444
|
+
color="primary"
|
|
445
|
+
onClick={handleRecharge}
|
|
446
|
+
disabled={!amount || parseFloat(amount) <= 0 || !!amountError}
|
|
447
|
+
sx={{
|
|
448
|
+
mb: 4,
|
|
449
|
+
py: 1.5,
|
|
450
|
+
borderRadius: '8px',
|
|
451
|
+
boxShadow: '0 4px 10px rgba(0,0,0,0.1)',
|
|
452
|
+
'&:hover': {
|
|
453
|
+
boxShadow: '0 6px 15px rgba(0,0,0,0.15)',
|
|
454
|
+
transform: 'translateY(-2px)',
|
|
455
|
+
},
|
|
456
|
+
transition: 'all 0.2s',
|
|
457
|
+
}}>
|
|
458
|
+
{t('customer.recharge.submit')}
|
|
459
|
+
</Button>
|
|
460
|
+
</Stack>
|
|
461
|
+
{relatedSubscriptions.length > 0 && (
|
|
462
|
+
<Box sx={{ mb: 3 }}>
|
|
463
|
+
<Typography variant="h4" gutterBottom>
|
|
464
|
+
{t('customer.recharge.relatedSubscriptions')}
|
|
465
|
+
</Typography>
|
|
466
|
+
|
|
467
|
+
<Stack direction="row" sx={{ flexWrap: 'wrap', gap: 2 }}>
|
|
468
|
+
{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>
|
|
491
|
+
|
|
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>
|
|
498
|
+
))}
|
|
499
|
+
</Stack>
|
|
500
|
+
</Box>
|
|
501
|
+
)}
|
|
502
|
+
<Divider sx={{ mb: 3 }} />
|
|
503
|
+
|
|
504
|
+
<Typography variant="h4" gutterBottom>
|
|
505
|
+
{t('customer.recharge.history')}
|
|
506
|
+
</Typography>
|
|
507
|
+
|
|
508
|
+
<RechargeList currency_id={currencyId} />
|
|
509
|
+
</Box>
|
|
510
|
+
)}
|
|
511
|
+
</Root>
|
|
512
|
+
);
|
|
513
|
+
}
|
|
@@ -28,17 +28,17 @@ import {
|
|
|
28
28
|
} from '@blocklet/payment-react';
|
|
29
29
|
import { joinURL } from 'ufo';
|
|
30
30
|
import type { TSubscriptionExpanded } from '@blocklet/payment-types';
|
|
31
|
-
import { ArrowBackOutlined, ArrowForwardOutlined } from '@mui/icons-material';
|
|
31
|
+
import { AccountBalanceWalletOutlined, ArrowBackOutlined, ArrowForwardOutlined } from '@mui/icons-material';
|
|
32
32
|
import { BN, fromUnitToToken } from '@ocap/util';
|
|
33
|
-
import RechargeList from '
|
|
34
|
-
import SubscriptionDescription from '
|
|
35
|
-
import InfoRow from '
|
|
36
|
-
import Currency from '
|
|
37
|
-
import SubscriptionMetrics from '
|
|
38
|
-
import { getTokenBalanceLink, goBackOrFallback } from '
|
|
39
|
-
import CustomerLink from '
|
|
40
|
-
import { useSessionContext } from '
|
|
41
|
-
import { formatSmartDuration, TimeUnit } from '
|
|
33
|
+
import RechargeList from '../../../components/invoice/recharge';
|
|
34
|
+
import SubscriptionDescription from '../../../components/subscription/description';
|
|
35
|
+
import InfoRow from '../../../components/info-row';
|
|
36
|
+
import Currency from '../../../components/currency';
|
|
37
|
+
import SubscriptionMetrics from '../../../components/subscription/metrics';
|
|
38
|
+
import { getTokenBalanceLink, goBackOrFallback } from '../../../libs/util';
|
|
39
|
+
import CustomerLink from '../../../components/customer/link';
|
|
40
|
+
import { useSessionContext } from '../../../contexts/session';
|
|
41
|
+
import { formatSmartDuration, TimeUnit } from '../../../libs/dayjs';
|
|
42
42
|
|
|
43
43
|
const Root = styled(Stack)(({ theme }) => ({
|
|
44
44
|
marginBottom: theme.spacing(3),
|
|
@@ -328,7 +328,7 @@ export default function RechargePage() {
|
|
|
328
328
|
<Divider />
|
|
329
329
|
|
|
330
330
|
<Box sx={{ maxWidth: 600 }} ref={rechargeRef}>
|
|
331
|
-
<Typography variant="
|
|
331
|
+
<Typography variant="h4" gutterBottom>
|
|
332
332
|
{t('customer.recharge.title')}
|
|
333
333
|
</Typography>
|
|
334
334
|
|
|
@@ -344,14 +344,11 @@ export default function RechargePage() {
|
|
|
344
344
|
<BalanceCard
|
|
345
345
|
onClick={() => balanceLink && window.open(balanceLink, '_blank')}
|
|
346
346
|
sx={{
|
|
347
|
-
|
|
348
|
-
color: 'var(--tags-tag-orange-text, #007C52)',
|
|
347
|
+
color: 'text.primary',
|
|
349
348
|
borderRadius: 'var(--radius-m, 8px)',
|
|
350
|
-
border: 'none',
|
|
351
|
-
|
|
352
349
|
transition: 'all 0.2s ease-in-out',
|
|
353
|
-
position: 'relative',
|
|
354
350
|
cursor: balanceLink ? 'pointer' : 'default',
|
|
351
|
+
position: 'relative',
|
|
355
352
|
'&:hover': balanceLink
|
|
356
353
|
? {
|
|
357
354
|
'& .MuiSvgIcon-root': {
|
|
@@ -363,8 +360,14 @@ export default function RechargePage() {
|
|
|
363
360
|
}}>
|
|
364
361
|
<Stack direction="row" alignItems="center" spacing={1}>
|
|
365
362
|
<Stack flex={1}>
|
|
366
|
-
<Typography
|
|
367
|
-
|
|
363
|
+
<Typography
|
|
364
|
+
variant="body1"
|
|
365
|
+
color="inherit"
|
|
366
|
+
sx={{ display: 'flex', alignItems: 'center', gap: 1, color: 'text.secondary' }}>
|
|
367
|
+
<AccountBalanceWalletOutlined color="success" fontSize="small" />
|
|
368
|
+
{t('admin.customer.summary.balance')}
|
|
369
|
+
</Typography>
|
|
370
|
+
<Typography variant="h3" sx={{ fontWeight: 'bold', mt: 1, color: 'text.primary' }}>
|
|
368
371
|
{currentBalance} {subscription.paymentCurrency.symbol}
|
|
369
372
|
</Typography>
|
|
370
373
|
</Stack>
|
|
@@ -492,7 +495,7 @@ export default function RechargePage() {
|
|
|
492
495
|
)}
|
|
493
496
|
</Box>
|
|
494
497
|
<Divider />
|
|
495
|
-
<Typography variant="
|
|
498
|
+
<Typography variant="h4" gutterBottom>
|
|
496
499
|
{t('customer.recharge.history')}
|
|
497
500
|
</Typography>
|
|
498
501
|
<RechargeList subscription_id={subscriptionId} currency_id={paymentCurrency?.id} />
|
|
@@ -216,6 +216,7 @@ export default function SubscriptionEmbed() {
|
|
|
216
216
|
)}
|
|
217
217
|
name={`${subscription.customer.name} (${subscription.customer.email})`}
|
|
218
218
|
description={<DidAddress did={subscription.customer.did} responsive={false} compact />}
|
|
219
|
+
className="owner-info-card"
|
|
219
220
|
/>
|
|
220
221
|
),
|
|
221
222
|
});
|
|
@@ -241,7 +242,21 @@ export default function SubscriptionEmbed() {
|
|
|
241
242
|
</Typography>
|
|
242
243
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
|
243
244
|
{infoList.map(({ name, value }) => {
|
|
244
|
-
return
|
|
245
|
+
return (
|
|
246
|
+
<InfoRow
|
|
247
|
+
label={name}
|
|
248
|
+
value={value}
|
|
249
|
+
sx={{
|
|
250
|
+
mb: 0,
|
|
251
|
+
'.info-row-label': {
|
|
252
|
+
whiteSpace: 'nowrap',
|
|
253
|
+
},
|
|
254
|
+
'.info-row-value': {
|
|
255
|
+
flex: 'none',
|
|
256
|
+
},
|
|
257
|
+
}}
|
|
258
|
+
/>
|
|
259
|
+
);
|
|
245
260
|
})}
|
|
246
261
|
</Box>
|
|
247
262
|
<Divider />
|