payment-kit 1.14.33 → 1.14.36

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.
@@ -110,29 +110,35 @@ export const getMinRetryMail = (interval: string) => {
110
110
  return 15; // 18 hours
111
111
  };
112
112
 
113
+ const ZERO = new BN(0);
113
114
  export function getSubscriptionStakeSetup(items: TLineItemExpanded[], currencyId: string, billingThreshold = '0') {
114
115
  const staking = {
115
116
  licensed: new BN(0),
116
117
  metered: new BN(0),
117
118
  };
118
119
 
119
- items.forEach((x) => {
120
- const price = getSubscriptionItemPrice(x);
121
- const unit = getPriceUintAmountByCurrency(price, currencyId);
122
- const amount = new BN(unit).mul(new BN(x.quantity));
123
- if (price.type === 'recurring' && price.recurring) {
124
- if (price.recurring.usage_type === 'licensed') {
125
- staking.licensed = staking.licensed.add(amount);
126
- }
127
- if (price.recurring.usage_type === 'metered') {
128
- if (+billingThreshold) {
129
- staking.metered = new BN(billingThreshold);
130
- } else {
131
- staking.metered = staking.metered.add(amount);
120
+ const recurringItems = items
121
+ .map((x) => x.upsell_price || x.price)
122
+ .filter((x) => x.type === 'recurring' && x.recurring);
123
+ if (recurringItems.length > 0) {
124
+ if (new BN(billingThreshold).gt(ZERO)) {
125
+ staking.licensed = new BN(billingThreshold);
126
+ } else {
127
+ items.forEach((x) => {
128
+ const price = getSubscriptionItemPrice(x);
129
+ const unit = getPriceUintAmountByCurrency(price, currencyId);
130
+ const amount = new BN(unit).mul(new BN(x.quantity));
131
+ if (price.type === 'recurring' && price.recurring) {
132
+ if (price.recurring.usage_type === 'licensed') {
133
+ staking.licensed = staking.licensed.add(amount);
134
+ }
135
+ if (price.recurring.usage_type === 'metered') {
136
+ staking.metered = staking.metered.add(amount);
137
+ }
132
138
  }
133
- }
139
+ });
134
140
  }
135
- });
141
+ }
136
142
 
137
143
  return staking;
138
144
  }
@@ -660,10 +666,37 @@ export async function getRemainingStakes(subscriptionIds: string[], subscription
660
666
  return total.toString();
661
667
  }
662
668
 
669
+ export async function getSubscriptionStakeSlashSetup(
670
+ subscription: Subscription,
671
+ address: string,
672
+ paymentMethod: PaymentMethod
673
+ ) {
674
+ const lastInvoice = await Invoice.findByPk(subscription.latest_invoice_id);
675
+ const result = await getSubscriptionRemainingStakeSetup(subscription, address, paymentMethod, 'slash');
676
+ return {
677
+ ...result,
678
+ lastInvoice,
679
+ };
680
+ }
681
+
663
682
  export async function getSubscriptionStakeReturnSetup(
664
683
  subscription: Subscription,
665
684
  address: string,
666
685
  paymentMethod: PaymentMethod
686
+ ) {
687
+ const lastInvoice = await Invoice.findByPk(subscription.latest_invoice_id);
688
+ const result = await getSubscriptionRemainingStakeSetup(subscription, address, paymentMethod, 'return');
689
+ return {
690
+ ...result,
691
+ lastInvoice,
692
+ };
693
+ }
694
+
695
+ export async function getSubscriptionRemainingStakeSetup(
696
+ subscription: Subscription,
697
+ address: string,
698
+ paymentMethod: PaymentMethod,
699
+ action: 'return' | 'slash' = 'return'
667
700
  ) {
668
701
  const client = paymentMethod.getOcapClient();
669
702
  const { state } = await client.getStakeState({ address });
@@ -675,7 +708,12 @@ export async function getSubscriptionStakeReturnSetup(
675
708
  sender: '',
676
709
  };
677
710
  }
678
- const total = new BN(state.tokens.find((x: any) => x.address === currency.contract)?.value || '0');
711
+ let total = new BN(state.tokens.find((x: any) => x.address === currency.contract)?.value || '0');
712
+ if (action === 'slash') {
713
+ // add revoked tokens to total
714
+ const revoked = state.revokedTokens?.find((x: any) => x.address === currency.contract);
715
+ total = total.add(new BN(revoked?.value || '0'));
716
+ }
679
717
  const [summary] = await Invoice.getUncollectibleAmount({
680
718
  subscriptionId: subscription.id,
681
719
  currencyId: subscription.currency_id,
@@ -684,14 +722,12 @@ export async function getSubscriptionStakeReturnSetup(
684
722
  const subscriptionInitStakes = JSON.parse(state.data?.value || '{}');
685
723
  const initStake = subscriptionInitStakes[subscription.id];
686
724
  const uncollectibleAmountBN = new BN(summary?.[subscription.currency_id] || '0');
687
- const lastInvoice = await Invoice.findByPk(subscription.latest_invoice_id);
688
725
  if (state.nonce) {
689
726
  const returnStake = total.sub(uncollectibleAmountBN);
690
727
  return {
691
728
  total: total.toString(),
692
729
  return_amount: returnStake.lt(new BN(0)) ? '0' : returnStake.toString(),
693
730
  sender: state.sender,
694
- lastInvoice,
695
731
  };
696
732
  }
697
733
  const getReturnState = async () => {
@@ -709,7 +745,6 @@ export async function getSubscriptionStakeReturnSetup(
709
745
  total: initStake,
710
746
  return_amount: returnStake.lt(new BN(0)) ? '0' : returnStake.toString(),
711
747
  sender: state.sender,
712
- lastInvoice,
713
748
  };
714
749
  }
715
750
 
@@ -729,14 +764,22 @@ export async function checkRemainingStake(
729
764
  const client = paymentMethod.getOcapClient();
730
765
  const { state } = await client.getStakeState({ address });
731
766
 
732
- const staked = state.tokens.find((x: any) => x.address === paymentCurrency.contract);
733
- const revoked = state.revokedTokens.find((x: any) => x.address === paymentCurrency.contract);
767
+ if (!state) {
768
+ logger.warn('getStakeState failed in checkRemainingStake', { address, paymentMethod, paymentCurrency });
769
+ return {
770
+ enough: false,
771
+ staked: '0',
772
+ revoked: '0',
773
+ };
774
+ }
775
+ const staked = state.tokens?.find((x: any) => x.address === paymentCurrency.contract);
776
+ const revoked = state.revokedTokens?.find((x: any) => x.address === paymentCurrency.contract);
734
777
  let total = new BN(0);
735
778
  if (staked) {
736
- total = total.add(new BN(staked.value));
779
+ total = total.add(new BN(staked?.value || '0'));
737
780
  }
738
781
  if (revoked) {
739
- total = total.add(new BN(revoked.value));
782
+ total = total.add(new BN(revoked?.value || '0'));
740
783
  }
741
784
  return {
742
785
  enough: total.gte(new BN(amount)),
@@ -165,6 +165,11 @@ export const handleInvoice = async (job: InvoiceJob) => {
165
165
  return;
166
166
  }
167
167
 
168
+ const existJob = await paymentQueue.get(paymentIntent.id);
169
+ if (existJob) {
170
+ logger.warn('Payment job already scheduled', { invoice: invoice.id, paymentIntent: paymentIntent.id });
171
+ return;
172
+ }
168
173
  logger.info('Payment job scheduled', { invoice: invoice.id, paymentIntent: paymentIntent.id });
169
174
  if (job.waitForPayment) {
170
175
  await paymentQueue.pushAndWait({
@@ -16,6 +16,7 @@ import {
16
16
  getSubscriptionCycleAmount,
17
17
  getSubscriptionCycleSetup,
18
18
  getSubscriptionStakeReturnSetup,
19
+ getSubscriptionStakeSlashSetup,
19
20
  shouldCancelSubscription,
20
21
  } from '../libs/subscription';
21
22
  import { ensureInvoiceAndItems } from '../routes/connect/shared';
@@ -538,7 +539,7 @@ const slashStakeOnCancel = async (subscription: Subscription) => {
538
539
  });
539
540
  return;
540
541
  }
541
- const result = await getSubscriptionStakeReturnSetup(subscription, address, paymentMethod);
542
+ const result = await getSubscriptionStakeSlashSetup(subscription, address, paymentMethod);
542
543
  const stakeEnough = await checkRemainingStake(paymentMethod, currency, address, result.return_amount);
543
544
  if (!stakeEnough.enough) {
544
545
  logger.warn('Stake slashing aborted because no enough staking', {
@@ -46,7 +46,7 @@ import {
46
46
  import { CHECKOUT_SESSION_TTL, formatAmountPrecisionLimit, formatMetadata, getDataObjectFromQuery } from '../libs/util';
47
47
  import { invoiceQueue } from '../queues/invoice';
48
48
  import { paymentQueue } from '../queues/payment';
49
- import type { LineItem, TPriceExpanded, TProductExpanded } from '../store/models';
49
+ import type { LineItem, SubscriptionData, TPriceExpanded, TProductExpanded } from '../store/models';
50
50
  import { CheckoutSession } from '../store/models/checkout-session';
51
51
  import { Customer } from '../store/models/customer';
52
52
  import { PaymentCurrency } from '../store/models/payment-currency';
@@ -378,10 +378,20 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
378
378
  raw.submit_type = link.submit_type;
379
379
  raw.currency_id = link.currency_id || req.currency.id;
380
380
  raw.payment_link_id = link.id;
381
- raw.subscription_data = merge(link.subscription_data, getDataObjectFromQuery(req.query, 'subscription_data'), {
382
- billing_threshold_amount: getBillingThreshold(link.subscription_data),
383
- min_stake_amount: getMinStakeAmount(link.subscription_data),
384
- });
381
+
382
+ // Settings priority: PaymentLink.subscription_data > req.query > environments
383
+ const protectedSettings: Partial<SubscriptionData> = {};
384
+ if (link.subscription_data?.min_stake_amount) {
385
+ protectedSettings.min_stake_amount = getMinStakeAmount(link.subscription_data);
386
+ }
387
+ if (link.subscription_data?.billing_threshold_amount) {
388
+ protectedSettings.billing_threshold_amount = getBillingThreshold(link.subscription_data);
389
+ }
390
+ raw.subscription_data = merge(
391
+ link.subscription_data,
392
+ getDataObjectFromQuery(req.query, 'subscription_data'),
393
+ protectedSettings
394
+ );
385
395
 
386
396
  if (link.after_completion?.hosted_confirmation?.custom_message) {
387
397
  raw.payment_intent_data = {
@@ -740,6 +740,7 @@ export async function getStakeTxClaim({
740
740
  const threshold = fromTokenToUnit(Math.max(billingThreshold, minStakeAmount), paymentCurrency.decimal);
741
741
  const staking = getSubscriptionStakeSetup(items, paymentCurrency.id, threshold.toString());
742
742
  const amount = staking.licensed.add(staking.metered).toString();
743
+ logger.info('getStakeTxClaim', { subscriptionId: subscription.id, billingThreshold, minStakeAmount, threshold: threshold.toString(), staking, amount: amount.toString() });
743
744
 
744
745
  if (paymentMethod.type === 'arcblock') {
745
746
  // create staking data
@@ -147,7 +147,7 @@ router.get('/', authMine, async (req, res) => {
147
147
 
148
148
  let stakeAmount = data[subscription.id];
149
149
  if (state.nonce) {
150
- stakeAmount = state.tokens.find((x: any) => x.address === currency?.contract)?.value;
150
+ stakeAmount = state.tokens?.find((x: any) => x.address === currency?.contract)?.value;
151
151
  // stakeAmount should not be zero if nonce exist
152
152
  if (!Number(stakeAmount)) {
153
153
  if (subscription.cancelation_details?.return_stake) {
@@ -21,11 +21,12 @@ import {
21
21
  getSubscriptionCreateSetup,
22
22
  getSubscriptionRefundSetup,
23
23
  getSubscriptionStakeReturnSetup,
24
+ getSubscriptionStakeSlashSetup,
24
25
  getUpcomingInvoiceAmount,
25
26
  } from '../libs/subscription';
26
27
  import { MAX_SUBSCRIPTION_ITEM_COUNT, formatMetadata } from '../libs/util';
27
28
  import { invoiceQueue } from '../queues/invoice';
28
- import { addSubscriptionJob, subscriptionQueue } from '../queues/subscription';
29
+ import { addSubscriptionJob, slashStakeQueue, subscriptionQueue } from '../queues/subscription';
29
30
  import type { TLineItemExpanded } from '../store/models';
30
31
  import { Customer } from '../store/models/customer';
31
32
  import { Invoice } from '../store/models/invoice';
@@ -1235,9 +1236,6 @@ router.get('/:id/staking', authPortal, async (req, res) => {
1235
1236
  if (!subscription) {
1236
1237
  return res.status(404).json({ error: 'Subscription not found' });
1237
1238
  }
1238
- if (subscription.isActive() === false) {
1239
- return res.status(400).json({ error: 'Subscription is not active' });
1240
- }
1241
1239
 
1242
1240
  const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
1243
1241
  if (paymentMethod?.type !== 'arcblock') {
@@ -1249,12 +1247,15 @@ router.get('/:id/staking', authPortal, async (req, res) => {
1249
1247
  if (!address) {
1250
1248
  return res.status(400).json({ error: 'Staking not found on subscription payment detail' });
1251
1249
  }
1252
- const result = await getSubscriptionStakeReturnSetup(subscription, address, paymentMethod);
1250
+ const returnResult = await getSubscriptionStakeReturnSetup(subscription, address, paymentMethod);
1251
+ const slashResult = await getSubscriptionStakeSlashSetup(subscription, address, paymentMethod);
1253
1252
  return res.json({
1254
- return_amount: result.return_amount,
1255
- total: result.total,
1253
+ return_amount: returnResult.return_amount,
1254
+ total: returnResult.total,
1255
+ slash_amount: slashResult.return_amount,
1256
1256
  });
1257
1257
  } catch (err) {
1258
+ logger.error('subscription staking simulation failed', { error: err });
1258
1259
  console.error(err);
1259
1260
  return res.status(400).json({ error: err.message });
1260
1261
  }
@@ -1608,4 +1609,43 @@ router.get('/:id/upcoming', authPortal, async (req, res) => {
1608
1609
  }
1609
1610
  });
1610
1611
 
1612
+ // slash stake
1613
+ router.put('/:id/slash-stake', auth, async (req, res) => {
1614
+ const subscription = await Subscription.findByPk(req.params.id);
1615
+ if (!subscription) {
1616
+ return res.status(404).json({ error: 'Subscription not found' });
1617
+ }
1618
+
1619
+ if (subscription.status !== 'canceled') {
1620
+ return res.status(400).json({ error: `Subscription for ${subscription.id} not canceled` });
1621
+ }
1622
+
1623
+ const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
1624
+ if (paymentMethod?.type !== 'arcblock') {
1625
+ return res
1626
+ .status(400)
1627
+ .json({ error: `Stake slash not supported for subscription with payment method ${paymentMethod?.type}` });
1628
+ }
1629
+ const address = subscription?.payment_details?.arcblock?.staking?.address ?? undefined;
1630
+ if (!address) {
1631
+ return res.status(400).json({ error: 'Staking not found on subscription payment detail' });
1632
+ }
1633
+ try {
1634
+ await subscription.update({
1635
+ // @ts-ignore
1636
+ cancelation_details: {
1637
+ ...subscription.cancelation_details,
1638
+ slash_stake: true,
1639
+ },
1640
+ });
1641
+ const result = await slashStakeQueue.pushAndWait({
1642
+ id: `slash-stake-${subscription.id}`,
1643
+ job: { subscriptionId: subscription.id },
1644
+ });
1645
+ return res.json(result);
1646
+ } catch (err) {
1647
+ logger.error('subscription slash stake failed', { subscription: subscription.id, error: err.message });
1648
+ return res.status(400).json({ error: err.message });
1649
+ }
1650
+ });
1611
1651
  export default router;
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.14.33
17
+ version: 1.14.36
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.14.33",
3
+ "version": "1.14.36",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -42,7 +42,7 @@
42
42
  ]
43
43
  },
44
44
  "dependencies": {
45
- "@abtnode/cron": "1.16.28",
45
+ "@abtnode/cron": "1.16.30",
46
46
  "@arcblock/did": "^1.18.132",
47
47
  "@arcblock/did-auth-storage-nedb": "^1.7.1",
48
48
  "@arcblock/did-connect": "^2.10.23",
@@ -50,12 +50,12 @@
50
50
  "@arcblock/jwt": "^1.18.132",
51
51
  "@arcblock/ux": "^2.10.23",
52
52
  "@arcblock/validator": "^1.18.132",
53
- "@blocklet/js-sdk": "1.16.28",
54
- "@blocklet/logger": "1.16.28",
55
- "@blocklet/payment-react": "1.14.33",
56
- "@blocklet/sdk": "1.16.28",
53
+ "@blocklet/js-sdk": "1.16.30",
54
+ "@blocklet/logger": "1.16.30",
55
+ "@blocklet/payment-react": "1.14.36",
56
+ "@blocklet/sdk": "1.16.30",
57
57
  "@blocklet/ui-react": "^2.10.23",
58
- "@blocklet/uploader": "^0.1.23",
58
+ "@blocklet/uploader": "^0.1.27",
59
59
  "@mui/icons-material": "^5.16.6",
60
60
  "@mui/lab": "^5.0.0-alpha.173",
61
61
  "@mui/material": "^5.16.6",
@@ -117,9 +117,9 @@
117
117
  "validator": "^13.12.0"
118
118
  },
119
119
  "devDependencies": {
120
- "@abtnode/types": "1.16.28",
120
+ "@abtnode/types": "1.16.30",
121
121
  "@arcblock/eslint-config-ts": "^0.3.2",
122
- "@blocklet/payment-types": "1.14.33",
122
+ "@blocklet/payment-types": "1.14.36",
123
123
  "@types/cookie-parser": "^1.4.7",
124
124
  "@types/cors": "^2.8.17",
125
125
  "@types/debug": "^4.1.12",
@@ -145,7 +145,7 @@
145
145
  "typescript": "^4.9.5",
146
146
  "vite": "^5.3.5",
147
147
  "vite-node": "^2.0.4",
148
- "vite-plugin-blocklet": "^0.8.17",
148
+ "vite-plugin-blocklet": "^0.9.1",
149
149
  "vite-plugin-node-polyfills": "^0.21.0",
150
150
  "vite-plugin-svgr": "^4.2.0",
151
151
  "vite-tsconfig-paths": "^4.3.2",
@@ -161,5 +161,5 @@
161
161
  "parser": "typescript"
162
162
  }
163
163
  },
164
- "gitHead": "e5ae487796f7c104b577dbab2a937013c3fbac12"
164
+ "gitHead": "927e39d554524434d20f22a950eec503166ae529"
165
165
  }
@@ -1,8 +1,8 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
2
  import { stopEvent } from '@blocklet/payment-react';
3
3
  import { ExpandMoreOutlined, MoreHorizOutlined } from '@mui/icons-material';
4
- import { Button, IconButton, ListItemText, Menu, MenuItem } from '@mui/material';
5
- import React, { useState } from 'react';
4
+ import { Button, IconButton, ListItemText, Menu, MenuItem, Skeleton } from '@mui/material';
5
+ import React, { useRef, useState } from 'react';
6
6
  import type { LiteralUnion } from 'type-fest';
7
7
 
8
8
  type ActionItem = {
@@ -18,20 +18,34 @@ export type ActionsProps = {
18
18
  actions: ActionItem[];
19
19
  variant?: LiteralUnion<'compact' | 'normal', string>;
20
20
  sx?: any;
21
+ onOpenCallback?: Function;
21
22
  };
22
23
 
23
24
  Actions.defaultProps = {
24
25
  variant: 'compact',
25
26
  sx: {},
27
+ onOpenCallback: null,
26
28
  };
27
29
 
28
30
  export default function Actions(props: ActionsProps) {
29
31
  const { t } = useLocaleContext();
30
32
  const [anchorEl, setAnchorEl] = useState(null);
31
33
  const open = Boolean(anchorEl);
34
+ const anchorRef = useRef(null);
35
+ const [openLoading, setOpenLoading] = useState(false);
32
36
 
33
37
  const onOpen = (e: React.SyntheticEvent<any>) => {
34
38
  stopEvent(e);
39
+ anchorRef.current = e.currentTarget;
40
+ if (props.onOpenCallback && typeof props.onOpenCallback === 'function') {
41
+ const result = props.onOpenCallback();
42
+ if (result instanceof Promise) {
43
+ setOpenLoading(true);
44
+ result.finally(() => {
45
+ setOpenLoading(false);
46
+ });
47
+ }
48
+ }
35
49
  setAnchorEl(e.currentTarget);
36
50
  };
37
51
 
@@ -62,16 +76,22 @@ export default function Actions(props: ActionsProps) {
62
76
  onClose={onClose}
63
77
  anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
64
78
  transformOrigin={{ vertical: 'top', horizontal: 'right' }}>
65
- {props.actions.map((action) => (
66
- <MenuItem
67
- key={action.label}
68
- divider={!!action.divider}
69
- dense={!!action.dense}
70
- disabled={!!action.disabled}
71
- onClick={(e) => onClose(e, action.handler)}>
72
- <ListItemText primary={action.label} primaryTypographyProps={{ color: action.color }} />
73
- </MenuItem>
74
- ))}
79
+ {props.actions.map((action) =>
80
+ openLoading ? (
81
+ <MenuItem key={action.label} dense disabled>
82
+ <ListItemText primary={<Skeleton />} primaryTypographyProps={{ width: '56px' }} />
83
+ </MenuItem>
84
+ ) : (
85
+ <MenuItem
86
+ key={action.label}
87
+ divider={!!action.divider}
88
+ dense={!!action.dense}
89
+ disabled={!!action.disabled}
90
+ onClick={(e) => onClose(e, action.handler)}>
91
+ <ListItemText primary={action.label} primaryTypographyProps={{ color: action.color }} />
92
+ </MenuItem>
93
+ )
94
+ )}
75
95
  </Menu>
76
96
  </>
77
97
  );
@@ -10,7 +10,7 @@ const fetchData = (id: string, time: string): Promise<{ total: string; unused: s
10
10
  return api.get(`/api/subscriptions/${id}/proration?time=${encodeURIComponent(time)}`).then((res: any) => res.data);
11
11
  };
12
12
 
13
- const fetchStakingData = (id: string, time: string): Promise<{ return_amount: string }> => {
13
+ const fetchStakingData = (id: string, time: string): Promise<{ return_amount: string; slash_amount: string }> => {
14
14
  return api.get(`/api/subscriptions/${id}/staking?time=${encodeURIComponent(time)}`).then((res: any) => res.data);
15
15
  };
16
16
 
@@ -153,7 +153,7 @@ export default function SubscriptionCancelForm({ data }: { data: TSubscriptionEx
153
153
  onClick={() => !(loading || !staking) && setValue('cancel.staking', 'slash')}
154
154
  control={<Radio checked={stakingType === 'slash'} />}
155
155
  label={t('admin.subscription.cancel.staking.slash', {
156
- unused: formatAmount(staking?.return_amount || '0', decimal),
156
+ unused: formatAmount(staking?.slash_amount || '0', decimal),
157
157
  symbol,
158
158
  })}
159
159
  />
@@ -1,12 +1,13 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
2
  import Toast from '@arcblock/ux/lib/Toast';
3
- import { ConfirmDialog, api, formatError } from '@blocklet/payment-react';
3
+ import { ConfirmDialog, api, formatBNStr, formatError } from '@blocklet/payment-react';
4
4
  import type { TSubscriptionExpanded } from '@blocklet/payment-types';
5
- import { useSetState } from 'ahooks';
5
+ import { useRequest, useSetState } from 'ahooks';
6
6
  import { FormProvider, useForm, useFormContext } from 'react-hook-form';
7
7
  import { useNavigate } from 'react-router-dom';
8
8
  import type { LiteralUnion } from 'type-fest';
9
9
 
10
+ import { useMemo } from 'react';
10
11
  import Actions from '../../actions';
11
12
  import ClickBoundary from '../../click-boundary';
12
13
  import SubscriptionCancelForm from './cancel';
@@ -22,6 +23,10 @@ SubscriptionActionsInner.defaultProps = {
22
23
  variant: 'compact',
23
24
  };
24
25
 
26
+ const fetchStakingData = (id: string, time: string): Promise<{ return_amount: string; slash_amount: string }> => {
27
+ return api.get(`/api/subscriptions/${id}/staking?time=${encodeURIComponent(time)}`).then((res: any) => res.data);
28
+ };
29
+
25
30
  function SubscriptionActionsInner({ data, variant, onChange }: Props) {
26
31
  const { t } = useLocaleContext();
27
32
  const navigate = useNavigate();
@@ -31,6 +36,21 @@ function SubscriptionActionsInner({ data, variant, onChange }: Props) {
31
36
  loading: false,
32
37
  });
33
38
 
39
+ const {
40
+ data: stakeResult = {
41
+ return_amount: '0',
42
+ total: '0',
43
+ slash_amount: '0',
44
+ },
45
+ runAsync: fetchStakeResultAsync,
46
+ } = useRequest(() => fetchStakingData(data.id, ''), {
47
+ manual: true,
48
+ });
49
+
50
+ const stakeValue = useMemo(() => {
51
+ return formatBNStr(stakeResult?.slash_amount, data?.paymentCurrency?.decimal);
52
+ }, [stakeResult]);
53
+
34
54
  const createHandler = (action: string) => {
35
55
  return async () => {
36
56
  const values = getValues();
@@ -79,6 +99,8 @@ function SubscriptionActionsInner({ data, variant, onChange }: Props) {
79
99
  color: 'primary',
80
100
  };
81
101
 
102
+ const showSlashStake = data.status === 'canceled' && Number(stakeValue) > 0;
103
+
82
104
  const actions = [
83
105
  {
84
106
  label: t('admin.subscription.update'),
@@ -92,8 +114,20 @@ function SubscriptionActionsInner({ data, variant, onChange }: Props) {
92
114
  handler: () => setState({ action: 'cancel' }),
93
115
  disabled: data.status === 'canceled',
94
116
  color: 'error',
95
- divider: true,
117
+ divider: !showSlashStake,
96
118
  },
119
+ ...(showSlashStake
120
+ ? [
121
+ {
122
+ label: t('admin.subscription.cancel.staking.slashTitle'),
123
+ handler: () => {
124
+ setState({ action: 'slashStake' });
125
+ },
126
+ color: 'error',
127
+ divider: true,
128
+ },
129
+ ]
130
+ : []),
97
131
  {
98
132
  label: t('admin.customer.view'),
99
133
  handler: () => navigate(`/admin/customers/${data.customer_id}`),
@@ -111,7 +145,7 @@ function SubscriptionActionsInner({ data, variant, onChange }: Props) {
111
145
 
112
146
  return (
113
147
  <ClickBoundary>
114
- <Actions variant={variant} actions={actions} />
148
+ <Actions variant={variant} actions={actions} onOpenCallback={fetchStakeResultAsync} />
115
149
  {state.action === 'cancel' && (
116
150
  <ConfirmDialog
117
151
  onConfirm={createHandler('cancel')}
@@ -139,6 +173,18 @@ function SubscriptionActionsInner({ data, variant, onChange }: Props) {
139
173
  loading={state.loading}
140
174
  />
141
175
  )}
176
+ {state.action === 'slashStake' && (
177
+ <ConfirmDialog
178
+ onConfirm={createHandler('slash-stake')}
179
+ onCancel={handleCancel}
180
+ title={t('admin.subscription.cancel.staking.slashTitle')}
181
+ message={t('admin.subscription.cancel.staking.slashTip', {
182
+ unused: stakeValue,
183
+ symbol: data.paymentCurrency?.symbol,
184
+ })}
185
+ loading={state.loading}
186
+ />
187
+ )}
142
188
  </ClickBoundary>
143
189
  );
144
190
  }
@@ -465,6 +465,9 @@ export default flat({
465
465
  none: 'No return or slash',
466
466
  proration: 'Return Remaining Stake {unused}{symbol}',
467
467
  slash: 'Slash Remaining Stake {unused}{symbol}',
468
+ slashTip:
469
+ 'The remaining stake of this subscription {unused}{symbol} will be slashed, please confirm to continue?',
470
+ slashTitle: 'Slash stake',
468
471
  },
469
472
  },
470
473
  pause: {
@@ -456,6 +456,8 @@ export default flat({
456
456
  none: '不退还 / 罚没质押',
457
457
  proration: '退还剩余部分 {unused}{symbol}',
458
458
  slash: '罚没剩余部分 {unused}{symbol}',
459
+ slashTip: '该订阅剩余的质押部分 {unused}{symbol} 将被罚没, 请确认是否继续?',
460
+ slashTitle: '罚没质押',
459
461
  },
460
462
  },
461
463
  pause: {
@@ -64,7 +64,7 @@ export default function CustomersList() {
64
64
  variant="square"
65
65
  sx={{ borderRadius: 'var(--radius-m, 8px)' }}
66
66
  />
67
- <Typography>{item.name}</Typography>
67
+ <Typography sx={{ wordBreak: 'break-all' }}>{item.name}</Typography>
68
68
  </Stack>
69
69
  </Link>
70
70
  );