payment-kit 1.22.32 → 1.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/api/src/index.ts +4 -0
  2. package/api/src/integrations/arcblock/token.ts +599 -0
  3. package/api/src/libs/credit-grant.ts +7 -6
  4. package/api/src/libs/util.ts +34 -0
  5. package/api/src/queues/credit-consume.ts +29 -4
  6. package/api/src/queues/credit-grant.ts +245 -50
  7. package/api/src/queues/credit-reconciliation.ts +253 -0
  8. package/api/src/queues/refund.ts +263 -30
  9. package/api/src/queues/token-transfer.ts +331 -0
  10. package/api/src/routes/checkout-sessions.ts +94 -29
  11. package/api/src/routes/credit-grants.ts +35 -9
  12. package/api/src/routes/credit-tokens.ts +38 -0
  13. package/api/src/routes/credit-transactions.ts +20 -3
  14. package/api/src/routes/index.ts +2 -0
  15. package/api/src/routes/meter-events.ts +4 -0
  16. package/api/src/routes/meters.ts +32 -10
  17. package/api/src/routes/payment-currencies.ts +103 -0
  18. package/api/src/routes/payment-links.ts +3 -1
  19. package/api/src/routes/products.ts +2 -2
  20. package/api/src/routes/settings.ts +4 -3
  21. package/api/src/store/migrations/20251120-add-token-config-to-currencies.ts +20 -0
  22. package/api/src/store/migrations/20251204-add-chain-fields.ts +74 -0
  23. package/api/src/store/migrations/20251211-optimize-slow-queries.ts +33 -0
  24. package/api/src/store/models/credit-grant.ts +47 -9
  25. package/api/src/store/models/credit-transaction.ts +18 -1
  26. package/api/src/store/models/index.ts +2 -1
  27. package/api/src/store/models/payment-currency.ts +31 -4
  28. package/api/src/store/models/refund.ts +12 -2
  29. package/api/src/store/models/types.ts +48 -0
  30. package/api/src/store/sequelize.ts +1 -0
  31. package/api/third.d.ts +2 -0
  32. package/blocklet.yml +1 -1
  33. package/package.json +7 -6
  34. package/src/app.tsx +10 -0
  35. package/src/components/customer/credit-overview.tsx +19 -3
  36. package/src/components/meter/form.tsx +191 -18
  37. package/src/components/price/form.tsx +49 -37
  38. package/src/locales/en.tsx +25 -1
  39. package/src/locales/zh.tsx +27 -1
  40. package/src/pages/admin/billing/meters/create.tsx +42 -13
  41. package/src/pages/admin/billing/meters/detail.tsx +56 -5
  42. package/src/pages/admin/customers/customers/credit-grant/detail.tsx +13 -0
  43. package/src/pages/admin/customers/customers/credit-transaction/detail.tsx +324 -0
  44. package/src/pages/admin/customers/index.tsx +5 -0
  45. package/src/pages/customer/credit-grant/detail.tsx +14 -1
  46. package/src/pages/customer/credit-transaction/detail.tsx +289 -0
  47. package/src/pages/customer/invoice/detail.tsx +1 -1
  48. package/src/pages/customer/recharge/subscription.tsx +1 -1
  49. package/src/pages/customer/subscription/detail.tsx +1 -1
@@ -835,3 +835,51 @@ export type StructuredSourceDataField = {
835
835
  };
836
836
 
837
837
  export type SourceData = SimpleSourceData | StructuredSourceDataField[];
