payment-kit 1.20.8 → 1.20.10

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 (36) hide show
  1. package/api/src/crons/index.ts +1 -1
  2. package/api/src/index.ts +2 -2
  3. package/api/src/integrations/stripe/handlers/index.ts +10 -2
  4. package/api/src/libs/{vendor → vendor-util}/adapters/factory.ts +9 -5
  5. package/api/src/libs/{vendor → vendor-util}/adapters/launcher-adapter.ts +20 -18
  6. package/api/src/libs/{vendor → vendor-util}/adapters/types.ts +1 -4
  7. package/api/src/libs/{vendor → vendor-util}/fulfillment.ts +24 -127
  8. package/api/src/queues/payment.ts +1 -5
  9. package/api/src/queues/{vendor → vendors}/commission.ts +19 -18
  10. package/api/src/queues/{vendor → vendors}/fulfillment-coordinator.ts +35 -6
  11. package/api/src/queues/{vendor → vendors}/fulfillment.ts +2 -2
  12. package/api/src/queues/{vendor → vendors}/status-check.ts +13 -8
  13. package/api/src/routes/payment-links.ts +2 -1
  14. package/api/src/routes/products.ts +1 -0
  15. package/api/src/routes/vendor.ts +157 -216
  16. package/api/src/store/migrations/20250911-add-vendor-type.ts +26 -0
  17. package/api/src/store/migrations/20250916-add-vendor-did.ts +20 -0
  18. package/api/src/store/models/payout.ts +2 -2
  19. package/api/src/store/models/product-vendor.ts +11 -24
  20. package/api/src/store/models/product.ts +2 -0
  21. package/blocklet.yml +1 -1
  22. package/doc/vendor_fulfillment_system.md +1 -1
  23. package/package.json +5 -5
  24. package/src/components/metadata/form.tsx +12 -19
  25. package/src/components/payment-link/before-pay.tsx +40 -0
  26. package/src/components/product/vendor-config.tsx +4 -11
  27. package/src/components/subscription/description.tsx +1 -6
  28. package/src/components/subscription/portal/list.tsx +82 -6
  29. package/src/components/subscription/vendor-service-list.tsx +128 -0
  30. package/src/components/vendor/actions.tsx +1 -33
  31. package/src/locales/en.tsx +16 -3
  32. package/src/locales/zh.tsx +18 -5
  33. package/src/pages/admin/products/links/create.tsx +2 -0
  34. package/src/pages/admin/products/vendors/create.tsx +140 -190
  35. package/src/pages/admin/products/vendors/index.tsx +14 -22
  36. package/src/pages/customer/subscription/detail.tsx +26 -11
@@ -15,24 +15,23 @@ 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';
33
+ vendor_did?: string;
34
34
  status: 'active' | 'inactive';
35
- order_create_params: Record<string, any>;
36
35
  metadata: Record<string, any>;
37
36
  created_at: string;
38
37
  updated_at: string;
