payment-kit 1.20.7 → 1.20.9

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 (32) hide show
  1. package/api/src/crons/index.ts +1 -1
  2. package/api/src/index.ts +2 -2
  3. package/api/src/libs/{vendor → vendor-util}/adapters/factory.ts +9 -5
  4. package/api/src/libs/{vendor → vendor-util}/adapters/launcher-adapter.ts +3 -8
  5. package/api/src/libs/{vendor → vendor-util}/adapters/types.ts +1 -4
  6. package/api/src/libs/{vendor → vendor-util}/fulfillment.ts +11 -8
  7. package/api/src/queues/{vendor → vendors}/commission.ts +4 -5
  8. package/api/src/queues/{vendor → vendors}/fulfillment-coordinator.ts +33 -4
  9. package/api/src/queues/{vendor → vendors}/fulfillment.ts +2 -2
  10. package/api/src/queues/{vendor → vendors}/status-check.ts +2 -2
  11. package/api/src/routes/payment-links.ts +2 -1
  12. package/api/src/routes/products.ts +1 -0
  13. package/api/src/routes/vendor.ts +135 -213
  14. package/api/src/store/migrations/20250911-add-vendor-type.ts +26 -0
  15. package/api/src/store/models/product-vendor.ts +6 -24
  16. package/api/src/store/models/product.ts +1 -0
  17. package/blocklet.yml +1 -1
  18. package/doc/vendor_fulfillment_system.md +1 -1
  19. package/package.json +23 -22
  20. package/src/components/metadata/form.tsx +12 -19
  21. package/src/components/payment-link/before-pay.tsx +40 -0
  22. package/src/components/product/vendor-config.tsx +4 -11
  23. package/src/components/subscription/description.tsx +1 -6
  24. package/src/components/subscription/portal/list.tsx +82 -6
  25. package/src/components/subscription/vendor-service-list.tsx +128 -0
  26. package/src/components/vendor/actions.tsx +1 -33
  27. package/src/locales/en.tsx +13 -3
  28. package/src/locales/zh.tsx +13 -3
  29. package/src/pages/admin/products/links/create.tsx +2 -0
  30. package/src/pages/admin/products/vendors/create.tsx +108 -194
  31. package/src/pages/admin/products/vendors/index.tsx +14 -22
  32. package/src/pages/customer/subscription/detail.tsx +26 -11
