payment-kit 1.18.47 → 1.18.48

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/index.ts CHANGED
@@ -40,6 +40,7 @@ import subscribeHandlers from './routes/connect/subscribe';
40
40
  import delegationHandlers from './routes/connect/delegation';
41
41
  import overdraftProtectionHandlers from './routes/connect/overdraft-protection';
42
42
  import rechargeAccountHandlers from './routes/connect/recharge-account';
43
+ import reStakeHandlers from './routes/connect/re-stake';
43
44
  import { initialize } from './store/models';
44
45
  import { sequelize } from './store/sequelize';
45
46
  import { initUserHandler } from './integrations/blocklet/user';
@@ -79,6 +80,7 @@ handlers.attach(Object.assign({ app: router }, rechargeHandlers));
79
80
  handlers.attach(Object.assign({ app: router }, rechargeAccountHandlers));
80
81
  handlers.attach(Object.assign({ app: router }, delegationHandlers));
81
82
  handlers.attach(Object.assign({ app: router }, overdraftProtectionHandlers));
83
+ handlers.attach(Object.assign({ app: router }, reStakeHandlers));
82
84
  router.use('/api', routes);
83
85
 
84
86
  const isProduction = process.env.BLOCKLET_MODE === 'production';
@@ -145,10 +145,11 @@ function calculateFetchStrategy<T>(
145
145
  if (sources.length > 1) {
146
146
  // For multi-source scenarios, we need more data to ensure correct merging
147
147
  // Especially for later pages, estimation can be inaccurate
148
- const bufferMultiplier = Math.max(2, Math.ceil(page / 2)); // More buffer for later pages
148
+ const bufferMultiplier = Math.max(3, Math.ceil(page * 1.5)); // More aggressive buffer for later pages
149
+ const minDataRatio = page <= 2 ? 0.6 : 0.8; // Get more data for later pages
149
150
  const fetchLimit = Math.min(
150
151
  sourceCount,
151
- Math.max(pageSize * bufferMultiplier, Math.ceil(sourceCount * 0.5)) // At least 50% of source data
152
+ Math.max(pageSize * bufferMultiplier, Math.ceil(sourceCount * minDataRatio))
152
153
  );
153
154
  return { fetchLimit, fetchOffset: 0 };
154
155
  }
@@ -163,7 +164,8 @@ function calculateFetchStrategy<T>(
163
164
  if (sourceMeta?.type === 'cached' || sourceMeta?.type === 'computed') {
164
165
  // For multi-source, be more conservative
165
166
  if (sources.length > 1) {
166
- const fetchLimit = Math.min(sourceCount, Math.max(pageSize * 3, Math.ceil(sourceCount * 0.8))); // Get 80% of data
167
+ const minDataRatio = page <= 2 ? 0.7 : 0.9; // Get more data for later pages
168
+ const fetchLimit = Math.min(sourceCount, Math.max(pageSize * 3, Math.ceil(sourceCount * minDataRatio)));
167
169
  return { fetchLimit, fetchOffset: 0 };
168
170
  }
169
171
  const bufferSize = Math.min(sourceMeta.estimatedSize ?? sourceCount, pageSize * 2);
@@ -172,7 +174,8 @@ function calculateFetchStrategy<T>(
172
174
 
173
175
  // Default strategy: more conservative for multi-source
174
176
  if (sources.length > 1) {
175
- const fetchLimit = Math.min(sourceCount, Math.max(pageSize * 2, Math.ceil(sourceCount * 0.6))); // Get 60% of data
177
+ const minDataRatio = page <= 2 ? 0.6 : 0.8; // Get more data for later pages
178
+ const fetchLimit = Math.min(sourceCount, Math.max(pageSize * 2, Math.ceil(sourceCount * minDataRatio)));
176
179
  return { fetchLimit, fetchOffset: 0 };
177
180
  }
178
181
  const fetchLimit = Math.min(pageSize * 2, sourceCount);
@@ -904,7 +904,7 @@ export async function checkRemainingStake(
904
904
  }
905
905
 
906
906
  return {
907
- enough: total.gte(new BN(amount)),
907
+ enough: total.gte(new BN(amount || '0')),
908
908
  staked,
909
909
  revoked,
910
910
  };
@@ -0,0 +1,116 @@
1
+ import type { CallbackArgs } from '../../libs/auth';
2
+ import { type TLineItemExpanded } from '../../store/models';
3
+ import { ensureReStakeContext, executeOcapTransactions, getAuthPrincipalClaim, getStakeTxClaim } from './shared';
4
+ import { ensureStakeInvoice } from '../../libs/invoice';
5
+ import logger from '../../libs/logger';
6
+ import { addSubscriptionJob, subscriptionQueue } from '../../queues/subscription';
7
+ import { SubscriptionWillCanceledSchedule } from '../../crons/subscription-will-canceled';
8
+
9
+ export default {
10
+ action: 're-stake',
11
+ authPrincipal: false,
12
+ persistentDynamicClaims: false,
13
+ claims: {
14
+ authPrincipal: async ({ extraParams }: CallbackArgs) => {
15
+ const { paymentMethod } = await ensureReStakeContext(extraParams.subscriptionId);
16
+ return getAuthPrincipalClaim(paymentMethod, 'continue');
17
+ },
18
+ },
19
+ onConnect: async ({ userDid, userPk, extraParams }: CallbackArgs) => {
20
+ const { subscriptionId } = extraParams;
21
+ const { subscription, paymentMethod, paymentCurrency, payerAddress } = await ensureReStakeContext(subscriptionId);
22
+ if (userDid !== payerAddress) {
23
+ throw new Error(
24
+ `You are not the payer for this subscription. Expected payer: ${payerAddress}, but found: ${userDid}.`
25
+ );
26
+ }
27
+ // @ts-ignore
28
+ const items = subscription!.items as TLineItemExpanded[];
29
+
30
+ if (paymentMethod.type === 'arcblock') {
31
+ return [
32
+ {
33
+ prepareTx: await getStakeTxClaim({
34
+ userDid,
35
+ userPk,
36
+ paymentCurrency,
37
+ paymentMethod,
38
+ items,
39
+ subscription,
40
+ }),
41
+ },
42
+ ];
43
+ }
44
+
45
+ throw new Error(`Payment method ${paymentMethod.type} not supported`);
46
+ },
47
+
48
+ onAuth: async ({ request, userDid, userPk, claims, extraParams }: CallbackArgs) => {
49
+ const { subscriptionId } = extraParams;
50
+ const { subscription, paymentMethod, paymentCurrency, customer } = await ensureReStakeContext(subscriptionId);
51
+
52
+ if (paymentMethod.type === 'arcblock') {
53
+ const { stakingAmount, ...paymentDetails } = await executeOcapTransactions(
54
+ userDid,
55
+ userPk,
56
+ claims,
57
+ paymentMethod,
58
+ request,
59
+ subscription?.id,
60
+ paymentCurrency.contract
61
+ );
62
+
63
+ // 创建质押账单
64
+ await ensureStakeInvoice(
65
+ {
66
+ total: stakingAmount,
67
+ description: 'Re-stake to resume subscription',
68
+ currency_id: paymentCurrency.id,
69
+ metadata: {
70
+ payment_details: {
71
+ arcblock: {
72
+ tx_hash: paymentDetails?.staking?.tx_hash,
73
+ payer: paymentDetails?.payer,
74
+ address: paymentDetails?.staking?.address,
75
+ },
76
+ },
77
+ },
78
+ },
79
+ subscription!,
80
+ paymentMethod,
81
+ customer!
82
+ );
83
+
84
+ await subscription.update({
85
+ cancel_at_period_end: false,
86
+ cancel_at: 0,
87
+ canceled_at: 0,
88
+ // @ts-ignore
89
+ cancelation_details: null,
90
+ payment_details: {
91
+ ...subscription.payment_details,
92
+ [paymentMethod.type]: paymentDetails,
93
+ },
94
+ });
95
+
96
+ await new SubscriptionWillCanceledSchedule().deleteScheduleSubscriptionJobs([subscription]);
97
+
98
+ subscriptionQueue
99
+ .delete(`cancel-${subscription.id}`)
100
+ .then(() => logger.info('subscription cancel job is canceled'))
101
+ .catch((err) => logger.error('subscription cancel job failed to cancel', { error: err }));
102
+ await addSubscriptionJob(subscription, 'cycle');
103
+
104
+ logger.info('Subscription resumed with re-stake', {
105
+ subscriptionId: subscription.id,
106
+ stakingTxHash: paymentDetails?.staking?.tx_hash,
107
+ stakingAddress: paymentDetails?.staking?.address,
108
+ });
109
+ return {
110
+ hash: paymentDetails.staking?.tx_hash,
111
+ };
112
+ }
113
+
114
+ throw new Error(`Payment method ${paymentMethod.type} not supported`);
115
+ },
116
+ };
@@ -15,6 +15,7 @@ import {
15
15
  getAuthPrincipalClaim,
16
16
  getDelegationTxClaim,
17
17
  getStakeTxClaim,
18
+ returnStakeForCanceledSubscription,
18
19
  } from './shared';
19
20
  import { ensureStakeInvoice } from '../../libs/invoice';
20
21
  import { EVM_CHAIN_TYPES } from '../../libs/constants';
@@ -239,6 +240,10 @@ export default {
239
240
  );
240
241
  await afterTxExecution(paymentDetails);
241
242
 
243
+ if (subscription.recovered_from) {
244
+ returnStakeForCanceledSubscription(subscription.recovered_from);
245
+ }
246
+
242
247
  return { hash: paymentDetails.tx_hash };
243
248
  } catch (err) {
244
249
  logger.error('Failed to finalize setup', { setupIntent: setupIntent.id, error: err });
@@ -43,6 +43,7 @@ import { SetupIntent } from '../../store/models/setup-intent';
43
43
  import { Subscription } from '../../store/models/subscription';
44
44
  import { ensureInvoiceAndItems } from '../../libs/invoice';
45
45
  import { CHARGE_SUPPORTED_CHAIN_TYPES, EVM_CHAIN_TYPES } from '../../libs/constants';
46
+ import { returnStakeQueue } from '../../queues/subscription';
46
47
 
47
48
  type Result = {
48
49
  checkoutSession: CheckoutSession;
@@ -783,6 +784,15 @@ export async function getDelegationTxClaim({
783
784
  throw new Error(`getDelegationTxClaim: Payment method ${paymentMethod.type} not supported`);
784
785
  }
785
786
 
787
+ export function getStakeAmount(subscription: Subscription, paymentCurrency: PaymentCurrency, items: TLineItemExpanded[]) {
788
+ const billingThreshold = Number(subscription.billing_thresholds?.amount_gte || 0);
789
+ const minStakeAmount = Number(subscription.billing_thresholds?.stake_gte || 0);
790
+ const threshold = fromTokenToUnit(Math.max(billingThreshold, minStakeAmount), paymentCurrency.decimal);
791
+ const staking = getSubscriptionStakeSetup(items, paymentCurrency.id, threshold.toString());
792
+ const amount = staking.licensed.add(staking.metered).toString();
793
+ return amount;
794
+ }
795
+
786
796
  export async function getStakeTxClaim({
787
797
  userDid,
788
798
  userPk,
@@ -1135,6 +1145,50 @@ export async function ensureChangePaymentContext(subscriptionId: string) {
1135
1145
  };
1136
1146
  }
1137
1147
 
1148
+ export async function ensureReStakeContext(subscriptionId: string) {
1149
+ const subscription = await Subscription.findByPk(subscriptionId);
1150
+ if (!subscription) {
1151
+ throw new Error(`Subscription not found: ${subscriptionId}`);
1152
+ }
1153
+ if (subscription.status === 'canceled') {
1154
+ throw new Error(`Subscription ${subscriptionId} not recoverable from cancellation`);
1155
+ }
1156
+ if (!subscription.cancel_at_period_end) {
1157
+ throw new Error(`Subscription ${subscriptionId} not recoverable from cancellation config`);
1158
+ }
1159
+ if (subscription.cancelation_details?.reason === 'payment_failed') {
1160
+ throw new Error(`Subscription ${subscriptionId} not recoverable from payment failed`);
1161
+ }
1162
+
1163
+ const paymentCurrency = await PaymentCurrency.findByPk(subscription.currency_id);
1164
+ if (!paymentCurrency) {
1165
+ throw new Error(`PaymentCurrency ${subscription.currency_id} not found for subscription ${subscriptionId}`);
1166
+ }
1167
+ const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
1168
+ if (!paymentMethod) {
1169
+ throw new Error(`Payment method not found for subscription ${subscriptionId}`);
1170
+ }
1171
+
1172
+ if (paymentMethod.type !== 'arcblock') {
1173
+ throw new Error(`Payment method ${paymentMethod.type} not supported for subscription ${subscriptionId}`);
1174
+ }
1175
+
1176
+ const customer = await Customer.findByPk(subscription.customer_id);
1177
+ if (!customer) {
1178
+ throw new Error(`Customer not found for subscription ${subscriptionId}`);
1179
+ }
1180
+
1181
+ // @ts-ignore
1182
+ subscription.items = await expandSubscriptionItems(subscription.id);
1183
+
1184
+ return {
1185
+ subscription,
1186
+ paymentCurrency,
1187
+ paymentMethod,
1188
+ customer,
1189
+ payerAddress: getSubscriptionPaymentAddress(subscription, paymentMethod?.type),
1190
+ };
1191
+ }
1138
1192
  export async function ensureSubscriptionForCollectBatch(
1139
1193
  subscriptionId?: string,
1140
1194
  currencyId?: string,
@@ -1370,3 +1424,28 @@ export async function updateStripeSubscriptionAfterChangePayment(setupIntent: Se
1370
1424
  }
1371
1425
  }
1372
1426
 
1427
+ export async function returnStakeForCanceledSubscription(subscriptionId: string) {
1428
+ if (!subscriptionId) {
1429
+ return;
1430
+ }
1431
+ try {
1432
+ const subscription = await Subscription.findByPk(subscriptionId);
1433
+ if (!subscription) {
1434
+ throw new Error(`Subscription ${subscriptionId} not found`);
1435
+ }
1436
+ if (subscription.status !== 'canceled') {
1437
+ throw new Error(`Subscription ${subscriptionId} is not canceled`);
1438
+ }
1439
+
1440
+ if (!subscription.payment_details?.arcblock?.staking?.tx_hash) {
1441
+ throw new Error(`No staking transaction found in subscription ${subscriptionId}`);
1442
+ }
1443
+ returnStakeQueue.push({ id: `return-stake-${subscription.id}`, job: { subscriptionId: subscription.id } });
1444
+ logger.info('Subscription return stake job scheduled', {
1445
+ jobId: `return-stake-${subscription.id}`,
1446
+ subscription: subscription.id,
1447
+ });
1448
+ } catch (err) {
1449
+ logger.error('returnStakeForCanceledSubscription failed', { error: err, subscriptionId });
1450
+ }
1451
+ }
@@ -17,6 +17,7 @@ import { isDelegationSufficientForPayment } from '../libs/payment';
17
17
  import { authenticate } from '../libs/security';
18
18
  import { expandLineItems, getFastCheckoutAmount, getSubscriptionCreateSetup, isLineItemAligned } from '../libs/session';
19
19
  import {
20
+ checkRemainingStake,
20
21
  createProration,
21
22
  finalizeSubscriptionUpdate,
22
23
  getPastInvoicesAmount,
@@ -33,6 +34,7 @@ import { invoiceQueue } from '../queues/invoice';
33
34
  import {
34
35
  addSubscriptionJob,
35
36
  returnOverdraftProtectionQueue,
37
+ returnStakeQueue,
36
38
  slashOverdraftProtectionQueue,
37
39
  slashStakeQueue,
38
40
  subscriptionQueue,
@@ -426,6 +428,42 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
426
428
  return res.json(subscription);
427
429
  });
428
430
 
431
+ router.get('/:id/recover-info', authPortal, async (req, res) => {
432
+ const doc = await Subscription.findByPk(req.params.id);
433
+
434
+ if (!doc) {
435
+ return res.status(404).json({ error: 'Subscription not found' });
436
+ }
437
+
438
+ const paymentMethod = await PaymentMethod.findByPk(doc.default_payment_method_id);
439
+ const paymentCurrency = await PaymentCurrency.findByPk(doc.currency_id);
440
+
441
+ let needStake = false;
442
+ let revokedStake = '0';
443
+
444
+ if (paymentMethod?.type === 'arcblock' && paymentCurrency) {
445
+ const address = doc.payment_details?.arcblock?.staking?.address;
446
+ if (address) {
447
+ try {
448
+ const { revoked } = await checkRemainingStake(paymentMethod, paymentCurrency, address, '0');
449
+ const cancelReason = doc.cancelation_details?.reason;
450
+ if (revoked && revoked !== '0' && cancelReason === 'stake_revoked') {
451
+ needStake = true;
452
+ revokedStake = revoked;
453
+ }
454
+ } catch (err) {
455
+ console.error(`Failed to check remaining stake for subscription ${doc.id}`, { error: err });
456
+ }
457
+ }
458
+ }
459
+
460
+ return res.json({
461
+ subscription: doc,
462
+ needStake,
463
+ revokedStake,
464
+ });
465
+ });
466
+
429
467
  router.put('/:id/recover', authPortal, async (req, res) => {
430
468
  const doc = await Subscription.findByPk(req.params.id);
431
469
 
@@ -442,13 +480,43 @@ router.put('/:id/recover', authPortal, async (req, res) => {
442
480
  return res.status(400).json({ error: 'Subscription not recoverable from cancellation' });
443
481
  }
444
482
 
483
+ const paymentMethod = await PaymentMethod.findByPk(doc.default_payment_method_id);
484
+ if (!paymentMethod) {
485
+ return res.status(400).json({ error: 'Payment method not found' });
486
+ }
487
+ const paymentCurrency = await PaymentCurrency.findByPk(doc.currency_id);
488
+ if (!paymentCurrency) {
489
+ return res.status(400).json({ error: 'Payment currency not found' });
490
+ }
491
+
492
+ // check if need stake
493
+ if (paymentMethod.type === 'arcblock') {
494
+ const address = doc.payment_details?.arcblock?.staking?.address;
495
+ if (address) {
496
+ try {
497
+ const { revoked } = await checkRemainingStake(paymentMethod, paymentCurrency, address, '0');
498
+ const cancelReason = doc.cancelation_details?.reason;
499
+ if (revoked && revoked !== '0' && cancelReason === 'stake_revoked') {
500
+ return res.json({
501
+ needStake: true,
502
+ subscription: doc,
503
+ revoked,
504
+ });
505
+ }
506
+ } catch (err) {
507
+ logger.error('subscription recover failed to check remaining stake', { error: err });
508
+ }
509
+ }
510
+ }
511
+
445
512
  if (doc.cancel_at_period_end) {
446
513
  await updateStripeSubscription(doc, { cancel_at_period_end: false });
447
514
  } else {
448
515
  await updateStripeSubscription(doc, { cancel_at: null });
449
516
  }
450
517
 
451
- await doc.update({ cancel_at_period_end: false, cancel_at: 0, canceled_at: 0 });
518
+ // @ts-ignore
519
+ await doc.update({ cancel_at_period_end: false, cancel_at: 0, canceled_at: 0, cancelation_details: null });
452
520
  await new SubscriptionWillCanceledSchedule().deleteScheduleSubscriptionJobs([doc]);
453
521
  // reschedule jobs
454
522
  subscriptionQueue
@@ -457,7 +525,7 @@ router.put('/:id/recover', authPortal, async (req, res) => {
457
525
  .catch((err) => logger.error('subscription cancel job failed to cancel', { error: err }));
458
526
  await addSubscriptionJob(doc, 'cycle');
459
527
 
460
- return res.json(doc);
528
+ return res.json({ subscription: doc });
461
529
  });
462
530
 
463
531
  router.put('/:id/pause', auth, async (req, res) => {
@@ -520,6 +588,26 @@ router.put('/:id/resume', auth, async (req, res) => {
520
588
  return res.json(doc);
521
589
  });
522
590
 
591
+ router.put('/:id/return-stake', authPortal, async (req, res) => {
592
+ const doc = await Subscription.findByPk(req.params.id);
593
+ if (!doc) {
594
+ return res.status(404).json({ error: 'Subscription not found' });
595
+ }
596
+ if (doc.status !== 'canceled') {
597
+ return res.status(400).json({ error: 'Subscription is not canceled' });
598
+ }
599
+
600
+ if (!doc.payment_details?.arcblock?.staking?.tx_hash) {
601
+ return res.status(400).json({ error: 'No staking transaction found in subscription' });
602
+ }
603
+ returnStakeQueue.push({ id: `return-stake-${doc.id}`, job: { subscriptionId: doc.id } });
604
+ logger.info('Subscription return stake job scheduled', {
605
+ jobId: `return-stake-${doc.id}`,
606
+ subscription: doc.id,
607
+ });
608
+ return res.json({ success: true, subscriptionId: doc.id });
609
+ });
610
+
523
611
  const isValidSubscriptionItemChange = (item: SubscriptionUpdateItem) => {
524
612
  if (item.deleted) {
525
613
  if (!item.id) {
@@ -300,6 +300,33 @@ describe('mergePaginate', () => {
300
300
  expect(result.data.map((item) => item.id)).toEqual(['id_4', 'id_2', 'id_3', 'id_1']);
301
301
  });
302
302
 
303
+ it('should handle 29 records with last page showing correct 9 items', async () => {
304
+ const baseDate = new Date('2024-01-01T00:00:00Z');
305
+
306
+ // Create 3 data sources with uneven distribution: 10, 10, 9 records
307
+ const source1 = createTestDataSource(createTestItems(10, baseDate));
308
+ const source2 = createTestDataSource(createTestItems(10, new Date(baseDate.getTime() + 10000)));
309
+ const source3 = createTestDataSource(createTestItems(9, new Date(baseDate.getTime() + 20000)));
310
+
311
+ // Test the last page (page 3, should have 9 items: records 21-29)
312
+ const result = await mergePaginate(
313
+ [source1, source2, source3],
314
+ { page: 3, pageSize: 10 },
315
+ defaultTimeOrderBy('desc')
316
+ );
317
+
318
+ expect(result.total).toBe(29);
319
+ expect(result.data).toHaveLength(9); // Should show all 9 remaining items, not just 2
320
+ expect(result.paging).toEqual({
321
+ page: 3,
322
+ pageSize: 10,
323
+ totalPages: 3,
324
+ });
325
+
326
+ // Verify we got the correct range of items (the oldest ones)
327
+ expect(result.data.every((item) => item.value < 20)).toBe(true); // These should be the oldest items
328
+ });
329
+
303
330
  it('should handle multiple data sources with late pages correctly', async () => {
304
331
  const baseDate = new Date('2024-01-01T00:00:00Z');
305
332
 
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.18.47
17
+ version: 1.18.48
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.18.47",
3
+ "version": "1.18.48",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -47,17 +47,17 @@
47
47
  "@abtnode/cron": "^1.16.43",
48
48
  "@arcblock/did": "^1.20.11",
49
49
  "@arcblock/did-auth-storage-nedb": "^1.7.1",
50
- "@arcblock/did-connect": "^2.13.47",
50
+ "@arcblock/did-connect": "^2.13.54",
51
51
  "@arcblock/did-util": "^1.20.11",
52
52
  "@arcblock/jwt": "^1.20.11",
53
- "@arcblock/ux": "^2.13.47",
53
+ "@arcblock/ux": "^2.13.54",
54
54
  "@arcblock/validator": "^1.20.11",
55
55
  "@blocklet/did-space-js": "^1.0.56",
56
56
  "@blocklet/js-sdk": "^1.16.43",
57
57
  "@blocklet/logger": "^1.16.43",
58
- "@blocklet/payment-react": "1.18.47",
58
+ "@blocklet/payment-react": "1.18.48",
59
59
  "@blocklet/sdk": "^1.16.43",
60
- "@blocklet/ui-react": "^2.13.47",
60
+ "@blocklet/ui-react": "^2.13.54",
61
61
  "@blocklet/uploader": "^0.1.93",
62
62
  "@blocklet/xss": "^0.1.36",
63
63
  "@mui/icons-material": "^5.16.6",
@@ -123,7 +123,7 @@
123
123
  "devDependencies": {
124
124
  "@abtnode/types": "^1.16.43",
125
125
  "@arcblock/eslint-config-ts": "^0.3.3",
126
- "@blocklet/payment-types": "1.18.47",
126
+ "@blocklet/payment-types": "1.18.48",
127
127
  "@types/cookie-parser": "^1.4.7",
128
128
  "@types/cors": "^2.8.17",
129
129
  "@types/debug": "^4.1.12",
@@ -169,5 +169,5 @@
169
169
  "parser": "typescript"
170
170
  }
171
171
  },
172
- "gitHead": "e781286c5a20e54ef0eb7e029505f32c00cb33e2"
172
+ "gitHead": "4042dc2996b11ab6ccf6a973af53042cff913474"
173
173
  }
package/scripts/sdk.js CHANGED
@@ -539,6 +539,13 @@ const subscriptionModule = {
539
539
  console.log('createBatchUsageRecords', results);
540
540
  return results;
541
541
  },
542
+
543
+ // 返回质押
544
+ async returnStake() {
545
+ const result = await payment.subscriptions.returnStake('sub_pWYXj2fale49qeFfEpunTmZq');
546
+ console.log('returnStake', result);
547
+ return result;
548
+ },
542
549
  };
543
550
 
544
551
  const testModules = {
@@ -5,12 +5,12 @@ import {
5
5
  ConfirmDialog,
6
6
  api,
7
7
  formatError,
8
- formatToDate,
9
8
  getPrefix,
10
9
  getSubscriptionAction,
11
10
  usePaymentContext,
12
11
  OverdueInvoicePayment,
13
12
  formatBNStr,
13
+ ResumeSubscription,
14
14
  } from '@blocklet/payment-react';
15
15
  import type { TSubscriptionExpanded } from '@blocklet/payment-types';
16
16
  import { Button, Link, Stack, Tooltip, Typography, Box, Alert } from '@mui/material';
@@ -27,7 +27,7 @@ import CustomerCancelForm from './cancel';
27
27
  import OverdraftProtectionDialog from '../../customer/overdraft-protection';
28
28
  import Actions from '../../actions';
29
29
  import { useUnpaidInvoicesCheckForSubscription } from '../../../hooks/subscription';
30
- import { isWillCanceled } from '../../../libs/util';
30
+ import { isActive, isWillCanceled } from '../../../libs/util';
31
31
 
32
32
  interface ActionConfig {
33
33
  key: string;
@@ -105,34 +105,26 @@ SubscriptionActions.defaultProps = {
105
105
  forceShowDetailAction: false,
106
106
  buttonSize: 'small',
107
107
  };
108
- const fetchExtraActions = async ({
109
- id,
110
- showExtra,
111
- }: {
112
- id: string;
113
- showExtra: boolean;
114
- }): Promise<{ changePlan: boolean; batchPay: string }> => {
115
- if (!showExtra) {
116
- return Promise.resolve({ changePlan: false, batchPay: '' });
117
- }
118
108
 
119
- const [changePlan, batchPay] = await Promise.all([
120
- api
121
- .get(`/api/subscriptions/${id}/change-plan`)
122
- .then((res) => !!res.data)
123
- .catch(() => false),
124
- api
125
- .get(`/api/subscriptions/${id}/summary`)
126
- .then((res) => {
127
- if (!isEmpty(res.data) && Object.keys(res.data).length === 1) {
128
- return Object.keys(res.data)[0] as string;
129
- }
130
- return '';
131
- })
132
- .catch(() => ''),
133
- ]);
109
+ const fetchChangePlan = async (id: string): Promise<boolean> => {
110
+ try {
111
+ const res = await api.get(`/api/subscriptions/${id}/change-plan`);
112
+ return !!res.data;
113
+ } catch {
114
+ return false;
115
+ }
116
+ };
134
117
 
135
- return { changePlan, batchPay };
118
+ const fetchBatchPay = async (id: string): Promise<string> => {
119
+ try {
120
+ const res = await api.get(`/api/subscriptions/${id}/summary`);
121
+ if (!isEmpty(res.data) && Object.keys(res.data).length === 1) {
122
+ return Object.keys(res.data)[0] as string;
123
+ }
124
+ return '';
125
+ } catch {
126
+ return '';
127
+ }
136
128
  };
137
129
 
138
130
  const supportRecharge = (subscription: TSubscriptionExpanded) => {
@@ -177,7 +169,13 @@ export function SubscriptionActionsInner({
177
169
  return true;
178
170
  };
179
171
 
180
- const { data: extraActions } = useRequest(() => fetchExtraActions({ id: subscription.id, showExtra: !!showExtra }));
172
+ const { data: changePlanAvailable = false } = useRequest(() => fetchChangePlan(subscription.id), {
173
+ ready: !!showExtra && isActive(subscription),
174
+ });
175
+
176
+ const { data: batchPayAvailable = '' } = useRequest(() => fetchBatchPay(subscription.id), {
177
+ ready: !!showExtra,
178
+ });
181
179
 
182
180
  const { data: upcoming = {} } = useRequest(
183
181
  () => api.get(`/api/subscriptions/${subscription.id}/upcoming`).then((res) => res.data),
@@ -269,21 +267,6 @@ export function SubscriptionActionsInner({
269
267
  }
270
268
  };
271
269
 
272
- const handleRecover = async () => {
273
- try {
274
- setState({ loading: true });
275
- const sub = await api.put(`/api/subscriptions/${state.subscription}/recover`).then((res) => res.data);
276
- setSubscription(sub);
277
- Toast.success(t('common.saved'));
278
- if (onChange) onChange(state.action);
279
- } catch (err) {
280
- console.error(err);
281
- Toast.error(formatError(err));
282
- } finally {
283
- setState({ loading: false, action: '', subscription: '' });
284
- }
285
- };
286
-
287
270
  const handleDelegate = () => {
288
271
  connect.open({
289
272
  containerEl: undefined as unknown as Element,
@@ -503,7 +486,7 @@ export function SubscriptionActionsInner({
503
486
  },
504
487
  {
505
488
  key: 'changePlan',
506
- show: !!extraActions?.changePlan,
489
+ show: changePlanAvailable,
507
490
  label: action?.text || t('payment.customer.changePlan.button'),
508
491
  onClick: (e) => {
509
492
  e?.stopPropagation();
@@ -515,7 +498,7 @@ export function SubscriptionActionsInner({
515
498
  },
516
499
  {
517
500
  key: 'batchPay',
518
- show: !!extraActions?.batchPay,
501
+ show: !!batchPayAvailable,
519
502
  label: action?.text || t('admin.subscription.batchPay.button'),
520
503
  onClick: (e) => {
521
504
  e?.stopPropagation();
@@ -530,7 +513,7 @@ export function SubscriptionActionsInner({
530
513
  },
531
514
  {
532
515
  key: 'mainAction',
533
- show: !!(!extraActions?.batchPay && supportAction),
516
+ show: !!(!batchPayAvailable && supportAction),
534
517
  label: action?.text || t(`payment.customer.${action?.action}.button`),
535
518
  onClick: (e) => {
536
519
  e?.stopPropagation();
@@ -660,18 +643,17 @@ export function SubscriptionActionsInner({
660
643
  />
661
644
  )}
662
645
  {state.action === 'recover' && state.subscription && (
663
- <ConfirmDialog
664
- onConfirm={handleRecover}
665
- onCancel={(e) => {
666
- e.stopPropagation();
646
+ <ResumeSubscription
647
+ subscriptionId={state.subscription}
648
+ onResumed={(updatedSubscription) => {
667
649
  setState({ action: '', subscription: '' });
650
+ onChange?.(state.action);
651
+ setSubscription(updatedSubscription as TSubscriptionExpanded);
652
+ }}
653
+ dialogProps={{
654
+ open: true,
655
+ onClose: () => setState({ action: '', subscription: '' }),
668
656
  }}
669
- title={t('payment.customer.recover.title')}
670
- message={t('payment.customer.recover.description', {
671
- date: formatToDate(subscription.current_period_end * 1000),
672
- })}
673
- loading={state.loading}
674
- color="primary"
675
657
  />
676
658
  )}
677
659
 
package/src/libs/util.ts CHANGED
@@ -348,6 +348,9 @@ export function getAppInfo(address: string): { name: string; avatar: string; typ
348
348
  return null;
349
349
  }
350
350
 
351
+ export function isActive(subscription: TSubscriptionExpanded) {
352
+ return ['active', 'trialing'].includes(subscription.status);
353
+ }
351
354
  export function isWillCanceled(subscription: TSubscriptionExpanded) {
352
355
  const now = Date.now() / 1000;
353
356
  if (