payment-kit 1.18.56 → 1.19.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 (214) hide show
  1. package/.eslintrc.js +6 -0
  2. package/api/src/crons/index.ts +8 -0
  3. package/api/src/index.ts +4 -0
  4. package/api/src/libs/credit-grant.ts +146 -0
  5. package/api/src/libs/env.ts +1 -0
  6. package/api/src/libs/invoice.ts +4 -3
  7. package/api/src/libs/notification/template/base.ts +388 -2
  8. package/api/src/libs/notification/template/customer-credit-grant-granted.ts +149 -0
  9. package/api/src/libs/notification/template/customer-credit-grant-low-balance.ts +151 -0
  10. package/api/src/libs/notification/template/customer-credit-insufficient.ts +254 -0
  11. package/api/src/libs/notification/template/subscription-canceled.ts +193 -202
  12. package/api/src/libs/notification/template/subscription-refund-succeeded.ts +215 -237
  13. package/api/src/libs/notification/template/subscription-renewed.ts +130 -200
  14. package/api/src/libs/notification/template/subscription-succeeded.ts +100 -202
  15. package/api/src/libs/notification/template/subscription-trial-start.ts +142 -188
  16. package/api/src/libs/notification/template/subscription-trial-will-end.ts +146 -174
  17. package/api/src/libs/notification/template/subscription-upgraded.ts +96 -192
  18. package/api/src/libs/notification/template/subscription-will-canceled.ts +94 -135
  19. package/api/src/libs/notification/template/subscription-will-renew.ts +220 -245
  20. package/api/src/libs/payment.ts +69 -0
  21. package/api/src/libs/queue/index.ts +3 -2
  22. package/api/src/libs/session.ts +8 -0
  23. package/api/src/libs/subscription.ts +74 -3
  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 +715 -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/index.ts +8 -0
  37. package/api/src/routes/meter-events.ts +347 -0
  38. package/api/src/routes/meters.ts +219 -0
  39. package/api/src/routes/payment-currencies.ts +14 -2
  40. package/api/src/routes/payment-links.ts +1 -1
  41. package/api/src/routes/payment-methods.ts +14 -2
  42. package/api/src/routes/prices.ts +43 -0
  43. package/api/src/routes/pricing-table.ts +13 -7
  44. package/api/src/routes/products.ts +63 -4
  45. package/api/src/routes/settings.ts +1 -1
  46. package/api/src/routes/subscriptions.ts +4 -0
  47. package/api/src/store/migrations/20250610-billing-credit.ts +43 -0
  48. package/api/src/store/models/credit-grant.ts +486 -0
  49. package/api/src/store/models/credit-transaction.ts +268 -0
  50. package/api/src/store/models/customer.ts +8 -0
  51. package/api/src/store/models/index.ts +52 -1
  52. package/api/src/store/models/meter-event.ts +423 -0
  53. package/api/src/store/models/meter.ts +176 -0
  54. package/api/src/store/models/payment-currency.ts +66 -14
  55. package/api/src/store/models/price.ts +6 -0
  56. package/api/src/store/models/product.ts +2 -2
  57. package/api/src/store/models/subscription.ts +24 -0
  58. package/api/src/store/models/types.ts +28 -2
  59. package/api/tests/libs/subscription.spec.ts +53 -0
  60. package/blocklet.yml +9 -1
  61. package/package.json +57 -58
  62. package/scripts/sdk.js +233 -1
  63. package/src/app.tsx +10 -0
  64. package/src/components/actions.tsx +22 -9
  65. package/src/components/balance-list.tsx +40 -12
  66. package/src/components/collapse.tsx +33 -15
  67. package/src/components/copyable.tsx +8 -7
  68. package/src/components/currency.tsx +15 -7
  69. package/src/components/customer/actions.tsx +1 -5
  70. package/src/components/customer/credit-grant-item-list.tsx +99 -0
  71. package/src/components/customer/credit-overview.tsx +233 -0
  72. package/src/components/customer/form.tsx +7 -2
  73. package/src/components/customer/link.tsx +4 -12
  74. package/src/components/customer/notification-preference.tsx +18 -9
  75. package/src/components/customer/overdraft-protection.tsx +112 -41
  76. package/src/components/drawer-form.tsx +42 -18
  77. package/src/components/error.tsx +1 -5
  78. package/src/components/event/list.tsx +9 -10
  79. package/src/components/filter-toolbar.tsx +20 -19
  80. package/src/components/info-card.tsx +32 -18
  81. package/src/components/info-metric.tsx +16 -6
  82. package/src/components/info-row-group.tsx +1 -7
  83. package/src/components/info-row.tsx +30 -24
  84. package/src/components/invoice/action.tsx +1 -7
  85. package/src/components/invoice/list.tsx +34 -26
  86. package/src/components/invoice/recharge.tsx +5 -7
  87. package/src/components/invoice/table.tsx +17 -12
  88. package/src/components/layout/user.tsx +1 -1
  89. package/src/components/metadata/form.tsx +290 -94
  90. package/src/components/metadata/list.tsx +11 -3
  91. package/src/components/meter/actions.tsx +101 -0
  92. package/src/components/meter/add-usage-dialog.tsx +239 -0
  93. package/src/components/meter/events-list.tsx +657 -0
  94. package/src/components/meter/form.tsx +245 -0
  95. package/src/components/meter/products.tsx +264 -0
  96. package/src/components/meter/usage-guide.tsx +174 -0
  97. package/src/components/passport/actions.tsx +9 -4
  98. package/src/components/payment-currency/add.tsx +16 -3
  99. package/src/components/payment-currency/form.tsx +14 -6
  100. package/src/components/payment-intent/actions.tsx +24 -16
  101. package/src/components/payment-intent/list.tsx +30 -9
  102. package/src/components/payment-link/actions.tsx +1 -5
  103. package/src/components/payment-link/after-pay.tsx +4 -2
  104. package/src/components/payment-link/before-pay.tsx +14 -4
  105. package/src/components/payment-link/item.tsx +27 -6
  106. package/src/components/payment-link/preview.tsx +9 -9
  107. package/src/components/payment-link/product-select.tsx +69 -15
  108. package/src/components/payment-method/arcblock.tsx +8 -1
  109. package/src/components/payment-method/base.tsx +8 -1
  110. package/src/components/payment-method/bitcoin.tsx +8 -1
  111. package/src/components/payment-method/ethereum.tsx +8 -1
  112. package/src/components/payment-method/evm-rpc-input.tsx +11 -7
  113. package/src/components/payment-method/form.tsx +2 -7
  114. package/src/components/payment-method/stripe.tsx +2 -0
  115. package/src/components/payouts/actions.tsx +1 -5
  116. package/src/components/payouts/list.tsx +30 -10
  117. package/src/components/payouts/portal/list.tsx +11 -9
  118. package/src/components/price/currency-select.tsx +63 -32
  119. package/src/components/price/form.tsx +895 -370
  120. package/src/components/price/upsell-select.tsx +10 -2
  121. package/src/components/price/upsell.tsx +7 -2
  122. package/src/components/pricing-table/actions.tsx +1 -5
  123. package/src/components/pricing-table/customer-settings.tsx +5 -1
  124. package/src/components/pricing-table/payment-settings.tsx +14 -4
  125. package/src/components/pricing-table/preview.tsx +9 -9
  126. package/src/components/pricing-table/price-item.tsx +6 -1
  127. package/src/components/pricing-table/product-item.tsx +6 -1
  128. package/src/components/pricing-table/product-settings.tsx +17 -4
  129. package/src/components/product/actions.tsx +1 -5
  130. package/src/components/product/add-price.tsx +9 -7
  131. package/src/components/product/create.tsx +8 -9
  132. package/src/components/product/cross-sell-select.tsx +5 -1
  133. package/src/components/product/cross-sell.tsx +7 -2
  134. package/src/components/product/edit-price.tsx +21 -12
  135. package/src/components/product/features.tsx +26 -6
  136. package/src/components/product/form.tsx +115 -72
  137. package/src/components/progress-bar.tsx +1 -1
  138. package/src/components/refund/actions.tsx +1 -7
  139. package/src/components/refund/list.tsx +31 -18
  140. package/src/components/section/header.tsx +12 -14
  141. package/src/components/subscription/actions/cancel.tsx +22 -5
  142. package/src/components/subscription/actions/index.tsx +9 -10
  143. package/src/components/subscription/actions/pause.tsx +32 -6
  144. package/src/components/subscription/actions/slash-stake.tsx +5 -3
  145. package/src/components/subscription/description.tsx +12 -8
  146. package/src/components/subscription/items/index.tsx +31 -16
  147. package/src/components/subscription/items/usage-records.tsx +19 -5
  148. package/src/components/subscription/list.tsx +5 -7
  149. package/src/components/subscription/metrics.tsx +62 -15
  150. package/src/components/subscription/portal/actions.tsx +78 -71
  151. package/src/components/subscription/portal/cancel.tsx +10 -3
  152. package/src/components/subscription/portal/list.tsx +48 -26
  153. package/src/components/uploader.tsx +5 -13
  154. package/src/components/webhook/attempts.tsx +51 -16
  155. package/src/components/webhook/request-info.tsx +8 -6
  156. package/src/contexts/products.tsx +27 -10
  157. package/src/hooks/subscription.ts +34 -0
  158. package/src/libs/meter-utils.ts +196 -0
  159. package/src/libs/util.ts +4 -0
  160. package/src/locales/en.tsx +385 -4
  161. package/src/locales/zh.tsx +364 -0
  162. package/src/pages/admin/billing/index.tsx +61 -33
  163. package/src/pages/admin/billing/invoices/detail.tsx +49 -13
  164. package/src/pages/admin/billing/meters/create.tsx +60 -0
  165. package/src/pages/admin/billing/meters/detail.tsx +435 -0
  166. package/src/pages/admin/billing/meters/index.tsx +210 -0
  167. package/src/pages/admin/billing/meters/meter-event.tsx +346 -0
  168. package/src/pages/admin/billing/subscriptions/detail.tsx +90 -25
  169. package/src/pages/admin/customers/customers/credit-grant/detail.tsx +391 -0
  170. package/src/pages/admin/customers/customers/detail.tsx +67 -14
  171. package/src/pages/admin/customers/customers/index.tsx +6 -1
  172. package/src/pages/admin/customers/index.tsx +5 -0
  173. package/src/pages/admin/developers/events/detail.tsx +37 -11
  174. package/src/pages/admin/developers/index.tsx +1 -1
  175. package/src/pages/admin/developers/webhooks/detail.tsx +41 -11
  176. package/src/pages/admin/index.tsx +15 -2
  177. package/src/pages/admin/overview.tsx +107 -19
  178. package/src/pages/admin/payments/intents/detail.tsx +58 -14
  179. package/src/pages/admin/payments/payouts/detail.tsx +63 -15
  180. package/src/pages/admin/payments/refunds/detail.tsx +58 -14
  181. package/src/pages/admin/products/index.tsx +11 -4
  182. package/src/pages/admin/products/links/create.tsx +22 -4
  183. package/src/pages/admin/products/links/detail.tsx +43 -14
  184. package/src/pages/admin/products/passports/index.tsx +23 -4
  185. package/src/pages/admin/products/prices/actions.tsx +16 -9
  186. package/src/pages/admin/products/prices/detail.tsx +73 -14
  187. package/src/pages/admin/products/prices/list.tsx +15 -3
  188. package/src/pages/admin/products/pricing-tables/create.tsx +45 -12
  189. package/src/pages/admin/products/pricing-tables/detail.tsx +45 -14
  190. package/src/pages/admin/products/products/create.tsx +233 -54
  191. package/src/pages/admin/products/products/detail.tsx +74 -18
  192. package/src/pages/admin/settings/index.tsx +8 -1
  193. package/src/pages/admin/settings/payment-methods/index.tsx +87 -19
  194. package/src/pages/admin/settings/vault-config/edit-form.tsx +42 -28
  195. package/src/pages/admin/settings/vault-config/index.tsx +57 -10
  196. package/src/pages/customer/credit-grant/detail.tsx +308 -0
  197. package/src/pages/customer/index.tsx +76 -17
  198. package/src/pages/customer/invoice/detail.tsx +63 -14
  199. package/src/pages/customer/invoice/past-due.tsx +11 -3
  200. package/src/pages/customer/payout/detail.tsx +56 -13
  201. package/src/pages/customer/recharge/account.tsx +78 -18
  202. package/src/pages/customer/recharge/subscription.tsx +86 -25
  203. package/src/pages/customer/refund/list.tsx +60 -24
  204. package/src/pages/customer/subscription/change-payment.tsx +17 -6
  205. package/src/pages/customer/subscription/change-plan.tsx +34 -7
  206. package/src/pages/customer/subscription/detail.tsx +134 -34
  207. package/src/pages/customer/subscription/embed.tsx +25 -5
  208. package/src/pages/home.tsx +26 -4
  209. package/src/pages/integrations/donations/edit-form.tsx +25 -9
  210. package/src/pages/integrations/donations/index.tsx +26 -9
  211. package/src/pages/integrations/donations/preview.tsx +59 -15
  212. package/src/pages/integrations/index.tsx +10 -1
  213. package/src/pages/integrations/overview.tsx +78 -17
  214. package/vite.config.ts +60 -30
@@ -0,0 +1,245 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import { FormInput } from '@blocklet/payment-react';
3
+ import { FormControl, Select, MenuItem, Typography, Box, FormHelperText, Stack, FormLabel } 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>('meter');
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.meterService')} value="meter" />
187
+ <Tab label={t('admin.meter.products.creditCharge')} value="credit" />
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
+ }));