payment-kit 1.19.0 → 1.19.2

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 (139) hide show
  1. package/api/src/crons/index.ts +8 -0
  2. package/api/src/index.ts +4 -0
  3. package/api/src/libs/credit-grant.ts +146 -0
  4. package/api/src/libs/env.ts +1 -0
  5. package/api/src/libs/invoice.ts +4 -3
  6. package/api/src/libs/notification/template/base.ts +388 -2
  7. package/api/src/libs/notification/template/customer-credit-grant-granted.ts +149 -0
  8. package/api/src/libs/notification/template/customer-credit-grant-low-balance.ts +151 -0
  9. package/api/src/libs/notification/template/customer-credit-insufficient.ts +254 -0
  10. package/api/src/libs/notification/template/subscription-canceled.ts +193 -202
  11. package/api/src/libs/notification/template/subscription-refund-succeeded.ts +215 -237
  12. package/api/src/libs/notification/template/subscription-renewed.ts +130 -200
  13. package/api/src/libs/notification/template/subscription-succeeded.ts +100 -202
  14. package/api/src/libs/notification/template/subscription-trial-start.ts +142 -188
  15. package/api/src/libs/notification/template/subscription-trial-will-end.ts +146 -174
  16. package/api/src/libs/notification/template/subscription-upgraded.ts +96 -192
  17. package/api/src/libs/notification/template/subscription-will-canceled.ts +94 -135
  18. package/api/src/libs/notification/template/subscription-will-renew.ts +220 -245
  19. package/api/src/libs/payment.ts +69 -0
  20. package/api/src/libs/queue/index.ts +3 -2
  21. package/api/src/libs/session.ts +8 -0
  22. package/api/src/libs/subscription.ts +74 -3
  23. package/api/src/libs/util.ts +3 -1
  24. package/api/src/libs/ws.ts +23 -1
  25. package/api/src/locales/en.ts +33 -0
  26. package/api/src/locales/zh.ts +31 -0
  27. package/api/src/queues/credit-consume.ts +728 -0
  28. package/api/src/queues/credit-grant.ts +572 -0
  29. package/api/src/queues/notification.ts +173 -128
  30. package/api/src/queues/payment.ts +210 -122
  31. package/api/src/queues/subscription.ts +179 -0
  32. package/api/src/routes/checkout-sessions.ts +157 -9
  33. package/api/src/routes/connect/shared.ts +3 -2
  34. package/api/src/routes/credit-grants.ts +241 -0
  35. package/api/src/routes/credit-transactions.ts +208 -0
  36. package/api/src/routes/customers.ts +34 -5
  37. package/api/src/routes/index.ts +8 -0
  38. package/api/src/routes/meter-events.ts +347 -0
  39. package/api/src/routes/meters.ts +219 -0
  40. package/api/src/routes/payment-currencies.ts +20 -2
  41. package/api/src/routes/payment-links.ts +1 -1
  42. package/api/src/routes/payment-methods.ts +14 -2
  43. package/api/src/routes/prices.ts +43 -0
  44. package/api/src/routes/pricing-table.ts +13 -7
  45. package/api/src/routes/products.ts +63 -4
  46. package/api/src/routes/settings.ts +1 -1
  47. package/api/src/routes/subscriptions.ts +4 -0
  48. package/api/src/routes/webhook-endpoints.ts +0 -3
  49. package/api/src/store/migrations/20250610-billing-credit.ts +43 -0
  50. package/api/src/store/models/credit-grant.ts +486 -0
  51. package/api/src/store/models/credit-transaction.ts +268 -0
  52. package/api/src/store/models/customer.ts +8 -0
  53. package/api/src/store/models/index.ts +52 -1
  54. package/api/src/store/models/meter-event.ts +423 -0
  55. package/api/src/store/models/meter.ts +176 -0
  56. package/api/src/store/models/payment-currency.ts +66 -14
  57. package/api/src/store/models/price.ts +6 -0
  58. package/api/src/store/models/product.ts +2 -2
  59. package/api/src/store/models/subscription.ts +24 -0
  60. package/api/src/store/models/types.ts +28 -2
  61. package/api/tests/libs/subscription.spec.ts +53 -0
  62. package/blocklet.yml +9 -1
  63. package/package.json +4 -4
  64. package/scripts/sdk.js +233 -1
  65. package/src/app.tsx +10 -0
  66. package/src/components/collapse.tsx +11 -1
  67. package/src/components/conditional-section.tsx +87 -0
  68. package/src/components/customer/credit-grant-item-list.tsx +99 -0
  69. package/src/components/customer/credit-overview.tsx +246 -0
  70. package/src/components/customer/form.tsx +7 -3
  71. package/src/components/invoice/list.tsx +19 -1
  72. package/src/components/metadata/form.tsx +287 -91
  73. package/src/components/meter/actions.tsx +101 -0
  74. package/src/components/meter/add-usage-dialog.tsx +239 -0
  75. package/src/components/meter/events-list.tsx +657 -0
  76. package/src/components/meter/form.tsx +245 -0
  77. package/src/components/meter/products.tsx +264 -0
  78. package/src/components/meter/usage-guide.tsx +174 -0
  79. package/src/components/payment-currency/form.tsx +2 -0
  80. package/src/components/payment-intent/list.tsx +19 -1
  81. package/src/components/payment-link/item.tsx +2 -2
  82. package/src/components/payment-link/preview.tsx +1 -1
  83. package/src/components/payment-link/product-select.tsx +52 -12
  84. package/src/components/payment-method/arcblock.tsx +2 -0
  85. package/src/components/payment-method/base.tsx +2 -0
  86. package/src/components/payment-method/bitcoin.tsx +2 -0
  87. package/src/components/payment-method/ethereum.tsx +2 -0
  88. package/src/components/payment-method/stripe.tsx +2 -0
  89. package/src/components/payouts/list.tsx +19 -1
  90. package/src/components/payouts/portal/list.tsx +6 -11
  91. package/src/components/price/currency-select.tsx +56 -32
  92. package/src/components/price/form.tsx +912 -407
  93. package/src/components/pricing-table/preview.tsx +1 -1
  94. package/src/components/product/add-price.tsx +9 -7
  95. package/src/components/product/create.tsx +7 -4
  96. package/src/components/product/edit-price.tsx +21 -12
  97. package/src/components/product/features.tsx +17 -7
  98. package/src/components/product/form.tsx +100 -90
  99. package/src/components/refund/list.tsx +19 -1
  100. package/src/components/section/header.tsx +5 -18
  101. package/src/components/subscription/items/index.tsx +1 -1
  102. package/src/components/subscription/metrics.tsx +37 -5
  103. package/src/components/subscription/portal/actions.tsx +2 -1
  104. package/src/contexts/products.tsx +26 -9
  105. package/src/hooks/subscription.ts +34 -0
  106. package/src/libs/meter-utils.ts +196 -0
  107. package/src/libs/util.ts +4 -0
  108. package/src/locales/en.tsx +389 -5
  109. package/src/locales/zh.tsx +368 -1
  110. package/src/pages/admin/billing/index.tsx +61 -33
  111. package/src/pages/admin/billing/invoices/detail.tsx +1 -1
  112. package/src/pages/admin/billing/meters/create.tsx +60 -0
  113. package/src/pages/admin/billing/meters/detail.tsx +435 -0
  114. package/src/pages/admin/billing/meters/index.tsx +210 -0
  115. package/src/pages/admin/billing/meters/meter-event.tsx +346 -0
  116. package/src/pages/admin/billing/subscriptions/detail.tsx +47 -14
  117. package/src/pages/admin/customers/customers/credit-grant/detail.tsx +391 -0
  118. package/src/pages/admin/customers/customers/detail.tsx +14 -10
  119. package/src/pages/admin/customers/index.tsx +5 -0
  120. package/src/pages/admin/developers/events/detail.tsx +1 -1
  121. package/src/pages/admin/developers/index.tsx +1 -1
  122. package/src/pages/admin/payments/intents/detail.tsx +1 -1
  123. package/src/pages/admin/payments/payouts/detail.tsx +1 -1
  124. package/src/pages/admin/payments/refunds/detail.tsx +1 -1
  125. package/src/pages/admin/products/index.tsx +3 -2
  126. package/src/pages/admin/products/links/detail.tsx +1 -1
  127. package/src/pages/admin/products/prices/actions.tsx +16 -4
  128. package/src/pages/admin/products/prices/detail.tsx +30 -3
  129. package/src/pages/admin/products/prices/list.tsx +8 -1
  130. package/src/pages/admin/products/pricing-tables/detail.tsx +1 -1
  131. package/src/pages/admin/products/products/create.tsx +233 -57
  132. package/src/pages/admin/products/products/detail.tsx +2 -1
  133. package/src/pages/admin/settings/payment-methods/index.tsx +3 -0
  134. package/src/pages/customer/credit-grant/detail.tsx +308 -0
  135. package/src/pages/customer/index.tsx +44 -9
  136. package/src/pages/customer/recharge/account.tsx +5 -5
  137. package/src/pages/customer/subscription/change-payment.tsx +4 -2
  138. package/src/pages/customer/subscription/detail.tsx +48 -14
  139. package/src/pages/customer/subscription/embed.tsx +1 -1
