payment-kit 1.19.2 → 1.19.4

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.
@@ -17,6 +17,7 @@ type PermissionSpec<T extends Model> = {
17
17
  // allow record owner
18
18
  model: T;
19
19
  field: string;
20
+ findById?: (id: string) => Promise<T | null>;
20
21
  };
21
22
  mine?: boolean;
22
23
  embed?: boolean;
@@ -105,9 +106,11 @@ export function authenticate<T extends Model>({ component, roles, record, mine,
105
106
 
106
107
  // authenticate by record owner
107
108
  if (record) {
108
- const { model, field = 'customer_id' } = record;
109
- // @ts-ignore
110
- const doc: T | null = await model.findByPk(req.params.id);
109
+ const { model, field = 'customer_id', findById } = record;
110
+ const doc: T | null =
111
+ findById && typeof findById === 'function'
112
+ ? await findById(req.params.id as string)
113
+ : await (model as any).findByPk(req.params.id);
111
114
  if (doc && doc[field as keyof T]) {
112
115
  const customer = await Customer.findOne({ where: { did: req.user.did } });
113
116
  req.doc = doc;
@@ -1010,7 +1010,6 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
1010
1010
  }
1011
1011
 
1012
1012
  const checkoutSession = req.doc as CheckoutSession;
1013
- logger.info('---checkoutSession---', checkoutSession.line_items);
1014
1013
  if (checkoutSession.line_items) {
1015
1014
  try {
1016
1015
  await validateInventory(checkoutSession.line_items);
@@ -1091,9 +1090,15 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
1091
1090
  } else {
1092
1091
  const updates: Record<string, any> = {};
1093
1092
  if (checkoutSession.customer_update?.name) {
1094
- updates.name = req.body.customer_name;
1095
- updates.email = req.body.customer_email;
1096
- updates.phone = req.body.customer_phone;
1093
+ if (req.body.customer_name) {
1094
+ updates.name = req.body.customer_name;
1095
+ }
1096
+ if (req.body.customer_email) {
1097
+ updates.email = req.body.customer_email;
1098
+ }
1099
+ if (req.body.customer_phone) {
1100
+ updates.phone = req.body.customer_phone;
1101
+ }
1097
1102
  }
1098
1103
  if (checkoutSession.customer_update?.address) {
1099
1104
  updates.address = Customer.formatUpdateAddress(req.body.billing_address, customer);
@@ -10,6 +10,7 @@ import { authenticate } from '../libs/security';
10
10
  import { CreditGrant, Customer, PaymentCurrency, Price, Subscription } from '../store/models';
11
11
  import { createCreditGrant } from '../libs/credit-grant';
12
12
  import { getMeterPriceIdsFromSubscription } from '../libs/subscription';
13
+ import { blocklet } from '../libs/auth';
13
14
 
14
15
  const router = Router();
15
16
  const auth = authenticate<CreditGrant>({ component: true, roles: ['owner', 'admin'] });
@@ -179,9 +180,30 @@ router.post('/', auth, async (req, res) => {
179
180
  return res.status(404).json({ error: `PaymentCurrency ${currencyId} not found` });
180
181
  }
181
182
 
182
- const customer = await Customer.findByPkOrDid(req.body.customer_id);
183
+ let customer = await Customer.findByPkOrDid(req.body.customer_id);
183
184
  if (!customer) {
184
- return res.status(404).json({ error: `Customer ${req.body.customer_id} not found` });
185
+ const { user: userInfo } = await blocklet.getUser(req.body.customer_id);
186
+ if (!userInfo) {
187
+ return res.status(404).json({ error: `User ${req.body.customer_id} not found` });
188
+ }
189
+ customer = await Customer.create({
190
+ livemode: true,
191
+ did: userInfo.did,
192
+ name: userInfo.fullName,
193
+ email: userInfo.email || '',
194
+ phone: userInfo.phone || '',
195
+ address: Customer.formatAddressFromUser(userInfo),
196
+ description: userInfo.remark || '',
197
+ metadata: {},
198
+ balance: '0',
199
+ next_invoice_sequence: 1,
200
+ delinquent: false,
201
+ invoice_prefix: Customer.getInvoicePrefix(),
202
+ });
203
+ logger.info('Customer created on credit grant', {
204
+ customerId: customer.id,
205
+ customer: customer.toJSON(),
206
+ });
185
207
  }
186
208
 
187
209
  const unitAmount = fromTokenToUnit(req.body.amount, paymentCurrency.decimal).toString();
@@ -23,6 +23,7 @@ const listSchema = createListParamSchema<{
23
23
  customer_id?: string;
24
24
  subscription_id?: string;
25
25
  credit_grant_id?: string;
26
+ meter_id?: string;
26
27
  start?: number;
27
28
  end?: number;
28
29
  source?: string;
@@ -30,6 +31,7 @@ const listSchema = createListParamSchema<{
30
31
  customer_id: Joi.string().empty(''),
31
32
  subscription_id: Joi.string().empty(''),
32
33
  credit_grant_id: Joi.string().empty(''),
34
+ meter_id: Joi.string().empty(''),
33
35
  start: Joi.number().integer().optional(),
34
36
  end: Joi.number().integer().optional(),
35
37
  source: Joi.string().empty(''),
@@ -40,6 +42,9 @@ router.get('/', authMine, async (req, res) => {
40
42
  const { page, pageSize, ...query } = await listSchema.validateAsync(req.query, { stripUnknown: true });
41
43
  const where = getWhereFromKvQuery(query.q);
42
44
 
45
+ if (query.meter_id) {
46
+ where.meter_id = query.meter_id;
47
+ }
43
48
  if (query.customer_id) {
44
49
  where.customer_id = query.customer_id;
45
50
  }
@@ -123,6 +128,7 @@ const summarySchema = Joi.object({
123
128
  customer_id: Joi.string().optional(),
124
129
  subscription_id: Joi.string().optional(),
125
130
  currency_id: Joi.string().optional(),
131
+ meter_id: Joi.string().optional(),
126
132
  start: Joi.number().integer().optional(),
127
133
  end: Joi.number().integer().optional(),
128
134
  }).unknown(true);
@@ -134,6 +140,7 @@ router.get('/summary', authMine, async (req, res) => {
134
140
  customer_id: customerId,
135
141
  subscription_id: subscriptionId,
136
142
  currency_id: currencyId,
143
+ meter_id: meterId,
137
144
  start,
138
145
  end,
139
146
  } = await summarySchema.validateAsync(req.query, { stripUnknown: true });
@@ -142,6 +149,7 @@ router.get('/summary', authMine, async (req, res) => {
142
149
  customerId,
143
150
  subscriptionId,
144
151
  currencyId,
152
+ meterId,
145
153
  startTime: start ? new Date(start * 1000) : undefined,
146
154
  endTime: end ? new Date(end * 1000) : undefined,
147
155
  });
@@ -37,6 +37,7 @@ const authPortal = authenticate<Customer>({
37
37
  // @ts-ignore
38
38
  model: Customer,
39
39
  field: 'id',
40
+ findById: (id: string) => Customer.findByPkOrDid(id),
40
41
  },
41
42
  });
42
43
 
@@ -217,18 +218,12 @@ router.post('/sync-to-space', sessionMiddleware(), async (req, res) => {
217
218
  });
218
219
 
219
220
  // get overdue invoices
220
- router.get('/:id/overdue/invoices', sessionMiddleware(), async (req, res) => {
221
- if (!req.user) {
222
- return res.status(403).json({ error: 'Unauthorized' });
223
- }
221
+ router.get('/:id/overdue/invoices', authPortal, async (req, res) => {
224
222
  try {
225
223
  const doc = await Customer.findByPkOrDid(req.params.id as string);
226
224
  if (!doc) {
227
225
  return res.status(404).json({ error: 'Customer not found' });
228
226
  }
229
- if (doc.did !== req.user.did && !['admin', 'owner'].includes(req.user?.role)) {
230
- return res.status(403).json({ error: 'You are not allowed to access this customer invoices' });
231
- }
232
227
  const { rows: invoices, count } = await Invoice.findAndCountAll({
233
228
  where: {
234
229
  customer_id: doc.id,
@@ -84,6 +84,7 @@ router.post('/', auth, async (req, res) => {
84
84
  symbol: info.symbol,
85
85
  decimal: info.decimal,
86
86
  type: 'standard',
87
+ maximum_precision: 6,
87
88
 
88
89
  // FIXME: make these configurable
89
90
  minimum_payment_amount: fromTokenToUnit(0.000001, info.decimal).toString(),
@@ -121,6 +122,7 @@ router.post('/', auth, async (req, res) => {
121
122
  symbol: state.symbol,
122
123
  decimal: state.decimal,
123
124
  type: 'standard',
125
+ maximum_precision: 6,
124
126
 
125
127
  // FIXME: make these configurable
126
128
  minimum_payment_amount: fromTokenToUnit(0.000001, state.decimal).toString(),
@@ -173,6 +173,7 @@ router.post('/', auth, async (req, res) => {
173
173
  decimal: 18,
174
174
 
175
175
  minimum_payment_amount: fromTokenToUnit(0.000001, 18).toString(),
176
+ maximum_precision: 6,
176
177
  maximum_payment_amount: fromTokenToUnit(100000000, 18).toString(),
177
178
 
178
179
  contract: '',
@@ -21,9 +21,6 @@ export const up: Migration = async ({ context }) => {
21
21
  },
22
22
  ],
23
23
  });
24
- await context.sequelize.query(`
25
- UPDATE payment_currencies SET maximum_precision = 2 WHERE type = 'standard';
26
- `);
27
24
 
28
25
  await context.sequelize.query(`
29
26
  UPDATE payment_currencies
@@ -0,0 +1,14 @@
1
+ import { Migration } from '../migrate';
2
+
3
+ export const up: Migration = async ({ context }) => {
4
+ await context.sequelize.query(`
5
+ UPDATE payment_currencies
6
+ SET maximum_precision = 6
7
+ WHERE payment_method_id IN (
8
+ SELECT id FROM payment_methods WHERE type IN ('arcblock', 'ethereum', 'base')
9
+ )
10
+ AND type != 'credit';
11
+ `);
12
+ };
13
+
14
+ export const down = () => {};
@@ -142,6 +142,7 @@ export class CreditTransaction extends Model<
142
142
  subscriptionId,
143
143
  meterEventName,
144
144
  currencyId,
145
+ meterId,
145
146
  startTime,
146
147
  endTime,
147
148
  }: {
@@ -149,6 +150,7 @@ export class CreditTransaction extends Model<
149
150
  subscriptionId?: string;
150
151
  meterEventName?: string;
151
152
  currencyId?: string;
153
+ meterId?: string;
152
154
  startTime?: Date;
153
155
  endTime?: Date;
154
156
  }) {
@@ -169,6 +171,10 @@ export class CreditTransaction extends Model<
169
171
  whereClause['$creditGrant.currency_id$'] = currencyId;
170
172
  }
171
173
 
174
+ if (meterId) {
175
+ whereClause.meter_id = meterId;
176
+ }
177
+
172
178
  if (startTime || endTime) {
173
179
  whereClause.created_at = {};
174
180
  if (startTime) {
@@ -212,6 +218,7 @@ export class CreditTransaction extends Model<
212
218
  customer_id: customerId,
213
219
  subscription_id: subscriptionId,
214
220
  meter_event_name: meterEventName,
221
+ meter_id: meterId,
215
222
  currency_id: currencyId,
216
223
  start_time: startTime?.toISOString(),
217
224
  end_time: endTime?.toISOString(),
@@ -42,7 +42,7 @@ export class WebhookAttempt extends Model<InferAttributes<WebhookAttempt>, Infer
42
42
  allowNull: false,
43
43
  },
44
44
  status: {
45
- type: DataTypes.ENUM('enabled', 'disabled'),
45
+ type: DataTypes.ENUM('succeeded', 'failed'),
46
46
  allowNull: false,
47
47
  },
48
48
  response_status: {
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.19.2
17
+ version: 1.19.4
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.19.2",
3
+ "version": "1.19.4",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
@@ -43,31 +43,31 @@
43
43
  ]
44
44
  },
45
45
  "dependencies": {
46
- "@abtnode/cron": "^1.16.44",
47
- "@arcblock/did": "^1.20.14",
46
+ "@abtnode/cron": "^1.16.46",
47
+ "@arcblock/did": "^1.20.15",
48
48
  "@arcblock/did-auth-storage-nedb": "^1.7.1",
49
- "@arcblock/did-connect": "^3.0.1",
50
- "@arcblock/did-util": "^1.20.14",
51
- "@arcblock/jwt": "^1.20.14",
52
- "@arcblock/ux": "^3.0.1",
53
- "@arcblock/validator": "^1.20.14",
54
- "@blocklet/did-space-js": "^1.0.62",
55
- "@blocklet/js-sdk": "^1.16.44",
56
- "@blocklet/logger": "^1.16.44",
57
- "@blocklet/payment-react": "1.19.2",
58
- "@blocklet/sdk": "^1.16.44",
59
- "@blocklet/ui-react": "^3.0.1",
60
- "@blocklet/uploader": "^0.1.97",
61
- "@blocklet/xss": "^0.1.36",
49
+ "@arcblock/did-connect": "^3.0.24",
50
+ "@arcblock/did-util": "^1.20.15",
51
+ "@arcblock/jwt": "^1.20.15",
52
+ "@arcblock/ux": "^3.0.24",
53
+ "@arcblock/validator": "^1.20.15",
54
+ "@blocklet/did-space-js": "^1.1.5",
55
+ "@blocklet/js-sdk": "^1.16.46",
56
+ "@blocklet/logger": "^1.16.46",
57
+ "@blocklet/payment-react": "1.19.4",
58
+ "@blocklet/sdk": "^1.16.46",
59
+ "@blocklet/ui-react": "^3.0.24",
60
+ "@blocklet/uploader": "^0.2.4",
61
+ "@blocklet/xss": "^0.2.2",
62
62
  "@mui/icons-material": "^7.1.2",
63
63
  "@mui/lab": "7.0.0-beta.14",
64
64
  "@mui/material": "^7.1.2",
65
65
  "@mui/system": "^7.1.1",
66
- "@ocap/asset": "^1.20.14",
67
- "@ocap/client": "^1.20.14",
68
- "@ocap/mcrypto": "^1.20.14",
69
- "@ocap/util": "^1.20.14",
70
- "@ocap/wallet": "^1.20.14",
66
+ "@ocap/asset": "^1.20.15",
67
+ "@ocap/client": "^1.20.15",
68
+ "@ocap/mcrypto": "^1.20.15",
69
+ "@ocap/util": "^1.20.15",
70
+ "@ocap/wallet": "^1.20.15",
71
71
  "@stripe/react-stripe-js": "^2.9.0",
72
72
  "@stripe/stripe-js": "^2.4.0",
73
73
  "ahooks": "^3.8.5",
@@ -120,9 +120,9 @@
120
120
  "web3": "^4.16.0"
121
121
  },
122
122
  "devDependencies": {
123
- "@abtnode/types": "^1.16.44",
123
+ "@abtnode/types": "^1.16.46",
124
124
  "@arcblock/eslint-config-ts": "^0.3.3",
125
- "@blocklet/payment-types": "1.19.2",
125
+ "@blocklet/payment-types": "1.19.4",
126
126
  "@types/cookie-parser": "^1.4.9",
127
127
  "@types/cors": "^2.8.19",
128
128
  "@types/debug": "^4.1.12",
@@ -152,7 +152,7 @@
152
152
  "vite": "^7.0.0",
153
153
  "vite-node": "^3.2.4",
154
154
  "vite-plugin-babel-import": "^2.0.5",
155
- "vite-plugin-blocklet": "^0.9.33",
155
+ "vite-plugin-blocklet": "^0.10.1",
156
156
  "vite-plugin-node-polyfills": "^0.23.0",
157
157
  "vite-plugin-svgr": "^4.3.0",
158
158
  "vite-tsconfig-paths": "^5.1.4",
@@ -168,5 +168,5 @@
168
168
  "parser": "typescript"
169
169
  }
170
170
  },
171
- "gitHead": "741c897204afc412721a942201516932bff59235"
171
+ "gitHead": "ced79cc9e205a344b6fdce2f43ac8d27bb37f5f4"
172
172
  }
@@ -0,0 +1,197 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import Toast from '@arcblock/ux/lib/Toast';
3
+ import { formatError } from '@blocklet/payment-react';
4
+ import { Check, Close, EditOutlined } from '@mui/icons-material';
5
+ import { IconButton, Stack, TextField, Typography } from '@mui/material';
6
+ import { useState, useEffect } from 'react';
7
+
8
+ interface EditInLineProps {
9
+ value: string;
10
+ onSave: (newValue: string) => Promise<void>;
11
+ placeholder?: string;
12
+ required?: boolean;
13
+ disabled?: boolean;
14
+ multiline?: boolean;
15
+ maxLength?: number;
16
+ validate?: (value: string) => string | null;
17
+ successMessage?: string;
18
+ autoFocus?: boolean;
19
+ onEdit?: () => void;
20
+ onCancel?: () => void;
21
+ hideSuccessToast?: boolean;
22
+ }
23
+
24
+ export default function EditInLine({
25
+ value,
26
+ onSave,
27
+ placeholder = '',
28
+ required = true,
29
+ disabled = false,
30
+ multiline = false,
31
+ maxLength = 255,
32
+ validate = () => null,
33
+ successMessage = '',
34
+ autoFocus = false,
35
+ onEdit = () => {},
36
+ onCancel = () => {},
37
+ hideSuccessToast = false,
38
+ }: EditInLineProps) {
39
+ const [edit, setEdit] = useState(false);
40
+ const [tempValue, setTempValue] = useState(value);
41
+ const [error, setError] = useState('');
42
+ const [loading, setLoading] = useState(false);
43
+ const { t } = useLocaleContext();
44
+
45
+ // 当外部 value 变化时同步更新
46
+ useEffect(() => {
47
+ if (!edit) {
48
+ setTempValue(value);
49
+ }
50
+ }, [value, edit]);
51
+
52
+ const validateValue = (val: string): string => {
53
+ if (required && !val.trim()) {
54
+ return t('common.required');
55
+ }
56
+ if (maxLength && val.length > maxLength) {
57
+ return t('common.maxLength', { len: maxLength });
58
+ }
59
+ if (validate) {
60
+ const customError = validate(val);
61
+ if (customError) {
62
+ return customError;
63
+ }
64
+ }
65
+ return '';
66
+ };
67
+
68
+ const handleSave = async () => {
69
+ const validationError = validateValue(tempValue);
70
+ if (validationError) {
71
+ setError(validationError);
72
+ return;
73
+ }
74
+
75
+ setLoading(true);
76
+ try {
77
+ await onSave(tempValue);
78
+ if (!hideSuccessToast) {
79
+ Toast.success(successMessage || t('common.saved'));
80
+ }
81
+ setEdit(false);
82
+ setError('');
83
+ } catch (err) {
84
+ Toast.error(formatError(err));
85
+ } finally {
86
+ setLoading(false);
87
+ }
88
+ };
89
+
90
+ const handleCancel = () => {
91
+ setTempValue(value);
92
+ setEdit(false);
93
+ setError('');
94
+ onCancel?.();
95
+ };
96
+
97
+ const handleChange = (newValue: string) => {
98
+ setTempValue(newValue);
99
+ const validationError = validateValue(newValue);
100
+ setError(validationError);
101
+ };
102
+
103
+ const handleEdit = () => {
104
+ setEdit(true);
105
+ setTempValue(value);
106
+ setError('');
107
+ onEdit?.();
108
+ };
109
+
110
+ if (disabled) {
111
+ return (
112
+ <Typography variant="body2" sx={{ color: 'text.secondary' }}>
113
+ {value || <span style={{ fontStyle: 'italic' }}>{placeholder}</span>}
114
+ </Typography>
115
+ );
116
+ }
117
+
118
+ return (
119
+ <Stack
120
+ direction="row"
121
+ spacing={1}
122
+ sx={{
123
+ alignItems: multiline ? 'flex-start' : 'center',
124
+ flexWrap: 'wrap',
125
+ width: '100%',
126
+ }}>
127
+ {edit ? (
128
+ <>
129
+ <TextField
130
+ value={tempValue}
131
+ onChange={(e) => handleChange(e.target.value)}
132
+ variant="outlined"
133
+ size="small"
134
+ multiline={multiline}
135
+ rows={multiline ? 3 : 1}
136
+ sx={{ flex: 1, minWidth: '200px' }}
137
+ placeholder={placeholder}
138
+ error={!!error}
139
+ disabled={loading}
140
+ autoFocus={autoFocus}
141
+ slotProps={{
142
+ input: {
143
+ endAdornment: error ? (
144
+ <Typography color="error" sx={{ whiteSpace: 'nowrap' }}>
145
+ {error}
146
+ </Typography>
147
+ ) : undefined,
148
+ },
149
+ htmlInput: {
150
+ maxLength,
151
+ },
152
+ }}
153
+ onKeyDown={(e) => {
154
+ if (e.key === 'Enter' && !multiline && !e.shiftKey) {
155
+ e.preventDefault();
156
+ handleSave();
157
+ }
158
+ if (e.key === 'Escape') {
159
+ e.preventDefault();
160
+ handleCancel();
161
+ }
162
+ }}
163
+ />
164
+ <Stack direction="row" spacing={0.5}>
165
+ <IconButton
166
+ onClick={handleSave}
167
+ size="small"
168
+ disabled={!!error || loading}
169
+ color="primary"
170
+ title={t('common.save')}>
171
+ <Check />
172
+ </IconButton>
173
+ <IconButton onClick={handleCancel} size="small" disabled={loading} title={t('common.cancel')}>
174
+ <Close />
175
+ </IconButton>
176
+ </Stack>
177
+ </>
178
+ ) : (
179
+ <>
180
+ <Typography
181
+ variant="body2"
182
+ sx={{
183
+ wordBreak: 'break-word',
184
+ whiteSpace: multiline ? 'pre-wrap' : 'nowrap',
185
+ overflow: multiline ? 'visible' : 'hidden',
186
+ textOverflow: multiline ? 'clip' : 'ellipsis',
187
+ }}>
188
+ {value || <span style={{ color: '#999', fontStyle: 'italic' }}>{placeholder}</span>}
189
+ </Typography>
190
+ <IconButton onClick={handleEdit} size="small" title={t('common.edit')}>
191
+ <EditOutlined fontSize="small" />
192
+ </IconButton>
193
+ </>
194
+ )}
195
+ </Stack>
196
+ );
197
+ }
@@ -138,7 +138,6 @@ export default function CurrentSubscriptions({
138
138
  sx={{
139
139
  flexWrap: 'wrap',
140
140
  padding: 2,
141
- height: '100%',
142
141
  borderRadius: 1,
143
142
  border: '1px solid',
144
143
  borderColor: 'divider',
@@ -3,8 +3,6 @@ import { ConfirmDialog, Switch, api, formatError, usePaymentContext } from '@blo
3
3
  import type { ChainType, TPaymentCurrency, TPaymentMethodExpanded } from '@blocklet/payment-types';
4
4
  import {
5
5
  AddOutlined,
6
- Check,
7
- Close,
8
6
  DeleteOutlined,
9
7
  EditOutlined,
10
8
  InfoOutlined,
@@ -27,7 +25,6 @@ import {
27
25
  ListItemText,
28
26
  Skeleton,
29
27
  Stack,
30
- TextField,
31
28
  Tooltip,
32
29
  Typography,
33
30
  useTheme,
@@ -35,7 +32,6 @@ import {
35
32
  import { useRequest, useSessionStorageState, useSetState } from 'ahooks';
36
33
  import useBus from 'use-bus';
37
34
 
38
- import { useState } from 'react';
39
35
  import Toast from '@arcblock/ux/lib/Toast';
40
36
  import { DIDDialog } from '@arcblock/ux/lib/DID';
41
37
  import { fromUnitToToken } from '@ocap/util';
@@ -45,6 +41,7 @@ import InfoCard from '../../../../components/info-card';
45
41
  import InfoRow from '../../../../components/info-row';
46
42
  import PaymentCurrencyAdd from '../../../../components/payment-currency/add';
47
43
  import PaymentCurrencyEdit from '../../../../components/payment-currency/edit';
44
+ import EditInLine from '../../../../components/edit-in-line';
48
45
  import { useRpcStatus } from '../../../../hooks/rpc-status';
49
46
  import PaymentMethodEdit from './edit';
50
47
 
@@ -62,94 +59,51 @@ const getMethods = (
62
59
  return api.get(`/api/payment-methods?${search.toString()}`).then((res) => res.data);
63
60
  };
64
61
 
65
- const updateApiHost = (id: string, apiHost: string) => {
66
- return api.put(`/api/payment-methods/${id}/settings`, { arcblock: { api_host: apiHost } }).then((res) => res.data);
62
+ const updateApiHost = (id: string, params: Record<string, any>) => {
63
+ return api.put(`/api/payment-methods/${id}/settings`, { arcblock: params }).then((res) => res.data);
67
64
  };
68
65
 
69
- function EditApiHost({ method }: { method: TPaymentMethodExpanded }) {
70
- const [edit, setEdit] = useState(false);
66
+ function EditApiHost({ method, refresh }: { method: TPaymentMethodExpanded; refresh: () => void }) {
71
67
  const { t } = useLocaleContext();
72
- const [value, setValue] = useState(method.settings?.arcblock?.api_host || '');
73
- const [tempValue, setTempValue] = useState(value);
74
- const [error, setError] = useState('');
75
68
 
76
- const handleSave = async () => {
77
- if (!tempValue.trim()) {
78
- setError(t('common.required'));
79
- return;
80
- }
81
- try {
82
- await updateApiHost(method.id, tempValue);
83
- Toast.success(t('common.saved'));
84
- setValue(tempValue);
85
- setEdit(false);
86
- setError('');
87
- } catch (err) {
88
- Toast.error(formatError(err));
89
- }
69
+ const handleSave = async (newValue: string) => {
70
+ await updateApiHost(method.id, { api_host: newValue });
71
+ refresh();
90
72
  };
91
73
 
92
- const handleCancel = () => {
93
- setTempValue(value);
94
- setEdit(false);
95
- setError('');
74
+ return (
75
+ <InfoRow
76
+ label={t('admin.paymentMethod.arcblock.api_host.label')}
77
+ value={
78
+ <EditInLine
79
+ value={method.settings?.arcblock?.api_host || ''}
80
+ onSave={handleSave}
81
+ placeholder={t('admin.paymentMethod.arcblock.api_host.tip')}
82
+ required
83
+ />
84
+ }
85
+ />
86
+ );
87
+ }
88
+
89
+ function EditApiExplorerHost({ method, refresh }: { method: TPaymentMethodExpanded; refresh: () => void }) {
90
+ const { t } = useLocaleContext();
91
+
92
+ const handleSave = async (newValue: string) => {
93
+ await updateApiHost(method.id, { explorer_host: newValue });
94
+ refresh();
96
95
  };
97
96
 
98
97
  return (
99
98
  <InfoRow
100
- label={t('admin.paymentMethod.arcblock.api_host.label')}
99
+ label={t('admin.paymentMethod.arcblock.explorer_host.label')}
101
100
  value={
102
- <Stack
103
- direction="row"
104
- spacing={1}
105
- sx={{
106
- alignItems: 'center',
107
- flexWrap: 'wrap',
108
- }}>
109
- {edit ? (
110
- <>
111
- <TextField
112
- value={tempValue}
113
- onChange={(e) => {
114
- const val = e.target.value;
115
- if (!val) {
116
- setError(t('common.required'));
117
- } else {
118
- setError('');
119
- }
120
- setTempValue(val);
121
- }}
122
- variant="outlined"
123
- size="small"
124
- sx={{ flex: 1 }}
125
- placeholder={t('admin.paymentMethod.arcblock.api_host.tip')}
126
- error={!!error}
127
- slotProps={{
128
- input: {
129
- endAdornment: error ? (
130
- <Typography color="error" sx={{ whiteSpace: 'nowrap' }}>
131
- {error}
132
- </Typography>
133
- ) : undefined,
134
- },
135
- }}
136
- />
137
- <IconButton onClick={handleSave} size="small">
138
- <Check />
139
- </IconButton>
140
- <IconButton onClick={handleCancel} size="small">
141
- <Close />
142
- </IconButton>
143
- </>
144
- ) : (
145
- <>
146
- <Typography variant="body2">{value}</Typography>
147
- <IconButton onClick={() => setEdit(true)} size="small">
148
- <EditOutlined fontSize="small" />
149
- </IconButton>
150
- </>
151
- )}
152
- </Stack>
101
+ <EditInLine
102
+ value={method.settings?.arcblock?.explorer_host || ''}
103
+ onSave={handleSave}
104
+ placeholder={t('admin.paymentMethod.arcblock.explorer_host.tip')}
105
+ required
106
+ />
153
107
  }
154
108
  />
155
109
  );
@@ -219,11 +173,13 @@ function Balance({
219
173
  balances,
220
174
  addresses,
221
175
  setDidDialog,
176
+ refresh,
222
177
  }: {
223
178
  method: TPaymentMethodExpanded;
224
179
  balances: { [currencyId: string]: string };
225
180
  addresses: { arcblock: string; ethereum: string };
226
181
  setDidDialog: (value: any) => void;
182
+ refresh: () => void;
227
183
  }) {
228
184
  const { t } = useLocaleContext();
229
185
  const defaultCurrency = (method.payment_currencies || [])?.find(
@@ -252,7 +208,11 @@ function Balance({
252
208
  };
253
209
  return (
254
210
  <>
255
- <InfoRow label={t('admin.paymentMethod.props.explorer_host')} value={explorerHost} />
211
+ {method.type === 'arcblock' ? (
212
+ <EditApiExplorerHost method={method} refresh={refresh} />
213
+ ) : (
214
+ <InfoRow label={t('admin.paymentMethod.props.explorer_host')} value={explorerHost} />
215
+ )}
256
216
  <InfoRow
257
217
  label={
258
218
  <Box
@@ -461,6 +421,11 @@ export default function PaymentMethods() {
461
421
  }
462
422
  };
463
423
 
424
+ const handleRefresh = () => {
425
+ runAsync();
426
+ refresh(true);
427
+ };
428
+
464
429
  return (
465
430
  <>
466
431
  {Object.keys(groups).map((x) => (
@@ -513,7 +478,7 @@ export default function PaymentMethods() {
513
478
  container
514
479
  spacing={2}
515
480
  sx={{
516
- mt: 0,
481
+ mt: 1,
517
482
  }}>
518
483
  <Grid
519
484
  size={{
@@ -521,10 +486,16 @@ export default function PaymentMethods() {
521
486
  md: 6,
522
487
  }}>
523
488
  <InfoRow label={t('admin.paymentMethod.props.type')} value={method.type} />
524
- {method.type === 'arcblock' && <EditApiHost method={method} />}
489
+ {method.type === 'arcblock' && <EditApiHost method={method} refresh={handleRefresh} />}
525
490
  {['ethereum', 'base'].includes(method.type) && <RpcStatus method={method} />}
526
491
  {['arcblock', 'ethereum', 'base'].includes(method.type) && (
527
- <Balance method={method} balances={balances} addresses={addresses} setDidDialog={setDidDialog} />
492
+ <Balance
493
+ method={method}
494
+ balances={balances}
495
+ addresses={addresses}
496
+ setDidDialog={setDidDialog}
497
+ refresh={handleRefresh}
498
+ />
528
499
  )}
529
500
 
530
501
  <InfoRow label={t('admin.paymentMethod.props.confirmation')} value={method.confirmation.type} />
@@ -260,7 +260,7 @@ export default function BalanceRechargePage() {
260
260
  if (Number(value) > MAX_SAFE_AMOUNT) {
261
261
  return;
262
262
  }
263
- const precision = currency.decimal;
263
+ const precision = currency.maximum_precision;
264
264
  const errorMessage = formatAmountPrecisionLimit(value, locale, precision || 6);
265
265
  setAmountError(errorMessage || '');
266
266
  setAmount(value);