payment-kit 1.17.7 → 1.17.8
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/blocklet.yml +1 -1
- package/package.json +6 -5
- package/src/components/collapse.tsx +28 -15
- package/src/components/customer/edit.tsx +1 -6
- package/src/components/drawer-form.tsx +43 -16
- package/src/components/info-row.tsx +1 -0
- package/src/components/payment-currency/add.tsx +8 -3
- package/src/components/payment-currency/edit.tsx +8 -3
- package/src/components/payment-method/base.tsx +2 -4
- package/src/components/payment-method/ethereum.tsx +2 -4
- package/src/components/payment-method/evm-rpc-input.tsx +66 -0
- package/src/hooks/rpc-status.ts +124 -0
- package/src/locales/en.tsx +7 -0
- package/src/locales/zh.tsx +7 -0
- package/src/pages/admin/settings/payment-methods/create.tsx +3 -3
- package/src/pages/admin/settings/payment-methods/index.tsx +55 -4
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.8",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"@arcblock/validator": "^1.19.3",
|
|
54
54
|
"@blocklet/js-sdk": "^1.16.37",
|
|
55
55
|
"@blocklet/logger": "^1.16.37",
|
|
56
|
-
"@blocklet/payment-react": "1.17.
|
|
56
|
+
"@blocklet/payment-react": "1.17.8",
|
|
57
57
|
"@blocklet/sdk": "^1.16.37",
|
|
58
58
|
"@blocklet/ui-react": "^2.11.27",
|
|
59
59
|
"@blocklet/uploader": "^0.1.64",
|
|
@@ -115,12 +115,13 @@
|
|
|
115
115
|
"ufo": "^1.5.4",
|
|
116
116
|
"umzug": "^3.8.1",
|
|
117
117
|
"use-bus": "^2.5.2",
|
|
118
|
-
"validator": "^13.12.0"
|
|
118
|
+
"validator": "^13.12.0",
|
|
119
|
+
"web3": "^4.16.0"
|
|
119
120
|
},
|
|
120
121
|
"devDependencies": {
|
|
121
122
|
"@abtnode/types": "^1.16.37",
|
|
122
123
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
123
|
-
"@blocklet/payment-types": "1.17.
|
|
124
|
+
"@blocklet/payment-types": "1.17.8",
|
|
124
125
|
"@types/cookie-parser": "^1.4.7",
|
|
125
126
|
"@types/cors": "^2.8.17",
|
|
126
127
|
"@types/debug": "^4.1.12",
|
|
@@ -166,5 +167,5 @@
|
|
|
166
167
|
"parser": "typescript"
|
|
167
168
|
}
|
|
168
169
|
},
|
|
169
|
-
"gitHead": "
|
|
170
|
+
"gitHead": "3a896417b3beb364f8c986ea0b5b968810da9ef7"
|
|
170
171
|
}
|
|
@@ -1,28 +1,49 @@
|
|
|
1
1
|
import { ExpandLessOutlined, ExpandMoreOutlined } from '@mui/icons-material';
|
|
2
2
|
import { Box, Collapse, Stack } from '@mui/material';
|
|
3
|
-
import { useState } from 'react';
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
4
|
|
|
5
5
|
type Props = {
|
|
6
|
-
trigger: string |
|
|
7
|
-
children
|
|
6
|
+
trigger: string | ((expanded: boolean) => React.ReactNode) | React.ReactNode;
|
|
7
|
+
children?: React.ReactNode;
|
|
8
8
|
expanded?: boolean;
|
|
9
9
|
addons?: React.ReactNode;
|
|
10
10
|
style?: Record<string, any>;
|
|
11
|
+
value?: string;
|
|
12
|
+
onChange?: (value: string, expanded: boolean) => void;
|
|
13
|
+
lazy?: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
IconCollapse.defaultProps = {
|
|
17
|
+
value: '',
|
|
18
|
+
onChange: () => {},
|
|
19
|
+
children: null,
|
|
20
|
+
expanded: false,
|
|
21
|
+
addons: null,
|
|
22
|
+
style: {},
|
|
23
|
+
lazy: true,
|
|
11
24
|
};
|
|
12
25
|
|
|
13
26
|
export default function IconCollapse(props: Props) {
|
|
14
27
|
const [expanded, setExpanded] = useState(props.expanded || false);
|
|
15
28
|
const toggleExpanded = () => {
|
|
16
|
-
|
|
29
|
+
const newExpanded = !expanded;
|
|
30
|
+
setExpanded(newExpanded);
|
|
17
31
|
};
|
|
18
32
|
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
setExpanded(props.expanded || false);
|
|
35
|
+
}, [props.expanded]);
|
|
36
|
+
|
|
19
37
|
return (
|
|
20
38
|
<>
|
|
21
39
|
<Stack
|
|
22
40
|
direction="row"
|
|
23
41
|
alignItems="center"
|
|
24
42
|
justifyContent="space-between"
|
|
25
|
-
onClick={
|
|
43
|
+
onClick={() => {
|
|
44
|
+
props.onChange?.(props.value || '', !expanded);
|
|
45
|
+
toggleExpanded();
|
|
46
|
+
}}
|
|
26
47
|
sx={{
|
|
27
48
|
width: 1,
|
|
28
49
|
cursor: 'pointer',
|
|
@@ -31,20 +52,12 @@ export default function IconCollapse(props: Props) {
|
|
|
31
52
|
'& :hover': { color: 'primary.main' },
|
|
32
53
|
...props.style,
|
|
33
54
|
}}>
|
|
34
|
-
<Box>{typeof props.trigger === 'function' ? props.trigger(expanded) : props.trigger}
|
|
55
|
+
<Box>{typeof props.trigger === 'function' ? props.trigger(expanded) : props.trigger}</Box>
|
|
35
56
|
<Stack direction="row" alignItems="center" spacing={2}>
|
|
36
57
|
{props.addons} {expanded ? <ExpandLessOutlined /> : <ExpandMoreOutlined />}
|
|
37
58
|
</Stack>
|
|
38
59
|
</Stack>
|
|
39
|
-
<Collapse in={expanded}>
|
|
40
|
-
<Box>{props.children}</Box>
|
|
41
|
-
</Collapse>
|
|
60
|
+
<Collapse in={expanded}>{expanded || props.lazy ? props.children : null}</Collapse>
|
|
42
61
|
</>
|
|
43
62
|
);
|
|
44
63
|
}
|
|
45
|
-
|
|
46
|
-
IconCollapse.defaultProps = {
|
|
47
|
-
expanded: false,
|
|
48
|
-
addons: null,
|
|
49
|
-
style: {},
|
|
50
|
-
};
|
|
@@ -62,12 +62,7 @@ export default function EditCustomer({
|
|
|
62
62
|
hideLiveMode
|
|
63
63
|
onClose={onCancel}
|
|
64
64
|
footer={
|
|
65
|
-
<Button
|
|
66
|
-
variant="contained"
|
|
67
|
-
color="primary"
|
|
68
|
-
disabled={loading}
|
|
69
|
-
onClick={onSubmit}
|
|
70
|
-
sx={{ width: '100%', mt: 2.5 }}>
|
|
65
|
+
<Button variant="contained" color="primary" disabled={loading} onClick={onSubmit} sx={{ width: '100%' }}>
|
|
71
66
|
{loading && <CircularProgress size="small" />} {t('common.save')}
|
|
72
67
|
</Button>
|
|
73
68
|
}>
|
|
@@ -9,7 +9,7 @@ import useBus from 'use-bus';
|
|
|
9
9
|
type Props = {
|
|
10
10
|
icon: React.ReactNode;
|
|
11
11
|
text: string;
|
|
12
|
-
addons
|
|
12
|
+
addons?: React.ReactNode;
|
|
13
13
|
children: React.ReactNode;
|
|
14
14
|
width?: number;
|
|
15
15
|
open?: boolean;
|
|
@@ -26,6 +26,7 @@ DrawerForm.defaultProps = {
|
|
|
26
26
|
onClose: noop,
|
|
27
27
|
hideLiveMode: false,
|
|
28
28
|
footer: null,
|
|
29
|
+
addons: null,
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
export default function DrawerForm(props: Props) {
|
|
@@ -60,26 +61,52 @@ export default function DrawerForm(props: Props) {
|
|
|
60
61
|
alignItems="center"
|
|
61
62
|
justifyContent="space-between"
|
|
62
63
|
className="drawer-form-header-wrapper"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
64
|
+
flexWrap="wrap"
|
|
65
|
+
sx={{
|
|
66
|
+
pl: 3,
|
|
67
|
+
pr: 3,
|
|
68
|
+
pb: 2,
|
|
69
|
+
pt: 2,
|
|
70
|
+
gap: 1,
|
|
71
|
+
borderBottom: '1px solid var(--stroke-sep, #EFF1F5)',
|
|
72
|
+
'@media (max-width: 600px)': {
|
|
73
|
+
'& > .addons-wrapper': {
|
|
74
|
+
width: '100%',
|
|
75
|
+
justifyContent: 'flex-end',
|
|
76
|
+
'& > .addons-divider': {
|
|
77
|
+
display: 'none',
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
}}>
|
|
82
|
+
<Box flex={1} display="flex" alignItems="center" justifyContent="space-between">
|
|
83
|
+
<Stack direction="row" alignItems="center">
|
|
84
|
+
<Typography className="drawer-form-header" variant="h6" sx={{ fontWeight: 600 }}>
|
|
85
|
+
{props.text}
|
|
86
|
+
</Typography>
|
|
87
|
+
{!settings.livemode && !props.hideLiveMode && <Livemode />}
|
|
88
|
+
</Stack>
|
|
89
|
+
<Stack direction="row" alignItems="center" justifyContent="space-between">
|
|
90
|
+
<Close
|
|
91
|
+
sx={{ mr: props.addons ? 1 : 0, color: 'text.secondary', cursor: 'pointer' }}
|
|
92
|
+
onClick={handleClose}
|
|
93
|
+
/>
|
|
94
|
+
</Stack>
|
|
95
|
+
</Box>
|
|
96
|
+
<Box display="flex" alignItems="center" className="addons-wrapper">
|
|
97
|
+
{props.addons && <Divider orientation="vertical" flexItem sx={{ mr: 2 }} className="addons-divider" />}
|
|
76
98
|
{props.addons}
|
|
77
|
-
</
|
|
99
|
+
</Box>
|
|
78
100
|
</Stack>
|
|
79
101
|
<Box className="drawer-form-body" sx={{ mx: 3, my: 2 }}>
|
|
80
102
|
{props.children}
|
|
81
103
|
</Box>
|
|
82
|
-
|
|
104
|
+
{props.footer && (
|
|
105
|
+
<Box className="drawer-form-footer">
|
|
106
|
+
<Divider sx={{ mb: 2 }} />
|
|
107
|
+
{props.footer}
|
|
108
|
+
</Box>
|
|
109
|
+
)}
|
|
83
110
|
</Container>
|
|
84
111
|
</>
|
|
85
112
|
);
|
|
@@ -121,9 +121,14 @@ export default function PaymentCurrencyAdd({
|
|
|
121
121
|
onClose={onClose}
|
|
122
122
|
text={t('admin.paymentCurrency.add')}
|
|
123
123
|
width={640}
|
|
124
|
-
|
|
125
|
-
<Button
|
|
126
|
-
|
|
124
|
+
footer={
|
|
125
|
+
<Button
|
|
126
|
+
variant="contained"
|
|
127
|
+
size="large"
|
|
128
|
+
onClick={handleSubmit(onSubmit)}
|
|
129
|
+
disabled={state.loading}
|
|
130
|
+
sx={{ width: '100%' }}>
|
|
131
|
+
{state.loading && <CircularProgress size={20} />} {t('common.save')}
|
|
127
132
|
</Button>
|
|
128
133
|
}>
|
|
129
134
|
<FormProvider {...methods}>
|
|
@@ -60,9 +60,14 @@ export default function PaymentCurrencyEdit({
|
|
|
60
60
|
onClose={onClose}
|
|
61
61
|
text={t('admin.paymentCurrency.edit')}
|
|
62
62
|
width={640}
|
|
63
|
-
|
|
64
|
-
<Button
|
|
65
|
-
|
|
63
|
+
footer={
|
|
64
|
+
<Button
|
|
65
|
+
variant="contained"
|
|
66
|
+
size="large"
|
|
67
|
+
onClick={handleSubmit(onSubmit)}
|
|
68
|
+
disabled={state.loading}
|
|
69
|
+
sx={{ width: '100%' }}>
|
|
70
|
+
{state.loading && <CircularProgress size={20} />} {t('common.save')}
|
|
66
71
|
</Button>
|
|
67
72
|
}>
|
|
68
73
|
<FormProvider {...methods}>
|
|
@@ -5,6 +5,7 @@ import { Stack, Typography } from '@mui/material';
|
|
|
5
5
|
import { useFormContext, useWatch } from 'react-hook-form';
|
|
6
6
|
|
|
7
7
|
import Uploader from '../uploader';
|
|
8
|
+
import EvmRpcInput from './evm-rpc-input';
|
|
8
9
|
|
|
9
10
|
export default function BaseMethodForm() {
|
|
10
11
|
const { t } = useLocaleContext();
|
|
@@ -38,11 +39,8 @@ export default function BaseMethodForm() {
|
|
|
38
39
|
label={t('admin.paymentMethod.description.label')}
|
|
39
40
|
placeholder={t('admin.paymentMethod.description.tip')}
|
|
40
41
|
/>
|
|
41
|
-
<
|
|
42
|
-
key="api_host"
|
|
42
|
+
<EvmRpcInput
|
|
43
43
|
name="settings.base.api_host"
|
|
44
|
-
type="text"
|
|
45
|
-
rules={{ required: true }}
|
|
46
44
|
label={t('admin.paymentMethod.base.api_host.label')}
|
|
47
45
|
placeholder={t('admin.paymentMethod.base.api_host.tip')}
|
|
48
46
|
/>
|
|
@@ -5,6 +5,7 @@ import { Stack, Typography } from '@mui/material';
|
|
|
5
5
|
import { useFormContext, useWatch } from 'react-hook-form';
|
|
6
6
|
|
|
7
7
|
import Uploader from '../uploader';
|
|
8
|
+
import EvmRpcInput from './evm-rpc-input';
|
|
8
9
|
|
|
9
10
|
export default function EthereumMethodForm() {
|
|
10
11
|
const { t } = useLocaleContext();
|
|
@@ -38,11 +39,8 @@ export default function EthereumMethodForm() {
|
|
|
38
39
|
label={t('admin.paymentMethod.description.label')}
|
|
39
40
|
placeholder={t('admin.paymentMethod.description.tip')}
|
|
40
41
|
/>
|
|
41
|
-
<
|
|
42
|
-
key="api_host"
|
|
42
|
+
<EvmRpcInput
|
|
43
43
|
name="settings.ethereum.api_host"
|
|
44
|
-
type="text"
|
|
45
|
-
rules={{ required: true }}
|
|
46
44
|
label={t('admin.paymentMethod.ethereum.api_host.label')}
|
|
47
45
|
placeholder={t('admin.paymentMethod.ethereum.api_host.tip')}
|
|
48
46
|
/>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/* eslint-disable no-nested-ternary */
|
|
2
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
|
+
import { FormInput } from '@blocklet/payment-react';
|
|
4
|
+
import { Box, Typography, CircularProgress } from '@mui/material';
|
|
5
|
+
import { useWatch, useFormContext } from 'react-hook-form';
|
|
6
|
+
import { CheckCircleOutline, ErrorOutline } from '@mui/icons-material';
|
|
7
|
+
import { useRpcStatus } from '../../hooks/rpc-status';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
name: string; // 表单字段名
|
|
11
|
+
label: string;
|
|
12
|
+
placeholder: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function EvmRpcInput({ name, label, placeholder }: Props) {
|
|
16
|
+
const { t } = useLocaleContext();
|
|
17
|
+
const { control } = useFormContext();
|
|
18
|
+
const apiHost = useWatch({
|
|
19
|
+
control,
|
|
20
|
+
name,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const { status } = useRpcStatus(apiHost, { debounce: 500, pollInterval: 2000 });
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<FormInput
|
|
27
|
+
key="api_host"
|
|
28
|
+
name={name}
|
|
29
|
+
type="text"
|
|
30
|
+
rules={{ required: true }}
|
|
31
|
+
label={
|
|
32
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
33
|
+
{label}
|
|
34
|
+
{apiHost && (
|
|
35
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
|
36
|
+
{status.loading ? (
|
|
37
|
+
<>
|
|
38
|
+
<CircularProgress size={14} />
|
|
39
|
+
<Typography variant="caption" color="text.secondary">
|
|
40
|
+
{t('admin.paymentMethod.evm.checking')}
|
|
41
|
+
</Typography>
|
|
42
|
+
</>
|
|
43
|
+
) : status.connected ? (
|
|
44
|
+
<>
|
|
45
|
+
<CheckCircleOutline sx={{ fontSize: 14 }} color="success" />
|
|
46
|
+
<Typography variant="caption" color="text.secondary">
|
|
47
|
+
{t('admin.paymentMethod.evm.connected')} | {t('admin.paymentMethod.evm.blockHeight')}:{' '}
|
|
48
|
+
{status.blockNumber?.toLocaleString()}
|
|
49
|
+
</Typography>
|
|
50
|
+
</>
|
|
51
|
+
) : status.error ? (
|
|
52
|
+
<>
|
|
53
|
+
<ErrorOutline sx={{ fontSize: 14 }} color="error" />
|
|
54
|
+
<Typography variant="caption" color="error">
|
|
55
|
+
{t('admin.paymentMethod.evm.connectionError')}
|
|
56
|
+
</Typography>
|
|
57
|
+
</>
|
|
58
|
+
) : null}
|
|
59
|
+
</Box>
|
|
60
|
+
)}
|
|
61
|
+
</Box>
|
|
62
|
+
}
|
|
63
|
+
placeholder={placeholder}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState, useMemo, useRef } from 'react';
|
|
2
|
+
import debounce from 'lodash/debounce';
|
|
3
|
+
|
|
4
|
+
interface NetworkStatus {
|
|
5
|
+
loading: boolean;
|
|
6
|
+
connected: boolean;
|
|
7
|
+
blockNumber?: bigint;
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let cachedWeb3Promise: Promise<typeof import('web3').default> | null = null;
|
|
12
|
+
|
|
13
|
+
const loadWeb3 = () => {
|
|
14
|
+
if (!cachedWeb3Promise) {
|
|
15
|
+
cachedWeb3Promise = import('web3')
|
|
16
|
+
.then((module) => module.default)
|
|
17
|
+
.catch((error) => {
|
|
18
|
+
cachedWeb3Promise = null;
|
|
19
|
+
throw error;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return cachedWeb3Promise;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Manages the network status of a Web3 connection with optional debouncing.
|
|
27
|
+
*
|
|
28
|
+
* @param apiHost - Optional URL of the Ethereum RPC endpoint
|
|
29
|
+
* @param options - Configuration options for connection checking
|
|
30
|
+
* @param options.debounce - Delay in milliseconds between connection checks (default: 500ms)
|
|
31
|
+
* @param options.pollInterval - Interval in milliseconds between connection checks (default: 2000ms)
|
|
32
|
+
*
|
|
33
|
+
* @returns An object containing the current network status and a manual connection check function
|
|
34
|
+
*
|
|
35
|
+
* @remarks
|
|
36
|
+
* This hook provides real-time monitoring of Web3 network connectivity, including:
|
|
37
|
+
* - Loading state tracking
|
|
38
|
+
* - Connection status detection
|
|
39
|
+
* - Block number retrieval
|
|
40
|
+
* - Error handling for connection attempts
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* const { status, checkConnection } = useRpcStatus('https://mainnet.infura.io/v3/YOUR-PROJECT-ID');
|
|
44
|
+
* // status will contain { loading, connected, blockNumber?, error? }
|
|
45
|
+
*/
|
|
46
|
+
export function useRpcStatus(apiHost?: string, options = { debounce: 500, pollInterval: 2000 }) {
|
|
47
|
+
const [status, setStatus] = useState<NetworkStatus>({
|
|
48
|
+
loading: false,
|
|
49
|
+
connected: false,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const initializedRef = useRef(false);
|
|
53
|
+
const checkingRef = useRef(false);
|
|
54
|
+
const checkConnection = useCallback(
|
|
55
|
+
async (isPolling = false) => {
|
|
56
|
+
if (!apiHost) {
|
|
57
|
+
setStatus({ loading: false, connected: false });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (checkingRef.current) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
checkingRef.current = true;
|
|
65
|
+
|
|
66
|
+
if (!isPolling && !initializedRef.current) {
|
|
67
|
+
setStatus((prev) => ({ ...prev, loading: true }));
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const Web3 = await loadWeb3();
|
|
71
|
+
const web3 = new Web3(apiHost);
|
|
72
|
+
|
|
73
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
74
|
+
setTimeout(() => reject(new Error('Connection timeout')), 5000);
|
|
75
|
+
});
|
|
76
|
+
const blockNumber = await Promise.race([web3.eth.getBlockNumber(), timeoutPromise]);
|
|
77
|
+
|
|
78
|
+
setStatus({
|
|
79
|
+
loading: false,
|
|
80
|
+
connected: true,
|
|
81
|
+
blockNumber,
|
|
82
|
+
});
|
|
83
|
+
initializedRef.current = true;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
setStatus({
|
|
86
|
+
loading: false,
|
|
87
|
+
connected: false,
|
|
88
|
+
error: error.message,
|
|
89
|
+
});
|
|
90
|
+
} finally {
|
|
91
|
+
checkingRef.current = false;
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
[apiHost]
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const debouncedCheck = useMemo(
|
|
98
|
+
() => debounce(checkConnection, options.debounce),
|
|
99
|
+
[checkConnection, options.debounce]
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
const check = options.debounce > 0 ? debouncedCheck : checkConnection;
|
|
104
|
+
initializedRef.current = false;
|
|
105
|
+
check();
|
|
106
|
+
|
|
107
|
+
let pollTimer: NodeJS.Timeout | null = null;
|
|
108
|
+
if (options.pollInterval > 0) {
|
|
109
|
+
pollTimer = setInterval(() => checkConnection(true), options.pollInterval);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return () => {
|
|
113
|
+
if (options.debounce > 0) {
|
|
114
|
+
debouncedCheck.cancel();
|
|
115
|
+
}
|
|
116
|
+
if (pollTimer) {
|
|
117
|
+
clearInterval(pollTimer);
|
|
118
|
+
}
|
|
119
|
+
checkingRef.current = false;
|
|
120
|
+
};
|
|
121
|
+
}, [apiHost, options.debounce, options.pollInterval, checkConnection, debouncedCheck]);
|
|
122
|
+
|
|
123
|
+
return { status, checkConnection };
|
|
124
|
+
}
|
package/src/locales/en.tsx
CHANGED
|
@@ -398,6 +398,13 @@ export default flat({
|
|
|
398
398
|
tip: 'How many blocks since transaction execution',
|
|
399
399
|
},
|
|
400
400
|
},
|
|
401
|
+
evm: {
|
|
402
|
+
checking: 'Checking connection...',
|
|
403
|
+
connected: 'Connected',
|
|
404
|
+
connectionError: 'Connection failed',
|
|
405
|
+
blockHeight: 'Block Height',
|
|
406
|
+
rpcStatus: 'RPC Status',
|
|
407
|
+
},
|
|
401
408
|
base: {
|
|
402
409
|
chain_id: {
|
|
403
410
|
label: 'Chain ID',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -387,6 +387,13 @@ export default flat({
|
|
|
387
387
|
tip: '交易标记为确认需要的区块数',
|
|
388
388
|
},
|
|
389
389
|
},
|
|
390
|
+
evm: {
|
|
391
|
+
checking: '正在检查连接...',
|
|
392
|
+
connected: '连接成功',
|
|
393
|
+
connectionError: '连接失败',
|
|
394
|
+
blockHeight: '区块高度',
|
|
395
|
+
rpcStatus: 'RPC 状态',
|
|
396
|
+
},
|
|
390
397
|
base: {
|
|
391
398
|
chain_id: {
|
|
392
399
|
label: '链 ID',
|
|
@@ -77,9 +77,9 @@ export default function PaymentMethodCreate() {
|
|
|
77
77
|
icon={<AddOutlined />}
|
|
78
78
|
text={t('admin.paymentMethod.add')}
|
|
79
79
|
width={640}
|
|
80
|
-
|
|
81
|
-
<Button variant="contained" size="
|
|
82
|
-
{t('
|
|
80
|
+
footer={
|
|
81
|
+
<Button variant="contained" size="large" onClick={handleSubmit(onSubmit)} sx={{ width: '100%' }}>
|
|
82
|
+
{t('common.save')}
|
|
83
83
|
</Button>
|
|
84
84
|
}>
|
|
85
85
|
<FormProvider {...methods}>
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
EditOutlined,
|
|
10
10
|
InfoOutlined,
|
|
11
11
|
QrCodeOutlined,
|
|
12
|
+
CheckCircleOutline,
|
|
13
|
+
ErrorOutline,
|
|
12
14
|
} from '@mui/icons-material';
|
|
13
15
|
import {
|
|
14
16
|
Alert,
|
|
@@ -26,7 +28,7 @@ import {
|
|
|
26
28
|
Tooltip,
|
|
27
29
|
Typography,
|
|
28
30
|
} from '@mui/material';
|
|
29
|
-
import { useRequest, useSetState } from 'ahooks';
|
|
31
|
+
import { useRequest, useSessionStorageState, useSetState } from 'ahooks';
|
|
30
32
|
import useBus from 'use-bus';
|
|
31
33
|
|
|
32
34
|
import { useState } from 'react';
|
|
@@ -37,6 +39,7 @@ import InfoCard from '../../../../components/info-card';
|
|
|
37
39
|
import InfoRow from '../../../../components/info-row';
|
|
38
40
|
import PaymentCurrencyAdd from '../../../../components/payment-currency/add';
|
|
39
41
|
import PaymentCurrencyEdit from '../../../../components/payment-currency/edit';
|
|
42
|
+
import { useRpcStatus } from '../../../../hooks/rpc-status';
|
|
40
43
|
|
|
41
44
|
const getMethods = (
|
|
42
45
|
params: Record<string, any> = {}
|
|
@@ -143,8 +146,49 @@ const groupByType = (methods: TPaymentMethodExpanded[]) => {
|
|
|
143
146
|
return groups;
|
|
144
147
|
};
|
|
145
148
|
|
|
149
|
+
function RpcStatus({ method }: { method: TPaymentMethodExpanded }) {
|
|
150
|
+
const { t } = useLocaleContext();
|
|
151
|
+
// @ts-ignore
|
|
152
|
+
const apiHost = method.settings?.[method.type]?.api_host;
|
|
153
|
+
const { status } = useRpcStatus(apiHost, { debounce: 0, pollInterval: 2000 });
|
|
154
|
+
|
|
155
|
+
const renderStatus = () => {
|
|
156
|
+
if (status.loading) {
|
|
157
|
+
return (
|
|
158
|
+
<Typography variant="caption" color="text.secondary">
|
|
159
|
+
{t('admin.paymentMethod.evm.checking')}
|
|
160
|
+
</Typography>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
if (status.connected) {
|
|
164
|
+
return (
|
|
165
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
|
166
|
+
<CheckCircleOutline sx={{ fontSize: 14 }} color="success" />
|
|
167
|
+
<Typography color="text.secondary">{t('admin.paymentMethod.evm.connected')}</Typography>
|
|
168
|
+
</Box>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return (
|
|
172
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
|
173
|
+
<ErrorOutline sx={{ fontSize: 14 }} color="error" />
|
|
174
|
+
<Typography color="error">{t('admin.paymentMethod.evm.connectionError')}</Typography>
|
|
175
|
+
</Box>
|
|
176
|
+
);
|
|
177
|
+
};
|
|
178
|
+
return (
|
|
179
|
+
<>
|
|
180
|
+
<InfoRow label={t(`admin.paymentMethod.${method.type}.api_host.label`)} value={apiHost} />
|
|
181
|
+
<InfoRow label={t('admin.paymentMethod.evm.rpcStatus')} value={renderStatus()} />
|
|
182
|
+
<InfoRow label={t('admin.paymentMethod.evm.blockHeight')} value={status.blockNumber?.toLocaleString()} />
|
|
183
|
+
</>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
146
187
|
export default function PaymentMethods() {
|
|
147
188
|
const { t } = useLocaleContext();
|
|
189
|
+
const [expandedId, setExpandedId] = useSessionStorageState('payment-method-expanded-id', {
|
|
190
|
+
defaultValue: '',
|
|
191
|
+
});
|
|
148
192
|
const {
|
|
149
193
|
loading,
|
|
150
194
|
error,
|
|
@@ -155,6 +199,9 @@ export default function PaymentMethods() {
|
|
|
155
199
|
addresses: true,
|
|
156
200
|
})
|
|
157
201
|
);
|
|
202
|
+
const handleExpand = (methodId: string, expanded: boolean) => {
|
|
203
|
+
setExpandedId(expanded ? methodId : '');
|
|
204
|
+
};
|
|
158
205
|
const [didDialog, setDidDialog] = useSetState({ open: false, chainId: '', did: '' });
|
|
159
206
|
const { refresh } = usePaymentContext();
|
|
160
207
|
const [currencyDialog, setCurrencyDialog] = useSetState({ action: '', value: null, method: '' });
|
|
@@ -224,7 +271,7 @@ export default function PaymentMethods() {
|
|
|
224
271
|
<>
|
|
225
272
|
{Object.keys(groups).map((x) => (
|
|
226
273
|
<Box key={x} mt={3}>
|
|
227
|
-
<Stack direction="row" alignItems="center" mb={1}>
|
|
274
|
+
<Stack direction="row" alignItems="center" mb={1} flexWrap="wrap" gap={1}>
|
|
228
275
|
<Typography variant="h6" sx={{ textTransform: 'uppercase' }}>
|
|
229
276
|
{x}
|
|
230
277
|
</Typography>
|
|
@@ -234,7 +281,6 @@ export default function PaymentMethods() {
|
|
|
234
281
|
alignItems="center"
|
|
235
282
|
spacing={0.5}
|
|
236
283
|
sx={{
|
|
237
|
-
ml: 1,
|
|
238
284
|
px: 0.5,
|
|
239
285
|
color: 'text.secondary',
|
|
240
286
|
}}>
|
|
@@ -273,11 +319,16 @@ export default function PaymentMethods() {
|
|
|
273
319
|
borderTop: '1px solid #eee',
|
|
274
320
|
borderBottom: '1px solid #eee',
|
|
275
321
|
'& :hover': { color: 'text.primary' },
|
|
276
|
-
}}
|
|
322
|
+
}}
|
|
323
|
+
value={method.id}
|
|
324
|
+
expanded={expandedId === method.id}
|
|
325
|
+
onChange={(value, expanded) => handleExpand(value, expanded)}
|
|
326
|
+
lazy={false}>
|
|
277
327
|
<Grid container spacing={2} mt={0}>
|
|
278
328
|
<Grid item xs={12} md={6}>
|
|
279
329
|
<InfoRow label={t('admin.paymentMethod.props.type')} value={method.type} />
|
|
280
330
|
{method.type === 'arcblock' && <EditApiHost method={method} />}
|
|
331
|
+
{['ethereum', 'base'].includes(method.type) && <RpcStatus method={method} />}
|
|
281
332
|
<InfoRow label={t('admin.paymentMethod.props.confirmation')} value={method.confirmation.type} />
|
|
282
333
|
<InfoRow
|
|
283
334
|
label={t('admin.paymentMethod.props.recurring')}
|