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 CHANGED
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.17.7
17
+ version: 1.17.8
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.17.7",
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.7",
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.7",
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": "f2a439af19e405746a6663b4dbef8a96231a1076"
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 | Function | React.ReactNode;
7
- children: React.ReactNode;
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
- setExpanded((prev) => !prev);
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={toggleExpanded}
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} </Box>
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: React.ReactNode;
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
- sx={{ pl: 3, pr: 3, pb: 2, pt: 2, borderBottom: '1px solid var(--stroke-sep, #EFF1F5)' }}>
64
- <Stack direction="row" alignItems="center">
65
- <Typography className="drawer-form-header" variant="h6" sx={{ fontWeight: 600 }}>
66
- {props.text}
67
- </Typography>
68
- {!settings.livemode && !props.hideLiveMode && <Livemode />}
69
- </Stack>
70
- <Stack direction="row" alignItems="center" justifyContent="space-between">
71
- <Close
72
- sx={{ mr: props.addons ? 1 : 0, color: 'text.secondary', cursor: 'pointer' }}
73
- onClick={handleClose}
74
- />
75
- {props.addons && <Divider orientation="vertical" flexItem sx={{ mr: 2 }} />}
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
- </Stack>
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
- <Box className="drawer-form-footer">{props.footer}</Box>
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
  );
@@ -33,6 +33,7 @@ export default function InfoRow(props: Props) {
33
33
  alignItems={props.alignItems}
34
34
  justifyContent="space-between"
35
35
  flexWrap="wrap"
36
+ gap={1}
36
37
  sx={{
37
38
  mb: 2,
38
39
  ...props.sx,
@@ -121,9 +121,14 @@ export default function PaymentCurrencyAdd({
121
121
  onClose={onClose}
122
122
  text={t('admin.paymentCurrency.add')}
123
123
  width={640}
124
- addons={
125
- <Button variant="contained" size="small" onClick={handleSubmit(onSubmit)} disabled={state.loading}>
126
- {state.loading ? <CircularProgress size={20} /> : t('admin.paymentCurrency.save')}
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
- addons={
64
- <Button variant="contained" size="small" onClick={handleSubmit(onSubmit)} disabled={state.loading}>
65
- {state.loading ? <CircularProgress size="small" /> : t('admin.paymentCurrency.save')}
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
- <FormInput
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
- <FormInput
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
+ }
@@ -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',
@@ -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
- addons={
81
- <Button variant="contained" size="small" onClick={handleSubmit(onSubmit)}>
82
- {t('admin.paymentMethod.save')}
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')}