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.
- package/api/src/queues/vendors/fulfillment-coordinator.ts +11 -2
- package/api/src/queues/vendors/status-check.ts +1 -3
- package/api/src/routes/vendor.ts +93 -47
- package/api/src/store/models/checkout-session.ts +1 -0
- package/blocklet.yml +1 -1
- package/package.json +6 -6
- package/src/components/subscription/vendor-service-list.tsx +56 -58
- package/src/locales/en.tsx +2 -2
- package/src/locales/zh.tsx +2 -2
- package/src/pages/admin/billing/subscriptions/detail.tsx +32 -1
- package/src/pages/admin/products/products/detail.tsx +4 -14
- package/src/pages/admin/products/vendors/index.tsx +57 -48
- package/src/pages/customer/subscription/detail.tsx +5 -1
|
@@ -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
|
-
|
|
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) {
|
package/api/src/routes/vendor.ts
CHANGED
|
@@ -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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
app_url
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
319
|
+
async function executeVendorOperation(vendorId: string, orderId: string, operation: 'getOrder' | 'getOrderStatus') {
|
|
316
320
|
if (!vendorId || !orderId) {
|
|
317
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
337
|
+
try {
|
|
338
|
+
const vendorAdapter = await VendorFulfillmentService.getVendorAdapter(vendor.vendor_key);
|
|
339
|
+
const data = await vendorAdapter[operation](vendor, orderId);
|
|
338
340
|
|
|
339
|
-
|
|
340
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
465
|
-
|
|
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' ?
|
|
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', {
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.20.
|
|
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.
|
|
60
|
-
"@blocklet/payment-react": "1.20.
|
|
61
|
-
"@blocklet/payment-vendor": "1.20.
|
|
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.
|
|
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": "
|
|
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({
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
78
|
+
fontWeight: 600,
|
|
79
|
+
fontSize: '1rem',
|
|
80
|
+
color: 'text.primary',
|
|
69
81
|
}}>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
128
|
+
</Box>
|
|
129
|
+
)}
|
|
132
130
|
</Box>
|
|
133
131
|
);
|
|
134
132
|
})}
|
package/src/locales/en.tsx
CHANGED
|
@@ -1170,8 +1170,8 @@ export default flat({
|
|
|
1170
1170
|
apiConfig: 'API Configuration',
|
|
1171
1171
|
commissionConfig: 'Commission Configuration',
|
|
1172
1172
|
status: 'Status',
|
|
1173
|
-
|
|
1174
|
-
|
|
1173
|
+
brokerDID: 'Broker DID',
|
|
1174
|
+
brokerPublicKey: 'Broker Public Key',
|
|
1175
1175
|
},
|
|
1176
1176
|
subscription: {
|
|
1177
1177
|
view: 'View subscription',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -1142,8 +1142,8 @@ export default flat({
|
|
|
1142
1142
|
apiConfig: 'API配置',
|
|
1143
1143
|
commissionConfig: '分成配置',
|
|
1144
1144
|
status: '状态',
|
|
1145
|
-
|
|
1146
|
-
|
|
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.
|
|
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
|
|
188
|
+
const handleCopyValue = async (value: string) => {
|
|
189
189
|
try {
|
|
190
|
-
await navigator.clipboard.writeText(
|
|
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
|
|
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
|
-
{
|
|
205
|
+
{/* Broker Information */}
|
|
206
|
+
{brokerInfo.length > 0 && (
|
|
202
207
|
<Box
|
|
203
208
|
sx={{
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
|
718
|
+
<VendorServiceList
|
|
719
|
+
vendorServices={vendorServices}
|
|
720
|
+
subscriptionId={id}
|
|
721
|
+
isCanceled={data.status === 'canceled'}
|
|
722
|
+
/>
|
|
719
723
|
</Box>
|
|
720
724
|
</>
|
|
721
725
|
);
|