payment-kit 1.15.34 → 1.15.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/api/src/libs/notification/template/subscription-canceled.ts +4 -0
  2. package/api/src/libs/refund.ts +4 -0
  3. package/api/src/libs/subscription.ts +25 -0
  4. package/api/src/queues/subscription.ts +2 -2
  5. package/api/src/routes/checkout-sessions.ts +2 -2
  6. package/api/src/routes/connect/recharge.ts +28 -3
  7. package/api/src/routes/connect/shared.ts +88 -0
  8. package/api/src/routes/customers.ts +2 -2
  9. package/api/src/routes/invoices.ts +5 -1
  10. package/api/src/routes/payment-links.ts +3 -0
  11. package/api/src/routes/refunds.ts +22 -1
  12. package/api/src/routes/subscriptions.ts +47 -5
  13. package/api/src/routes/webhook-attempts.ts +14 -1
  14. package/api/src/store/models/invoice.ts +2 -1
  15. package/blocklet.yml +1 -1
  16. package/package.json +4 -4
  17. package/src/app.tsx +3 -1
  18. package/src/components/invoice/list.tsx +40 -11
  19. package/src/components/invoice/recharge.tsx +244 -0
  20. package/src/components/payment-intent/actions.tsx +2 -1
  21. package/src/components/payment-link/actions.tsx +6 -6
  22. package/src/components/payment-link/item.tsx +53 -18
  23. package/src/components/pricing-table/actions.tsx +14 -3
  24. package/src/components/refund/actions.tsx +43 -1
  25. package/src/components/refund/list.tsx +1 -1
  26. package/src/components/subscription/portal/actions.tsx +22 -1
  27. package/src/components/subscription/portal/list.tsx +1 -0
  28. package/src/components/webhook/attempts.tsx +19 -121
  29. package/src/components/webhook/request-info.tsx +139 -0
  30. package/src/locales/en.tsx +4 -0
  31. package/src/locales/zh.tsx +8 -0
  32. package/src/pages/admin/payments/refunds/detail.tsx +2 -2
  33. package/src/pages/admin/products/links/create.tsx +4 -1
  34. package/src/pages/customer/invoice/detail.tsx +6 -0
  35. package/src/pages/customer/recharge.tsx +45 -35
  36. package/src/pages/customer/subscription/detail.tsx +8 -18
@@ -3,6 +3,7 @@
3
3
  import { fromUnitToToken } from '@ocap/util';
4
4
  import prettyMsI18n from 'pretty-ms-i18n';
5
5
 
6
+ import { Op } from 'sequelize';
6
7
  import { getUserLocale } from '../../../integrations/blocklet/notification';
7
8
  import { translate } from '../../../locales';
8
9
  import { Customer, PaymentMethod, Refund, Subscription } from '../../../store/models';
@@ -107,6 +108,9 @@ export class SubscriptionCanceledEmailTemplate implements BaseEmailTemplate<Subs
107
108
  where: {
108
109
  subscription_id: subscription.id,
109
110
  type: 'refund',
111
+ status: {
112
+ [Op.not]: 'canceled',
113
+ },
110
114
  },
111
115
  });
