payment-kit 1.14.34 → 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.
- package/api/src/libs/subscription.ts +66 -23
- package/api/src/queues/invoice.ts +5 -0
- package/api/src/queues/subscription.ts +2 -1
- package/api/src/routes/checkout-sessions.ts +15 -5
- package/api/src/routes/connect/shared.ts +1 -0
- package/api/src/routes/invoices.ts +1 -1
- package/api/src/routes/subscriptions.ts +47 -7
- package/blocklet.yml +1 -1
- package/package.json +4 -4
- package/src/components/actions.tsx +32 -12
- package/src/components/subscription/actions/cancel.tsx +2 -2
- package/src/components/subscription/actions/index.tsx +50 -4
- package/src/locales/en.tsx +3 -0
- package/src/locales/zh.tsx +2 -0
- package/src/pages/admin/customers/customers/index.tsx +1 -1
|
@@ -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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
-
|
|
733
|
-
|
|
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
|
|
779
|
+
total = total.add(new BN(staked?.value || '0'));
|
|
737
780
|
}
|
|
738
781
|
if (revoked) {
|
|
739
|
-
total = total.add(new BN(revoked
|
|
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
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
|
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
|
|
1250
|
+
const returnResult = await getSubscriptionStakeReturnSetup(subscription, address, paymentMethod);
|
|
1251
|
+
const slashResult = await getSubscriptionStakeSlashSetup(subscription, address, paymentMethod);
|
|
1253
1252
|
return res.json({
|
|
1254
|
-
return_amount:
|
|
1255
|
-
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.36",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"@arcblock/validator": "^1.18.132",
|
|
53
53
|
"@blocklet/js-sdk": "1.16.30",
|
|
54
54
|
"@blocklet/logger": "1.16.30",
|
|
55
|
-
"@blocklet/payment-react": "1.14.
|
|
55
|
+
"@blocklet/payment-react": "1.14.36",
|
|
56
56
|
"@blocklet/sdk": "1.16.30",
|
|
57
57
|
"@blocklet/ui-react": "^2.10.23",
|
|
58
58
|
"@blocklet/uploader": "^0.1.27",
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"devDependencies": {
|
|
120
120
|
"@abtnode/types": "1.16.30",
|
|
121
121
|
"@arcblock/eslint-config-ts": "^0.3.2",
|
|
122
|
-
"@blocklet/payment-types": "1.14.
|
|
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",
|
|
@@ -161,5 +161,5 @@
|
|
|
161
161
|
"parser": "typescript"
|
|
162
162
|
}
|
|
163
163
|
},
|
|
164
|
-
"gitHead": "
|
|
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
|
-
|
|
67
|
-
key={action.label}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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?.
|
|
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:
|
|
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
|
}
|
package/src/locales/en.tsx
CHANGED
|
@@ -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: {
|
package/src/locales/zh.tsx
CHANGED