payment-kit 1.18.13 → 1.18.15

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 (39) hide show
  1. package/api/src/index.ts +2 -0
  2. package/api/src/integrations/stripe/resource.ts +53 -11
  3. package/api/src/libs/auth.ts +14 -0
  4. package/api/src/libs/payment.ts +77 -2
  5. package/api/src/libs/util.ts +8 -0
  6. package/api/src/queues/payment.ts +50 -1
  7. package/api/src/queues/payout.ts +297 -0
  8. package/api/src/routes/checkout-sessions.ts +2 -7
  9. package/api/src/routes/payment-currencies.ts +120 -1
  10. package/api/src/routes/payment-methods.ts +19 -9
  11. package/api/src/routes/subscriptions.ts +2 -8
  12. package/api/src/store/migrations/20250305-vault-config.ts +21 -0
  13. package/api/src/store/models/payment-currency.ts +14 -0
  14. package/api/src/store/models/payout.ts +21 -0
  15. package/api/src/store/models/types.ts +6 -0
  16. package/blocklet.yml +1 -1
  17. package/package.json +18 -18
  18. package/src/app.tsx +116 -120
  19. package/src/components/customer/overdraft-protection.tsx +1 -0
  20. package/src/components/layout/admin.tsx +6 -0
  21. package/src/components/layout/user.tsx +1 -0
  22. package/src/components/metadata/editor.tsx +7 -1
  23. package/src/components/metadata/list.tsx +3 -0
  24. package/src/components/passport/assign.tsx +3 -0
  25. package/src/components/payment-link/rename.tsx +1 -0
  26. package/src/components/pricing-table/rename.tsx +1 -0
  27. package/src/components/product/add-price.tsx +1 -0
  28. package/src/components/product/edit-price.tsx +1 -0
  29. package/src/components/product/edit.tsx +1 -0
  30. package/src/components/subscription/actions/index.tsx +1 -0
  31. package/src/components/subscription/portal/actions.tsx +1 -0
  32. package/src/locales/en.tsx +42 -0
  33. package/src/locales/zh.tsx +37 -0
  34. package/src/pages/admin/payments/payouts/detail.tsx +47 -43
  35. package/src/pages/admin/settings/index.tsx +3 -3
  36. package/src/pages/admin/settings/payment-methods/index.tsx +33 -1
  37. package/src/pages/admin/settings/vault-config/edit-form.tsx +253 -0
  38. package/src/pages/admin/settings/vault-config/index.tsx +367 -0
  39. package/src/pages/integrations/donations/edit-form.tsx +0 -1