838
+
839
+ // Credit Grant on-chain operation status (combined mint, burn and transfer status)
840
+ export type CreditGrantChainStatus =
841
+ | 'mint_pending'
842
+ | 'mint_completed'
843
+ | 'mint_failed'
844
+ | 'burn_pending'
845
+ | 'burn_completed'
846
+ | 'burn_failed'
847
+ | 'transfer_completed' // for expired credits transferred to system wallet
848
+ | 'transfer_failed';
849
+
850
+ // Credit Grant chain operation details
851
+ export type CreditGrantChainDetail = {
852
+ // Mint operation details
853
+ mint?: {
854
+ hash?: string;
855
+ at?: number; // timestamp
856
+ error?: string;
857
+ failed_at?: number;
858
+ };
859
+ // Burn operation details (for expiration)
860
+ burn?: {
861
+ hash?: string;
862
+ at?: number; // timestamp
863
+ amount?: string;
864
+ error?: string;
865
+ failed_at?: number;
866
+ };
867
+ // Expired transfer operation details (for expiration - transfer to system wallet)
868
+ expired_transfer?: {
869
+ hash?: string;
870
+ at?: number; // timestamp
871
+ amount?: string;
872
+ error?: string;
873
+ failed_at?: number;
874
+ };
875
+ // Refund burn operation details
876
+ refund?: {
877
+ id?: string; // refund id
878
+ burn_hash?: string;
879
+ burned_amount?: string;
880
+ system_retained?: string; // amount retained by system (consumed portion)
881
+ burn_error?: string;
882
+ };
883
+ // Voided reason
884
+ voided_reason?: 'refund' | 'expired' | 'manual';
885
+ };
@@ -17,6 +17,7 @@ Sequelize.useCLS(namespace);
17
17
  export const sequelize = new Sequelize({
18
18
  dialect: 'sqlite',
19
19
  logging: process.env.SQL_LOG === '1',
20
+ benchmark: process.env.SQL_LOG === '1' && process.env.SQL_BENCHMARK === '1',
20
21
  storage: join(env.dataDir, 'payment-kit.db'),
21
22
  pool: {
22
23
  min: sequelizeOptionsPoolMin,
package/api/third.d.ts CHANGED
@@ -2,6 +2,8 @@ declare module 'vite-plugin-blocklet';
2
2
 
3
3
  declare module '@blocklet/sdk/service/notification';
4
4
 
5
+ declare module '@blocklet/sdk/service/blocklet';
6
+
5
7
  declare module 'express-history-api-fallback';
6
8
 
7
9
  declare module 'express-async-errors';
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.22.32
17
+ version: 1.23.1
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.22.32",
3
+ "version": "1.23.1",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
@@ -53,13 +53,14 @@
53
53
  "@arcblock/react-hooks": "^3.2.11",
54
54
  "@arcblock/ux": "^3.2.11",
55
55
  "@arcblock/validator": "^1.27.14",
56
+ "@arcblock/vc": "^1.27.14",
56
57
  "@blocklet/did-space-js": "^1.2.9",
57
58
  "@blocklet/error": "^0.3.4",
58
59
  "@blocklet/js-sdk": "^1.17.4",
59
60
  "@blocklet/logger": "^1.17.4",
60
- "@blocklet/payment-broker-client": "1.22.32",
61
- "@blocklet/payment-react": "1.22.32",
62
- "@blocklet/payment-vendor": "1.22.32",
61
+ "@blocklet/payment-broker-client": "1.23.1",
62
+ "@blocklet/payment-react": "1.23.1",
63
+ "@blocklet/payment-vendor": "1.23.1",
63
64
  "@blocklet/sdk": "^1.17.4",
64
65
  "@blocklet/ui-react": "^3.2.11",
65
66
  "@blocklet/uploader": "^0.3.14",
@@ -129,7 +130,7 @@
129
130
  "devDependencies": {
130
131
  "@abtnode/types": "^1.17.4",
131
132
  "@arcblock/eslint-config-ts": "^0.3.3",
132
- "@blocklet/payment-types": "1.22.32",
133
+ "@blocklet/payment-types": "1.23.1",
133
134
  "@types/cookie-parser": "^1.4.9",
134
135
  "@types/cors": "^2.8.19",
135
136
  "@types/debug": "^4.1.12",
@@ -176,5 +177,5 @@
176
177
  "parser": "typescript"
177
178
  }
178
179
  },
179
- "gitHead": "4b5eac2af9a6a10a4f971f7dcbe220e8049d0cc7"
180
+ "gitHead": "393e83d16adecbe9aa9d145732c45b2198a2e8b9"
180
181
  }
package/src/app.tsx CHANGED
@@ -28,6 +28,7 @@ const CustomerSubscriptionEmbed = React.lazy(() => import('./pages/customer/subs
28
28
  const CustomerSubscriptionChangePlan = React.lazy(() => import('./pages/customer/subscription/change-plan'));
29
29
  const CustomerSubscriptionChangePayment = React.lazy(() => import('./pages/customer/subscription/change-payment'));
30
30
  const CustomerCreditGrantDetail = React.lazy(() => import('./pages/customer/credit-grant/detail'));
31
+ const CustomerCreditTransactionDetail = React.lazy(() => import('./pages/customer/credit-transaction/detail'));
31
32
  const CustomerRecharge = React.lazy(() => import('./pages/customer/recharge/subscription'));
32
33
  const CustomerPayoutDetail = React.lazy(() => import('./pages/customer/payout/detail'));
33
34
  const IntegrationsPage = React.lazy(() => import('./pages/integrations'));
@@ -161,6 +162,15 @@ function App() {
161
162
  </UserLayout>
162
163
  }
163
164
  />
165
+ <Route
166
+ key="customer-credit-transaction"
167
+ path="/customer/credit-transaction/:id"
168
+ element={
169
+ <UserLayout>
170
+ <CustomerCreditTransactionDetail />
171
+ </UserLayout>
172
+ }
173
+ />
164
174
  <Route key="customer-fallback" path="/customer/*" element={<Navigate to="/customer" />} />,
165
175
  <Route path="*" element={<Navigate to="/" />} />
166
176
  </Routes>
@@ -136,7 +136,7 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
136
136
  const grantData = creditSummary?.grants?.[currencyId];
137
137
  const pendingAmount = creditSummary?.pendingAmount?.[currencyId] || '0';
138
138
 
139
- if (!grantData) {
139
+ if (!grantData || grantData.status === 'inactive') {
140
140
  return null;
141
141
  }
142
142
 
@@ -145,6 +145,8 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
145
145
  const totalAmount = grantData.totalAmount || '0';
146
146
  const remainingAmount = grantData.remainingAmount || '0';
147
147
 
148
+ const cardTitle = grantData.meter?.name || currency.name;
149
+
148
150
  return (
149
151
  <Card
150
152
  key={currency.id}
@@ -173,7 +175,7 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
173
175
  pb: 2,
174
176
  }}>
175
177
  <Typography variant="h6" component="div">
176
- {currency.name}
178
+ {cardTitle}
177
179
  </Typography>
178
180
  {showRecharge && (
179
181
  <SplitButton
@@ -217,7 +219,15 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
217
219
  {totalAmount === '0' && remainingAmount === '0' ? (
218
220
  <>0 </>
219
221
  ) : (
220
- <>{formatBNStr(remainingAmount, currency.decimal, 6, true)}</>
222
+ <>
223
+ {formatBNStr(remainingAmount, currency.decimal, 6, true)}
224
+ {currency.symbol !== cardTitle && (
225
+ <Typography variant="body2" component="span">
226
+ {' '}
227
+ {currency.symbol}
228
+ </Typography>
229
+ )}
230
+ </>
221
231
  )}
222
232
  </Typography>
223
233
  </Box>
@@ -239,6 +249,12 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
239
249
  color: 'error.main',
240
250
  }}>
241
251
  {formatBNStr(pendingAmount, currency.decimal, 6, true)}
252
+ {currency.symbol !== cardTitle && (
253
+ <Typography variant="body2" component="span">
254
+ {' '}
255
+ {currency.symbol}
256
+ </Typography>
257
+ )}
242
258
  </Typography>
243
259
  </Box>
244
260
  )}
@@ -1,23 +1,54 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
- import { FormInput, FormLabel, Collapse } from '@blocklet/payment-react';
3
- import { FormControl, Select, MenuItem, Typography, Box, FormHelperText, Stack } from '@mui/material';
4
- import { useFormContext, useWatch } from 'react-hook-form';
2
+ import { FormInput, FormLabel, Collapse, usePaymentContext } from '@blocklet/payment-react';
3
+ import {
4
+ FormControl,
5
+ Select,
6
+ MenuItem,
7
+ Typography,
8
+ Box,
9
+ FormHelperText,
10
+ Stack,
11
+ Radio,
12
+ RadioGroup,
13
+ FormControlLabel,
14
+ Switch,
15
+ Autocomplete,
16
+ TextField,
17
+ } from '@mui/material';
18
+ import { useFormContext, useWatch, Controller } from 'react-hook-form';
5
19
  import { InfoOutlined } from '@mui/icons-material';
20
+ import { useRequest } from 'ahooks';
21
+ import { useState, useMemo } from 'react';
22
+ import GraphQLClient from '@ocap/client';
6
23
 
7
- import type { TMeter } from '@blocklet/payment-types';
24
+ import type { TMeter, TPaymentCurrency } from '@blocklet/payment-types';
8
25
  import MetadataForm from '../metadata/form';
9
26
 
27
+ export type TokenConfig = { name: string; symbol: string; tokenFactoryAddress: string };
28
+
10
29
  type Props = {
11
30
  mode?: 'create' | 'edit';
31
+ tokenConfig?: TokenConfig;
32
+ onTokenConfigChange?: (next: TokenConfig) => void;
33
+ paymentCurrency?: TPaymentCurrency;
34
+ };
35
+
36
+ type TMeterForm = TMeter & {
37
+ mode?: 'OffChain' | 'OnChain';
12
38
  };
13
39
 
14
- function MeterForm({ mode = 'create' }: Props) {
40
+ function MeterForm({
41
+ mode = 'create',
42
+ tokenConfig = undefined,
43
+ onTokenConfigChange = undefined,
44
+ paymentCurrency = undefined,
45
+ }: Props) {
15
46
  const { t } = useLocaleContext();
16
47
  const {
17
48
  register,
18
49
  control,
19
50
  formState: { errors },
20
- } = useFormContext<TMeter>();
51
+ } = useFormContext<TMeterForm>();
21
52
 
22
53
  // 监听聚合方法的变化,用于显示动态帮助信息
23
54
  const aggregationMethod = useWatch({
@@ -26,20 +57,52 @@ function MeterForm({ mode = 'create' }: Props) {
26
57
  defaultValue: 'sum',
27
58
  });
28
59
 
29
- const getAggregationDescription = (method: string) => {
30
- switch (method) {
31
- case 'sum':
32
- return t('admin.meter.aggregationMethod.sumDescription');
33
- case 'count':
34
- return t('admin.meter.aggregationMethod.countDescription');
35
- case 'last':
36
- return t('admin.meter.aggregationMethod.lastDescription');
37
- default:
38
- return '';
39
- }
40
- };
60
+ const creditMode = useWatch({
61
+ control,
62
+ name: 'mode',
63
+ defaultValue: 'OffChain',
64
+ });
65
+
66
+ const [tokenType, setTokenType] = useState<'new' | 'existing'>(tokenConfig?.tokenFactoryAddress ? 'existing' : 'new');
41
67
 
42
68
  const isEditing = mode === 'edit';
69
+ const { settings } = usePaymentContext();
70
+
71
+ // Check if current currency already has token_config
72
+ const hasTokenConfig = !!paymentCurrency?.token_config;
73
+
74
+ // Get GraphQL client for arcblock chain
75
+ const arcblockClient = useMemo(() => {
76
+ const arcblockMethod = settings.paymentMethods?.find((m) => m.type === 'arcblock') as any;
77
+ const apiHost = arcblockMethod?.api_host;
78
+ return apiHost ? new GraphQLClient(apiHost) : null;
79
+ }, [settings.paymentMethods]);
80
+
81
+ // Fetch tokens created by blocklet owner
82
+ const { data: tokens = [] } = useRequest(
83
+ async () => {
84
+ if (!arcblockClient || !window.blocklet?.appId) {
85
+ return [];
86
+ }
87
+ const result = await arcblockClient.listTokens({
88
+ issuerAddress: window.blocklet.appId,
89
+ });
90
+ return result?.tokens || [];
91
+ },
92
+ {
93
+ refreshDeps: [creditMode, arcblockClient],
94
+ ready: creditMode === 'OnChain' && !hasTokenConfig && !!arcblockClient,
95
+ }
96
+ );
97
+
98
+ const getAggregationDescription = (method: string) => {
99
+ const map: Record<string, string> = {
100
+ sum: t('admin.meter.aggregationMethod.sumDescription'),
101
+ count: t('admin.meter.aggregationMethod.countDescription'),
102
+ last: t('admin.meter.aggregationMethod.lastDescription'),
103
+ };
104
+ return map[method] || '';
105
+ };
43
106
 
44
107
  return (
45
108
  <Box sx={{ maxWidth: 600 }}>
@@ -223,6 +286,116 @@ function MeterForm({ mode = 'create' }: Props) {
223
286
  </FormHelperText>
224
287
  </Box>
225
288
 
289
+ {/* On-chain token configuration */}
290
+ <Box sx={{ p: 2, border: '1px solid', borderColor: 'divider', borderRadius: 1 }}>
291
+ <Controller
292
+ name="mode"
293
+ control={control}
294
+ render={({ field }) => (
295
+ <Stack direction="row" justifyContent="space-between" alignItems="center">
296
+ <Box>
297
+ <Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
298
+ {t('admin.meter.creditMode.onchain')}
299
+ </Typography>
300
+ <Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
301
+ {t('admin.meter.creditMode.onChainDescription')}
302
+ </Typography>
303
+ </Box>
304
+ <Switch
305
+ checked={hasTokenConfig ? field.value !== 'OffChain' : field.value === 'OnChain'}
306
+ onChange={(e) => field.onChange(e.target.checked ? 'OnChain' : 'OffChain')}
307
+ />
308
+ </Stack>
309
+ )}
310
+ />
311
+
312
+ {(hasTokenConfig || creditMode === 'OnChain') && (
313
+ <Box sx={{ mt: 3, pt: 3, borderTop: '1px solid', borderColor: 'divider' }}>
314
+ <Typography variant="subtitle2" sx={{ mb: 2 }}>
315
+ {t('admin.meter.creditMode.tokenConfiguration')}
316
+ </Typography>
317
+ {!hasTokenConfig && (
318
+ <RadioGroup
319
+ value={tokenType}
320
+ onChange={(e) => {
321
+ setTokenType(e.target.value as any);
322
+ onTokenConfigChange?.({ name: '', symbol: '', tokenFactoryAddress: '' });
323
+ }}
324
+ row>
325
+ <FormControlLabel
326
+ value="new"
327
+ control={<Radio />}
328
+ label={t('admin.meter.creditMode.createNewToken')}
329
+ />
330
+ <FormControlLabel
331
+ value="existing"
332
+ control={<Radio />}
333
+ label={t('admin.meter.creditMode.useExistingToken')}
334
+ />
335
+ </RadioGroup>
336
+ )}
337
+ {!hasTokenConfig && tokenType === 'new' && tokenConfig && onTokenConfigChange ? (
338
+ <Box sx={{ mt: 2 }}>
339
+ <TextField
340
+ value={tokenConfig.name}
341
+ onChange={(e) => onTokenConfigChange({ ...tokenConfig, name: e.target.value })}
342
+ fullWidth
343
+ label={t('admin.meter.creditMode.tokenName')}
344
+ inputProps={{ maxLength: 64 }}
345
+ sx={{ mb: 2 }}
346
+ />
347
+ <TextField
348
+ value={tokenConfig.symbol}
349
+ onChange={(e) => onTokenConfigChange({ ...tokenConfig, symbol: e.target.value.toUpperCase() })}
350
+ fullWidth
351
+ label={t('admin.meter.creditMode.tokenSymbol')}
352
+ inputProps={{ maxLength: 6, style: { textTransform: 'uppercase', fontFamily: 'monospace' } }}
353
+ />
354
+ </Box>
355
+ ) : (
356
+ <Box sx={{ mt: 2 }}>
357
+ {hasTokenConfig && paymentCurrency?.token_config ? (
358
+ <TextField
359
+ fullWidth
360
+ label={t('admin.meter.creditMode.selectToken')}
361
+ value={`${paymentCurrency.token_config.symbol} (${paymentCurrency.token_config.name})`}
362
+ disabled
363
+ />
364
+ ) : (
365
+ <Autocomplete
366
+ options={tokens}
367
+ getOptionLabel={(o: any) => (typeof o === 'string' ? o : `${o.symbol} (${o.name})`)}
368
+ value={
369
+ tokens.find((f: any) => f.tokenFactoryAddress === tokenConfig?.tokenFactoryAddress) || null
370
+ }
371
+ onChange={(_, v: any) =>
372
+ onTokenConfigChange?.({
373
+ name: v?.name || '',
374
+ symbol: v?.symbol || '',
375
+ tokenFactoryAddress: v?.tokenFactoryAddress || '',
376
+ })
377
+ }
378
+ renderInput={(params) => (
379
+ <TextField {...params} label={t('admin.meter.creditMode.selectToken')} />
380
+ )}
381
+ renderOption={(props, o: any) => (
382
+ <li {...props}>
383
+ <Box>
384
+ <Typography variant="body1">{o.symbol}</Typography>
385
+ <Typography variant="caption" color="text.secondary">
386
+ {o.name}
387
+ </Typography>
388
+ </Box>
389
+ </li>
390
+ )}
391
+ />
392
+ )}
393
+ </Box>
394
+ )}
395
+ </Box>
396
+ )}
397
+ </Box>
398
+
226
399
  {/* Metadata Section */}
227
400
  <Collapse trigger={t('common.metadata.label')}>
228
401
  <Box sx={{ mb: 2 }}>
@@ -835,65 +835,77 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
835
835
  />
836
836
 
837
837
  {/* 可用时长配置 */}
838
- <Box sx={{ width: '100%', mb: 2 }}>
839
- <FormLabel tooltip={t('admin.creditProduct.validDuration.help')}>
840
- {t('admin.creditProduct.validDuration.label')}
841
- </FormLabel>
842
- <Stack
843
- direction="row"
844
- spacing={1}
845
- sx={{
846
- alignItems: 'center',
847
- }}>
848
- <Controller
849
- name={getFieldName('metadata.credit_config.valid_duration_value')}
850
- control={control}
851
- render={({ field }) => (
838
+ <Controller
839
+ name={getFieldName('metadata')}
840
+ control={control}
841
+ render={({ field }) => (
842
+ <Box sx={{ width: '100%', mb: 2 }}>
843
+ <FormLabel tooltip={t('admin.creditProduct.validDuration.help')}>
844
+ {t('admin.creditProduct.validDuration.label')}
845
+ </FormLabel>
846
+ <Stack
847
+ direction="row"
848
+ spacing={1}
849
+ sx={{
850
+ alignItems: 'center',
851
+ }}>
852
852
  <TextField
853
- {...field}
854
853
  size="small"
855
854
  type="number"
856
855
  sx={{ flex: 1 }}
857
- value={field.value ?? '0'}
856
+ value={field.value?.credit_config?.valid_duration_value ?? '0'}
858
857
  placeholder="0"
859
858
  disabled={isLocked}
859
+ onChange={(e) => {
860
+ const metadata = field.value || {};
861
+ const creditConfig = metadata.credit_config || {};
862
+ const value = +e.target.value;
863
+ if (!Number.isNaN(value) && value >= 0) {
864
+ creditConfig.valid_duration_value = +value;
865
+ } else {
866
+ delete creditConfig.valid_duration_value;
867
+ }
868
+ metadata.credit_config = creditConfig;
869
+ field.onChange(metadata);
870
+ }}
860
871
  slotProps={{
861
872
  htmlInput: {
862
873
  min: 0,
874
+ step: 0.1,
863
875
  },
864
876
  }}
865
877
  />
866
- )}
867
- />
868
- <Controller
869
- name={getFieldName('metadata.credit_config.valid_duration_unit')}
870
- control={control}
871
- render={({ field }) => (
872
878
  <Select
873
- {...field}
874
879
  size="small"
875
880
  sx={{ minWidth: 120 }}
876
881
  disabled={isLocked}
877
- value={field.value || 'days'}>
882
+ value={field.value?.credit_config?.valid_duration_unit || 'days'}
883
+ onChange={(e) => {
884
+ const metadata = field.value || {};
885
+ const creditConfig = metadata.credit_config || {};
886
+ creditConfig.valid_duration_unit = e.target.value;
887
+ metadata.credit_config = creditConfig;
888
+ field.onChange(metadata);
889
+ }}>
878
890
  {!livemode && <MenuItem value="hours">{t('admin.creditProduct.validDuration.hours')}</MenuItem>}
879
891
  <MenuItem value="days">{t('admin.creditProduct.validDuration.days')}</MenuItem>
880
892
  <MenuItem value="weeks">{t('admin.creditProduct.validDuration.weeks')}</MenuItem>
881
893
  <MenuItem value="months">{t('admin.creditProduct.validDuration.months')}</MenuItem>
882
894
  <MenuItem value="years">{t('admin.creditProduct.validDuration.years')}</MenuItem>
883
895
  </Select>
884
- )}
885
- />
886
- </Stack>
887
- <Typography
888
- variant="caption"
889
- sx={{
890
- color: 'text.secondary',
891
- display: 'block',
892
- mt: 0.5,
893
- }}>
894
- {t('admin.creditProduct.validDuration.description')}
895
- </Typography>
896
- </Box>
896
+ </Stack>
897
+ <Typography
898
+ variant="caption"
899
+ sx={{
900
+ color: 'text.secondary',
901
+ display: 'block',
902
+ mt: 0.5,
903
+ }}>
904
+ {t('admin.creditProduct.validDuration.description')}
905
+ </Typography>
906
+ </Box>
907
+ )}
908
+ />
897
909
 
898
910
  {/* 关联特定价格 */}
899
911
  <Controller
@@ -273,6 +273,30 @@ export default flat({
273
273
  edit: 'Edit meter',
274
274
  save: 'Save meter',
275
275
  saved: 'Meter successfully saved',
276
+ creditMode: {
277
+ onchain: 'OnChain',
278
+ onChainDescription: 'Once enabled, your credit will be published as a Token on the ArcBlock blockchain',
279
+ tokenConfiguration: 'Token Configuration',
280
+ createNewToken: 'Create New Token',
281
+ useExistingToken: 'Use Existing Token',
282
+ createTokenHint: 'Fill in the token name and symbol to create a new Token',
283
+ tokenName: 'Token Name',
284
+ tokenNamePlaceholder: 'Enter token name',
285
+ tokenNameRequired: 'Token name is required',
286
+ tokenSymbol: 'Token Symbol',
287
+ tokenSymbolPlaceholder: 'Enter token symbol',
288
+ tokenSymbolRequired: 'Token symbol is required',
289
+ tokenSymbolFormat: 'Symbol must be 1-6 uppercase letters or numbers',
290
+ createTokenButton: 'Create Token',
291
+ creatingToken: 'Creating...',
292
+ tokenCreated: 'Token created successfully',
293
+ tokenCreatedLabel: 'Token Created',
294
+ tokenAddress: 'Token Address',
295
+ selectToken: 'Select Token',
296
+ selectTokenPlaceholder: 'Select an existing token',
297
+ tokenAddressRequired: 'Token address is required',
298
+ enabled: 'Enabled',
299
+ },
276
300
  activate: 'Activate meter',
277
301
  activated: 'Meter activated successfully',
278
302
  deactivate: 'Deactivate meter',
@@ -293,7 +317,7 @@ export default flat({
293
317
  label: 'Meter name',
294
318
  required: 'Meter name is required',
295
319
  placeholder: 'API requests',
296
- help: 'A descriptive name for this meter that will be displayed in your dashboard.',
320
+ help: "A descriptive name for this meter that will be displayed in your dashboard and user's personal bill.",
297
321
  },
298
322
  eventName: {
299
323
  label: 'Event name',
@@ -283,13 +283,39 @@ export default flat({
283
283
  basicInfo: '基本信息',
284
284
  basicInfoDescription: '配置计量器的核心设置以跟踪使用事件。',
285
285
  editDescription: '更新计量器名称、描述和元数据。事件名称和聚合方法等核心设置无法更改。',
286
+
287
+ creditMode: {
288
+ onchain: 'OnChain',
289
+ onChainDescription:
290
+ '开启后你的 Credit 将作为 Token 发布在 ArcBlock 区块链上,用户的充值、退款和消费将在链上公开记录且不可篡改。',
291
+ tokenConfiguration: 'Token 配置',
292
+ createNewToken: '创建新 Token',
293
+ useExistingToken: '使用现有 Token',
294
+ createTokenHint: '填写 Token 名称和符号以创建新 Token',
295
+ tokenName: 'Token 名称',
296
+ tokenNamePlaceholder: '请输入 Token 名称',
297
+ tokenNameRequired: 'Token 名称不能为空',
298
+ tokenSymbol: 'Token 符号',
299
+ tokenSymbolPlaceholder: '请输入 Token 符号',
300
+ tokenSymbolRequired: 'Token 符号不能为空',
301
+ tokenSymbolFormat: '符号必须为 1-6 个大写字母或数字',
302
+ createTokenButton: '创建 Token',
303
+ creatingToken: '创建中...',
304
+ tokenCreated: 'Token 创建成功',
305
+ tokenCreatedLabel: 'Token 已创建',
306
+ tokenAddress: 'Token 地址',
307
+ selectToken: '选择 Token',
308
+ selectTokenPlaceholder: '请选择一个现有的 Token',
309
+ tokenAddressRequired: 'Token 地址不能为空',
310
+ enabled: '已启用',
311
+ },
286
312
  inactive: '计量器未激活',
287
313
  inactiveTip: '此计量器未收集使用数据。激活它以开始跟踪事件。',
288
314
  name: {
289
315
  label: '计量器名称',
290
316
  required: '计量器名称为必填项',
291
317
  placeholder: 'API 请求',
292
- help: '此计量器的描述性名称,将在您的仪表板中显示。',
318
+ help: '此计量器的描述性名称,将在您的仪表板以及用户个人账单中显示。',
293
319
  },
294
320
  eventName: {
295
321
  label: '事件名称',