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.
@@ -234,23 +234,25 @@ export class SubscriptionRenewFailedEmailTemplate
234
234
  text: productName,
235
235
  },
236
236
  },
237
- ...(payer && [
238
- {
239
- type: 'text',
240
- data: {
241
- type: 'plain',
242
- color: '#9397A1',
243
- text: translate('notification.common.payer', locale),
244
- },
245
- },
246
- {
247
- type: 'text',
248
- data: {
249
- type: 'plain',
250
- text: payer,
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
- type: 'text',
396
- data: {
397
- type: 'plain',
398
- color: '#9397A1',
399
- text: translate('notification.common.payer', locale),
400
- },
401
- },
402
- {
403
- type: 'text',
404
- data: {
405
- type: 'plain',
406
- text: payer,
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: checkoutSession.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
- 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,51 @@ 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(
320
+ vendorId: string,
321
+ orderId: string,
322
+ operation: 'getOrder' | 'getOrderStatus',
323
+ shortUrl: boolean
324
+ ) {
316
325
  if (!vendorId || !orderId) {
317
- throw new Error(`vendorId or orderId is required, vendorId: ${vendorId}, orderId: ${orderId}`);
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
- 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}`);
335
+ return {
336
+ error: 'Not Found',
337
+ message: `vendor not found, vendorId: ${vendorId}`,
338
+ code: 404,
339
+ };
335
340
  }
336
341
 
337
- const vendor = await ProductVendor.findByPk(vendorId);
342
+ try {
343
+ const vendorAdapter = await VendorFulfillmentService.getVendorAdapter(vendor.vendor_key);
344
+ const data = await vendorAdapter[operation](vendor, orderId, { shortUrl });
338
345
 
339
- if (!vendor) {
340
- throw new Error(`vendor not found, vendorId: ${vendorId}`);
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 doRequestVendor(sessionId: string, func: (vendorId: string, orderId: string) => Promise<any>) {
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
- return func(item.vendor_id, item.order_id);
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 doRequestVendor(sessionId, getVendorStatusById);
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 getVendor(sessionId);
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 detail = await getVendorById(vendorId, order.order_id || '');
465
- if (!detail) {
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' ? detail.dashboardUrl : detail.homeUrl;
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', {
@@ -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.21.0
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.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.3",
49
- "@arcblock/did-connect-react": "^3.1.41",
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.3",
52
- "@arcblock/jwt": "^1.25.3",
53
- "@arcblock/ux": "^3.1.41",
54
- "@arcblock/validator": "^1.25.3",
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.20.21",
60
- "@blocklet/payment-react": "1.20.21",
61
- "@blocklet/payment-vendor": "1.20.21",
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.41",
63
+ "@blocklet/ui-react": "^3.1.43",
64
64
  "@blocklet/uploader": "^0.2.12",
65
- "@blocklet/xss": "^0.2.7",
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.3",
71
- "@ocap/client": "^1.25.3",
72
- "@ocap/mcrypto": "^1.25.3",
73
- "@ocap/util": "^1.25.3",
74
- "@ocap/wallet": "^1.25.3",
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.20.21",
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": "43b26f9e7373a99000f6a14762fdab860a562967"
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({ 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
  })}
@@ -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
- servicePublicKey: 'Service Public Key',
1174
- servicePublicKeyDescription: 'Used for vendor communication authentication',
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
  });
@@ -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
- servicePublicKey: '服务公钥',
1146
- servicePublicKeyDescription: '用于供应商通信认证',
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.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
  );
@@ -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/arcblock-payment-kit/en/start-payment-js',
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
- <Typography
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
- </Typography>
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
  }