@@ -25,6 +25,7 @@ import {
25
25
  ListItem,
26
26
  ListItemAvatar,
27
27
  ListItemText,
28
+ Skeleton,
28
29
  Stack,
29
30
  TextField,
30
31
  Tooltip,
@@ -296,6 +297,37 @@ function Balance({
296
297
  );
297
298
  }
298
299
 
300
+ function PaymentMethodSkeleton() {
301
+ return (
302
+ <>
303
+ {[1].map((group) => (
304
+ <Box key={group} mt={3}>
305
+ <Stack direction="row" alignItems="center" mb={1} flexWrap="wrap" gap={1}>
306
+ <Skeleton variant="text" width={120} height={32} />
307
+ </Stack>
308
+ <Box
309
+ sx={{
310
+ py: 1,
311
+ borderTop: '1px solid #eee',
312
+ borderBottom: '1px solid #eee',
313
+ mb: 1,
314
+ }}>
315
+ <Stack direction="row" justifyContent="space-between" alignItems="center">
316
+ <Stack direction="row" spacing={2} alignItems="center" sx={{ flex: 1 }}>
317
+ <Skeleton variant="rectangular" width={40} height={40} />
318
+ <Box sx={{ flex: 1 }}>
319
+ <Skeleton variant="text" width="20%" height={24} />
320
+ <Skeleton variant="text" width="40%" height={20} />
321
+ </Box>
322
+ </Stack>
323
+ </Stack>
324
+ </Box>
325
+ </Box>
326
+ ))}
327
+ </>
328
+ );
329
+ }
330
+
299
331
  export default function PaymentMethods() {
300
332
  const { t } = useLocaleContext();
301
333
  const [expandedId, setExpandedId] = useSessionStorageState('payment-method-expanded-id', {
@@ -362,7 +394,7 @@ export default function PaymentMethods() {
362
394
  }
363
395
 
364
396
  if (loading || !data || methods?.length === 0) {
365
- return <CircularProgress />;
397
+ return <PaymentMethodSkeleton />;
366
398
  }
367
399
 
368
400
  const groups = groupByType(methods);
@@ -0,0 +1,253 @@
1
+ import { useForm, Controller } from 'react-hook-form';
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import Toast from '@arcblock/ux/lib/Toast';
4
+ import Dialog from '@arcblock/ux/lib/Dialog';
5
+ import { api, formatBNStr, Switch, formatAmountPrecisionLimit, LoadingButton } from '@blocklet/payment-react';
6
+ import { Box, Button, Stack, TextField, Typography, Alert, Tooltip, InputAdornment, Divider } from '@mui/material';
7
+ import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
8
+ import { useRequest } from 'ahooks';
9
+ import type { TPaymentCurrencyExpanded } from '@blocklet/payment-types';
10
+ import Currency from '../../../../components/currency';
11
+
12
+ type EditFormProps = {
13
+ item: TPaymentCurrencyExpanded | null;
14
+ onClose: () => void;
15
+ onSuccess: () => void;
16
+ isOwner: boolean;
17
+ };
18
+
19
+ type VaultConfigFormData = {
20
+ enabled: boolean;
21
+ deposit_threshold: string;
22
+ withdraw_threshold: string;
23
+ };
24
+
25
+ const DEFAULT_DEPOSIT_THRESHOLD = '100';
26
+
27
+ const updateVaultConfig = async (currencyId: string, data: VaultConfigFormData) => {
28
+ const res = await api.put(`/api/payment-currencies/${currencyId}/vault-config`, data);
29
+ return res.data;
30
+ };
31
+
32
+ function EditForm({ item, onClose, onSuccess, isOwner }: EditFormProps) {
33
+ const { t, locale } = useLocaleContext();
34
+ const {
35
+ control,
36
+ handleSubmit,
37
+ watch,
38
+ formState: { errors },
39
+ } = useForm<VaultConfigFormData>({
40
+ defaultValues: {
41
+ enabled: true,
42
+ deposit_threshold:
43
+ item?.vault_config?.deposit_threshold && item?.vault_config?.deposit_threshold !== '0'
44
+ ? formatBNStr(item.vault_config.deposit_threshold, item?.decimal || 18)
45
+ : DEFAULT_DEPOSIT_THRESHOLD,
46
+ withdraw_threshold: item?.vault_config?.withdraw_threshold
47
+ ? formatBNStr(item.vault_config.withdraw_threshold, item?.decimal || 18)
48
+ : '0',
49
+ },
50
+ mode: 'onChange',
51
+ });
52
+
53
+ const enabled = watch('enabled');
54
+ // const withdrawThreshold = watch('withdraw_threshold');
55
+
56
+ const getDepositHelperText = (error?: { message?: string }) => {
57
+ if (error) return error.message;
58
+ return t('admin.vaultConfig.depositThresholdHelp');
59
+ };
60
+
61
+ // const getWithdrawHelperText = (error?: { message?: string }, threshold?: string) => {
62
+ // if (error) return error.message;
63
+ // return Number(threshold) === 0
64
+ // ? t('admin.vaultConfig.withdrawThresholdNoLimit')
65
+ // : t('admin.vaultConfig.withdrawThresholdHelp');
66
+ // };
67
+
68
+ const { runAsync: runUpdateVaultConfig, loading } = useRequest(updateVaultConfig, {
69
+ manual: true,
70
+ });
71
+
72
+ const onSubmit = (data: VaultConfigFormData) => {
73
+ if (!item) return;
74
+
75
+ const getSuccessMessage = () => {
76
+ const originalEnabled = item.vault_config?.enabled;
77
+ if (!originalEnabled && data.enabled) {
78
+ return t('admin.vaultConfig.enableSuccess', {
79
+ currency: item.symbol,
80
+ });
81
+ }
82
+ if (originalEnabled && !data.enabled) {
83
+ return t('admin.vaultConfig.disableSuccess', {
84
+ currency: item.symbol,
85
+ });
86
+ }
87
+ return t('admin.vaultConfig.updateSuccess', {
88
+ currency: item.symbol,
89
+ });
90
+ };
91
+
92
+ runUpdateVaultConfig(item.id, data).then(() => {
93
+ Toast.success(getSuccessMessage());
94
+ onSuccess();
95
+ });
96
+ };
97
+
98
+ if (!item) return null;
99
+
100
+ const renderLabelWithTooltip = (label: string, tooltip: string) => (
101
+ <Stack direction="row" spacing={1} alignItems="center">
102
+ <Typography variant="subtitle2">{label}</Typography>
103
+ <Tooltip title={tooltip}>
104
+ <HelpOutlineIcon fontSize="small" sx={{ color: 'text.secondary' }} />
105
+ </Tooltip>
106
+ </Stack>
107
+ );
108
+
109
+ return (
110
+ <Dialog
111
+ open
112
+ fullWidth
113
+ onClose={onClose}
114
+ className="base-dialog"
115
+ title={
116
+ item.vault_config?.enabled
117
+ ? t('admin.vaultConfig.editTitle', { currency: item.symbol })
118
+ : t('admin.vaultConfig.enableTitle', { currency: item.symbol })
119
+ }
120
+ actions={
121
+ <Stack direction="row" spacing={2}>
122
+ <Button variant="outlined" onClick={onClose}>
123
+ {t('common.cancel')}
124
+ </Button>
125
+ <LoadingButton
126
+ variant="contained"
127
+ color="primary"
128
+ disabled={loading || !isOwner}
129
+ onClick={handleSubmit(onSubmit)}>
130
+ {t('common.save')}
131
+ </LoadingButton>
132
+ </Stack>
133
+ }>
134
+ <Box sx={{ pb: 2 }}>
135
+ <Alert severity="info" sx={{ mb: 3 }}>
136
+ {t('admin.vaultConfig.enableVaultHelp')}
137
+ </Alert>
138
+
139
+ <Stack spacing={3}>
140
+ {/* 启用设置 */}
141
+ <Stack direction="row" alignItems="center" gap={2}>
142
+ <Typography variant="subtitle1">{t('admin.vaultConfig.enableVault')}</Typography>
143
+ <Controller
144
+ name="enabled"
145
+ control={control}
146
+ render={({ field }) => (
147
+ <Switch checked={field.value} onChange={(e) => field.onChange(e.target.checked)} />
148
+ )}
149
+ />
150
+ </Stack>
151
+
152
+ {enabled && (
153
+ <>
154
+ <Divider sx={{ my: 1 }} />
155
+
156
+ {/* 存入阈值设置 */}
157
+ <Box>
158
+ {renderLabelWithTooltip(
159
+ t('admin.vaultConfig.depositThreshold'),
160
+ t('admin.vaultConfig.depositThresholdHelp')
161
+ )}
162
+ <Controller
163
+ name="deposit_threshold"
164
+ control={control}
165
+ rules={{
166
+ required: true,
167
+ validate: (value) => {
168
+ if (Number(value) <= 0) {
169
+ return t('admin.vaultConfig.depositThresholdRequired');
170
+ }
171
+ const validPrecision = formatAmountPrecisionLimit(value.toString(), locale, item.decimal || 6);
172
+ return validPrecision || true;
173
+ },
174
+ }}
175
+ render={({ field }) => (
176
+ <TextField
177
+ {...field}
178
+ fullWidth
179
+ type="number"
180
+ error={!!errors.deposit_threshold}
181
+ helperText={getDepositHelperText(errors.deposit_threshold)}
182
+ InputProps={{
183
+ endAdornment: (
184
+ <InputAdornment position="end">
185
+ <Box sx={{ display: 'flex', alignItems: 'center', ml: 1 }}>
186
+ <Currency logo={item.logo} name={item.symbol} />
187
+ </Box>
188
+ </InputAdornment>
189
+ ),
190
+ inputProps: {
191
+ min: 0,
192
+ },
193
+ }}
194
+ sx={{ mt: 1 }}
195
+ placeholder={DEFAULT_DEPOSIT_THRESHOLD}
196
+ />
197
+ )}
198
+ />
199
+ </Box>
200
+
201
+ {/* 提取阈值设置 */}
202
+ {/* <Box>
203
+ {renderLabelWithTooltip(
204
+ t('admin.vaultConfig.withdrawThreshold'),
205
+ t('admin.vaultConfig.withdrawThresholdHelp')
206
+ )}
207
+ <Controller
208
+ name="withdraw_threshold"
209
+ control={control}
210
+ rules={{
211
+ required: true,
212
+ validate: (value) => {
213
+ if (Number(value) < 0) {
214
+ return t('admin.vaultConfig.withdrawThresholdInvalid');
215
+ }
216
+ const validPrecision = formatAmountPrecisionLimit(value.toString(), locale, item.decimal || 6);
217
+ return validPrecision || true;
218
+ },
219
+ }}
220
+ render={({ field }) => (
221
+ <TextField
222
+ {...field}
223
+ fullWidth
224
+ type="number"
225
+ error={!!errors.withdraw_threshold}
226
+ helperText={getWithdrawHelperText(errors.withdraw_threshold, withdrawThreshold)}
227
+ InputProps={{
228
+ endAdornment: (
229
+ <InputAdornment position="end">
230
+ <Box sx={{ display: 'flex', alignItems: 'center', ml: 1 }}>
231
+ <Currency logo={item.logo} name={item.symbol} />
232
+ </Box>
233
+ </InputAdornment>
234
+ ),
235
+ inputProps: {
236
+ min: 0,
237
+ },
238
+ }}
239
+ sx={{ mt: 1 }}
240
+ placeholder="0"
241
+ />
242
+ )}
243
+ />
244
+ </Box> */}
245
+ </>
246
+ )}
247
+ </Stack>
248
+ </Box>
249
+ </Dialog>
250
+ );
251
+ }
252
+
253
+ export default EditForm;
@@ -0,0 +1,367 @@
1
+ /* eslint-disable react/no-unstable-nested-components */
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import { api, ConfirmDialog, formatBNStr, Link, parseMarkedText, Table, useMobile } from '@blocklet/payment-react';
4
+ import { Alert, Avatar, Box, Button, CircularProgress, Stack, Tooltip, Typography } from '@mui/material';
5
+ import { useRequest, useSetState } from 'ahooks';
6
+ import Empty from '@arcblock/ux/lib/Empty';
7
+ import { HelpOutline } from '@mui/icons-material';
8
+ import type { TPaymentCurrencyExpanded } from '@blocklet/payment-types';
9
+ import { Toast } from '@arcblock/ux';
10
+ import { styled } from '@mui/system';
11
+ import { useSessionContext } from '../../../../contexts/session';
12
+
13
+ import EditForm from './edit-form';
14
+
15
+ export const getVaultConfigs = async () => {
16
+ const res = await api.get('/api/payment-currencies/vault-config');
17
+ return res.data;
18
+ };
19
+
20
+ export const checkDepositVault = async (currencyId: string) => {
21
+ const res = await api.get(`/api/payment-currencies/${currencyId}/deposit-vault`);
22
+ return res.data;
23
+ };
24
+
25
+ export const depositToVault = async (currencyId: string) => {
26
+ const res = await api.put(`/api/payment-currencies/${currencyId}/deposit-vault`);
27
+ return res.data;
28
+ };
29
+
30
+ export default function VaultConfig() {
31
+ const { t } = useLocaleContext();
32
+ const { session } = useSessionContext();
33
+ const { isMobile } = useMobile();
34
+ const isOwner = session?.user?.role === 'owner';
35
+
36
+ const [state, setState] = useSetState({
37
+ item: null as TPaymentCurrencyExpanded | null,
38
+ edit: false,
39
+ confirm: false,
40
+ });
41
+
42
+ const {
43
+ data = {
44
+ list: [],
45
+ balances: {},
46
+ },
47
+ loading,
48
+ error,
49
+ refresh,
50
+ } = useRequest(getVaultConfigs, {
51
+ onSuccess: (res) => {
52
+ if (res.error) {
53
+ Toast.error(res.error);
54
+ }
55
+ },
56
+ });
57
+
58
+ const { runAsync: runCheckDepositVault } = useRequest(checkDepositVault, {
59
+ manual: true,
60
+ onSuccess: (result) => {
61
+ if (result.depositAmount && result.depositAmount !== '0') {
62
+ setState({
63
+ confirm: true,
64
+ });
65
+ }
66
+ },
67
+ });
68
+ const handleEditClick = (item: TPaymentCurrencyExpanded) => {
69
+ setState({
70
+ item,
71
+ edit: true,
72
+ });
73
+ };
74
+
75
+ const handleFormClose = () => {
76
+ setState({
77
+ edit: false,
78
+ });
79
+ };
80
+
81
+ // 确认转入冷钱包
82
+ const handleDepositConfirm = async () => {
83
+ if (!state.item) return;
84
+ try {
85
+ setState({ confirm: false });
86
+ await depositToVault(state.item.id);
87
+ Toast.success(t('admin.vaultConfig.depositQueued'));
88
+ } catch (err) {
89
+ Toast.error(t('admin.vaultConfig.depositFailed'));
90
+ }
91
+ };
92
+
93
+ const handleFormSuccess = () => {
94
+ if (state.item) {
95
+ runCheckDepositVault(state.item.id);
96
+ }
97
+ refresh();
98
+ handleFormClose();
99
+ };
100
+
101
+ const columns = [
102
+ {
103
+ label: t('common.currency'),
104
+ name: 'symbol',
105
+ options: {
106
+ customBodyRenderLite: (dataIndex: number) => {
107
+ const item = data.list?.[dataIndex];
108
+ if (!item) {
109
+ return '-';
110
+ }
111
+ return (
112
+ <Stack direction="row" spacing={1} alignItems="center">
113
+ <Avatar src={item.logo} alt={item.symbol} style={{ width: 24, height: 24 }} />
114
+ <Box>
115
+ <Typography variant="body2">{item.symbol}</Typography>
116
+ {!isMobile && (
117
+ <Typography variant="caption" color="text.secondary">
118
+ {item.payment_method?.name}
119
+ </Typography>
120
+ )}
121
+ </Box>
122
+ </Stack>
123
+ );
124
+ },
125
+ },
126
+ },
127
+ {
128
+ label: t('admin.vaultConfig.appBalance'),
129
+ name: 'id',
130
+ options: {
131
+ customBodyRenderLite: (dataIndex: number) => {
132
+ const item = data.list?.[dataIndex];
133
+ if (!item) {
134
+ return '-';
135
+ }
136
+ return (
137
+ <Typography variant="body1">
138
+ {formatBNStr(data.balances?.[item.id] || '0', item.decimal)} {item.symbol}
139
+ </Typography>
140
+ );
141
+ },
142
+ },
143
+ },
144
+ {
145
+ label: t('admin.vaultConfig.enabled'),
146
+ name: 'vault_enabled',
147
+ options: {
148
+ customBodyRenderLite: (dataIndex: number) => {
149
+ const item = data.list?.[dataIndex];
150
+ if (!item) {
151
+ return '-';
152
+ }
153
+ const enabled = item.vault_config?.enabled;
154
+ return (
155
+ <Stack direction="row" spacing={1} alignItems="center">
156
+ <Box
157
+ sx={{
158
+ width: 8,
159
+ height: 8,
160
+ borderRadius: '50%',
161
+ bgcolor: enabled ? 'success.main' : 'text.lighter',
162
+ }}
163
+ />
164
+ <Typography variant="body2" sx={{ color: 'text.secondary' }}>
165
+ {enabled ? t('admin.vaultConfig.enabledYes') : t('admin.vaultConfig.enabledNo')}
166
+ </Typography>
167
+ </Stack>
168
+ );
169
+ },
170
+ },
171
+ },
172
+ {
173
+ label: t('admin.vaultConfig.depositThreshold'),
174
+ name: 'deposit_threshold',
175
+ options: {
176
+ customBodyRenderLite: (dataIndex: number) => {
177
+ const item = data.list?.[dataIndex];
178
+ if (!item) {
179
+ return '-';
180
+ }
181
+ if (!item?.vault_config) {
182
+ return (
183
+ <Typography variant="body2" color="text.secondary">
184
+ {t('admin.vaultConfig.notConfig')}
185
+ </Typography>
186
+ );
187
+ }
188
+ return (
189
+ <Typography variant="body1">
190
+ {formatBNStr(item.vault_config?.deposit_threshold || '0', item.decimal)} {item.symbol}
191
+ </Typography>
192
+ );
193
+ },
194
+ customHeadLabelRender: () => {
195
+ return (
196
+ <Box display="flex" alignItems="center" gap={1}>
197
+ {t('admin.vaultConfig.depositThreshold')}
198
+ <Tooltip title={t('admin.vaultConfig.depositThresholdHelp')}>
199
+ <HelpOutline fontSize="small" sx={{ color: 'text.lighter' }} />
200
+ </Tooltip>
201
+ </Box>
202
+ );
203
+ },
204
+ },
205
+ },
206
+ // {
207
+ // label: t('admin.vaultConfig.withdrawThreshold'),
208
+ // name: 'withdraw_threshold',
209
+ // options: {
210
+ // customBodyRenderLite: (dataIndex: number) => {
211
+ // const item = data.list[dataIndex];
212
+ // if (!item.vault_config) {
213
+ // return (
214
+ // <Typography variant="body2" color="text.secondary">
215
+ // {t('admin.vaultConfig.notConfig')}
216
+ // </Typography>
217
+ // );
218
+ // }
219
+ // return (
220
+ // <Typography variant="body1">
221
+ // {item.vault_config?.withdraw_threshold === '0'
222
+ // ? t('admin.vaultConfig.noLimit')
223
+ // : `${formatBNStr(item.vault_config?.withdraw_threshold || '0', item.decimal)} ${item.symbol}`}
224
+ // </Typography>
225
+ // );
226
+ // },
227
+ // customHeadLabelRender: () => {
228
+ // return (
229
+ // <Box display="flex" alignItems="center" gap={1}>
230
+ // {t('admin.vaultConfig.withdrawThreshold')}
231
+ // <Tooltip title={t('admin.vaultConfig.withdrawThresholdHelp')}>
232
+ // <HelpOutline fontSize="small" sx={{ color: 'text.lighter' }} />
233
+ // </Tooltip>
234
+ // </Box>
235
+ // );
236
+ // },
237
+ // },
238
+ // },
239
+ ...(isOwner
240
+ ? [
241
+ {
242
+ label: t('common.actions'),
243
+ name: 'actions',
244
+ options: {
245
+ customBodyRenderLite: (dataIndex: number) => {
246
+ const item = data.list?.[dataIndex];
247
+ if (!item) {
248
+ return null;
249
+ }
250
+ const enabled = item.vault_config?.enabled;
251
+ return (
252
+ <Button
253
+ size="small"
254
+ variant="text"
255
+ onClick={() => handleEditClick(item)}
256
+ sx={{
257
+ color: 'text.link',
258
+ minWidth: 'fit-content',
259
+ }}>
260
+ {enabled ? t('common.edit') : t('admin.vaultConfig.enable')}
261
+ </Button>
262
+ );
263
+ },
264
+ },
265
+ },
266
+ ]
267
+ : []),
268
+ ];
269
+
270
+ if (loading) {
271
+ return <CircularProgress />;
272
+ }
273
+
274
+ if (error) {
275
+ return <Alert severity="error">{error.message}</Alert>;
276
+ }
277
+
278
+ const renderEmpty = () => {
279
+ const configFirst = t('admin.vaultConfig.configureFirst');
280
+ const parsedParts = parseMarkedText(configFirst);
281
+
282
+ return (
283
+ <Empty>
284
+ <Typography variant="body1">{t('admin.vaultConfig.notConfigured')}</Typography>
285
+ <Typography variant="body2" mt={1} sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
286
+ {parsedParts.map((part: { type: 'text' | 'marked'; content: string }) =>
287
+ part.type === 'text' ? (
288
+ part.content
289
+ ) : (
290
+ <Link
291
+ key={part.content}
292
+ to={`${window.location.origin}/.well-known/service/admin/configuration`}
293
+ style={{ color: '#3b82f6' }}>
294
+ {part.content}
295
+ </Link>
296
+ )
297
+ )}
298
+ </Typography>
299
+ </Empty>
300
+ );
301
+ };
302
+
303
+ return (
304
+ <Root>
305
+ <Box mb={2}>
306
+ <Typography variant="body2" color="text.secondary" mt={1}>
307
+ {t('admin.vaultConfig.description')}
308
+ <Box
309
+ component="a"
310
+ href="https://www.arcblock.io/docs/arcblock-payment-kit/en/vault-config"
311
+ target="_blank"
312
+ rel="noopener noreferrer"
313
+ sx={{ color: 'text.link', textDecoration: 'none', ml: 0.5 }}>
314
+ {t('admin.vaultConfig.learnMore')}
315
+ </Box>
316
+ </Typography>
317
+
318
+ {!isOwner && (
319
+ <Alert severity="warning" sx={{ mt: 2 }}>
320
+ {t('admin.vaultConfig.ownerOnly')}
321
+ </Alert>
322
+ )}
323
+ </Box>
324
+ <Table
325
+ data={Array.isArray(data.list) ? data.list : []}
326
+ columns={columns}
327
+ options={{
328
+ pagination: false,
329
+ selectableRows: 'none',
330
+ }}
331
+ mobileTDFlexDirection="row"
332
+ toolbar={false}
333
+ footer={false}
334
+ emptyNode={renderEmpty()}
335
+ />
336
+ {state.edit && state.item && (
337
+ <EditForm item={state.item} onClose={handleFormClose} onSuccess={handleFormSuccess} isOwner={isOwner} />
338
+ )}
339
+ {state.confirm && state.item && (
340
+ <ConfirmDialog
341
+ title={t('admin.vaultConfig.depositConfirmTitle')}
342
+ message={t('admin.vaultConfig.depositConfirmMessage', {
343
+ currency: state.item.symbol,
344
+ })}
345
+ onCancel={() => setState({ confirm: false })}
346
+ onConfirm={handleDepositConfirm}
347
+ color="primary"
348
+ />
349
+ )}
350
+ </Root>
351
+ );
352
+ }
353
+
354
+ const Root = styled(Box)`
355
+ @media (max-width: ${({ theme }) => theme.breakpoints.values.md}px) {
356
+ .MuiTable-root > .MuiTableBody-root > .MuiTableRow-root > td.MuiTableCell-root {
357
+ > div {
358
+ width: fit-content;
359
+ flex: inherit;
360
+ font-size: 14px;
361
+ }
362
+ }
363
+ .invoice-summary {
364
+ padding-right: 20px;
365
+ }
366
+ }
367
+ `;
@@ -136,7 +136,6 @@ export default function EditDonationForm({
136
136
  display: 'flex',
137
137
  flexDirection: 'column',
138
138
  height: '100%',
139
- minHeight: '100vh',
140
139
  }}>
141
140
  <Box
142
141
  sx={{