@@ -0,0 +1,245 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import { FormInput, FormLabel } from '@blocklet/payment-react';
3
+ import { FormControl, Select, MenuItem, Typography, Box, FormHelperText, Stack } from '@mui/material';
4
+ import { useFormContext, useWatch } from 'react-hook-form';
5
+ import { InfoOutlined } from '@mui/icons-material';
6
+
7
+ import type { TMeter } from '@blocklet/payment-types';
8
+ import Collapse from '../collapse';
9
+ import MetadataForm from '../metadata/form';
10
+
11
+ type Props = {
12
+ mode?: 'create' | 'edit';
13
+ };
14
+
15
+ function MeterForm({ mode = 'create' }: Props) {
16
+ const { t } = useLocaleContext();
17
+ const {
18
+ register,
19
+ control,
20
+ formState: { errors },
21
+ } = useFormContext<TMeter>();
22
+
23
+ // 监听聚合方法的变化,用于显示动态帮助信息
24
+ const aggregationMethod = useWatch({
25
+ control,
26
+ name: 'aggregation_method',
27
+ defaultValue: 'sum',
28
+ });
29
+
30
+ const getAggregationDescription = (method: string) => {
31
+ switch (method) {
32
+ case 'sum':
33
+ return t('admin.meter.aggregationMethod.sumDescription');
34
+ case 'count':
35
+ return t('admin.meter.aggregationMethod.countDescription');
36
+ case 'last':
37
+ return t('admin.meter.aggregationMethod.lastDescription');
38
+ default:
39
+ return '';
40
+ }
41
+ };
42
+
43
+ const isEditing = mode === 'edit';
44
+
45
+ return (
46
+ <Box sx={{ maxWidth: 600 }}>
47
+ {/* Header Section */}
48
+ <Box sx={{ mb: 4 }}>
49
+ <Typography
50
+ variant="body2"
51
+ sx={{
52
+ color: 'text.secondary',
53
+ }}>
54
+ {isEditing ? t('admin.meter.editDescription') : t('admin.meter.basicInfoDescription')}
55
+ </Typography>
56
+ </Box>
57
+ <Stack spacing={4}>
58
+ {/* Meter Name */}
59
+ <Box>
60
+ <FormInput
61
+ name="name"
62
+ rules={{
63
+ required: t('admin.meter.name.required'),
64
+ maxLength: {
65
+ value: 64,
66
+ message: t('common.maxLength', { len: 64 }),
67
+ },
68
+ }}
69
+ label={t('admin.meter.name.label')}
70
+ placeholder={t('admin.meter.name.placeholder')}
71
+ error={!!errors.name}
72
+ helperText={errors.name?.message}
73
+ autoFocus
74
+ inputProps={{ maxLength: 64 }}
75
+ />
76
+ <FormHelperText sx={{ mt: 1 }} color="text.secondary">
77
+ {t('admin.meter.name.help')}
78
+ </FormHelperText>
79
+ </Box>
80
+
81
+ {/* Event Name - Only in create mode */}
82
+ {!isEditing && (
83
+ <Box>
84
+ <FormInput
85
+ name="event_name"
86
+ rules={{
87
+ required: t('admin.meter.eventName.required'),
88
+ maxLength: {
89
+ value: 64,
90
+ message: t('common.maxLength', { len: 64 }),
91
+ },
92
+ }}
93
+ label={t('admin.meter.eventName.label')}
94
+ placeholder={t('admin.meter.eventName.placeholder')}
95
+ error={!!errors.event_name}
96
+ helperText={errors.event_name?.message}
97
+ inputProps={{
98
+ maxLength: 64,
99
+ style: { fontFamily: 'monospace' },
100
+ }}
101
+ />
102
+ <Box sx={{ mt: 1, display: 'flex', alignItems: 'center', gap: 1 }}>
103
+ <InfoOutlined color="info" sx={{ fontSize: 'small' }} />
104
+ <Typography
105
+ variant="caption"
106
+ sx={{
107
+ color: 'text.secondary',
108
+ }}>
109
+ {t('admin.meter.eventName.help')}
110
+ </Typography>
111
+ </Box>
112
+ </Box>
113
+ )}
114
+
115
+ {/* Event Name - Read-only in edit mode */}
116
+ {isEditing && (
117
+ <Box>
118
+ <FormInput
119
+ name="event_name"
120
+ label={t('admin.meter.eventName.label')}
121
+ disabled
122
+ inputProps={{
123
+ style: { fontFamily: 'monospace' },
124
+ }}
125
+ />
126
+ <FormHelperText sx={{ mt: 1 }} color="text.secondary">
127
+ {t('admin.meter.eventName.editHelp')}
128
+ </FormHelperText>
129
+ </Box>
130
+ )}
131
+
132
+ {/* Aggregation Method - Only in create mode */}
133
+ {!isEditing && (
134
+ <Box>
135
+ <FormControl fullWidth error={!!errors.aggregation_method}>
136
+ <FormLabel sx={{ color: 'text.primary' }}>{t('admin.meter.aggregationMethod.label')}</FormLabel>
137
+ <Select
138
+ {...register('aggregation_method', { required: t('admin.meter.aggregationMethod.required') })}
139
+ label=""
140
+ defaultValue="sum">
141
+ <MenuItem value="sum">{t('admin.meter.aggregationMethod.sum')}</MenuItem>
142
+ <MenuItem value="count" disabled>
143
+ {t('admin.meter.aggregationMethod.count')}
144
+ </MenuItem>
145
+ <MenuItem value="last" disabled>
146
+ {t('admin.meter.aggregationMethod.last')}
147
+ </MenuItem>
148
+ </Select>
149
+ </FormControl>
150
+ <FormHelperText sx={{ mt: 1 }} color="text.secondary">
151
+ {getAggregationDescription(aggregationMethod)}
152
+ </FormHelperText>
153
+ </Box>
154
+ )}
155
+
156
+ {/* Aggregation Method - Read-only in edit mode */}
157
+ {isEditing && (
158
+ <Box>
159
+ <FormInput
160
+ name="aggregation_method"
161
+ label={t('admin.meter.aggregationMethod.label')}
162
+ disabled
163
+ value={t(`admin.meter.aggregationMethod.${aggregationMethod}`)}
164
+ />
165
+ <FormHelperText sx={{ mt: 1 }} color="text.secondary">
166
+ {t('admin.meter.aggregationMethod.editHelp')}
167
+ </FormHelperText>
168
+ </Box>
169
+ )}
170
+
171
+ {/* Unit - Only in create mode */}
172
+ {!isEditing && (
173
+ <Box>
174
+ <FormInput
175
+ name="unit"
176
+ rules={{
177
+ required: t('admin.meter.unit.required'),
178
+ maxLength: {
179
+ value: 32,
180
+ message: t('common.maxLength', { len: 32 }),
181
+ },
182
+ }}
183
+ label={t('admin.meter.unit.label')}
184
+ placeholder={t('admin.meter.unit.placeholder')}
185
+ error={!!errors.unit}
186
+ helperText={errors.unit?.message}
187
+ inputProps={{ maxLength: 32 }}
188
+ />
189
+ <FormHelperText sx={{ mt: 1 }} color="text.secondary">
190
+ {t('admin.meter.unit.help')}
191
+ </FormHelperText>
192
+ </Box>
193
+ )}
194
+
195
+ {/* Unit - Read-only in edit mode */}
196
+ {isEditing && (
197
+ <Box>
198
+ <FormInput name="unit" label={t('admin.meter.unit.label')} disabled />
199
+ <FormHelperText sx={{ mt: 1 }} color="text.secondary">
200
+ {t('admin.meter.unit.editHelp')}
201
+ </FormHelperText>
202
+ </Box>
203
+ )}
204
+
205
+ {/* Description */}
206
+ <Box>
207
+ <FormInput
208
+ name="description"
209
+ rules={{
210
+ maxLength: {
211
+ value: 255,
212
+ message: t('common.maxLength', { len: 255 }),
213
+ },
214
+ }}
215
+ label={t('admin.meter.description.label')}
216
+ placeholder={t('admin.meter.description.placeholder')}
217
+ multiline
218
+ minRows={3}
219
+ maxRows={5}
220
+ inputProps={{ maxLength: 255 }}
221
+ />
222
+ <FormHelperText sx={{ mt: 1 }} color="text.secondary">
223
+ {t('admin.meter.description.help')}
224
+ </FormHelperText>
225
+ </Box>
226
+
227
+ {/* Metadata Section */}
228
+ <Collapse trigger={t('common.metadata.label')}>
229
+ <Box sx={{ mb: 2 }}>
230
+ <Typography
231
+ variant="body2"
232
+ sx={{
233
+ color: 'text.secondary',
234
+ }}>
235
+ {t('common.metadata.description')}
236
+ </Typography>
237
+ </Box>
238
+ <MetadataForm />
239
+ </Collapse>
240
+ </Stack>
241
+ </Box>
242
+ );
243
+ }
244
+
245
+ export default MeterForm;
@@ -0,0 +1,264 @@
1
+ /* eslint-disable react/no-unstable-nested-components */
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import { api, formatTime, Status, Table, findCurrency, usePaymentContext } from '@blocklet/payment-react';
4
+ import type { TProductExpanded, TMeter } from '@blocklet/payment-types';
5
+ import { Add } from '@mui/icons-material';
6
+ import { Box, Button, Typography, Alert, Tabs, Tab, Stack } from '@mui/material';
7
+ import { useEffect, useState } from 'react';
8
+ import { Link } from 'react-router-dom';
9
+
10
+ import ProductsCreate from '../../pages/admin/products/products/create';
11
+ import InfoCard from '../info-card';
12
+ import ProductActions from '../product/actions';
13
+ import { formatProductPrice } from '../../libs/util';
14
+
15
+ interface MeterProductsProps {
16
+ meterId: string;
17
+ meter?: TMeter;
18
+ }
19
+
20
+ export interface MeterProductsRef {
21
+ refresh: () => void;
22
+ }
23
+
24
+ type ProductType = 'meter' | 'credit';
25
+
26
+ const fetchMeterProducts = (meterId: string): Promise<{ list: TProductExpanded[]; count: number }> => {
27
+ return api.get(`/api/products?meter_id=${meterId}`).then((res: any) => res.data);
28
+ };
29
+
30
+ const fetchCreditProducts = (meterId: string): Promise<{ list: TProductExpanded[]; count: number }> => {
31
+ return api.get(`/api/products?type=credit&meter_id=${meterId}`).then((res: any) => res.data);
32
+ };
33
+
34
+ export default function MeterProducts({ meterId, meter = undefined }: MeterProductsProps) {
35
+ const { t, locale } = useLocaleContext();
36
+ const { settings } = usePaymentContext();
37
+ const [products, setProducts] = useState<TProductExpanded[]>([]);
38
+ const [loading, setLoading] = useState(false);
39
+ const [creating, setCreating] = useState(false);
40
+ const [error, setError] = useState<string | null>(null);
41
+ const [activeTab, setActiveTab] = useState<ProductType>('credit');
42
+
43
+ const loadProducts = async (type: ProductType = activeTab) => {
44
+ setLoading(true);
45
+ setError(null);
46
+ try {
47
+ const result = type === 'meter' ? await fetchMeterProducts(meterId) : await fetchCreditProducts(meterId);
48
+ setProducts(result.list);
49
+ } catch (err) {
50
+ console.error('Failed to fetch products:', err);
51
+ setError('Failed to load products');
52
+ } finally {
53
+ setLoading(false);
54
+ }
55
+ };
56
+
57
+ useEffect(() => {
58
+ loadProducts();
59
+ }, [meterId, activeTab]);
60
+
61
+ const handleTabChange = (_: React.SyntheticEvent, newValue: ProductType) => {
62
+ setActiveTab(newValue);
63
+ };
64
+
65
+ const handleCreateProduct = () => {
66
+ setCreating(true);
67
+ };
68
+
69
+ const handleCloseCreate = () => {
70
+ setCreating(false);
71
+ loadProducts(); // 刷新产品列表
72
+ };
73
+
74
+ if (error) {
75
+ return (
76
+ <Alert severity="error" sx={{ mt: 1 }}>
77
+ {error}
78
+ </Alert>
79
+ );
80
+ }
81
+
82
+ const columns = [
83
+ {
84
+ label: t('admin.product.name.label'),
85
+ name: 'name',
86
+ options: {
87
+ filter: false,
88
+ customBodyRenderLite: (_: string, index: number) => {
89
+ const item = products[index];
90
+ if (!item) return null;
91
+ const currency =
92
+ findCurrency(settings.paymentMethods, item.prices[0]?.currency_id ?? '') || settings.baseCurrency;
93
+ return (
94
+ <Link to={`/admin/products/${item.id}`}>
95
+ <InfoCard
96
+ name={item.name}
97
+ description={formatProductPrice(item as any, currency!, locale)}
98
+ logo={item.images[0]}
99
+ />
100
+ </Link>
101
+ );
102
+ },
103
+ },
104
+ },
105
+ {
106
+ label: t('common.status'),
107
+ name: 'active',
108
+ options: {
109
+ filter: false,
110
+ customBodyRenderLite: (_: string, index: number) => {
111
+ const item = products[index];
112
+ if (!item) return null;
113
+ return (
114
+ <Link to={`/admin/products/${item.id}`}>
115
+ <Status label={item?.active ? 'Active' : 'Archived'} color={item?.active ? 'success' : 'default'} />
116
+ </Link>
117
+ );
118
+ },
119
+ },
120
+ },
121
+ {
122
+ label: t('common.createdAt'),
123
+ name: 'created_at',
124
+ options: {
125
+ sort: false,
126
+ customBodyRenderLite: (_: string, index: number) => {
127
+ const item = products[index];
128
+ if (!item) return null;
129
+ return <Link to={`/admin/products/${item.id}`}>{formatTime(item.created_at)}</Link>;
130
+ },
131
+ },
132
+ },
133
+ {
134
+ label: t('common.actions'),
135
+ name: 'id',
136
+ width: 100,
137
+ align: 'center',
138
+ options: {
139
+ sort: false,
140
+ customBodyRenderLite: (_: string, index: number) => {
141
+ const product = products[index];
142
+ if (!product) return null;
143
+ return <ProductActions data={product} onChange={() => loadProducts()} />;
144
+ },
145
+ },
146
+ },
147
+ ];
148
+
149
+ const getEmptyText = () => {
150
+ if (activeTab === 'meter') {
151
+ return {
152
+ text: t('admin.meter.products.emptyTip'),
153
+ buttonText: t('admin.meter.products.create'),
154
+ };
155
+ }
156
+ return {
157
+ text: t('admin.meter.creditProducts.emptyTip'),
158
+ buttonText: t('admin.creditProduct.create'),
159
+ };
160
+ };
161
+
162
+ const emptyConfig = getEmptyText();
163
+
164
+ return (
165
+ <Box>
166
+ <Stack
167
+ direction="row"
168
+ sx={{
169
+ justifyContent: 'space-between',
170
+ alignItems: 'center',
171
+ }}>
172
+ <Tabs
173
+ value={activeTab}
174
+ onChange={handleTabChange}
175
+ sx={{
176
+ flex: '1 0 auto',
177
+ maxWidth: '100%',
178
+ fontSize: 14,
179
+ borderBottom: 1,
180
+ borderColor: 'divider',
181
+ '.Mui-selected': {
182
+ fontSize: '14px !important',
183
+ color: 'primary.main',
184
+ },
185
+ }}>
186
+ <Tab label={t('admin.meter.products.creditCharge')} value="credit" />
187
+ <Tab label={t('admin.meter.products.meterService')} value="meter" />
188
+ </Tabs>
189
+ </Stack>
190
+ <Stack
191
+ direction="row"
192
+ sx={{
193
+ justifyContent: 'flex-end',
194
+ mb: 2,
195
+ }}>
196
+ <Button
197
+ variant="text"
198
+ color="inherit"
199
+ size="small"
200
+ sx={{ color: 'text.link', float: 'right' }}
201
+ startIcon={<Add />}
202
+ onClick={handleCreateProduct}>
203
+ {activeTab === 'meter' ? t('admin.meter.products.create') : t('admin.creditProduct.create')}
204
+ </Button>
205
+ </Stack>
206
+ <Table
207
+ hasRowLink
208
+ data={products}
209
+ columns={columns}
210
+ options={{
211
+ count: products.length,
212
+ page: 0,
213
+ rowsPerPage: products.length,
214
+ pagination: false,
215
+ search: false,
216
+ filter: false,
217
+ sort: false,
218
+ viewColumns: false,
219
+ download: false,
220
+ print: false,
221
+ selectableRows: 'none',
222
+ }}
223
+ loading={loading}
224
+ emptyNodeText={
225
+ <Box>
226
+ <Typography>{emptyConfig.text}</Typography>
227
+ <Button
228
+ size="small"
229
+ variant="text"
230
+ startIcon={<Add />}
231
+ onClick={handleCreateProduct}
232
+ sx={{ mt: 1, color: 'text.link' }}>
233
+ {emptyConfig.buttonText}
234
+ </Button>
235
+ </Box>
236
+ }
237
+ />
238
+ {creating && (
239
+ <ProductsCreate
240
+ open={creating}
241
+ onClose={handleCloseCreate}
242
+ onSubmit={() => loadProducts(activeTab)}
243
+ mode={activeTab === 'credit' ? 'credit' : 'standard'}
244
+ meterId={meterId}
245
+ initialPrice={
246
+ activeTab === 'meter'
247
+ ? {
248
+ currency_id: meter?.currency_id || '',
249
+ priceModel: 'credit_metered',
250
+ }
251
+ : {
252
+ metadata: {
253
+ credit_config: {
254
+ currency_id: meter?.currency_id || '',
255
+ },
256
+ },
257
+ name: meter?.unit || '',
258
+ }
259
+ }
260
+ />
261
+ )}
262
+ </Box>
263
+ );
264
+ }
@@ -0,0 +1,174 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import { Box, Typography, Paper, Alert, Link } from '@mui/material';
3
+ import { styled } from '@mui/system';
4
+
5
+ type TMeter = {
6
+ id: string;
7
+ name: string;
8
+ event_name: string;
9
+ unit: string;
10
+ aggregation_method: string;
11
+ };
12
+
13
+ interface MeterUsageGuideProps {
14
+ meter: TMeter;
15
+ }
16
+
17
+ export default function MeterUsageGuide({ meter }: MeterUsageGuideProps) {
18
+ const { t } = useLocaleContext();
19
+
20
+ const curlExample = `curl -X POST https://your-domain.com/api/meter-events \\
21
+ -H "Content-Type: application/json" \\
22
+ -H "Authorization: Bearer YOUR_API_KEY" \\
23
+ -d '{
24
+ "event_name": "${meter.event_name}",
25
+ "payload": {
26
+ "customer_id": "cus_example123",
27
+ "value": "10",
28
+ "subscription_id": "sub_example123"
29
+ },
30
+ "identifier": "unique_event_id_${Date.now()}",
31
+ "metadata": {
32
+ "source": "api",
33
+ "version": "1.0"
34
+ }
35
+ }'`;
36
+
37
+ const jsExample = `// Using fetch API
38
+ const response = await fetch('/api/meter-events', {
39
+ method: 'POST',
40
+ headers: {
41
+ 'Content-Type': 'application/json',
42
+ 'Authorization': 'Bearer YOUR_API_KEY'
43
+ },
44
+ body: JSON.stringify({
45
+ event_name: '${meter.event_name}',
46
+ payload: {
47
+ customer_id: 'cus_example123',
48
+ value: '10',
49
+ subscription_id: 'sub_example123'
50
+ },
51
+ identifier: \`unique_event_id_\${Date.now()}\`,
52
+ metadata: {
53
+ source: 'web',
54
+ version: '1.0'
55
+ }
56
+ })
57
+ });
58
+
59
+ const result = await response.json();
60
+ console.log('Event recorded:', result);`;
61
+
62
+ const pythonExample = `import requests
63
+ import time
64
+
65
+ url = "https://your-domain.com/api/meter-events"
66
+ headers = {
67
+ "Content-Type": "application/json",
68
+ "Authorization": "Bearer YOUR_API_KEY"
69
+ }
70
+
71
+ data = {
72
+ "event_name": "${meter.event_name}",
73
+ "payload": {
74
+ "customer_id": "cus_example123",
75
+ "value": "10",
76
+ "subscription_id": "sub_example123"
77
+ },
78
+ "identifier": f"unique_event_id_{int(time.time())}",
79
+ "metadata": {
80
+ "source": "python",
81
+ "version": "1.0"
82
+ }
83
+ }
84
+
85
+ response = requests.post(url, json=data, headers=headers)
86
+ print("Event recorded:", response.json())`;
87
+
88
+ return (
89
+ <Box>
90
+ <Typography variant="h6" sx={{ mb: 2, fontWeight: 600 }}>
91
+ {t('admin.meter.usageGuide.title')}
92
+ </Typography>
93
+ <Alert severity="info" sx={{ mb: 3 }}>
94
+ <Typography variant="body2">
95
+ {t('admin.meter.usageGuide.description', {
96
+ eventName: meter.event_name,
97
+ unit: meter.unit,
98
+ method: meter.aggregation_method,
99
+ })}
100
+ </Typography>
101
+ </Alert>
102
+ <Typography variant="subtitle1" sx={{ mb: 2, fontWeight: 600 }}>
103
+ {t('admin.meter.usageGuide.apiEndpoint')}
104
+ </Typography>
105
+ <CodeBlock>
106
+ <Typography variant="body2" component="pre">
107
+ POST /api/meter-events
108
+ </Typography>
109
+ </CodeBlock>
110
+ <Typography variant="subtitle1" sx={{ mb: 2, mt: 3, fontWeight: 600 }}>
111
+ {t('admin.meter.usageGuide.curlExample')}
112
+ </Typography>
113
+ <CodeBlock>
114
+ <Typography variant="body2" component="pre">
115
+ {curlExample}
116
+ </Typography>
117
+ </CodeBlock>
118
+ <Typography variant="subtitle1" sx={{ mb: 2, mt: 3, fontWeight: 600 }}>
119
+ {t('admin.meter.usageGuide.jsExample')}
120
+ </Typography>
121
+ <CodeBlock>
122
+ <Typography variant="body2" component="pre">
123
+ {jsExample}
124
+ </Typography>
125
+ </CodeBlock>
126
+ <Typography variant="subtitle1" sx={{ mb: 2, mt: 3, fontWeight: 600 }}>
127
+ {t('admin.meter.usageGuide.pythonExample')}
128
+ </Typography>
129
+ <CodeBlock>
130
+ <Typography variant="body2" component="pre">
131
+ {pythonExample}
132
+ </Typography>
133
+ </CodeBlock>
134
+ <Alert severity="warning" sx={{ mt: 3 }}>
135
+ <Typography variant="body2">
136
+ <strong>{t('admin.meter.usageGuide.important')}:</strong>
137
+ </Typography>
138
+ <ul style={{ margin: '8px 0', paddingLeft: '20px' }}>
139
+ <li>{t('admin.meter.usageGuide.tip1')}</li>
140
+ <li>{t('admin.meter.usageGuide.tip2')}</li>
141
+ <li>{t('admin.meter.usageGuide.tip3')}</li>
142
+ </ul>
143
+ </Alert>
144
+ <Box sx={{ mt: 3 }}>
145
+ <Typography
146
+ variant="body2"
147
+ sx={{
148
+ color: 'text.secondary',
149
+ }}>
150
+ {t('admin.meter.usageGuide.moreInfo')}{' '}
151
+ <Link href="/docs/api/meter-events" target="_blank">
152
+ {t('admin.meter.usageGuide.apiDocs')}
153
+ </Link>
154
+ </Typography>
155
+ </Box>
156
+ </Box>
157
+ );
158
+ }
159
+
160
+ const CodeBlock = styled(Paper)(({ theme }) => ({
161
+ padding: theme.spacing(2),
162
+ backgroundColor: theme.palette.grey[50],
163
+ border: `1px solid ${theme.palette.grey[200]}`,
164
+ borderRadius: theme.shape.borderRadius,
165
+ overflow: 'auto',
166
+ '& pre': {
167
+ margin: 0,
168
+ fontFamily: 'Monaco, Consolas, "Courier New", monospace',
169
+ fontSize: '0.875rem',
170
+ lineHeight: 1.5,
171
+ whiteSpace: 'pre-wrap',
172
+ wordBreak: 'break-all',
173
+ },
174
+ }));
@@ -39,6 +39,7 @@ export default function PaymentCurrencyForm({ disableKeys = [] }: TPaymentCurren
39
39
  label={t('admin.paymentMethod.name.label')}
40
40
  placeholder={t('admin.paymentMethod.name.tip')}
41
41
  disabled={disableKeys.includes('name')}
42
+ inputProps={{ maxLength: 32 }}
42
43
  />
43
44
  <FormInput
44
45
  key="description"
@@ -48,6 +49,7 @@ export default function PaymentCurrencyForm({ disableKeys = [] }: TPaymentCurren
48
49
  label={t('admin.paymentMethod.description.label')}
49
50
  placeholder={t('admin.paymentMethod.description.tip')}
50
51
  disabled={disableKeys.includes('description')}
52
+ inputProps={{ maxLength: 255 }}
51
53
  />
52
54
  <FormInput
53
55
  key="contract"