112
116
  const conditions = [
@@ -1,6 +1,7 @@
1
1
  import { BN } from '@ocap/util';
2
2
  import { Op, type WhereOptions } from 'sequelize';
3
3
  import { PaymentIntent, Refund } from '../store/models';
4
+ import logger from './logger';
4
5
 
5
6
  export async function getRefundAmountSetup({
6
7
  currencyId,
@@ -45,6 +46,9 @@ export async function getRefundAmountSetup({
45
46
  include: [],
46
47
  });
47
48
  if (count === 0) {
49
+ logger.info('No refund found for payment intent', {
50
+ paymentIntentId,
51
+ });
48
52
  return {
49
53
  amount: paymentIntent.amount_received,
50
54
  totalAmount: paymentIntent.amount_received,
@@ -313,6 +313,8 @@ export async function createProration(
313
313
  prorations: [],
314
314
  newCredit: '0',
315
315
  appliedCredit: '0',
316
+ remaining: '0',
317
+ remainingUnused: '0',
316
318
  };
317
319
  }
318
320
 
@@ -363,6 +365,25 @@ export async function createProration(
363
365
 
364
366
  // 5. adjust invoice total && update customer token balance
365
367
  const total = setup.amount.setup;
368
+
369
+ // 6. calculate remaining amount
370
+ const refunds = await Refund.findAll({
371
+ where: {
372
+ status: { [Op.not]: 'canceled' },
373
+ subscription_id: subscription.id,
374
+ currency_id: lastInvoice?.currency_id,
375
+ invoice_id: lastInvoice?.id,
376
+ },
377
+ });
378
+ const refundAmount = refunds.reduce((acc, x) => acc.add(new BN(x.amount || '0')), new BN(0));
379
+
380
+ // 7. calculate remaining amount, default refund recurring amount
381
+ const calcRemaining = (amount: BN, subtract: BN) =>
382
+ amount.sub(subtract).lt(new BN(0)) ? '0' : amount.sub(subtract).toString();
383
+
384
+ const remaining = calcRemaining(new BN(total), refundAmount);
385
+ const remainingUnused = calcRemaining(unused, refundAmount);
386
+
366
387
  let due = setup.amount.setup;
367
388
  let newCredit = '0';
368
389
  let appliedCredit = '0';
@@ -384,6 +405,8 @@ export async function createProration(
384
405
  prorationEnd,
385
406
  prorationRate,
386
407
  unused: unused.toString(),
408
+ remaining,
409
+ remainingUnused,
387
410
  total,
388
411
  due,
389
412
  newCredit,
@@ -394,6 +417,8 @@ export async function createProration(
394
417
  lastInvoice,
395
418
  total,
396
419
  due,
420
+ remaining,
421
+ remainingUnused,
397
422
  used: new BN(lastInvoice.amount_due).sub(unused).toString(),
398
423
  unused: unused.toString(),
399
424
  prorations,
@@ -702,7 +702,7 @@ const ensureRefundOnCancel = async (subscription: Subscription) => {
702
702
  }
703
703
 
704
704
  const result = await getSubscriptionRefundSetup(subscription, subscription.cancel_at, lastInvoice.currency_id);
705
- if (result.unused === '0') {
705
+ if (result.remainingUnused === '0') {
706
706
  logger.warn('Refund skipped because unused amount is 0', {
707
707
  subscription: subscription.id,
708
708
  unused: result.unused,
@@ -713,7 +713,7 @@ const ensureRefundOnCancel = async (subscription: Subscription) => {
713
713
  const item = await Refund.create({
714
714
  type: 'refund',
715
715
  livemode: subscription.livemode,
716
- amount: refund === 'last' ? result.total : result.unused,
716
+ amount: refund === 'last' ? result.remaining : result.remainingUnused,
717
717
  description: 'refund_transfer_on_subscription_cancel',
718
718
  status: 'pending',
719
719
  reason: 'requested_by_admin',
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable consistent-return */
2
2
  import { isValid } from '@arcblock/did';
3
3
  import { getUrl } from '@blocklet/sdk/lib/component';
4
- import userMiddleware from '@blocklet/sdk/lib/middlewares/user';
4
+ import sessionMiddleware from '@blocklet/sdk/lib/middlewares/session';
5
5
  import { BN, fromUnitToToken } from '@ocap/util';
6
6
  import { NextFunction, Request, Response, Router } from 'express';
7
7
  import Joi from 'joi';
@@ -73,7 +73,7 @@ import { ensureInvoiceForCheckout } from './connect/shared';
73
73
 
74
74
  const router = Router();
75
75
 
76
- const user = userMiddleware();
76
+ const user = sessionMiddleware();
77
77
  const auth = authenticate<CheckoutSession>({ component: true, roles: ['owner', 'admin'] });
78
78
 
79
79
  const getPaymentMethods = async (doc: CheckoutSession) => {
@@ -6,7 +6,7 @@ import { executeEvmTransaction, waitForEvmTxConfirm } from 'api/src/integrations
6
6
  import type { CallbackArgs } from '../../libs/auth';
7
7
  import { getGasPayerExtra } from '../../libs/payment';
8
8
  import { getTxMetadata } from '../../libs/util';
9
- import { ensureSubscriptionRecharge, getAuthPrincipalClaim } from './shared';
9
+ import { ensureRechargeInvoice, ensureSubscriptionRecharge, getAuthPrincipalClaim } from './shared';
10
10
  import logger from '../../libs/logger';
11
11
 
12
12
  export default {
@@ -74,10 +74,29 @@ export default {
74
74
  },
75
75
  onAuth: async ({ request, userDid, claims, extraParams }: CallbackArgs) => {
76
76
  const { subscriptionId } = extraParams;
77
- const { paymentMethod, paymentCurrency, receiverAddress } = await ensureSubscriptionRecharge(subscriptionId);
77
+ const { paymentMethod, paymentCurrency, receiverAddress, subscription, customer } =
78
+ await ensureSubscriptionRecharge(subscriptionId);
78
79
  let { amount } = extraParams;
79
80
  amount = fromTokenToUnit(amount, paymentCurrency.decimal).toString();
80
81
 
82
+ const afterTxExecution = async (paymentDetails: any) => {
83
+ await ensureRechargeInvoice(
84
+ {
85
+ total: amount,
86
+ description: 'Subscription recharge',
87
+ currency_id: paymentCurrency.id,
88
+ metadata: {
89
+ payment_details: {
90
+ [paymentMethod.type]: paymentDetails,
91
+ receiverAddress,
92
+ },
93
+ },
94
+ },
95
+ subscription!,
96
+ paymentMethod,
97
+ customer!
98
+ );
99
+ };
81
100
  if (paymentMethod.type === 'arcblock') {
82
101
  try {
83
102
  const client = paymentMethod.getOcapClient();
@@ -104,6 +123,11 @@ export default {
104
123
  paymentMethod: paymentMethod.type,
105
124
  });
106
125
 
126
+ await afterTxExecution({
127
+ tx_hash: txHash,
128
+ payer: userDid,
129
+ type: 'transfer',
130
+ });
107
131
  return { hash: txHash };
108
132
  } catch (err) {
109
133
  console.error(err);
@@ -120,13 +144,14 @@ export default {
120
144
  Number(paymentDetails.block_height),
121
145
  paymentMethod.confirmation.block
122
146
  )
123
- .then(() => {
147
+ .then(async () => {
124
148
  logger.info('Recharge successful', {
125
149
  receiverAddress,
126
150
  amount,
127
151
  subscriptionId,
128
152
  paymentMethod: paymentMethod.type,
129
153
  });
154
+ await afterTxExecution(paymentDetails);
130
155
  })
131
156
  .catch(console.error);
132
157
 
@@ -624,10 +624,17 @@ export async function ensureSubscriptionRecharge(subscriptionId: string) {
624
624
  if (!receiverAddress) {
625
625
  throw new Error(`Receiver address not found for subscription ${subscriptionId}`);
626
626
  }
627
+
628
+ const customer = await Customer.findByPk(subscription.customer_id);
629
+ if (!customer) {
630
+ throw new Error(`Customer ${subscription.customer_id} not found`);
631
+ }
627
632
  return {
628
633
  paymentCurrency: paymentCurrency as PaymentCurrency,
629
634
  paymentMethod: paymentMethod as PaymentMethod,
630
635
  receiverAddress,
636
+ subscription,
637
+ customer
631
638
  };
632
639
  }
633
640
 
@@ -1200,4 +1207,85 @@ export async function updateStripeSubscriptionAfterChangePayment(setupIntent: Se
1200
1207
  }
1201
1208
  }
1202
1209
  }
1210
+ }
1211
+
1212
+ export async function ensureRechargeInvoice(
1213
+ invoiceProps: { total: string; description?: string; checkout_session_id?: string; currency_id: string; metadata?: any; payment_settings?: any },
1214
+ subscription: Subscription,
1215
+ paymentMethod: PaymentMethod,
1216
+ customer: Customer
1217
+ ) {
1218
+ try {
1219
+ const rechargeInvoice = await Invoice.create({
1220
+ livemode: subscription.livemode,
1221
+ number: await customer.getInvoiceNumber(),
1222
+ description: invoiceProps?.description || 'Subscription recharge',
1223
+ statement_descriptor: '',
1224
+ period_start: dayjs().unix(),
1225
+ period_end: dayjs().unix(),
1226
+
1227
+ auto_advance: false,
1228
+ paid: true,
1229
+ paid_out_of_band: false,
1230
+
1231
+ status: 'paid',
1232
+ collection_method: 'charge_automatically',
1233
+ billing_reason: 'recharge',
1234
+
1235
+ currency_id: invoiceProps.currency_id,
1236
+ customer_id: customer.id,
1237
+ payment_intent_id: '',
1238
+ subscription_id: subscription?.id,
1239
+ checkout_session_id: invoiceProps?.checkout_session_id || '',
1240
+
1241
+ total: invoiceProps.total || '0',
1242
+ subtotal: invoiceProps.total || '0',
1243
+ tax: '0',
1244
+ subtotal_excluding_tax: invoiceProps.total || '0',
1245
+
1246
+ amount_due: '0',
1247
+ amount_paid: invoiceProps.total || '0',
1248
+ amount_remaining: '0',
1249
+ amount_shipping: '0',
1250
+
1251
+ starting_balance: '0',
1252
+ ending_balance: '0',
1253
+ starting_token_balance: {},
1254
+ ending_token_balance: {},
1255
+
1256
+ attempt_count: 0,
1257
+ attempted: false,
1258
+ // next_payment_attempt: undefined,
1259
+
1260
+ custom_fields: [],
1261
+ customer_address: customer.address,
1262
+ customer_email: customer.email,
1263
+ customer_name: customer.name,
1264
+ customer_phone: customer.phone,
1265
+
1266
+ discounts: [],
1267
+ total_discount_amounts: [],
1268
+
1269
+ due_date: undefined,
1270
+ effective_at: dayjs().unix(),
1271
+ status_transitions: {
1272
+ finalized_at: dayjs().unix(),
1273
+ },
1274
+
1275
+ payment_settings: invoiceProps?.payment_settings || subscription?.payment_settings,
1276
+ default_payment_method_id: paymentMethod.id,
1277
+
1278
+ account_country: '',
1279
+ account_name: '',
1280
+ metadata: invoiceProps.metadata || {},
1281
+ });
1282
+ logger.info('create recharge invoice success', {
1283
+ rechargeInvoice,
1284
+ subscriptionId: subscription?.id,
1285
+ paymentMethod: paymentMethod.id,
1286
+ customerId: customer.id,
1287
+ });
1288
+ } catch (error) {
1289
+ logger.error('ensureRechargeInvoice: create invoice failed', { error, subscriptionId: subscription?.id, paymentMethod: paymentMethod.id, customerId: customer.id });
1290
+ }
1203
1291
  }
@@ -1,4 +1,4 @@
1
- import { user } from '@blocklet/sdk/lib/middlewares';
1
+ import sessionMiddleware from '@blocklet/sdk/lib/middlewares/session';
2
2
  import { Router } from 'express';
3
3
  import Joi from 'joi';
4
4
  import pick from 'lodash/pick';
@@ -79,7 +79,7 @@ router.get('/search', auth, async (req, res) => {
79
79
  });
80
80
 
81
81
  // eslint-disable-next-line consistent-return
82
- router.get('/me', user(), async (req, res) => {
82
+ router.get('/me', sessionMiddleware(), async (req, res) => {
83
83
  if (!req.user) {
84
84
  return res.status(403).json({ error: 'Unauthorized' });
85
85
  }
@@ -107,8 +107,12 @@ router.get('/', authMine, async (req, res) => {
107
107
  // @ts-ignore
108
108
  where[key] = query[key];
109
109
  });
110
+ const excludeBillingReasons = ['recharge'];
110
111
  if (!!(include_staking && query.subscription_id) || !include_staking) {
111
- where.billing_reason = { [Op.ne]: 'stake' };
112
+ excludeBillingReasons.push('stake');
113
+ }
114
+ if (excludeBillingReasons.length > 0) {
115
+ where.billing_reason = { [Op.notIn]: excludeBillingReasons };
112
116
  }
113
117
  try {
114
118
  const { rows: list, count } = await Invoice.findAndCountAll({
@@ -93,6 +93,9 @@ const formatBeforeSave = (payload: any) => {
93
93
 
94
94
  raw.line_items?.forEach((x) => {
95
95
  if (x.adjustable_quantity?.enabled) {
96
+ if (Number(x.adjustable_quantity?.minimum) >= Number(x.adjustable_quantity?.maximum)) {
97
+ throw new Error('adjustable_quantity.minimum must be less than adjustable_quantity.maximum');
98
+ }
96
99
  x.adjustable_quantity.minimum = Number(x.adjustable_quantity?.minimum);
97
100
  x.adjustable_quantity.maximum = Number(x.adjustable_quantity?.maximum);
98
101
  }
@@ -261,7 +261,28 @@ router.put('/:id', authAdmin, async (req, res) => {
261
261
  requestBody: req.body,
262
262
  requestedBy: req.user?.did,
263
263
  });
264
- res.status(500).json({ error: 'Internal server error' });
264
+ return res.status(400).json({ error: err.message });
265
+ }
266
+ });
267
+
268
+ router.post('/:id/cancel', authAdmin, async (req, res) => {
269
+ const doc = await Refund.findByPk(req.params.id as string);
270
+ if (!doc) {
271
+ return res.status(404).json({ error: 'Refund not found' });
272
+ }
273
+ if (doc.status === 'succeeded') {
274
+ return res.status(400).json({ error: 'Refund is already succeeded' });
275
+ }
276
+ try {
277
+ await doc.update({ status: 'canceled' });
278
+ return res.json(doc);
279
+ } catch (err) {
280
+ logger.error('Cancel refund failed', {
281
+ refundId: req.params.id,
282
+ error: err.message,
283
+ requestedBy: req.user?.did,
284
+ });
285
+ return res.status(400).json({ error: err.message });
265
286
  }
266
287
  });
267
288
 
@@ -389,7 +389,7 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
389
389
  return res.status(403).json({ error: 'Not authorized to refund' });
390
390
  }
391
391
  const result = await getSubscriptionRefundSetup(subscription, updates.cancel_at);
392
- if (result.unused !== '0') {
392
+ if (result.remainingUnused !== '0') {
393
393
  // @ts-ignore
394
394
  updates.cancelation_details = {
395
395
  ...(updates.cancelation_details || {}),
@@ -1279,7 +1279,7 @@ router.get('/:id/proration', authPortal, async (req, res) => {
1279
1279
 
1280
1280
  const anchor = req.query.time ? dayjs(req.query.time as any).unix() : dayjs().unix();
1281
1281
  const result = await getSubscriptionRefundSetup(subscription, anchor, invoice?.currency_id);
1282
- if (result.total === '0') {
1282
+ if (result.remaining === '0') {
1283
1283
  return res.json(null);
1284
1284
  }
1285
1285
  const paymentCurrency = await PaymentCurrency.findByPk(result.lastInvoice?.currency_id);
@@ -1289,9 +1289,9 @@ router.get('/:id/proration', authPortal, async (req, res) => {
1289
1289
  }
1290
1290
 
1291
1291
  return res.json({
1292
- total: result.total,
1292
+ total: result.remaining,
1293
1293
  latest: invoice?.total,
1294
- unused: result.unused,
1294
+ unused: result.remainingUnused,
1295
1295
  used: result.used,
1296
1296
  prorations: result.prorations,
1297
1297
  paymentCurrency,
@@ -1815,5 +1815,47 @@ router.get('/:id/payer-token', authMine, async (req, res) => {
1815
1815
  return res.json({ token, paymentAddress });
1816
1816
  });
1817
1817
 
1818
- router.get('/:id/change-plan');
1818
+ const rechargeSchema = createListParamSchema<{
1819
+ customer_id?: string;
1820
+ customer_did?: string;
1821
+ currency_id?: string;
1822
+ }>({
1823
+ customer_id: Joi.string().empty(''),
1824
+ customer_did: Joi.string().empty(''),
1825
+ currency_id: Joi.string().empty(''),
1826
+ });
1827
+ router.get('/:id/recharge', authMine, async (req, res) => {
1828
+ const subscription = await Subscription.findByPk(req.params.id);
1829
+ if (!subscription) {
1830
+ return res.status(404).json({ error: `Subscription(${req.params.id}) not found` });
1831
+ }
1832
+ const { page, pageSize, ...query } = await rechargeSchema.validateAsync(req.query, {
1833
+ stripUnknown: false,
1834
+ allowUnknown: true,
1835
+ });
1836
+ const where = getWhereFromKvQuery(query.q);
1837
+
1838
+ try {
1839
+ const { rows: invoices, count } = await Invoice.findAndCountAll({
1840
+ where: {
1841
+ subscription_id: subscription.id,
1842
+ billing_reason: 'recharge',
1843
+ paid: true,
1844
+ ...where,
1845
+ },
1846
+ offset: (page - 1) * pageSize,
1847
+ limit: pageSize,
1848
+ order: [['created_at', 'DESC']],
1849
+ include: [
1850
+ { model: PaymentCurrency, as: 'paymentCurrency' },
1851
+ { model: PaymentMethod, as: 'paymentMethod' },
1852
+ ],
1853
+ });
1854
+
1855
+ return res.json({ count, list: invoices, subscription, paging: { page, pageSize } });
1856
+ } catch (err) {
1857
+ console.error(err);
1858
+ return res.status(400).json({ error: err.message });
1859
+ }
1860
+ });
1819
1861
  export default router;
@@ -5,6 +5,7 @@ import type { WhereOptions } from 'sequelize';
5
5
  import { createListParamSchema } from '../libs/api';
6
6
  import { authenticate } from '../libs/security';
7
7
  import { Event, WebhookAttempt, WebhookEndpoint } from '../store/models';
8
+ import { blocklet } from '../libs/auth';
8
9
 
9
10
  const router = Router();
10
11
  const auth = authenticate<WebhookAttempt>({ component: true, roles: ['owner', 'admin'] });
@@ -42,7 +43,19 @@ router.get('/', auth, async (req, res) => {
42
43
  ],
43
44
  });
44
45
 
45
- res.json({ count, list, paging: { page, pageSize } });
46
+ const updatedList = await Promise.all(
47
+ list.map(async (attempt: any) => {
48
+ const updated = attempt.toJSON();
49
+ if (updated.event?.request?.requested_by) {
50
+ const { user } = await blocklet.getUser(updated.event.request.requested_by);
51
+ if (user) {
52
+ updated.event.requestInfo = user;
53
+ }
54
+ }
55
+ return updated;
56
+ })
57
+ );
58
+ res.json({ count, list: updatedList, paging: { page, pageSize } });
46
59
  } catch (err) {
47
60
  console.error(err);
48
61
  res.json({ count: 0, list: [], paging: { page, pageSize } });
@@ -60,7 +60,8 @@ export class Invoice extends Model<InferAttributes<Invoice>, InferCreationAttrib
60
60
  | 'manual'
61
61
  | 'upcoming'
62
62
  | 'slash_stake'
63
- | 'stake',
63
+ | 'stake'
64
+ | 'recharge',
64
65
  string
65
66
  >;
66
67
 
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.15.34
17
+ version: 1.15.35
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.15.34",
3
+ "version": "1.15.35",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -53,7 +53,7 @@
53
53
  "@arcblock/validator": "^1.18.139",
54
54
  "@blocklet/js-sdk": "1.16.33-beta-20241031-073543-49b1ff9b",
55
55
  "@blocklet/logger": "1.16.33-beta-20241031-073543-49b1ff9b",
56
- "@blocklet/payment-react": "1.15.34",
56
+ "@blocklet/payment-react": "1.15.35",
57
57
  "@blocklet/sdk": "1.16.33-beta-20241031-073543-49b1ff9b",
58
58
  "@blocklet/ui-react": "^2.10.65",
59
59
  "@blocklet/uploader": "^0.1.51",
@@ -120,7 +120,7 @@
120
120
  "devDependencies": {
121
121
  "@abtnode/types": "1.16.33-beta-20241031-073543-49b1ff9b",
122
122
  "@arcblock/eslint-config-ts": "^0.3.3",
123
- "@blocklet/payment-types": "1.15.34",
123
+ "@blocklet/payment-types": "1.15.35",
124
124
  "@types/cookie-parser": "^1.4.7",
125
125
  "@types/cors": "^2.8.17",
126
126
  "@types/debug": "^4.1.12",
@@ -165,5 +165,5 @@
165
165
  "parser": "typescript"
166
166
  }
167
167
  },
168
- "gitHead": "95c0d3ddd12382f694014c05015ca46c19424e3e"
168
+ "gitHead": "279b6f2fee57a20cffdc78c0c1cf3a140cd142c4"
169
169
  }
package/src/app.tsx CHANGED
@@ -8,7 +8,7 @@ import { ToastProvider } from '@arcblock/ux/lib/Toast';
8
8
  import { CircularProgress } from '@mui/material';
9
9
  import React, { Suspense } from 'react';
10
10
  import { ErrorBoundary } from 'react-error-boundary';
11
- import { PaymentThemeProvider } from '@blocklet/payment-react';
11
+ import { PaymentThemeProvider, usePreventWheel } from '@blocklet/payment-react';
12
12
  import { Navigate, Route, BrowserRouter as Router, Routes } from 'react-router-dom';
13
13
  import { joinURL } from 'ufo';
14
14
 
@@ -150,6 +150,8 @@ export default function WrappedApp() {
150
150
  // While the blocklet is deploy to a sub path, this will be work properly.
151
151
  const prefix = window?.blocklet?.prefix || '/';
152
152
 
153
+ usePreventWheel();
154
+
153
155
  return (
154
156
  <ToastProvider>
155
157
  <SessionProvider
@@ -9,6 +9,7 @@ import {
9
9
  Table,
10
10
  useDefaultPageSize,
11
11
  getInvoiceDescriptionAndReason,
12
+ getTxLink,
12
13
  } from '@blocklet/payment-react';
13
14
  import type { TInvoiceExpanded } from '@blocklet/payment-types';
14
15
  import { CircularProgress, Typography } from '@mui/material';
@@ -87,6 +88,33 @@ InvoiceList.defaultProps = {
87
88
  mode: 'admin',
88
89
  };
89
90
 
91
+ const getInvoiceLink = (invoice: TInvoiceExpanded) => {
92
+ if (invoice.id.startsWith('in_')) {
93
+ return {
94
+ external: false,
95
+ url: `/admin/billing/${invoice.id}`,
96
+ };
97
+ }
98
+
99
+ return {
100
+ external: true,
101
+ connect: false,
102
+ url: getTxLink(invoice.paymentMethod, invoice.metadata?.payment_details).link,
103
+ };
104
+ };
105
+
106
+ function InvoiceLink({ invoice, children }: { invoice: TInvoiceExpanded; children: React.ReactNode }) {
107
+ const link = getInvoiceLink(invoice);
108
+ if (link.external) {
109
+ return (
110
+ <a href={link.url} target="_blank" rel="noreferrer">
111
+ {children}
112
+ </a>
113
+ );
114
+ }
115
+ return <Link to={link.url}>{children}</Link>;
116
+ }
117
+
90
118
  export default function InvoiceList({
91
119
  customer_id,
92
120
  subscription_id,
@@ -145,12 +173,12 @@ export default function InvoiceList({
145
173
  customBodyRenderLite: (_: string, index: number) => {
146
174
  const item = data.list[index] as TInvoiceExpanded;
147
175
  return (
148
- <Link to={`/admin/billing/${item.id}`}>
176
+ <InvoiceLink invoice={item}>
149
177
  <Typography component="strong" fontWeight={600}>
150
178
  {formatBNStr(item?.total, item?.paymentCurrency.decimal)}&nbsp;
151
179
  {item?.paymentCurrency.symbol}
152
180
  </Typography>
153
- </Link>
181
+ </InvoiceLink>
154
182
  );
155
183
  },
156
184
  },
@@ -163,9 +191,9 @@ export default function InvoiceList({
163
191
  customBodyRenderLite: (_: string, index: number) => {
164
192
  const item = data.list[index] as TInvoiceExpanded;
165
193
  return (
166
- <Link to={`/admin/billing/${item.id}`}>
194
+ <InvoiceLink invoice={item}>
167
195
  <Status label={item?.status} color={getInvoiceStatusColor(item?.status)} />
168
- </Link>
196
+ </InvoiceLink>
169
197
  );
170
198
  },
171
199
  },
@@ -177,9 +205,9 @@ export default function InvoiceList({
177
205
  customBodyRenderLite: (_: string, index: number) => {
178
206
  const item = data.list[index] as TInvoiceExpanded;
179
207
  return (
180
- <Link to={`/admin/billing/${item.id}`}>
208
+ <InvoiceLink invoice={item}>
181
209
  <Status label={getInvoiceDescriptionAndReason(item, locale)?.type} />
182
- </Link>
210
+ </InvoiceLink>
183
211
  );
184
212
  },
185
213
  },
@@ -190,20 +218,21 @@ export default function InvoiceList({
190
218
  options: {
191
219
  customBodyRenderLite: (_: string, index: number) => {
192
220
  const item = data.list[index] as TInvoiceExpanded;
193
- return <Link to={`/admin/billing/${item.id}`}>{item.number}</Link>;
221
+ return <InvoiceLink invoice={item}>{item.number}</InvoiceLink>;
194
222
  },
195
223
  },
196
224
  },
197
225
  {
198
226
  label: t('common.description'),
199
227
  name: 'description',
228
+ minWidth: 120,
200
229
  options: {
201
230
  customBodyRenderLite: (_: string, index: number) => {
202
231
  const item = data.list[index] as TInvoiceExpanded;
203
232
  return (
204
- <Link to={`/admin/billing/${item.id}`}>
233
+ <InvoiceLink invoice={item}>
205
234
  {getInvoiceDescriptionAndReason(item, locale)?.description || item?.id}
206
- </Link>
235
+ </InvoiceLink>
207
236
  );
208
237
  },
209
238
  },
@@ -215,7 +244,7 @@ export default function InvoiceList({
215
244
  sort: true,
216
245
  customBodyRenderLite: (_: string, index: number) => {
217
246
  const item = data.list[index] as TInvoiceExpanded;
218
- return <Link to={`/admin/billing/${item.id}`}>{formatTime(item.created_at)}</Link>;
247
+ return <InvoiceLink invoice={item}>{formatTime(item.created_at)}</InvoiceLink>;
219
248
  },
220
249
  },
221
250
  },
@@ -226,7 +255,7 @@ export default function InvoiceList({
226
255
  sort: true,
227
256
  customBodyRenderLite: (_: string, index: number) => {
228
257
  const item = data.list[index] as TInvoiceExpanded;
229
- return <Link to={`/admin/billing/${item.id}`}>{formatTime(item.updated_at)}</Link>;
258
+ return <InvoiceLink invoice={item}>{formatTime(item.updated_at)}</InvoiceLink>;
230
259
  },
231
260
  },
232
261
  },