payment-kit 1.20.21 → 1.20.22

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.
@@ -212,6 +212,8 @@ export async function startVendorFulfillment(checkoutSessionId: string, invoiceI
212
212
  const initialVendorInfo: VendorInfo[] = vendorConfigs.map((config) => ({
213
213
  vendor_id: config.vendor_id,
214
214
  vendor_key: config.vendor_key,
215
+ vendor_type: config.vendor_type,
216
+ name: config.name,
215
217
  order_id: '',
216
218
  status: 'pending' as 'pending',
217
219
  amount: config.amount || '0',
@@ -229,6 +231,7 @@ export async function startVendorFulfillment(checkoutSessionId: string, invoiceI
229
231
  // Coordinated fulfillment: didnames first, then launcher with bindDomainCap
230
232
  logger.info('Starting coordinated domain binding fulfillment', {
231
233
  checkoutSessionId,
234
+ invoiceId,
232
235
  didnamesVendorId: didnamesVendor.vendor_id,
233
236
  launcherVendorId: launcherVendor.vendor_id,
234
237
  });
@@ -261,6 +264,7 @@ export async function startVendorFulfillment(checkoutSessionId: string, invoiceI
261
264
 
262
265
  logger.info('Vendor fulfillment process has been triggered', {
263
266
  checkoutSessionId,
267
+ invoiceId,
264
268
  vendorCount: vendorConfigs.length,
265
269
  });
266
270
  } catch (error: any) {
@@ -556,14 +560,19 @@ export function triggerCoordinatorCheck(checkoutSessionId: string, invoiceId: st
556
560
  }
557
561
 
558
562
  export async function triggerCommissionProcess(checkoutSessionId: string, invoiceId: string): Promise<void> {
559
- logger.info('Triggering commission process', { checkoutSessionId });
563
+ logger.info('Triggering commission process', { checkoutSessionId, invoiceId });
560
564
 
561
565
  const checkoutSession = await CheckoutSession.findByPk(checkoutSessionId);
562
566
  if (!checkoutSession) {
563
567
  logger.error('Checkout session not found[triggerCommissionProcess]', { checkoutSessionId });
564
568
  return;
565
569
  }
566
- const invoice = await Invoice.findByPk(invoiceId);
570
+
571
+ if (!invoiceId) {
572
+ logger.warn('Invoice ID not found[triggerCommissionProcess]', { checkoutSessionId, invoiceId });
573
+ }
574
+
575
+ const invoice = await Invoice.findByPk(invoiceId || checkoutSession.invoice_id);
567
576
  if (!invoice) {
568
577
  logger.error('Invoice not found[triggerCommissionProcess]', { invoiceId });
569
578
  return;
@@ -41,9 +41,7 @@ export const handleVendorStatusCheck = async (job: VendorStatusCheckJob) => {
41
41
  const { checkoutSessionId, vendorId } = job;
42
42
  logger.info('handleVendorStatusCheck', { checkoutSessionId, vendorId });
43
43
 
44
- const checkoutSession = await CheckoutSession.findByPk(checkoutSessionId, {
45
- attributes: ['vendor_info', 'payment_intent_id'],
46
- });
44
+ const checkoutSession = await CheckoutSession.findByPk(checkoutSessionId);
47
45
 
48
46
  const vendor = checkoutSession?.vendor_info?.find((v) => v.vendor_id === vendorId);
49
47
  if (!vendor) {
@@ -146,13 +146,19 @@ async function createVendor(req: any, res: any) {
146
146
  vendor_type: type,
147
147
  name,
148
148
  description,
149
- app_url: appUrl,
150
149
  metadata,
151
150
  app_pid: appPid,
152
151
  app_logo: appLogo,
153
152
  status,
154
153
  } = value;
155
154
 
155
+ let appUrl = '';
156
+ try {
157
+ appUrl = new URL(value.app_url).origin;
158
+ } catch {
159
+ return res.status(400).json({ error: 'Invalid app URL' });
160
+ }
161
+
156
162
  const vendorType = (type || 'launcher') as 'launcher' | 'didnames';
157
163
  const vendorDid = VENDOR_DID[vendorType];
158
164
 
@@ -215,16 +221,14 @@ async function updateVendor(req: any, res: any) {
215
221
  });
216
222
  }
217
223
 
218
- const {
219
- vendor_type: type,
220
- name,
221
- description,
222
- app_url: appUrl,
223
- status,
224
- metadata,
225
- app_pid: appPid,
226
- app_logo: appLogo,
227
- } = value;
224
+ const { vendor_type: type, name, description, status, metadata, app_pid: appPid, app_logo: appLogo } = value;
225
+
226
+ let appUrl = '';
227
+ try {
228
+ appUrl = new URL(value.app_url).origin;
229
+ } catch {
230
+ return res.status(400).json({ error: 'Invalid app URL' });
231
+ }
228
232
 
229
233
  const vendorType = (type || 'launcher') as 'launcher' | 'didnames';
230
234
  const vendorDid = VENDOR_DID[vendorType];
@@ -312,42 +316,46 @@ async function testVendorConnection(req: any, res: any) {
312
316
  }
313
317
  }
314
318
 
315
- const getVendorById = async (vendorId: string, orderId: string) => {
319
+ async function executeVendorOperation(vendorId: string, orderId: string, operation: 'getOrder' | 'getOrderStatus') {
316
320
  if (!vendorId || !orderId) {
317
- throw new Error(`vendorId or orderId is required, vendorId: ${vendorId}, orderId: ${orderId}`);
321
+ return {
322
+ error: 'Bad Request',
323
+ message: `vendorId or orderId is required, vendorId: ${vendorId}, orderId: ${orderId}`,
324
+ code: 400,
325
+ };
318
326
  }
319
327
 
320
328
  const vendor = await ProductVendor.findByPk(vendorId);
321
329
  if (!vendor) {
322
- throw new Error(`vendor not found, vendorId: ${vendorId}`);
323
- }
324
-
325
- const vendorAdapter = await VendorFulfillmentService.getVendorAdapter(vendor.vendor_key);
326
-
327
- const data = await vendorAdapter.getOrder(vendor, orderId);
328
-
329
- return data;
330
- };
331
-
332
- async function getVendorStatusById(vendorId: string, orderId: string) {
333
- if (!vendorId || !orderId) {
334
- throw new Error(`vendorId or orderId is required, vendorId: ${vendorId}, orderId: ${orderId}`);
330
+ return {
331
+ error: 'Not Found',
332
+ message: `vendor not found, vendorId: ${vendorId}`,
333
+ code: 404,
334
+ };
335
335
  }
336
336
 
337
- const vendor = await ProductVendor.findByPk(vendorId);
337
+ try {
338
+ const vendorAdapter = await VendorFulfillmentService.getVendorAdapter(vendor.vendor_key);
339
+ const data = await vendorAdapter[operation](vendor, orderId);
338
340
 
339
- if (!vendor) {
340
- throw new Error(`vendor not found, vendorId: ${vendorId}`);
341
+ return { data: { ...data, vendorType: vendor.vendor_type }, code: 200 };
342
+ } catch (error: any) {
343
+ const operationName = operation === 'getOrder' ? 'order' : 'order status';
344
+ logger.error(`Failed to get vendor ${operationName}`, {
345
+ error,
346
+ vendorId,
347
+ orderId,
348
+ vendorKey: vendor.vendor_key,
349
+ });
350
+ return {
351
+ error: 'Service Unavailable',
352
+ message: `Failed to get vendor ${operationName}`,
353
+ code: 503,
354
+ };
341
355
  }
342
-
343
- const vendorAdapter = await VendorFulfillmentService.getVendorAdapter(vendor.vendor_key);
344
-
345
- const data = await vendorAdapter.getOrderStatus(vendor, orderId);
346
-
347
- return { ...data, vendorType: vendor.vendor_type };
348
356
  }
349
357
 
350
- async function doRequestVendor(sessionId: string, func: (vendorId: string, orderId: string) => Promise<any>) {
358
+ async function processVendorOrders(sessionId: string, operation: 'getOrder' | 'getOrderStatus') {
351
359
  const doc = await CheckoutSession.findByPk(sessionId);
352
360
 
353
361
  if (!doc) {
@@ -378,8 +386,39 @@ async function doRequestVendor(sessionId: string, func: (vendorId: string, order
378
386
  };
379
387
  }
380
388
 
381
- const vendors = doc.vendor_info.map((item) => {
382
- return func(item.vendor_id, item.order_id);
389
+ const vendors = doc.vendor_info.map(async (item) => {
390
+ if (!item.order_id) {
391
+ return {
392
+ key: item.vendor_key,
393
+ progress: 0,
394
+ status: 'pending',
395
+ vendorType: item.vendor_type,
396
+ appUrl: item.app_url,
397
+ };
398
+ }
399
+
400
+ const result = await executeVendorOperation(item.vendor_id, item.order_id, operation);
401
+
402
+ // Handle error responses from vendor functions
403
+ if (result.error) {
404
+ logger.warn('Vendor operation returned error', {
405
+ vendorId: item.vendor_id,
406
+ orderId: item.order_id,
407
+ operation,
408
+ error: result.error,
409
+ message: result.message,
410
+ });
411
+ return {
412
+ key: item.vendor_key,
413
+ error: result.error,
414
+ message: result.message,
415
+ status: 'error',
416
+ vendorType: item.vendor_type,
417
+ appUrl: item.app_url,
418
+ };
419
+ }
420
+
421
+ return result.data;
383
422
  });
384
423
 
385
424
  return {
@@ -392,7 +431,7 @@ async function doRequestVendor(sessionId: string, func: (vendorId: string, order
392
431
  }
393
432
 
394
433
  async function getVendorStatus(sessionId: string) {
395
- const result: any = await doRequestVendor(sessionId, getVendorStatusById);
434
+ const result: any = await processVendorOrders(sessionId, 'getOrderStatus');
396
435
 
397
436
  if (result.subscriptionId) {
398
437
  const subscriptionUrl = getUrl(`/customer/subscription/${result.subscriptionId}`);
@@ -407,10 +446,6 @@ async function getVendorStatus(sessionId: string) {
407
446
  return result;
408
447
  }
409
448
 
410
- function getVendor(sessionId: string) {
411
- return doRequestVendor(sessionId, getVendorById);
412
- }
413
-
414
449
  async function getVendorFulfillmentStatus(req: any, res: any) {
415
450
  const { sessionId } = req.params;
416
451
 
@@ -430,7 +465,7 @@ async function getVendorFulfillmentDetail(req: any, res: any) {
430
465
  const { sessionId } = req.params;
431
466
 
432
467
  try {
433
- const detail = await getVendor(sessionId);
468
+ const detail = await processVendorOrders(sessionId, 'getOrder');
434
469
  if (detail.code) {
435
470
  return res.status(detail.code).json({ error: detail.error });
436
471
  }
@@ -461,17 +496,28 @@ async function redirectToVendor(req: any, res: any) {
461
496
  return res.redirect('/404');
462
497
  }
463
498
 
464
- const detail = await getVendorById(vendorId, order.order_id || '');
465
- if (!detail) {
499
+ const isOwner = req.user.did === checkoutSession.customer_did;
500
+
501
+ if (!isOwner) {
502
+ if (order.app_url) {
503
+ return res.redirect(order.app_url);
504
+ }
505
+ return res.redirect('/404');
506
+ }
507
+
508
+ const result = await executeVendorOperation(vendorId, order.order_id || '', 'getOrder');
509
+ if (result.error || !result.data) {
466
510
  logger.warn('Vendor status detail not found', {
467
511
  subscriptionId,
468
512
  vendorId,
469
513
  orderId: order.order_id,
514
+ error: result.error,
515
+ message: result.message,
470
516
  });
471
517
  return res.redirect('/404');
472
518
  }
473
519
 
474
- const redirectUrl = target === 'dashboard' ? detail.dashboardUrl : detail.homeUrl;
520
+ const redirectUrl = target === 'dashboard' ? result.data.dashboardUrl : result.data.homeUrl;
475
521
  return res.redirect(redirectUrl);
476
522
  } catch (error: any) {
477
523
  logger.error('Failed to redirect to vendor service', {
@@ -220,6 +220,7 @@ export class CheckoutSession extends Model<InferAttributes<CheckoutSession>, Inf
220
220
  declare vendor_info?: Array<{
221
221
  vendor_id: string;
222
222
  vendor_key: string;
223
+ vendor_type: string;
223
224
  order_id: string;
224
225
  status:
225
226
  | 'pending'
package/blocklet.yml CHANGED
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.20.21
17
+ version: 1.20.22
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.20.21",
3
+ "version": "1.20.22",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
@@ -56,9 +56,9 @@
56
56
  "@blocklet/error": "^0.2.5",
57
57
  "@blocklet/js-sdk": "^1.16.52-beta-20250912-112002-e3499e9c",
58
58
  "@blocklet/logger": "^1.16.52-beta-20250912-112002-e3499e9c",
59
- "@blocklet/payment-broker-client": "1.20.21",
60
- "@blocklet/payment-react": "1.20.21",
61
- "@blocklet/payment-vendor": "1.20.21",
59
+ "@blocklet/payment-broker-client": "1.20.22",
60
+ "@blocklet/payment-react": "1.20.22",
61
+ "@blocklet/payment-vendor": "1.20.22",
62
62
  "@blocklet/sdk": "^1.16.52-beta-20250912-112002-e3499e9c",
63
63
  "@blocklet/ui-react": "^3.1.41",
64
64
  "@blocklet/uploader": "^0.2.12",
@@ -128,7 +128,7 @@
128
128
  "devDependencies": {
129
129
  "@abtnode/types": "^1.16.52-beta-20250912-112002-e3499e9c",
130
130
  "@arcblock/eslint-config-ts": "^0.3.3",
131
- "@blocklet/payment-types": "1.20.21",
131
+ "@blocklet/payment-types": "1.20.22",
132
132
  "@types/cookie-parser": "^1.4.9",
133
133
  "@types/cors": "^2.8.19",
134
134
  "@types/debug": "^4.1.12",
@@ -175,5 +175,5 @@
175
175
  "parser": "typescript"
176
176
  }
177
177
  },
178
- "gitHead": "43b26f9e7373a99000f6a14762fdab860a562967"
178
+ "gitHead": "a0a691de0cd6e47efbc0e4de01d200bb9193c435"
179
179
  }
@@ -13,9 +13,16 @@ interface VendorConfig {
13
13
  interface VendorServiceListProps {
14
14
  vendorServices: VendorConfig[];
15
15
  subscriptionId: string;
16
+ isOwner?: boolean;
17
+ isCanceled: boolean;
16
18
  }
17
19
 
18
- export default function VendorServiceList({ vendorServices, subscriptionId }: VendorServiceListProps) {
20
+ export default function VendorServiceList({
21
+ vendorServices,
22
+ subscriptionId,
23
+ isOwner = true,
24
+ isCanceled,
25
+ }: VendorServiceListProps) {
19
26
  const { t } = useLocaleContext();
20
27
 
21
28
  if (!vendorServices || vendorServices.length === 0) {
@@ -31,7 +38,6 @@ export default function VendorServiceList({ vendorServices, subscriptionId }: Ve
31
38
  </Typography>
32
39
  <Box className="section-body">
33
40
  <Stack
34
- spacing={2}
35
41
  sx={{
36
42
  display: 'grid',
37
43
  gridTemplateColumns: { xs: '1fr', md: '1fr 1fr', lg: '1fr 1fr 1fr' },
@@ -39,12 +45,14 @@ export default function VendorServiceList({ vendorServices, subscriptionId }: Ve
39
45
  }}>
40
46
  {vendorServices.map((vendor, index) => {
41
47
  const isLauncher = vendor.vendor_type === 'launcher';
42
-
43
48
  return (
44
49
  <Box
45
50
  key={vendor.vendor_key || index}
51
+ className="vendor-service-item"
46
52
  sx={{
47
53
  p: 2,
54
+ display: 'flex',
55
+ alignItems: 'center',
48
56
  border: '1px solid',
49
57
  borderColor: 'divider',
50
58
  borderRadius: 2,
@@ -54,40 +62,28 @@ export default function VendorServiceList({ vendorServices, subscriptionId }: Ve
54
62
  },
55
63
  transition: 'background-color 0.2s ease',
56
64
  }}>
57
- <Stack
58
- direction="row"
59
- sx={{
60
- justifyContent: 'space-between',
61
- alignItems: 'flex-start',
62
- }}>
63
- <Stack
64
- direction="row"
65
- spacing={1}
65
+ <Stack direction="row" spacing={1} sx={{ alignItems: 'center', flex: 1 }}>
66
+ <Box
67
+ sx={{
68
+ width: 8,
69
+ height: 8,
70
+ borderRadius: '50%',
71
+ bgcolor: isCanceled ? 'error.main' : 'success.main',
72
+ flexShrink: 0,
73
+ }}
74
+ />
75
+ <Typography
76
+ variant="body1"
66
77
  sx={{
67
- alignItems: 'center',
68
- flex: 1,
78
+ fontWeight: 600,
79
+ fontSize: '1rem',
80
+ color: 'text.primary',
69
81
  }}>
70
- <Box
71
- sx={{
72
- width: 8,
73
- height: 8,
74
- borderRadius: '50%',
75
- bgcolor: 'success.main',
76
- flexShrink: 0,
77
- }}
78
- />
79
- <Typography
80
- variant="body1"
81
- sx={{
82
- fontWeight: 600,
83
- fontSize: '1rem',
84
- color: 'text.primary',
85
- }}>
86
- {vendor.name || vendor.vendor_key}
87
- </Typography>
88
- </Stack>
89
- {/* Launcher 类型的链接 */}
90
- {isLauncher && (
82
+ {vendor.name || vendor.vendor_key}
83
+ </Typography>
84
+ </Stack>
85
+ {isLauncher && (
86
+ <Box>
91
87
  <Stack direction="row" spacing={0.5}>
92
88
  <Tooltip title={t('admin.subscription.serviceHome')} placement="top">
93
89
  <IconButton
@@ -105,30 +101,32 @@ export default function VendorServiceList({ vendorServices, subscriptionId }: Ve
105
101
  <Home fontSize="small" />
106
102
  </IconButton>
107
103
  </Tooltip>
108
- <Tooltip title={t('admin.subscription.serviceDashboard')} placement="top">
109
- <IconButton
110
- size="small"
111
- component="a"
112
- href={joinURL(
113
- prefix,
114
- '/api/vendors/open/',
115
- subscriptionId,
116
- `?vendorId=${vendor.vendor_id}&target=dashboard`
117
- )}
118
- target="_blank"
119
- rel="noopener noreferrer"
120
- sx={{
121
- color: 'primary.main',
122
- '&:hover': {
123
- backgroundColor: 'primary.lighter',
124
- },
125
- }}>
126
- <Dashboard fontSize="small" />
127
- </IconButton>
128
- </Tooltip>
104
+ {isOwner && (
105
+ <Tooltip title={t('admin.subscription.serviceDashboard')} placement="top">
106
+ <IconButton
107
+ size="small"
108
+ component="a"
109
+ href={joinURL(
110
+ prefix,
111
+ '/api/vendors/open/',
112
+ subscriptionId,
113
+ `?vendorId=${vendor.vendor_id}&target=dashboard`
114
+ )}
115
+ target="_blank"
116
+ rel="noopener noreferrer"
117
+ sx={{
118
+ color: 'primary.main',
119
+ '&:hover': {
120
+ backgroundColor: 'primary.lighter',
121
+ },
122
+ }}>
123
+ <Dashboard fontSize="small" />
124
+ </IconButton>
125
+ </Tooltip>
126
+ )}
129
127
  </Stack>
130
- )}
131
- </Stack>
128
+ </Box>
129
+ )}
132
130
  </Box>
133
131
  );
134
132
  })}
@@ -1170,8 +1170,8 @@ export default flat({
1170
1170
  apiConfig: 'API Configuration',
1171
1171
  commissionConfig: 'Commission Configuration',
1172
1172
  status: 'Status',
1173
- servicePublicKey: 'Service Public Key',
1174
- servicePublicKeyDescription: 'Used for vendor communication authentication',
1173
+ brokerDID: 'Broker DID',
1174
+ brokerPublicKey: 'Broker Public Key',
1175
1175
  },
1176
1176
  subscription: {
1177
1177
  view: 'View subscription',
@@ -1142,8 +1142,8 @@ export default flat({
1142
1142
  apiConfig: 'API配置',
1143
1143
  commissionConfig: '分成配置',
1144
1144
  status: '状态',
1145
- servicePublicKey: '服务公钥',
1146
- servicePublicKeyDescription: '用于供应商通信认证',
1145
+ brokerDID: '平台 DID',
1146
+ brokerPublicKey: '平台公钥',
1147
1147
  },
1148
1148
  subscription: {
1149
1149
  view: '查看订阅',
@@ -33,6 +33,8 @@ import SubscriptionMetrics from '../../../../components/subscription/metrics';
33
33
  import DiscountInfo from '../../../../components/discount/discount-info';
34
34
  import { goBackOrFallback } from '../../../../libs/util';
35
35
  import InfoRowGroup from '../../../../components/info-row-group';
36
+ import VendorServiceList from '../../../../components/subscription/vendor-service-list';
37
+ import { useSessionContext } from '../../../../contexts/session';
36
38
 
37
39
  const fetchData = (id: string): Promise<TSubscriptionExpanded> => {
38
40
  return api.get(`/api/subscriptions/${id}`).then((res) => res.data);
@@ -40,6 +42,7 @@ const fetchData = (id: string): Promise<TSubscriptionExpanded> => {
40
42
 
41
43
  export default function SubscriptionDetail(props: { id: string }) {
42
44
  const { t } = useLocaleContext();
45
+ const { session } = useSessionContext();
43
46
  const { isMobile } = useMobile();
44
47
  const [state, setState] = useSetState({
45
48
  adding: {
@@ -313,7 +316,6 @@ export default function SubscriptionDetail(props: { id: string }) {
313
316
  </InfoRowGroup>
314
317
  </Box>
315
318
  <Divider />
316
-
317
319
  {/* Discount Information */}
318
320
  {(data as any).discountStats && (
319
321
  <Box className="section">
@@ -332,6 +334,35 @@ export default function SubscriptionDetail(props: { id: string }) {
332
334
  <SubscriptionItemList data={data.items} currency={data.paymentCurrency} mode="admin" />
333
335
  </Box>
334
336
  </Box>
337
+ {(() => {
338
+ const vendorServices = data.items?.map((item) => item.price?.product?.vendor_config || []).flat();
339
+ if (!vendorServices || vendorServices.length === 0) return null;
340
+ return (
341
+ <>
342
+ <Divider />
343
+ <Box
344
+ className="section"
345
+ sx={{
346
+ '.section-header': {
347
+ fontSize: {
348
+ xs: '18px',
349
+ md: '1.09375rem',
350
+ },
351
+ },
352
+ '.vendor-service-item': {
353
+ maxWidth: '400px',
354
+ },
355
+ }}>
356
+ <VendorServiceList
357
+ vendorServices={vendorServices}
358
+ subscriptionId={data.id}
359
+ isOwner={session?.user?.did === data.customer?.did}
360
+ isCanceled={data.status === 'canceled'}
361
+ />
362
+ </Box>
363
+ </>
364
+ );
365
+ })()}
335
366
  <Divider />
336
367
  {isCredit ? (
337
368
  <Box className="section">
@@ -389,7 +389,6 @@ export default function ProductDetail(props: { id: string }) {
389
389
  </Box>
390
390
  </Box>
391
391
  <Divider />
392
- {/* 供应商配置展示 */}
393
392
  {data.type === 'service' && (
394
393
  <>
395
394
  <Box className="section">
@@ -429,13 +428,9 @@ export default function ProductDetail(props: { id: string }) {
429
428
  borderColor: 'divider',
430
429
  borderRadius: 1,
431
430
  backgroundColor: 'background.paper',
431
+ maxWidth: '600px',
432
432
  }}>
433
- <Stack
434
- direction="row"
435
- sx={{
436
- justifyContent: 'space-between',
437
- alignItems: 'center',
438
- }}>
433
+ <Stack direction="row" sx={{ justifyContent: 'space-between', alignItems: 'center' }}>
439
434
  <Box>
440
435
  <Typography variant="body1" sx={{ color: 'text.primary', fontWeight: 500 }}>
441
436
  {vendor.name || vendor.vendor_key || vendor.vendor_id}
@@ -446,15 +441,10 @@ export default function ProductDetail(props: { id: string }) {
446
441
  </Typography>
447
442
  )}
448
443
  </Box>
449
- <Stack
450
- direction="row"
451
- spacing={3}
452
- sx={{
453
- alignItems: 'center',
454
- }}>
444
+ <Stack direction="row" spacing={3} sx={{ alignItems: 'center' }}>
455
445
  <Typography variant="body2" sx={{ color: 'text.secondary' }}>
456
446
  {vendor.commission_type === 'percentage'
457
- ? t('admin.vendor.percentage')
447
+ ? t('admin.vendor.commission')
458
448
  : t('admin.vendor.fixedAmount')}
459
449
  </Typography>
460
450
  <Typography variant="body1" sx={{ color: 'text.primary', fontWeight: 600 }}>
@@ -185,69 +185,78 @@ export default function VendorsList() {
185
185
  setDetailOpen(true);
186
186
  };
187
187
 
188
- const handleCopyPublicKey = async () => {
188
+ const handleCopyValue = async (value: string) => {
189
189
  try {
190
- await navigator.clipboard.writeText(window.blocklet.appPk);
190
+ await navigator.clipboard.writeText(value);
191
191
  setCopySuccess(true);
192
192
  setTimeout(() => setCopySuccess(false), 2000);
193
193
  } catch (err) {
194
- console.error('Failed to copy public key:', err);
194
+ console.error('Failed to copy value:', err);
195
195
  }
196
196
  };
197
197
 
198
+ const brokerInfo = [
199
+ ...(window.blocklet.appId ? [{ label: t('admin.vendor.brokerDID'), value: window.blocklet.appId }] : []),
200
+ ...(window.blocklet.appPk ? [{ label: t('admin.vendor.brokerPublicKey'), value: window.blocklet.appPk }] : []),
201
+ ];
202
+
198
203
  return (
199
204
  <>
200
- {/* 供应商服务公钥展示 */}
201
- {window.blocklet.appPk && (
205
+ {/* Broker Information */}
206
+ {brokerInfo.length > 0 && (
202
207
  <Box
203
208
  sx={{
204
- display: 'flex',
205
- flexDirection: { xs: 'column', md: 'row' },
206
- alignItems: { xs: 'flex-start', md: 'center' },
207
- gap: { xs: 1, md: 0 },
208
- p: { xs: 1.5, md: 2 },
209
- mt: { xs: 1.5, md: 1 },
209
+ px: 2,
210
+ py: 1.5,
211
+ my: 2,
210
212
  borderRadius: 1,
211
213
  border: '1px solid',
212
214
  borderColor: 'divider',
215
+ backgroundColor: 'transparent',
216
+ display: 'grid',
217
+ gridTemplateColumns: 'max-content 1fr',
218
+ gap: 1,
219
+ alignItems: 'center',
213
220
  }}>
214
- <Typography
215
- variant="body2"
216
- sx={{
217
- color: 'text.secondary',
218
- minWidth: 'fit-content',
219
- }}>
220
- {t('admin.vendor.servicePublicKey')}:
221
- <Tooltip title={copySuccess ? t('common.copied') : t('common.copy')}>
222
- <IconButton
223
- size="small"
224
- onClick={handleCopyPublicKey}
225
- sx={{
226
- color: copySuccess ? 'success.main' : 'text.secondary',
227
- minWidth: { xs: '24px', md: '32px' },
228
- width: { xs: '24px', md: '32px' },
229
- height: { xs: '24px', md: '32px' },
230
- '&:hover': { backgroundColor: 'grey.100' },
231
- }}>
232
- <ContentCopy sx={{ fontSize: { xs: 16, md: 18 } }} />
233
- </IconButton>
234
- </Tooltip>
235
- </Typography>
236
- <Box
237
- sx={{
238
- display: 'flex',
239
- alignItems: 'center',
240
- gap: 1,
241
- width: { xs: '100%', md: 'auto' },
242
- flex: { xs: 'none', md: 1 },
243
- }}>
244
- <Chip
245
- sx={{ backgroundColor: 'grey.200', color: 'text.secondary' }}
246
- label={window.blocklet.appPk}
247
- variant="outlined"
248
- size="small"
249
- />
250
- </Box>
221
+ {brokerInfo.map((info) => (
222
+ <>
223
+ <Typography
224
+ key={`${info.label}-label`}
225
+ variant="body2"
226
+ color="text.secondary"
227
+ sx={{ justifySelf: 'start' }}>
228
+ {info.label}:
229
+ </Typography>
230
+ <Box sx={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}>
231
+ <Chip
232
+ key={`${info.label}-chip`}
233
+ label={info.value}
234
+ size="small"
235
+ sx={{
236
+ flexShrink: 1,
237
+ overflow: 'hidden',
238
+ '& .MuiChip-label': {
239
+ overflow: 'hidden',
240
+ textOverflow: 'ellipsis',
241
+ },
242
+ }}
243
+ />
244
+ <Tooltip key={`${info.label}-tooltip`} title={copySuccess ? t('common.copied') : t('common.copy')}>
245
+ <IconButton
246
+ size="small"
247
+ onClick={() => handleCopyValue(info.value)}
248
+ sx={{
249
+ color: copySuccess ? 'success.main' : 'text.secondary',
250
+ '&:hover': {
251
+ color: 'primary.main',
252
+ },
253
+ }}>
254
+ <ContentCopy sx={{ fontSize: 16 }} />
255
+ </IconButton>
256
+ </Tooltip>
257
+ </Box>
258
+ </>
259
+ ))}
251
260
  </Box>
252
261
  )}
253
262
  <Table
@@ -715,7 +715,11 @@ export default function CustomerSubscriptionDetail() {
715
715
  <>
716
716
  <Divider />
717
717
  <Box className="section">
718
- <VendorServiceList vendorServices={vendorServices} subscriptionId={id} />
718
+ <VendorServiceList
719
+ vendorServices={vendorServices}
720
+ subscriptionId={id}
721
+ isCanceled={data.status === 'canceled'}
722
+ />
719
723
  </Box>
720
724
  </>
721
725
  );