payment-kit 1.17.6 → 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 +138 -10
- package/src/components/payment-currency/edit.tsx +8 -3
- package/src/components/payment-currency/tokenList.json +8158 -0
- 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 +10 -0
- package/src/locales/zh.tsx +10 -0
- package/src/pages/admin/settings/payment-methods/create.tsx +3 -3
- package/src/pages/admin/settings/payment-methods/index.tsx +55 -4
|
@@ -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',
|
|
@@ -430,6 +437,9 @@ export default flat({
|
|
|
430
437
|
delete: 'Delete payment currency',
|
|
431
438
|
deleteConfirm: 'Are you sure you want to delete this payment currency? Once deleted, it cannot be recovered',
|
|
432
439
|
deleted: 'Payment currency successfully deleted',
|
|
440
|
+
quickAdd: 'Quick add supported currency',
|
|
441
|
+
searchToken: 'Search currency',
|
|
442
|
+
orManualInput: 'Or manually fill in currency information below',
|
|
433
443
|
logo: {
|
|
434
444
|
label: 'Logo',
|
|
435
445
|
tip: 'Displayed on payment page',
|
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',
|
|
@@ -419,6 +426,9 @@ export default flat({
|
|
|
419
426
|
delete: '删除货币',
|
|
420
427
|
deleteConfirm: '确定要删除此货币吗?一旦删除,将无法恢复',
|
|
421
428
|
deleted: '货币已成功删除',
|
|
429
|
+
quickAdd: '快速添加已支持的货币',
|
|
430
|
+
searchToken: '搜索货币',
|
|
431
|
+
orManualInput: '或在下方手动填写货币信息',
|
|
422
432
|
logo: {
|
|
423
433
|
label: 'Logo',
|
|
424
434
|
tip: '在支付页面显示',
|
|
@@ -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')}
|