@@ -66,6 +66,7 @@ export default function CreatePaymentLink() {
66
66
  subscription_data: {
67
67
  description: '',
68
68
  trial_period_days: 0,
69
+ no_stake: false,
69
70
  },
70
71
  nft_mint_settings: {
71
72
  enabled: false,
@@ -86,6 +87,7 @@ export default function CreatePaymentLink() {
86
87
  'billing_address_collection',
87
88
  'phone_number_collection',
88
89
  'currency_id',
90
+ 'metadata',
89
91
  ]);
90
92
 
91
93
  useEffect(() => {
@@ -15,24 +15,22 @@ import {
15
15
  MenuItem,
16
16
  } from '@mui/material';
17
17
  import { useState } from 'react';
18
- import { useForm, Controller } from 'react-hook-form';
18
+ import { useForm, Controller, FormProvider } from 'react-hook-form';
19
19
  import { dispatch } from 'use-bus';
20
20
 
21
21
  import { joinURL, withQuery } from 'ufo';
22
22
  import DrawerForm from '../../../../components/drawer-form';
23
+ import MetadataForm from '../../../../components/metadata/form';
23
24
  import { formatProxyUrl } from '../../../../libs/util';
24
25
 
25
26
  interface Vendor {
26
27
  id: string;
27
28
  vendor_key: string;
29
+ vendor_type: string;
28
30
  name: string;
29
31
  description: string;
30
32
  app_url: string;
31
- webhook_path: string;
32
- default_commission_rate: number;
33
- default_commission_type: 'percentage' | 'fixed_amount';
34
33
  status: 'active' | 'inactive';
35
- order_create_params: Record<string, any>;
36
34
  metadata: Record<string, any>;
37
35
  created_at: string;
38
36
  updated_at: string;
@@ -47,16 +45,12 @@ interface VendorCreateProps {
47
45
 
48
46
  interface VendorFormData {
49
47
  vendor_key: string;
48
+ vendor_type: string;
50
49
  name: string;
51
50
  description: string;
52
51
  app_url: string;
53
- webhook_path: string;
54
- blocklet_meta_url: string;
55
- default_commission_rate: number;
56
- default_commission_type: 'percentage' | 'fixed_amount';
57
52
  status: 'active' | 'inactive';
58
- order_create_params: Record<string, any>;
59
- metadata: Record<string, any>;
53
+ metadata: Array<{ key: string; value: string }>;
60
54
  app_pid?: string;
61
55
  app_logo?: string;
62
56
  }
@@ -72,41 +66,43 @@ export default function VendorCreate({
72
66
 
73
67
  const isEditMode = !!vendorData;
74
68
 
69
+ const defaultValues = vendorData
70
+ ? {
71
+ ...vendorData,
72
+ metadata: vendorData.metadata
73
+ ? Object.entries(vendorData.metadata).map(([key, value]) => ({
74
+ key,
75
+ value: typeof value === 'object' ? JSON.stringify(value) : String(value),
76
+ }))
77
+ : [],
78
+ }
79
+ : {
80
+ vendor_key: '',
81
+ vendor_type: 'launcher',
82
+ name: '',
83
+ description: '',
84
+ app_url: '',
85
+ status: 'inactive' as const,
86
+ metadata: [{ key: 'blockletMetaUrl', value: '' }],
87
+ app_pid: '',
88
+ app_logo: '',
89
+ };
90
+
91
+ const methods = useForm<VendorFormData>({
92
+ mode: 'onChange',
93
+ defaultValues,
94
+ });
95
+
75
96
  const {
76
97
  control,
77
98
  handleSubmit,
78
99
  reset,
79
100
  formState: { errors, isValid },
80
101
  watch,
81
- } = useForm<VendorFormData>({
82
- mode: 'onChange',
83
- defaultValues: vendorData
84
- ? {
85
- ...vendorData,
86
- default_commission_rate: vendorData.default_commission_rate,
87
- blocklet_meta_url: vendorData.metadata?.blockletMetaUrl || '',
88
- }
89
- : {
90
- vendor_key: '',
91
- name: '',
92
- description: '',
93
- app_url: '',
94
- webhook_path: '',
95
- blocklet_meta_url: '',
96
- default_commission_rate: 80,
97
- default_commission_type: 'percentage',
98
- status: 'inactive',
99
- order_create_params: {},
100
- metadata: {},
101
- app_pid: '',
102
- app_logo: '',
103
- },
104
- });
102
+ } = methods;
105
103
 
106
104
  const watchedValues = watch();
107
105
 
108
- const isPercentage = watchedValues.default_commission_type === 'percentage';
109
-
110
106
  const validateUrl = (url: string) => {
111
107
  if (!url) return t('admin.vendor.appUrlRequired');
112
108
  try {
@@ -118,25 +114,6 @@ export default function VendorCreate({
118
114
  }
119
115
  };
120
116
 
121
- const validateWebhookPath = (path: string) => {
122
- if (!path) return true;
123
- if (!path.startsWith('/')) {
124
- return t('admin.vendor.webhookPathInvalid');
125
- }
126
- return true;
127
- };
128
-
129
- const validateBlockletMetaUrl = (url: string) => {
130
- if (!url) return t('admin.vendor.blockletMetaUrlRequired'); // 必填字段
131
- try {
132
- // eslint-disable-next-line no-new
133
- new URL(url);
134
- return true;
135
- } catch {
136
- return t('admin.vendor.blockletMetaUrlInvalid');
137
- }
138
- };
139
-
140
117
  const onSubmit = async (data: VendorFormData) => {
141
118
  try {
142
119
  setLoading(true);
@@ -151,14 +128,23 @@ export default function VendorCreate({
151
128
  return;
152
129
  }
153
130
 
154
- // 准备提交数据,将 blocklet_meta_url 放入 metadata
155
- const { blocklet_meta_url: blockletMetaUrl, ...restData } = data;
131
+ // 准备提交数据,将 metadata 数组转换为对象
132
+ const { metadata: metadataArray, ...restData } = data;
133
+ const metadataObj = metadataArray.reduce((acc: Record<string, any>, item) => {
134
+ if (item.key && item.value) {
135
+ try {
136
+ // 尝试解析 JSON,如果失败则作为字符串处理
137
+ acc[item.key] = JSON.parse(item.value);
138
+ } catch {
139
+ acc[item.key] = item.value;
140
+ }
141
+ }
142
+ return acc;
143
+ }, {});
144
+
156
145
  const submitData = {
157
146
  ...restData,
158
- metadata: {
159
- ...data.metadata,
160
- ...(blockletMetaUrl && { blockletMetaUrl }),
161
- },
147
+ metadata: metadataObj,
162
148
  };
163
149
 
164
150
  // 如果状态为启用,则检测应用地址可用性
@@ -227,20 +213,35 @@ export default function VendorCreate({
227
213
  };
228
214
 
229
215
  return (
230
- <DrawerForm
231
- icon={isEditMode ? null : <AddOutlined />}
232
- text={isEditMode ? t('admin.vendor.edit') : t('admin.vendor.create')}
233
- open={open}
234
- onClose={handleClose}
235
- width={800}
236
- addons={
237
- <Button variant="contained" size="small" onClick={handleSubmit(onSubmit)} disabled={loading || !isValid}>
238
- {loading && <CircularProgress size="small" sx={{ mr: 1 }} />}
239
- {isEditMode ? t('admin.save') : t('admin.vendor.save')}
240
- </Button>
241
- }>
242
- <Stack spacing={3}>
243
- <Stack direction="row" spacing={2}>
216
+ <FormProvider {...methods}>
217
+ <DrawerForm
218
+ icon={isEditMode ? null : <AddOutlined />}
219
+ text={isEditMode ? t('admin.vendor.edit') : t('admin.vendor.create')}
220
+ open={open}
221
+ onClose={handleClose}
222
+ width={800}
223
+ addons={
224
+ <Button variant="contained" size="small" onClick={handleSubmit(onSubmit)} disabled={loading || !isValid}>
225
+ {loading && <CircularProgress size="small" sx={{ mr: 1 }} />}
226
+ {isEditMode ? t('admin.save') : t('admin.vendor.save')}
227
+ </Button>
228
+ }>
229
+ <Stack spacing={3}>
230
+ <Controller
231
+ name="vendor_type"
232
+ control={control}
233
+ rules={{
234
+ required: t('admin.vendor.vendorTypeRequired'),
235
+ }}
236
+ render={({ field }) => (
237
+ <FormControl fullWidth required error={!!errors.vendor_type}>
238
+ <InputLabel>{t('admin.vendor.vendorType')}</InputLabel>
239
+ <Select {...field} label={t('admin.vendor.vendorType')}>
240
+ <MenuItem value="launcher">{t('admin.vendor.launcher')}</MenuItem>
241
+ </Select>
242
+ </FormControl>
243
+ )}
244
+ />
244
245
  <Controller
245
246
  name="vendor_key"
246
247
  control={control}
@@ -266,9 +267,7 @@ export default function VendorCreate({
266
267
  <Controller
267
268
  name="name"
268
269
  control={control}
269
- rules={{
270
- required: t('admin.vendor.nameRequired'),
271
- }}
270
+ rules={{ required: t('admin.vendor.nameRequired') }}
272
271
  render={({ field }) => (
273
272
  <TextField
274
273
  {...field}
@@ -276,143 +275,58 @@ export default function VendorCreate({
276
275
  required
277
276
  fullWidth
278
277
  error={!!errors.name}
279
- helperText={errors.name?.message}
278
+ helperText={errors.name?.message || t('admin.vendor.displayNameHelp')}
280
279
  />
281
280
  )}
282
281
  />
283
- </Stack>
284
-
285
- <Controller
286
- name="description"
287
- control={control}
288
- render={({ field }) => (
289
- <TextField {...field} label={t('admin.vendor.description')} multiline rows={3} fullWidth />
290
- )}
291
- />
292
-
293
- <Controller
294
- name="app_url"
295
- control={control}
296
- rules={{
297
- required: t('admin.vendor.appUrlRequired'),
298
- validate: validateUrl,
299
- }}
300
- render={({ field }) => (
301
- <TextField
302
- {...field}
303
- label={t('admin.vendor.appUrl')}
304
- required
305
- fullWidth
306
- error={!!errors.app_url}
307
- helperText={errors.app_url?.message || t('admin.vendor.appUrlHelp')}
308
- />
309
- )}
310
- />
311
282
 
312
- <Controller
313
- name="webhook_path"
314
- control={control}
315
- rules={{
316
- validate: validateWebhookPath,
317
- }}
318
- render={({ field }) => (
319
- <TextField
320
- {...field}
321
- label={t('admin.vendor.webhookPath')}
322
- fullWidth
323
- error={!!errors.webhook_path}
324
- helperText={errors.webhook_path?.message || t('admin.vendor.webhookPathHelp')}
325
- />
326
- )}
327
- />
328
-
329
- <Controller
330
- name="blocklet_meta_url"
331
- control={control}
332
- rules={{
333
- required: t('admin.vendor.blockletMetaUrlRequired'),
334
- validate: validateBlockletMetaUrl,
335
- }}
336
- render={({ field }) => (
337
- <TextField
338
- {...field}
339
- label={t('admin.vendor.blockletMetaUrl')}
340
- required
341
- fullWidth
342
- error={!!errors.blocklet_meta_url}
343
- helperText={errors.blocklet_meta_url?.message || t('admin.vendor.blockletMetaUrlHelp')}
344
- />
345
- )}
346
- />
347
-
348
- <Stack direction="row" spacing={0} sx={{ width: '30%' }}>
349
- {/* 分成类型 - 隐藏,固定为比例分成 */}
350
283
  <Controller
351
- name="default_commission_type"
284
+ name="description"
352
285
  control={control}
353
286
  render={({ field }) => (
354
- <FormControl fullWidth sx={{ display: 'none' }}>
355
- <InputLabel>{t('admin.vendor.commissionType')}</InputLabel>
356
- <Select {...field} label={t('admin.vendor.commissionType')}>
357
- <MenuItem value="percentage">{t('admin.vendor.percentage')}</MenuItem>
358
- <MenuItem value="fixed_amount">{t('admin.vendor.fixedAmount')}</MenuItem>
359
- </Select>
360
- </FormControl>
287
+ <TextField {...field} label={t('admin.vendor.description')} multiline rows={3} fullWidth />
361
288
  )}
362
289
  />
290
+
363
291
  <Controller
364
- name="default_commission_rate"
292
+ name="app_url"
365
293
  control={control}
366
294
  rules={{
367
- required: t('admin.vendor.commissionRateRequired'),
368
- min: {
369
- value: 0,
370
- message: t('admin.vendor.commissionRateMin'),
371
- },
372
- max: {
373
- value: isPercentage ? 100 : 999999,
374
- message: t('admin.vendor.commissionRateMax'),
375
- },
295
+ required: t('admin.vendor.appUrlRequired'),
296
+ validate: validateUrl,
376
297
  }}
377
298
  render={({ field }) => (
378
299
  <TextField
379
300
  {...field}
380
- label={`${t('admin.vendor.commissionRate')} ${isPercentage ? '(%)' : ''}`}
381
- type="number"
301
+ label={t('admin.vendor.appUrl')}
382
302
  required
383
303
  fullWidth
384
- placeholder="20"
385
- error={!!errors.default_commission_rate}
386
- helperText={
387
- errors.default_commission_rate?.message ||
388
- (isPercentage ? t('admin.vendor.commissionRateHelp') : t('admin.vendor.commissionAmountHelp'))
304
+ error={!!errors.app_url}
305
+ helperText={errors.app_url?.message || t('admin.vendor.appUrlHelp')}
306
+ />
307
+ )}
308
+ />
309
+
310
+ <MetadataForm title={t('common.metadata.label')} />
311
+
312
+ <Controller
313
+ name="status"
314
+ control={control}
315
+ render={({ field }) => (
316
+ <FormControlLabel
317
+ sx={{ maxWidth: 'max-content' }}
318
+ control={
319
+ <Switch
320
+ checked={field.value === 'active'}
321
+ onChange={(e) => field.onChange(e.target.checked ? 'active' : 'inactive')}
322
+ />
389
323
  }
390
- inputProps={{
391
- min: 0,
392
- max: isPercentage ? 100 : 999999,
393
- step: isPercentage ? 10 : 1,
394
- }}
324
+ label={watchedValues.status === 'active' ? t('admin.vendor.enabled') : t('admin.vendor.disabled')}
395
325
  />
396
326
  )}
397
327
  />
398
328
  </Stack>
399
-
400
- <Controller
401
- name="status"
402
- control={control}
403
- render={({ field }) => (
404
- <FormControlLabel
405
- control={
406
- <Switch
407
- checked={field.value === 'active'}
408
- onChange={(e) => field.onChange(e.target.checked ? 'active' : 'inactive')}
409
- />
410
- }
411
- label={watchedValues.status === 'active' ? t('admin.vendor.enabled') : t('admin.vendor.disabled')}
412
- />
413
- )}
414
- />
415
- </Stack>
416
- </DrawerForm>
329
+ </DrawerForm>
330
+ </FormProvider>
417
331
  );
418
332
  }
@@ -16,14 +16,11 @@ import VendorCreate from './create';
16
16
  interface Vendor {
17
17
  id: string;
18
18
  vendor_key: string;
19
+ vendor_type: string;
19
20
  name: string;
20
21
  description: string;
21
22
  app_url: string;
22
- webhook_path: string;
23
- default_commission_rate: number;
24
- default_commission_type: 'percentage' | 'fixed_amount';
25
23
  status: 'active' | 'inactive';
26
- order_create_params: Record<string, any>;
27
24
  metadata: Record<string, any>;
28
25
  created_at: string;
29
26
  updated_at: string;
@@ -95,7 +92,17 @@ export default function VendorsList() {
95
92
  filter: true,
96
93
  customBodyRenderLite: (_: string, index: number) => {
97
94
  const item = data.list[index] as Vendor;
98
- return <InfoCard name={item.name} description={item.vendor_key} logo={undefined} />;
95
+ return (
96
+ <InfoCard
97
+ name={item.name}
98
+ description={
99
+ <Typography variant="body2" color="text.secondary" sx={{ textTransform: 'capitalize' }}>
100
+ {item.vendor_type}
101
+ </Typography>
102
+ }
103
+ logo={undefined}
104
+ />
105
+ );
99
106
  },
100
107
  },
101
108
  },
@@ -114,21 +121,6 @@ export default function VendorsList() {
114
121
  },
115
122
  },
116
123
  },
117
- {
118
- label: t('admin.vendor.commission'),
119
- name: 'commission',
120
- options: {
121
- filter: true,
122
- customBodyRenderLite: (_: string, index: number) => {
123
- const item = data.list[index] as Vendor;
124
- const commissionText =
125
- item.default_commission_type === 'percentage'
126
- ? `${item.default_commission_rate}%`
127
- : `${item.default_commission_rate}`;
128
- return <Typography variant="body2">{commissionText}</Typography>;
129
- },
130
- },
131
- },
132
124
  {
133
125
  label: t('common.status'),
134
126
  name: 'status',
@@ -197,7 +189,7 @@ export default function VendorsList() {
197
189
  return (
198
190
  <>
199
191
  {/* 供应商服务公钥展示 */}
200
- {data.pk && (
192
+ {window.blocklet.appPk && (
201
193
  <Box
202
194
  sx={{
203
195
  display: 'flex',
@@ -237,7 +229,7 @@ export default function VendorsList() {
237
229
  }}>
238
230
  <Chip
239
231
  sx={{ backgroundColor: 'grey.200', color: 'text.secondary' }}
240
- label={data.pk}
232
+ label={window.blocklet.appPk}
241
233
  variant="outlined"
242
234
  size="small"
243
235
  />
@@ -1,13 +1,13 @@
1
1
  /* eslint-disable react/no-unstable-nested-components */
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import {
4
+ api,
4
5
  CreditTransactionsList,
5
6
  CustomerInvoiceList,
6
- TxLink,
7
- api,
8
7
  formatBNStr,
9
8
  formatTime,
10
9
  hasDelegateTxHash,
10
+ TxLink,
11
11
  useMobile,
12
12
  } from '@blocklet/payment-react';
13
13
  import type { TPaymentCurrency, TSubscriptionExpanded } from '@blocklet/payment-types';
@@ -25,31 +25,32 @@ import {
25
25
  Button,
26
26
  CircularProgress,
27
27
  Divider,
28
- Stack,
29
- Typography,
30
- Link as MuiLink,
31
28
  IconButton,
29
+ Link as MuiLink,
30
+ Stack,
32
31
  Tooltip,
32
+ Typography,
33
33
  useTheme,
34
34
  } from '@mui/material';
35
- import { useRequest } from 'ahooks';
36
- import { Link, useNavigate, useParams } from 'react-router-dom';
37
35
  import { styled } from '@mui/system';
38
36
  import { BN, fromUnitToToken } from '@ocap/util';
37
+ import { useRequest } from 'ahooks';
39
38
  import { useCallback, useRef } from 'react';
39
+ import { Link, useNavigate, useParams } from 'react-router-dom';
40
40
  import Currency from '../../../components/currency';
41
41
  import CustomerLink from '../../../components/customer/link';
42
+ import InfoMetric from '../../../components/info-metric';
42
43
  import InfoRow from '../../../components/info-row';
44
+ import InfoRowGroup from '../../../components/info-row-group';
43
45
  import SubscriptionDescription from '../../../components/subscription/description';
44
46
  import SubscriptionItemList from '../../../components/subscription/items';
45
47
  import SubscriptionMetrics from '../../../components/subscription/metrics';
46
48
  import SubscriptionActions, { ActionMethods } from '../../../components/subscription/portal/actions';
47
- import { canChangePaymentMethod } from '../../../libs/util';
49
+ import VendorServiceList from '../../../components/subscription/vendor-service-list';
48
50
  import { useSessionContext } from '../../../contexts/session';
49
- import InfoMetric from '../../../components/info-metric';
50
- import { useUnpaidInvoicesCheckForSubscription, usePendingAmountForSubscription } from '../../../hooks/subscription';
51
+ import { usePendingAmountForSubscription, useUnpaidInvoicesCheckForSubscription } from '../../../hooks/subscription';
51
52
  import { formatSmartDuration, TimeUnit } from '../../../libs/dayjs';
52
- import InfoRowGroup from '../../../components/info-row-group';
53
+ import { canChangePaymentMethod } from '../../../libs/util';
53
54
 
54
55
  const fetchData = (id: string | undefined): Promise<TSubscriptionExpanded> => {
55
56
  return api.get(`/api/subscriptions/${id}`).then((res) => res.data);
@@ -691,6 +692,20 @@ export default function CustomerSubscriptionDetail() {
691
692
  <SubscriptionItemList data={data.items} currency={data.paymentCurrency} mode="customer" />
692
693
  </Box>
693
694
  </Box>
695
+ {(() => {
696
+ const vendorServices = data.items?.map((item) => item.price?.product?.vendor_config || []).flat();
697
+
698
+ if (!vendorServices || vendorServices.length === 0) return null;
699
+
700
+ return (
701
+ <>
702
+ <Divider />
703
+ <Box className="section">
704
+ <VendorServiceList vendorServices={vendorServices} subscriptionId={id} />
705
+ </Box>
706
+ </>
707
+ );
708
+ })()}
694
709
  <Divider />
695
710
  {isCredit ? (
696
711
  <Box className="section">