@@ -47,16 +46,13 @@ interface VendorCreateProps {
47
46
 
48
47
  interface VendorFormData {
49
48
  vendor_key: string;
49
+ vendor_type: string;
50
50
  name: string;
51
51
  description: string;
52
52
  app_url: string;
53
- webhook_path: string;
54
- blocklet_meta_url: string;
55
- default_commission_rate: number;
56
- default_commission_type: 'percentage' | 'fixed_amount';
53
+ vendor_did?: string;
57
54
  status: 'active' | 'inactive';
58
- order_create_params: Record<string, any>;
59
- metadata: Record<string, any>;
55
+ metadata: Array<{ key: string; value: string }>;
60
56
  app_pid?: string;
61
57
  app_logo?: string;
62
58
  }
@@ -72,41 +68,44 @@ export default function VendorCreate({
72
68
 
73
69
  const isEditMode = !!vendorData;
74
70
 
71
+ const defaultValues = vendorData
72
+ ? {
73
+ ...vendorData,
74
+ metadata: vendorData.metadata
75
+ ? Object.entries(vendorData.metadata).map(([key, value]) => ({
76
+ key,
77
+ value: typeof value === 'object' ? JSON.stringify(value) : String(value),
78
+ }))
79
+ : [],
80
+ }
81
+ : {
82
+ vendor_key: '',
83
+ vendor_type: 'launcher',
84
+ name: '',
85
+ description: '',
86
+ app_url: '',
87
+ vendor_did: '',
88
+ status: 'inactive' as const,
89
+ metadata: [{ key: 'blockletMetaUrl', value: '' }],
90
+ app_pid: '',
91
+ app_logo: '',
92
+ };
93
+
94
+ const methods = useForm<VendorFormData>({
95
+ mode: 'onChange',
96
+ defaultValues,
97
+ });
98
+
75
99
  const {
76
100
  control,
77
101
  handleSubmit,
78
102
  reset,
79
103
  formState: { errors, isValid },
80
104
  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
- });
105
+ } = methods;
105
106
 
106
107
  const watchedValues = watch();
107
108
 
108
- const isPercentage = watchedValues.default_commission_type === 'percentage';
109
-
110
109
  const validateUrl = (url: string) => {
111
110
  if (!url) return t('admin.vendor.appUrlRequired');
112
111
  try {
@@ -118,25 +117,16 @@ export default function VendorCreate({
118
117
  }
119
118
  };
120
119
 
121
- const validateWebhookPath = (path: string) => {
122
- if (!path) return true;
123
- if (!path.startsWith('/')) {
124
- return t('admin.vendor.webhookPathInvalid');
120
+ const validateDid = (did: string | undefined) => {
121
+ if (!did) return true; // DID 是可选的
122
+ // DID 格式验证
123
+ const didPattern = /^(did:abt:)?[1-9A-HJ-NP-Za-km-z]{37}$/;
124
+ if (!didPattern.test(did.trim())) {
125
+ return t('admin.vendor.vendorDidInvalid');
125
126
  }
126
127
  return true;
127
128
  };
128
129
 
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
130
  const onSubmit = async (data: VendorFormData) => {
141
131
  try {
142
132
  setLoading(true);
@@ -151,14 +141,24 @@ export default function VendorCreate({
151
141
  return;
152
142
  }
153
143
 
154
- // 准备提交数据,将 blocklet_meta_url 放入 metadata
155
- const { blocklet_meta_url: blockletMetaUrl, ...restData } = data;
144
+ // 准备提交数据,将 metadata 数组转换为对象
145
+ const { metadata: metadataArray, ...restData } = data;
146
+ const metadataObj = metadataArray.reduce((acc: Record<string, any>, item) => {
147
+ if (item.key && item.value) {
148
+ try {
149
+ // 尝试解析 JSON,如果失败则作为字符串处理
150
+ acc[item.key] = JSON.parse(item.value);
151
+ } catch {
152
+ acc[item.key] = item.value;
153
+ }
154
+ }
155
+ return acc;
156
+ }, {});
157
+
156
158
  const submitData = {
157
159
  ...restData,
158
- metadata: {
159
- ...data.metadata,
160
- ...(blockletMetaUrl && { blockletMetaUrl }),
161
- },
160
+ vendor_did: restData.vendor_did?.replace('did:abt:', '').trim(),
161
+ metadata: metadataObj,
162
162
  };
163
163
 
164
164
  // 如果状态为启用,则检测应用地址可用性
@@ -181,6 +181,10 @@ export default function VendorCreate({
181
181
  // 从响应中获取appPid和appLogo
182
182
  const blockletInfo = await response.json();
183
183
  if (blockletInfo) {
184
+ const component = blockletInfo.componentMountPoints?.find((x: any) => x.did === submitData.vendor_did);
185
+ if (component && !['', '/'].includes(component.mountPoint) && submitData.metadata) {
186
+ submitData.metadata.mountPoint = component.mountPoint;
187
+ }
184
188
  submitData.app_pid = blockletInfo.pid || blockletInfo.appPid;
185
189
  submitData.app_logo = blockletInfo.logo || blockletInfo.appLogo;
186
190
  }
@@ -227,20 +231,35 @@ export default function VendorCreate({
227
231
  };
228
232
 
229
233
  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}>
234
+ <FormProvider {...methods}>
235
+ <DrawerForm
236
+ icon={isEditMode ? null : <AddOutlined />}
237
+ text={isEditMode ? t('admin.vendor.edit') : t('admin.vendor.create')}
238
+ open={open}
239
+ onClose={handleClose}
240
+ width={800}
241
+ addons={
242
+ <Button variant="contained" size="small" onClick={handleSubmit(onSubmit)} disabled={loading || !isValid}>
243
+ {loading && <CircularProgress size="small" sx={{ mr: 1 }} />}
244
+ {isEditMode ? t('admin.save') : t('admin.vendor.save')}
245
+ </Button>
246
+ }>
247
+ <Stack spacing={3}>
248
+ <Controller
249
+ name="vendor_type"
250
+ control={control}
251
+ rules={{
252
+ required: t('admin.vendor.vendorTypeRequired'),
253
+ }}
254
+ render={({ field }) => (
255
+ <FormControl fullWidth required error={!!errors.vendor_type}>
256
+ <InputLabel>{t('admin.vendor.vendorType')}</InputLabel>
257
+ <Select {...field} label={t('admin.vendor.vendorType')}>
258
+ <MenuItem value="launcher">{t('admin.vendor.launcher')}</MenuItem>
259
+ </Select>
260
+ </FormControl>
261
+ )}
262
+ />
244
263
  <Controller
245
264
  name="vendor_key"
246
265
  control={control}
@@ -266,9 +285,7 @@ export default function VendorCreate({
266
285
  <Controller
267
286
  name="name"
268
287
  control={control}
269
- rules={{
270
- required: t('admin.vendor.nameRequired'),
271
- }}
288
+ rules={{ required: t('admin.vendor.nameRequired') }}
272
289
  render={({ field }) => (
273
290
  <TextField
274
291
  {...field}
@@ -276,143 +293,76 @@ export default function VendorCreate({
276
293
  required
277
294
  fullWidth
278
295
  error={!!errors.name}
279
- helperText={errors.name?.message}
296
+ helperText={errors.name?.message || t('admin.vendor.displayNameHelp')}
280
297
  />
281
298
  )}
282
299
  />
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
300
 
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
301
  <Controller
351
- name="default_commission_type"
302
+ name="description"
352
303
  control={control}
353
304
  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>
305
+ <TextField {...field} label={t('admin.vendor.description')} multiline rows={3} fullWidth />
361
306
  )}
362
307
  />
308
+
363
309
  <Controller
364
- name="default_commission_rate"
310
+ name="app_url"
365
311
  control={control}
366
312
  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
- },
313
+ required: t('admin.vendor.appUrlRequired'),
314
+ validate: validateUrl,
376
315
  }}
377
316
  render={({ field }) => (
378
317
  <TextField
379
318
  {...field}
380
- label={`${t('admin.vendor.commissionRate')} ${isPercentage ? '(%)' : ''}`}
381
- type="number"
319
+ label={t('admin.vendor.appUrl')}
382
320
  required
383
321
  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'))
322
+ error={!!errors.app_url}
323
+ helperText={errors.app_url?.message || t('admin.vendor.appUrlHelp')}
324
+ />
325
+ )}
326
+ />
327
+
328
+ <Controller
329
+ name="vendor_did"
330
+ control={control}
331
+ rules={{
332
+ required: t('admin.vendor.vendorDidRequired'),
333
+ validate: validateDid,
334
+ }}
335
+ render={({ field }) => (
336
+ <TextField
337
+ {...field}
338
+ label={t('admin.vendor.vendorDid')}
339
+ fullWidth
340
+ error={!!errors.vendor_did}
341
+ helperText={errors.vendor_did?.message || t('admin.vendor.vendorDidHelp')}
342
+ />
343
+ )}
344
+ />
345
+
346
+ <MetadataForm title={t('common.metadata.label')} />
347
+
348
+ <Controller
349
+ name="status"
350
+ control={control}
351
+ render={({ field }) => (
352
+ <FormControlLabel
353
+ sx={{ maxWidth: 'max-content' }}
354
+ control={
355
+ <Switch
356
+ checked={field.value === 'active'}
357
+ onChange={(e) => field.onChange(e.target.checked ? 'active' : 'inactive')}
358
+ />
389
359
  }
390
- inputProps={{
391
- min: 0,
392
- max: isPercentage ? 100 : 999999,
393
- step: isPercentage ? 10 : 1,
394
- }}
360
+ label={watchedValues.status === 'active' ? t('admin.vendor.enabled') : t('admin.vendor.disabled')}
395
361
  />
396
362
  )}
397
363
  />
398
364
  </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>
365
+ </DrawerForm>
366
+ </FormProvider>
417
367
  );
418
368
  }
@@ -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">