payment-kit 1.20.21 → 1.21.0
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/libs/notification/template/subscription-renew-failed.ts +19 -17
- package/api/src/libs/notification/template/subscription-will-renew.ts +19 -17
- package/api/src/libs/vendor-util/adapters/launcher-adapter.ts +7 -6
- package/api/src/libs/vendor-util/adapters/types.ts +2 -2
- package/api/src/libs/vendor-util/fulfillment.ts +1 -1
- 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 +99 -47
- package/api/src/store/models/checkout-session.ts +1 -0
- package/blocklet.yml +1 -1
- package/package.json +19 -19
- package/src/components/subscription/vendor-service-list.tsx +56 -58
- package/src/locales/en.tsx +39 -2
- package/src/locales/zh.tsx +35 -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
- package/src/pages/integrations/overview.tsx +148 -6
|
@@ -234,23 +234,25 @@ export class SubscriptionRenewFailedEmailTemplate
|
|
|
234
234
|
text: productName,
|
|
235
235
|
},
|
|
236
236
|
},
|
|
237
|
-
...(payer
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
237
|
+
...(payer
|
|
238
|
+
? [
|
|
239
|
+
{
|
|
240
|
+
type: 'text',
|
|
241
|
+
data: {
|
|
242
|
+
type: 'plain',
|
|
243
|
+
color: '#9397A1',
|
|
244
|
+
text: translate('notification.common.payer', locale),
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
type: 'text',
|
|
249
|
+
data: {
|
|
250
|
+
type: 'plain',
|
|
251
|
+
text: payer,
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
]
|
|
255
|
+
: []),
|
|
254
256
|
{
|
|
255
257
|
type: 'text',
|
|
256
258
|
data: {
|
|
@@ -390,23 +390,25 @@ export class SubscriptionWillRenewEmailTemplate extends BaseSubscriptionEmailTem
|
|
|
390
390
|
type: 'section',
|
|
391
391
|
fields: [
|
|
392
392
|
...commonFields,
|
|
393
|
-
...(payer
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
393
|
+
...(payer
|
|
394
|
+
? [
|
|
395
|
+
{
|
|
396
|
+
type: 'text',
|
|
397
|
+
data: {
|
|
398
|
+
type: 'plain',
|
|
399
|
+
color: '#9397A1',
|
|
400
|
+
text: translate('notification.common.payer', locale),
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
type: 'text',
|
|
405
|
+
data: {
|
|
406
|
+
type: 'plain',
|
|
407
|
+
text: payer,
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
]
|
|
411
|
+
: []),
|
|
410
412
|
...renewAmountFields,
|
|
411
413
|
...balanceFields,
|
|
412
414
|
...insufficientBalanceFields,
|
|
@@ -17,10 +17,11 @@ import {
|
|
|
17
17
|
} from './types';
|
|
18
18
|
import { formatVendorUrl } from './util';
|
|
19
19
|
|
|
20
|
-
const doRequestVendorData = (vendor: ProductVendor, orderId: string, url: string) => {
|
|
20
|
+
const doRequestVendorData = (vendor: ProductVendor, orderId: string, url: string, options: { shortUrl: boolean }) => {
|
|
21
21
|
const { headers } = VendorAuth.signRequestWithHeaders({});
|
|
22
22
|
const name = vendor?.name;
|
|
23
23
|
const key = vendor?.vendor_key;
|
|
24
|
+
const { shortUrl } = options;
|
|
24
25
|
|
|
25
26
|
return fetch(url, { headers })
|
|
26
27
|
.then(async (r) => {
|
|
@@ -38,7 +39,7 @@ const doRequestVendorData = (vendor: ProductVendor, orderId: string, url: string
|
|
|
38
39
|
`vendor status fetch failed, vendor: ${vendor.vendor_key}, orderId: ${orderId}, status: ${r.status}, url: ${url}`
|
|
39
40
|
);
|
|
40
41
|
}
|
|
41
|
-
if (!data.dashboardUrl) {
|
|
42
|
+
if (!shortUrl || !data.dashboardUrl) {
|
|
42
43
|
return {
|
|
43
44
|
...data,
|
|
44
45
|
name,
|
|
@@ -255,15 +256,15 @@ export class LauncherAdapter implements VendorAdapter {
|
|
|
255
256
|
}
|
|
256
257
|
}
|
|
257
258
|
|
|
258
|
-
getOrderStatus(vendor: ProductVendor, orderId: string): Promise<any> {
|
|
259
|
+
getOrderStatus(vendor: ProductVendor, orderId: string, options: { shortUrl: boolean }): Promise<any> {
|
|
259
260
|
const url = formatVendorUrl(vendor, `/api/vendor/status/${orderId}`);
|
|
260
261
|
|
|
261
|
-
return doRequestVendorData(vendor, orderId, url);
|
|
262
|
+
return doRequestVendorData(vendor, orderId, url, options);
|
|
262
263
|
}
|
|
263
264
|
|
|
264
|
-
getOrder(vendor: ProductVendor, orderId: string): Promise<any> {
|
|
265
|
+
getOrder(vendor: ProductVendor, orderId: string, options: { shortUrl: boolean }): Promise<any> {
|
|
265
266
|
const url = formatVendorUrl(vendor, `/api/vendor/orders/${orderId}`);
|
|
266
267
|
|
|
267
|
-
return doRequestVendorData(vendor, orderId, url);
|
|
268
|
+
return doRequestVendorData(vendor, orderId, url, options);
|
|
268
269
|
}
|
|
269
270
|
}
|
|
@@ -87,6 +87,6 @@ export interface VendorAdapter {
|
|
|
87
87
|
fulfillOrder(params: FulfillOrderParams): Promise<FulfillOrderResult>;
|
|
88
88
|
requestReturn(params: ReturnRequestParams): Promise<ReturnRequestResult>;
|
|
89
89
|
checkOrderStatus(params: CheckOrderStatusParams): Promise<CheckOrderStatusResult>;
|
|
90
|
-
getOrder(vendor: ProductVendor, orderId: string): Promise<any>;
|
|
91
|
-
getOrderStatus(vendor: ProductVendor, orderId: string): Promise<any>;
|
|
90
|
+
getOrder(vendor: ProductVendor, orderId: string, option?: Record<string, any>): Promise<any>;
|
|
91
|
+
getOrderStatus(vendor: ProductVendor, orderId: string, option?: Record<string, any>): Promise<any>;
|
|
92
92
|
}
|
|
@@ -158,7 +158,7 @@ export class VendorFulfillmentService {
|
|
|
158
158
|
destination,
|
|
159
159
|
amount: result.commissionAmount,
|
|
160
160
|
currency_id: checkoutSession.currency_id,
|
|
161
|
-
customer_id:
|
|
161
|
+
customer_id: '',
|
|
162
162
|
payment_intent_id: paymentIntentId,
|
|
163
163
|
payment_method_id: paymentMethodId,
|
|
164
164
|
status: paymentMethod?.type === 'stripe' ? 'deferred' : 'pending',
|
|
@@ -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,51 @@ async function testVendorConnection(req: any, res: any) {
|
|
|
312
316
|
}
|
|
313
317
|
}
|
|
314
318
|
|
|
315
|
-
|
|
319
|
+
async function executeVendorOperation(
|
|
320
|
+
vendorId: string,
|
|
321
|
+
orderId: string,
|
|
322
|
+
operation: 'getOrder' | 'getOrderStatus',
|
|
323
|
+
shortUrl: boolean
|
|
324
|
+
) {
|
|
316
325
|
if (!vendorId || !orderId) {
|
|
317
|
-
|
|
326
|
+
return {
|
|
327
|
+
error: 'Bad Request',
|
|
328
|
+
message: `vendorId or orderId is required, vendorId: ${vendorId}, orderId: ${orderId}`,
|
|
329
|
+
code: 400,
|
|
330
|
+
};
|
|
318
331
|
}
|
|
319
332
|
|
|
320
333
|
const vendor = await ProductVendor.findByPk(vendorId);
|
|
321
334
|
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}`);
|
|
335
|
+
return {
|
|
336
|
+
error: 'Not Found',
|
|
337
|
+
message: `vendor not found, vendorId: ${vendorId}`,
|
|
338
|
+
code: 404,
|
|
339
|
+
};
|
|
335
340
|
}
|
|
336
341
|
|
|
337
|
-
|
|
342
|
+
try {
|
|
343
|
+
const vendorAdapter = await VendorFulfillmentService.getVendorAdapter(vendor.vendor_key);
|
|
344
|
+
const data = await vendorAdapter[operation](vendor, orderId, { shortUrl });
|
|
338
345
|
|
|
339
|
-
|
|
340
|
-
|
|
346
|
+
return { data: { ...data, vendorType: vendor.vendor_type }, code: 200 };
|
|
347
|
+
} catch (error: any) {
|
|
348
|
+
const operationName = operation === 'getOrder' ? 'order' : 'order status';
|
|
349
|
+
logger.error(`Failed to get vendor ${operationName}`, {
|
|
350
|
+
error,
|
|
351
|
+
vendorId,
|
|
352
|
+
orderId,
|
|
353
|
+
vendorKey: vendor.vendor_key,
|
|
354
|
+
});
|
|
355
|
+
return {
|
|
356
|
+
error: 'Service Unavailable',
|
|
357
|
+
message: `Failed to get vendor ${operationName}`,
|
|
358
|
+
code: 503,
|
|
359
|
+
};
|
|
341
360
|
}
|
|
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
361
|
}
|
|
349
362
|
|
|
350
|
-
async function
|
|
363
|
+
async function processVendorOrders(sessionId: string, operation: 'getOrder' | 'getOrderStatus', shortUrl: boolean) {
|
|
351
364
|
const doc = await CheckoutSession.findByPk(sessionId);
|
|
352
365
|
|
|
353
366
|
if (!doc) {
|
|
@@ -378,8 +391,39 @@ async function doRequestVendor(sessionId: string, func: (vendorId: string, order
|
|
|
378
391
|
};
|
|
379
392
|
}
|
|
380
393
|
|
|
381
|
-
const vendors = doc.vendor_info.map((item) => {
|
|
382
|
-
|
|
394
|
+
const vendors = doc.vendor_info.map(async (item) => {
|
|
395
|
+
if (!item.order_id) {
|
|
396
|
+
return {
|
|
397
|
+
key: item.vendor_key,
|
|
398
|
+
progress: 0,
|
|
399
|
+
status: 'pending',
|
|
400
|
+
vendorType: item.vendor_type,
|
|
401
|
+
appUrl: item.app_url,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const result = await executeVendorOperation(item.vendor_id, item.order_id, operation, shortUrl);
|
|
406
|
+
|
|
407
|
+
// Handle error responses from vendor functions
|
|
408
|
+
if (result.error) {
|
|
409
|
+
logger.warn('Vendor operation returned error', {
|
|
410
|
+
vendorId: item.vendor_id,
|
|
411
|
+
orderId: item.order_id,
|
|
412
|
+
operation,
|
|
413
|
+
error: result.error,
|
|
414
|
+
message: result.message,
|
|
415
|
+
});
|
|
416
|
+
return {
|
|
417
|
+
key: item.vendor_key,
|
|
418
|
+
error: result.error,
|
|
419
|
+
message: result.message,
|
|
420
|
+
status: 'error',
|
|
421
|
+
vendorType: item.vendor_type,
|
|
422
|
+
appUrl: item.app_url,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return result.data;
|
|
383
427
|
});
|
|
384
428
|
|
|
385
429
|
return {
|
|
@@ -392,7 +436,7 @@ async function doRequestVendor(sessionId: string, func: (vendorId: string, order
|
|
|
392
436
|
}
|
|
393
437
|
|
|
394
438
|
async function getVendorStatus(sessionId: string) {
|
|
395
|
-
const result: any = await
|
|
439
|
+
const result: any = await processVendorOrders(sessionId, 'getOrderStatus', false);
|
|
396
440
|
|
|
397
441
|
if (result.subscriptionId) {
|
|
398
442
|
const subscriptionUrl = getUrl(`/customer/subscription/${result.subscriptionId}`);
|
|
@@ -407,10 +451,6 @@ async function getVendorStatus(sessionId: string) {
|
|
|
407
451
|
return result;
|
|
408
452
|
}
|
|
409
453
|
|
|
410
|
-
function getVendor(sessionId: string) {
|
|
411
|
-
return doRequestVendor(sessionId, getVendorById);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
454
|
async function getVendorFulfillmentStatus(req: any, res: any) {
|
|
415
455
|
const { sessionId } = req.params;
|
|
416
456
|
|
|
@@ -428,9 +468,10 @@ async function getVendorFulfillmentStatus(req: any, res: any) {
|
|
|
428
468
|
|
|
429
469
|
async function getVendorFulfillmentDetail(req: any, res: any) {
|
|
430
470
|
const { sessionId } = req.params;
|
|
471
|
+
const { shortUrl } = req.query;
|
|
431
472
|
|
|
432
473
|
try {
|
|
433
|
-
const detail = await
|
|
474
|
+
const detail = await processVendorOrders(sessionId, 'getOrder', shortUrl === 'true');
|
|
434
475
|
if (detail.code) {
|
|
435
476
|
return res.status(detail.code).json({ error: detail.error });
|
|
436
477
|
}
|
|
@@ -461,17 +502,28 @@ async function redirectToVendor(req: any, res: any) {
|
|
|
461
502
|
return res.redirect('/404');
|
|
462
503
|
}
|
|
463
504
|
|
|
464
|
-
const
|
|
465
|
-
|
|
505
|
+
const isOwner = req.user.did === checkoutSession.customer_did;
|
|
506
|
+
|
|
507
|
+
if (!isOwner) {
|
|
508
|
+
if (order.app_url) {
|
|
509
|
+
return res.redirect(order.app_url);
|
|
510
|
+
}
|
|
511
|
+
return res.redirect('/404');
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const result = await executeVendorOperation(vendorId, order.order_id || '', 'getOrder', false);
|
|
515
|
+
if (result.error || !result.data) {
|
|
466
516
|
logger.warn('Vendor status detail not found', {
|
|
467
517
|
subscriptionId,
|
|
468
518
|
vendorId,
|
|
469
519
|
orderId: order.order_id,
|
|
520
|
+
error: result.error,
|
|
521
|
+
message: result.message,
|
|
470
522
|
});
|
|
471
523
|
return res.redirect('/404');
|
|
472
524
|
}
|
|
473
525
|
|
|
474
|
-
const redirectUrl = target === 'dashboard' ?
|
|
526
|
+
const redirectUrl = target === 'dashboard' ? result.data.dashboardUrl : result.data.homeUrl;
|
|
475
527
|
return res.redirect(redirectUrl);
|
|
476
528
|
} catch (error: any) {
|
|
477
529
|
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.
|
|
3
|
+
"version": "1.21.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
|
|
@@ -45,33 +45,33 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@abtnode/cron": "^1.16.52-beta-20250912-112002-e3499e9c",
|
|
48
|
-
"@arcblock/did": "^1.25.
|
|
49
|
-
"@arcblock/did-connect-react": "^3.1.
|
|
48
|
+
"@arcblock/did": "^1.25.4",
|
|
49
|
+
"@arcblock/did-connect-react": "^3.1.43",
|
|
50
50
|
"@arcblock/did-connect-storage-nedb": "^1.8.0",
|
|
51
|
-
"@arcblock/did-util": "^1.25.
|
|
52
|
-
"@arcblock/jwt": "^1.25.
|
|
53
|
-
"@arcblock/ux": "^3.1.
|
|
54
|
-
"@arcblock/validator": "^1.25.
|
|
51
|
+
"@arcblock/did-util": "^1.25.4",
|
|
52
|
+
"@arcblock/jwt": "^1.25.4",
|
|
53
|
+
"@arcblock/ux": "^3.1.43",
|
|
54
|
+
"@arcblock/validator": "^1.25.4",
|
|
55
55
|
"@blocklet/did-space-js": "^1.1.27",
|
|
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.
|
|
60
|
-
"@blocklet/payment-react": "1.
|
|
61
|
-
"@blocklet/payment-vendor": "1.
|
|
59
|
+
"@blocklet/payment-broker-client": "1.21.0",
|
|
60
|
+
"@blocklet/payment-react": "1.21.0",
|
|
61
|
+
"@blocklet/payment-vendor": "1.21.0",
|
|
62
62
|
"@blocklet/sdk": "^1.16.52-beta-20250912-112002-e3499e9c",
|
|
63
|
-
"@blocklet/ui-react": "^3.1.
|
|
63
|
+
"@blocklet/ui-react": "^3.1.43",
|
|
64
64
|
"@blocklet/uploader": "^0.2.12",
|
|
65
|
-
"@blocklet/xss": "^0.2.
|
|
65
|
+
"@blocklet/xss": "^0.2.8",
|
|
66
66
|
"@mui/icons-material": "^7.1.2",
|
|
67
67
|
"@mui/lab": "7.0.0-beta.14",
|
|
68
68
|
"@mui/material": "^7.1.2",
|
|
69
69
|
"@mui/system": "^7.1.1",
|
|
70
|
-
"@ocap/asset": "^1.25.
|
|
71
|
-
"@ocap/client": "^1.25.
|
|
72
|
-
"@ocap/mcrypto": "^1.25.
|
|
73
|
-
"@ocap/util": "^1.25.
|
|
74
|
-
"@ocap/wallet": "^1.25.
|
|
70
|
+
"@ocap/asset": "^1.25.4",
|
|
71
|
+
"@ocap/client": "^1.25.4",
|
|
72
|
+
"@ocap/mcrypto": "^1.25.4",
|
|
73
|
+
"@ocap/util": "^1.25.4",
|
|
74
|
+
"@ocap/wallet": "^1.25.4",
|
|
75
75
|
"@stripe/react-stripe-js": "^2.9.0",
|
|
76
76
|
"@stripe/stripe-js": "^2.4.0",
|
|
77
77
|
"ahooks": "^3.8.5",
|
|
@@ -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.
|
|
131
|
+
"@blocklet/payment-types": "1.21.0",
|
|
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": "d133aa0e8c6681dd77bafe024c1e1dafb1addf0d"
|
|
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
|
@@ -94,6 +94,7 @@ export default flat({
|
|
|
94
94
|
promotional: 'Promotional',
|
|
95
95
|
viewInvoice: 'View Invoice',
|
|
96
96
|
viewSourceData: 'View Source',
|
|
97
|
+
goToConfigure: 'Go to Configure',
|
|
97
98
|
},
|
|
98
99
|
notification: {
|
|
99
100
|
preferences: {
|
|
@@ -1170,8 +1171,8 @@ export default flat({
|
|
|
1170
1171
|
apiConfig: 'API Configuration',
|
|
1171
1172
|
commissionConfig: 'Commission Configuration',
|
|
1172
1173
|
status: 'Status',
|
|
1173
|
-
|
|
1174
|
-
|
|
1174
|
+
brokerDID: 'Broker DID',
|
|
1175
|
+
brokerPublicKey: 'Broker Public Key',
|
|
1175
1176
|
},
|
|
1176
1177
|
subscription: {
|
|
1177
1178
|
view: 'View subscription',
|
|
@@ -1685,6 +1686,41 @@ export default flat({
|
|
|
1685
1686
|
title: 'Pricing Tables',
|
|
1686
1687
|
intro: 'Create beautiful pricing tables for your products',
|
|
1687
1688
|
},
|
|
1689
|
+
metering: {
|
|
1690
|
+
title: 'Usage Metering',
|
|
1691
|
+
intro: 'Track usage with meters and sell credits as top-up packages',
|
|
1692
|
+
dialog: {
|
|
1693
|
+
title: 'How to set up credit-based metering',
|
|
1694
|
+
description:
|
|
1695
|
+
'Credit-based metering allows you to track usage and charge customers based on consumption. Follow these steps to get started:',
|
|
1696
|
+
steps: {
|
|
1697
|
+
step1: {
|
|
1698
|
+
title: 'Create a meter to track your usage events',
|
|
1699
|
+
description:
|
|
1700
|
+
'Set up meters to monitor API calls, data usage, or any measurable activity in your application.',
|
|
1701
|
+
},
|
|
1702
|
+
step2: {
|
|
1703
|
+
title: 'Create Credit top-up products and pricing',
|
|
1704
|
+
description:
|
|
1705
|
+
'Define credit packages that customers can purchase, with flexible pricing models and validity periods.',
|
|
1706
|
+
},
|
|
1707
|
+
step3: {
|
|
1708
|
+
title: 'Create a payment link and share it so users can buy Credits',
|
|
1709
|
+
description: 'Generate shareable payment links that allow customers to easily purchase credit packages.',
|
|
1710
|
+
},
|
|
1711
|
+
step4: {
|
|
1712
|
+
title: 'Integrate and report Credit usage',
|
|
1713
|
+
description:
|
|
1714
|
+
'Use our SDK to report usage events and automatically deduct credits from customer balances.',
|
|
1715
|
+
},
|
|
1716
|
+
},
|
|
1717
|
+
docText: 'Read the credit billing guide',
|
|
1718
|
+
},
|
|
1719
|
+
},
|
|
1720
|
+
promotions: {
|
|
1721
|
+
title: 'Promotions',
|
|
1722
|
+
intro: 'Create coupons and promotion codes to drive conversions',
|
|
1723
|
+
},
|
|
1688
1724
|
donate: {
|
|
1689
1725
|
title: 'Donation',
|
|
1690
1726
|
intro: 'Add donation button to your application with <CheckoutDonate />',
|
|
@@ -1699,5 +1735,6 @@ export default flat({
|
|
|
1699
1735
|
link: 'https://www.npmjs.com/package/@blocklet/payment-js',
|
|
1700
1736
|
},
|
|
1701
1737
|
},
|
|
1738
|
+
viewDocs: 'View docs',
|
|
1702
1739
|
},
|
|
1703
1740
|
});
|
package/src/locales/zh.tsx
CHANGED
|
@@ -93,6 +93,7 @@ export default flat({
|
|
|
93
93
|
promotional: '促销',
|
|
94
94
|
viewInvoice: '查看账单',
|
|
95
95
|
viewSourceData: '查看来源',
|
|
96
|
+
goToConfigure: '前往配置',
|
|
96
97
|
},
|
|
97
98
|
notification: {
|
|
98
99
|
preferences: {
|
|
@@ -1142,8 +1143,8 @@ export default flat({
|
|
|
1142
1143
|
apiConfig: 'API配置',
|
|
1143
1144
|
commissionConfig: '分成配置',
|
|
1144
1145
|
status: '状态',
|
|
1145
|
-
|
|
1146
|
-
|
|
1146
|
+
brokerDID: '平台 DID',
|
|
1147
|
+
brokerPublicKey: '平台公钥',
|
|
1147
1148
|
},
|
|
1148
1149
|
subscription: {
|
|
1149
1150
|
view: '查看订阅',
|
|
@@ -1632,6 +1633,37 @@ export default flat({
|
|
|
1632
1633
|
title: '定价表',
|
|
1633
1634
|
intro: '为您的产品创建美观的定价表',
|
|
1634
1635
|
},
|
|
1636
|
+
metering: {
|
|
1637
|
+
title: '计量与额度',
|
|
1638
|
+
intro: '使用计量器跟踪用量,并通过额度套餐售卖 Credits',
|
|
1639
|
+
dialog: {
|
|
1640
|
+
title: '如何开启 Credit 计费',
|
|
1641
|
+
description: 'Credit 计费模式让您可以跟踪用量并基于消费向客户收费。按照以下步骤开始配置:',
|
|
1642
|
+
steps: {
|
|
1643
|
+
step1: {
|
|
1644
|
+
title: '创建计量器用于跟踪用量事件',
|
|
1645
|
+
description: '设置计量器来监控 API 调用、数据使用量或应用中任何可测量的活动。',
|
|
1646
|
+
},
|
|
1647
|
+
step2: {
|
|
1648
|
+
title: '创建 Credits 购买套餐和定价',
|
|
1649
|
+
description: '定义客户可以购买的额度套餐,支持灵活的定价模式和有效期设置。',
|
|
1650
|
+
},
|
|
1651
|
+
step3: {
|
|
1652
|
+
title: '创建支付链接并分享给用户',
|
|
1653
|
+
description: '生成可分享的支付链接,让客户轻松购买额度套餐。',
|
|
1654
|
+
},
|
|
1655
|
+
step4: {
|
|
1656
|
+
title: '集成并上报 Credits 用量',
|
|
1657
|
+
description: '使用我们的 SDK 上报使用事件,自动从客户余额中扣除额度。',
|
|
1658
|
+
},
|
|
1659
|
+
},
|
|
1660
|
+
docText: '查看 Credit 计费指南',
|
|
1661
|
+
},
|
|
1662
|
+
},
|
|
1663
|
+
promotions: {
|
|
1664
|
+
title: '促销与优惠',
|
|
1665
|
+
intro: '创建优惠券与促销码,提升转化率',
|
|
1666
|
+
},
|
|
1635
1667
|
donate: {
|
|
1636
1668
|
title: '打赏功能',
|
|
1637
1669
|
intro: '使用 <CheckoutDonate /> 组件为应用添加打赏按钮',
|
|
@@ -1646,5 +1678,6 @@ export default flat({
|
|
|
1646
1678
|
link: 'https://www.npmjs.com/package/@blocklet/payment-js',
|
|
1647
1679
|
},
|
|
1648
1680
|
},
|
|
1681
|
+
viewDocs: '查看文档',
|
|
1649
1682
|
},
|
|
1650
1683
|
});
|
|
@@ -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
|
);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
-
import { Box, Grid, Typography, Divider } from '@mui/material';
|
|
2
|
+
import { Box, Grid, Typography, Divider, Button, Link, Tooltip } from '@mui/material';
|
|
3
|
+
import Dialog from '@arcblock/ux/lib/Dialog';
|
|
3
4
|
import { useNavigate } from 'react-router-dom';
|
|
4
5
|
import {
|
|
5
6
|
Link as LinkIcon,
|
|
@@ -8,7 +9,11 @@ import {
|
|
|
8
9
|
Inventory2Outlined,
|
|
9
10
|
TableChartOutlined,
|
|
10
11
|
FavoriteBorderOutlined,
|
|
12
|
+
Speed,
|
|
13
|
+
LocalOfferOutlined,
|
|
14
|
+
HelpOutline,
|
|
11
15
|
} from '@mui/icons-material';
|
|
16
|
+
import { useState } from 'react';
|
|
12
17
|
|
|
13
18
|
const basicFeatures = [
|
|
14
19
|
{
|
|
@@ -32,6 +37,24 @@ const basicFeatures = [
|
|
|
32
37
|
];
|
|
33
38
|
|
|
34
39
|
const advancedFeatures = [
|
|
40
|
+
{
|
|
41
|
+
title: 'integrations.features.metering.title',
|
|
42
|
+
description: 'integrations.features.metering.intro',
|
|
43
|
+
icon: <Speed sx={{ fontSize: 32, color: 'text.lighter' }} />,
|
|
44
|
+
dialog: 'metering',
|
|
45
|
+
doc: {
|
|
46
|
+
url: 'https://www.arcblock.io/blog/en/payment-kit-enhanced-credit-management',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
title: 'integrations.features.promotions.title',
|
|
51
|
+
description: 'integrations.features.promotions.intro',
|
|
52
|
+
icon: <LocalOfferOutlined sx={{ fontSize: 32, color: 'text.lighter' }} />,
|
|
53
|
+
path: '/admin/products/coupons',
|
|
54
|
+
doc: {
|
|
55
|
+
url: 'https://www.arcblock.io/content/blog/en/payment-kit-promotion-support',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
35
58
|
{
|
|
36
59
|
title: 'integrations.features.donate.title',
|
|
37
60
|
description: 'integrations.features.donate.intro',
|
|
@@ -48,7 +71,7 @@ const advancedFeatures = [
|
|
|
48
71
|
title: 'integrations.features.api.title',
|
|
49
72
|
description: 'integrations.features.api.intro',
|
|
50
73
|
icon: <Code sx={{ fontSize: 32, color: 'text.lighter' }} />,
|
|
51
|
-
path: 'https://www.arcblock.io/docs/
|
|
74
|
+
path: 'https://www.staging.arcblock.io/content/docs/payment-kit-sdk',
|
|
52
75
|
external: true,
|
|
53
76
|
},
|
|
54
77
|
];
|
|
@@ -56,8 +79,13 @@ const advancedFeatures = [
|
|
|
56
79
|
export default function Overview() {
|
|
57
80
|
const { t } = useLocaleContext();
|
|
58
81
|
const navigate = useNavigate();
|
|
82
|
+
const [openMeterDialog, setOpenMeterDialog] = useState(false);
|
|
59
83
|
|
|
60
84
|
const handleClick = (item: any) => {
|
|
85
|
+
if (item.dialog === 'metering') {
|
|
86
|
+
setOpenMeterDialog(true);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
61
89
|
if (item.external) {
|
|
62
90
|
window.open(item.path, '_blank');
|
|
63
91
|
} else {
|
|
@@ -179,13 +207,37 @@ export default function Overview() {
|
|
|
179
207
|
mb: 1,
|
|
180
208
|
}}>
|
|
181
209
|
{item.icon}
|
|
182
|
-
<
|
|
183
|
-
variant="h4"
|
|
210
|
+
<Box
|
|
184
211
|
sx={{
|
|
212
|
+
display: 'flex',
|
|
213
|
+
alignItems: 'center',
|
|
185
214
|
mt: 1.5,
|
|
186
215
|
}}>
|
|
187
|
-
{t(item.title)}
|
|
188
|
-
|
|
216
|
+
<Typography variant="h4">{t(item.title)}</Typography>
|
|
217
|
+
{item.doc?.url && (
|
|
218
|
+
<Tooltip title={t('integrations.viewDocs')} placement="top">
|
|
219
|
+
<Link
|
|
220
|
+
href={item.doc.url}
|
|
221
|
+
target="_blank"
|
|
222
|
+
rel="noopener"
|
|
223
|
+
onClick={(e) => e.stopPropagation()}
|
|
224
|
+
sx={{
|
|
225
|
+
display: 'flex',
|
|
226
|
+
alignItems: 'center',
|
|
227
|
+
ml: 1,
|
|
228
|
+
color: 'text.secondary',
|
|
229
|
+
'&:hover': { color: 'primary.main' },
|
|
230
|
+
}}>
|
|
231
|
+
<HelpOutline
|
|
232
|
+
fontSize="small"
|
|
233
|
+
sx={{
|
|
234
|
+
cursor: 'pointer',
|
|
235
|
+
}}
|
|
236
|
+
/>
|
|
237
|
+
</Link>
|
|
238
|
+
</Tooltip>
|
|
239
|
+
)}
|
|
240
|
+
</Box>
|
|
189
241
|
</Box>
|
|
190
242
|
<Typography
|
|
191
243
|
variant="body2"
|
|
@@ -198,6 +250,96 @@ export default function Overview() {
|
|
|
198
250
|
</Grid>
|
|
199
251
|
))}
|
|
200
252
|
</Grid>
|
|
253
|
+
|
|
254
|
+
<Dialog
|
|
255
|
+
open={openMeterDialog}
|
|
256
|
+
onClose={() => setOpenMeterDialog(false)}
|
|
257
|
+
maxWidth="sm"
|
|
258
|
+
fullWidth
|
|
259
|
+
className="base-dialog"
|
|
260
|
+
title={t('integrations.features.metering.dialog.title')}
|
|
261
|
+
actions={
|
|
262
|
+
<Box sx={{ display: 'flex', gap: 2 }}>
|
|
263
|
+
<Button onClick={() => setOpenMeterDialog(false)}>{t('common.cancel')}</Button>
|
|
264
|
+
<Button onClick={() => navigate('/admin/meters')} variant="contained">
|
|
265
|
+
{t('common.goToConfigure')}
|
|
266
|
+
</Button>
|
|
267
|
+
</Box>
|
|
268
|
+
}>
|
|
269
|
+
<Typography variant="body2" sx={{ color: 'text.secondary', mb: 3 }}>
|
|
270
|
+
{t('integrations.features.metering.dialog.description')}
|
|
271
|
+
</Typography>
|
|
272
|
+
|
|
273
|
+
<Box
|
|
274
|
+
component="ol"
|
|
275
|
+
sx={{
|
|
276
|
+
pl: 0,
|
|
277
|
+
mb: 3,
|
|
278
|
+
listStyle: 'none',
|
|
279
|
+
counterReset: 'step-counter',
|
|
280
|
+
}}>
|
|
281
|
+
{['step1', 'step2', 'step3', 'step4'].map((stepKey) => (
|
|
282
|
+
<Box
|
|
283
|
+
key={stepKey}
|
|
284
|
+
component="li"
|
|
285
|
+
sx={{
|
|
286
|
+
mb: 3,
|
|
287
|
+
pl: 3,
|
|
288
|
+
position: 'relative',
|
|
289
|
+
counterIncrement: 'step-counter',
|
|
290
|
+
'&::before': {
|
|
291
|
+
content: 'counter(step-counter)',
|
|
292
|
+
position: 'absolute',
|
|
293
|
+
left: 0,
|
|
294
|
+
top: 0,
|
|
295
|
+
width: 24,
|
|
296
|
+
height: 24,
|
|
297
|
+
borderRadius: '50%',
|
|
298
|
+
backgroundColor: 'primary.main',
|
|
299
|
+
color: 'white',
|
|
300
|
+
fontSize: '12px',
|
|
301
|
+
fontWeight: 'bold',
|
|
302
|
+
display: 'flex',
|
|
303
|
+
alignItems: 'center',
|
|
304
|
+
justifyContent: 'center',
|
|
305
|
+
},
|
|
306
|
+
}}>
|
|
307
|
+
<Box sx={{ ml: 0.5 }}>
|
|
308
|
+
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 0.5 }}>
|
|
309
|
+
{t(`integrations.features.metering.dialog.steps.${stepKey}.title`)}
|
|
310
|
+
</Typography>
|
|
311
|
+
<Typography variant="body2" sx={{ color: 'text.secondary', lineHeight: 1.5 }}>
|
|
312
|
+
{t(`integrations.features.metering.dialog.steps.${stepKey}.description`)}
|
|
313
|
+
</Typography>
|
|
314
|
+
</Box>
|
|
315
|
+
</Box>
|
|
316
|
+
))}
|
|
317
|
+
</Box>
|
|
318
|
+
|
|
319
|
+
<Box
|
|
320
|
+
sx={{
|
|
321
|
+
p: 2,
|
|
322
|
+
backgroundColor: 'action.hover',
|
|
323
|
+
borderRadius: 1,
|
|
324
|
+
border: '1px solid',
|
|
325
|
+
borderColor: 'divider',
|
|
326
|
+
}}>
|
|
327
|
+
<Link
|
|
328
|
+
href="https://www.staging.arcblock.io/content/docs/payment-kit-sdk/en/payment-kit-sdk-core-concepts-credit-billing"
|
|
329
|
+
target="_blank"
|
|
330
|
+
rel="noopener"
|
|
331
|
+
underline="hover"
|
|
332
|
+
sx={{
|
|
333
|
+
color: 'primary.main',
|
|
334
|
+
fontWeight: 500,
|
|
335
|
+
display: 'flex',
|
|
336
|
+
alignItems: 'center',
|
|
337
|
+
gap: 0.5,
|
|
338
|
+
}}>
|
|
339
|
+
{t('integrations.features.metering.dialog.docText')}
|
|
340
|
+
</Link>
|
|
341
|
+
</Box>
|
|
342
|
+
</Dialog>
|
|
201
343
|
</Box>
|
|
202
344
|
);
|
|
203
345
|
}
|