payment-kit 1.14.34 → 1.14.37
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/integrations/stripe/resource.ts +3 -3
- package/api/src/libs/subscription.ts +96 -35
- package/api/src/queues/invoice.ts +5 -0
- package/api/src/queues/subscription.ts +2 -1
- package/api/src/routes/checkout-sessions.ts +30 -14
- package/api/src/routes/connect/setup.ts +8 -3
- package/api/src/routes/connect/shared.ts +1 -0
- package/api/src/routes/connect/subscribe.ts +6 -3
- package/api/src/routes/invoices.ts +1 -1
- package/api/src/routes/subscriptions.ts +47 -7
- package/api/src/store/models/types.ts +1 -0
- package/api/tests/libs/subscription.spec.ts +30 -6
- package/blocklet.yml +1 -2
- 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
|
@@ -204,7 +204,7 @@ export async function ensureStripeSubscription(
|
|
|
204
204
|
currency: PaymentCurrency,
|
|
205
205
|
items: TLineItemExpanded[],
|
|
206
206
|
trialInDays: number = 0,
|
|
207
|
-
|
|
207
|
+
trialEnd: number = 0
|
|
208
208
|
) {
|
|
209
209
|
const client = method.getStripeClient();
|
|
210
210
|
|
|
@@ -255,8 +255,8 @@ export async function ensureStripeSubscription(
|
|
|
255
255
|
|
|
256
256
|
if (trialInDays) {
|
|
257
257
|
props.trial_period_days = trialInDays;
|
|
258
|
-
} else if (
|
|
259
|
-
props.trial_end =
|
|
258
|
+
} else if (trialEnd) {
|
|
259
|
+
props.trial_end = trialEnd;
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
stripeSubscription = await client.subscriptions.create(props);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import component from '@blocklet/sdk/lib/component';
|
|
3
3
|
import { BN } from '@ocap/util';
|
|
4
4
|
import isEmpty from 'lodash/isEmpty';
|
|
5
|
+
import trim from 'lodash/trim';
|
|
5
6
|
import pick from 'lodash/pick';
|
|
6
7
|
import type { LiteralUnion } from 'type-fest';
|
|
7
8
|
import { withQuery } from 'ufo';
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
PriceRecurring,
|
|
19
20
|
Refund,
|
|
20
21
|
Subscription,
|
|
22
|
+
SubscriptionData,
|
|
21
23
|
SubscriptionItem,
|
|
22
24
|
SubscriptionUpdateItem,
|
|
23
25
|
TLineItemExpanded,
|
|
@@ -110,29 +112,35 @@ export const getMinRetryMail = (interval: string) => {
|
|
|
110
112
|
return 15; // 18 hours
|
|
111
113
|
};
|
|
112
114
|
|
|
115
|
+
const ZERO = new BN(0);
|
|
113
116
|
export function getSubscriptionStakeSetup(items: TLineItemExpanded[], currencyId: string, billingThreshold = '0') {
|
|
114
117
|
const staking = {
|
|
115
118
|
licensed: new BN(0),
|
|
116
119
|
metered: new BN(0),
|
|
117
120
|
};
|
|
118
121
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
122
|
+
const recurringItems = items
|
|
123
|
+
.map((x) => x.upsell_price || x.price)
|
|
124
|
+
.filter((x) => x.type === 'recurring' && x.recurring);
|
|
125
|
+
if (recurringItems.length > 0) {
|
|
126
|
+
if (new BN(billingThreshold).gt(ZERO)) {
|
|
127
|
+
staking.licensed = new BN(billingThreshold);
|
|
128
|
+
} else {
|
|
129
|
+
items.forEach((x) => {
|
|
130
|
+
const price = getSubscriptionItemPrice(x);
|
|
131
|
+
const unit = getPriceUintAmountByCurrency(price, currencyId);
|
|
132
|
+
const amount = new BN(unit).mul(new BN(x.quantity));
|
|
133
|
+
if (price.type === 'recurring' && price.recurring) {
|
|
134
|
+
if (price.recurring.usage_type === 'licensed') {
|
|
135
|
+
staking.licensed = staking.licensed.add(amount);
|
|
136
|
+
}
|
|
137
|
+
if (price.recurring.usage_type === 'metered') {
|
|
138
|
+
staking.metered = staking.metered.add(amount);
|
|
139
|
+
}
|
|
132
140
|
}
|
|
133
|
-
}
|
|
141
|
+
});
|
|
134
142
|
}
|
|
135
|
-
}
|
|
143
|
+
}
|
|
136
144
|
|
|
137
145
|
return staking;
|
|
138
146
|
}
|
|
@@ -141,7 +149,7 @@ export function getSubscriptionCreateSetup(
|
|
|
141
149
|
items: TLineItemExpanded[],
|
|
142
150
|
currencyId: string,
|
|
143
151
|
trialInDays = 0,
|
|
144
|
-
|
|
152
|
+
trialEnd = 0
|
|
145
153
|
) {
|
|
146
154
|
let setup = new BN(0);
|
|
147
155
|
|
|
@@ -164,18 +172,18 @@ export function getSubscriptionCreateSetup(
|
|
|
164
172
|
const recurring = (item?.upsell_price || item?.price)?.recurring as PriceRecurring;
|
|
165
173
|
const cycle = getRecurringPeriod(recurring);
|
|
166
174
|
|
|
167
|
-
let
|
|
168
|
-
let
|
|
169
|
-
if (+
|
|
170
|
-
|
|
171
|
-
|
|
175
|
+
let trialStartAt = 0;
|
|
176
|
+
let trialEndAt = 0;
|
|
177
|
+
if (+trialEnd && trialEnd > now) {
|
|
178
|
+
trialStartAt = now;
|
|
179
|
+
trialEndAt = trialEnd;
|
|
172
180
|
} else if (trialInDays) {
|
|
173
|
-
|
|
174
|
-
|
|
181
|
+
trialStartAt = now;
|
|
182
|
+
trialEndAt = dayjs().add(trialInDays, 'day').unix();
|
|
175
183
|
}
|
|
176
184
|
|
|
177
|
-
const periodStart =
|
|
178
|
-
const periodEnd =
|
|
185
|
+
const periodStart = trialStartAt || now;
|
|
186
|
+
const periodEnd = trialEndAt || dayjs().add(cycle, 'millisecond').unix();
|
|
179
187
|
|
|
180
188
|
return {
|
|
181
189
|
recurring,
|
|
@@ -184,8 +192,8 @@ export function getSubscriptionCreateSetup(
|
|
|
184
192
|
anchor: periodEnd,
|
|
185
193
|
},
|
|
186
194
|
trial: {
|
|
187
|
-
start:
|
|
188
|
-
end:
|
|
195
|
+
start: trialStartAt,
|
|
196
|
+
end: trialEndAt,
|
|
189
197
|
},
|
|
190
198
|
period: {
|
|
191
199
|
start: periodStart,
|
|
@@ -660,10 +668,37 @@ export async function getRemainingStakes(subscriptionIds: string[], subscription
|
|
|
660
668
|
return total.toString();
|
|
661
669
|
}
|
|
662
670
|
|
|
671
|
+
export async function getSubscriptionStakeSlashSetup(
|
|
672
|
+
subscription: Subscription,
|
|
673
|
+
address: string,
|
|
674
|
+
paymentMethod: PaymentMethod
|
|
675
|
+
) {
|
|
676
|
+
const lastInvoice = await Invoice.findByPk(subscription.latest_invoice_id);
|
|
677
|
+
const result = await getSubscriptionRemainingStakeSetup(subscription, address, paymentMethod, 'slash');
|
|
678
|
+
return {
|
|
679
|
+
...result,
|
|
680
|
+
lastInvoice,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
663
684
|
export async function getSubscriptionStakeReturnSetup(
|
|
664
685
|
subscription: Subscription,
|
|
665
686
|
address: string,
|
|
666
687
|
paymentMethod: PaymentMethod
|
|
688
|
+
) {
|
|
689
|
+
const lastInvoice = await Invoice.findByPk(subscription.latest_invoice_id);
|
|
690
|
+
const result = await getSubscriptionRemainingStakeSetup(subscription, address, paymentMethod, 'return');
|
|
691
|
+
return {
|
|
692
|
+
...result,
|
|
693
|
+
lastInvoice,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
export async function getSubscriptionRemainingStakeSetup(
|
|
698
|
+
subscription: Subscription,
|
|
699
|
+
address: string,
|
|
700
|
+
paymentMethod: PaymentMethod,
|
|
701
|
+
action: 'return' | 'slash' = 'return'
|
|
667
702
|
) {
|
|
668
703
|
const client = paymentMethod.getOcapClient();
|
|
669
704
|
const { state } = await client.getStakeState({ address });
|
|
@@ -675,7 +710,12 @@ export async function getSubscriptionStakeReturnSetup(
|
|
|
675
710
|
sender: '',
|
|
676
711
|
};
|
|
677
712
|
}
|
|
678
|
-
|
|
713
|
+
let total = new BN(state.tokens.find((x: any) => x.address === currency.contract)?.value || '0');
|
|
714
|
+
if (action === 'slash') {
|
|
715
|
+
// add revoked tokens to total
|
|
716
|
+
const revoked = state.revokedTokens?.find((x: any) => x.address === currency.contract);
|
|
717
|
+
total = total.add(new BN(revoked?.value || '0'));
|
|
718
|
+
}
|
|
679
719
|
const [summary] = await Invoice.getUncollectibleAmount({
|
|
680
720
|
subscriptionId: subscription.id,
|
|
681
721
|
currencyId: subscription.currency_id,
|
|
@@ -684,14 +724,12 @@ export async function getSubscriptionStakeReturnSetup(
|
|
|
684
724
|
const subscriptionInitStakes = JSON.parse(state.data?.value || '{}');
|
|
685
725
|
const initStake = subscriptionInitStakes[subscription.id];
|
|
686
726
|
const uncollectibleAmountBN = new BN(summary?.[subscription.currency_id] || '0');
|
|
687
|
-
const lastInvoice = await Invoice.findByPk(subscription.latest_invoice_id);
|
|
688
727
|
if (state.nonce) {
|
|
689
728
|
const returnStake = total.sub(uncollectibleAmountBN);
|
|
690
729
|
return {
|
|
691
730
|
total: total.toString(),
|
|
692
731
|
return_amount: returnStake.lt(new BN(0)) ? '0' : returnStake.toString(),
|
|
693
732
|
sender: state.sender,
|
|
694
|
-
lastInvoice,
|
|
695
733
|
};
|
|
696
734
|
}
|
|
697
735
|
const getReturnState = async () => {
|
|
@@ -709,7 +747,6 @@ export async function getSubscriptionStakeReturnSetup(
|
|
|
709
747
|
total: initStake,
|
|
710
748
|
return_amount: returnStake.lt(new BN(0)) ? '0' : returnStake.toString(),
|
|
711
749
|
sender: state.sender,
|
|
712
|
-
lastInvoice,
|
|
713
750
|
};
|
|
714
751
|
}
|
|
715
752
|
|
|
@@ -729,14 +766,22 @@ export async function checkRemainingStake(
|
|
|
729
766
|
const client = paymentMethod.getOcapClient();
|
|
730
767
|
const { state } = await client.getStakeState({ address });
|
|
731
768
|
|
|
732
|
-
|
|
733
|
-
|
|
769
|
+
if (!state) {
|
|
770
|
+
logger.warn('getStakeState failed in checkRemainingStake', { address, paymentMethod, paymentCurrency });
|
|
771
|
+
return {
|
|
772
|
+
enough: false,
|
|
773
|
+
staked: '0',
|
|
774
|
+
revoked: '0',
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
const staked = state.tokens?.find((x: any) => x.address === paymentCurrency.contract);
|
|
778
|
+
const revoked = state.revokedTokens?.find((x: any) => x.address === paymentCurrency.contract);
|
|
734
779
|
let total = new BN(0);
|
|
735
780
|
if (staked) {
|
|
736
|
-
total = total.add(new BN(staked
|
|
781
|
+
total = total.add(new BN(staked?.value || '0'));
|
|
737
782
|
}
|
|
738
783
|
if (revoked) {
|
|
739
|
-
total = total.add(new BN(revoked
|
|
784
|
+
total = total.add(new BN(revoked?.value || '0'));
|
|
740
785
|
}
|
|
741
786
|
return {
|
|
742
787
|
enough: total.gte(new BN(amount)),
|
|
@@ -744,3 +789,19 @@ export async function checkRemainingStake(
|
|
|
744
789
|
revoked,
|
|
745
790
|
};
|
|
746
791
|
}
|
|
792
|
+
|
|
793
|
+
// trialing can be customized with currency_id list
|
|
794
|
+
export function getSubscriptionTrialSetup(data: Partial<SubscriptionData>, currencyId: string) {
|
|
795
|
+
let trialInDays = Number(data?.trial_period_days || 0);
|
|
796
|
+
let trialEnd = Number(data?.trial_end || 0);
|
|
797
|
+
const trialCurrencyIds = (data?.trial_currency || '').split(',').map(trim).filter(Boolean);
|
|
798
|
+
if (trialCurrencyIds.length > 0 && trialCurrencyIds.includes(currencyId) === false) {
|
|
799
|
+
trialEnd = 0;
|
|
800
|
+
trialInDays = 0;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
return {
|
|
804
|
+
trialInDays,
|
|
805
|
+
trialEnd,
|
|
806
|
+
};
|
|
807
|
+
}
|
|
@@ -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', {
|
|
@@ -42,11 +42,12 @@ import {
|
|
|
42
42
|
getDaysUntilCancel,
|
|
43
43
|
getDaysUntilDue,
|
|
44
44
|
getSubscriptionCreateSetup,
|
|
45
|
+
getSubscriptionTrialSetup,
|
|
45
46
|
} from '../libs/subscription';
|
|
46
47
|
import { CHECKOUT_SESSION_TTL, formatAmountPrecisionLimit, formatMetadata, getDataObjectFromQuery } from '../libs/util';
|
|
47
48
|
import { invoiceQueue } from '../queues/invoice';
|
|
48
49
|
import { paymentQueue } from '../queues/payment';
|
|
49
|
-
import type { LineItem, TPriceExpanded, TProductExpanded } from '../store/models';
|
|
50
|
+
import type { LineItem, SubscriptionData, TPriceExpanded, TProductExpanded } from '../store/models';
|
|
50
51
|
import { CheckoutSession } from '../store/models/checkout-session';
|
|
51
52
|
import { Customer } from '../store/models/customer';
|
|
52
53
|
import { PaymentCurrency } from '../store/models/payment-currency';
|
|
@@ -249,8 +250,8 @@ export async function getCheckoutSessionAmounts(checkoutSession: CheckoutSession
|
|
|
249
250
|
const now = dayjs().unix();
|
|
250
251
|
const items = await Price.expand(checkoutSession.line_items);
|
|
251
252
|
const trialInDays = Number(checkoutSession.subscription_data?.trial_period_days || 0);
|
|
252
|
-
const
|
|
253
|
-
const amount = getCheckoutAmount(items, checkoutSession.currency_id, trialInDays > 0 ||
|
|
253
|
+
const trialEnd = Number(checkoutSession.subscription_data?.trial_end || 0);
|
|
254
|
+
const amount = getCheckoutAmount(items, checkoutSession.currency_id, trialInDays > 0 || trialEnd > now);
|
|
254
255
|
return {
|
|
255
256
|
amount_subtotal: amount.subtotal,
|
|
256
257
|
amount_total: amount.total,
|
|
@@ -378,10 +379,20 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
|
|
|
378
379
|
raw.submit_type = link.submit_type;
|
|
379
380
|
raw.currency_id = link.currency_id || req.currency.id;
|
|
380
381
|
raw.payment_link_id = link.id;
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
382
|
+
|
|
383
|
+
// Settings priority: PaymentLink.subscription_data > req.query > environments
|
|
384
|
+
const protectedSettings: Partial<SubscriptionData> = {};
|
|
385
|
+
if (link.subscription_data?.min_stake_amount) {
|
|
386
|
+
protectedSettings.min_stake_amount = getMinStakeAmount(link.subscription_data);
|
|
387
|
+
}
|
|
388
|
+
if (link.subscription_data?.billing_threshold_amount) {
|
|
389
|
+
protectedSettings.billing_threshold_amount = getBillingThreshold(link.subscription_data);
|
|
390
|
+
}
|
|
391
|
+
raw.subscription_data = merge(
|
|
392
|
+
link.subscription_data,
|
|
393
|
+
getDataObjectFromQuery(req.query, 'subscription_data'),
|
|
394
|
+
protectedSettings
|
|
395
|
+
);
|
|
385
396
|
|
|
386
397
|
if (link.after_completion?.hosted_confirmation?.custom_message) {
|
|
387
398
|
raw.payment_intent_data = {
|
|
@@ -564,11 +575,16 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
564
575
|
// always update payment amount in case currency has changed
|
|
565
576
|
const now = dayjs().unix();
|
|
566
577
|
const lineItems = await Price.expand(checkoutSession.line_items, { product: true, upsell: true });
|
|
567
|
-
|
|
568
|
-
|
|
578
|
+
|
|
579
|
+
// trialing can be customized with currency_id list
|
|
580
|
+
const { trialEnd, trialInDays } = getSubscriptionTrialSetup(
|
|
581
|
+
checkoutSession.subscription_data as any,
|
|
582
|
+
paymentCurrency.id
|
|
583
|
+
);
|
|
584
|
+
|
|
569
585
|
const billingThreshold = Number(checkoutSession.subscription_data?.billing_threshold_amount || 0);
|
|
570
586
|
const minStakeAmount = Number(checkoutSession.subscription_data?.min_stake_amount || 0);
|
|
571
|
-
const amount = getCheckoutAmount(lineItems, paymentCurrency.id, trialInDays > 0 ||
|
|
587
|
+
const amount = getCheckoutAmount(lineItems, paymentCurrency.id, trialInDays > 0 || trialEnd > now);
|
|
572
588
|
await checkoutSession.update({
|
|
573
589
|
amount_subtotal: amount.subtotal,
|
|
574
590
|
amount_total: amount.total,
|
|
@@ -760,7 +776,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
760
776
|
.status(403)
|
|
761
777
|
.json({ code: 'SUBSCRIPTION_INVALID', error: 'Checkout session subscription status unexpected' });
|
|
762
778
|
}
|
|
763
|
-
const setup = getSubscriptionCreateSetup(lineItems, paymentCurrency.id, trialInDays,
|
|
779
|
+
const setup = getSubscriptionCreateSetup(lineItems, paymentCurrency.id, trialInDays, trialEnd);
|
|
764
780
|
subscription = await subscription.update({
|
|
765
781
|
currency_id: paymentCurrency.id,
|
|
766
782
|
customer_id: customer.id,
|
|
@@ -803,7 +819,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
803
819
|
const recoveredFrom = recoveredFromId ? await Subscription.findByPk(recoveredFromId) : null;
|
|
804
820
|
|
|
805
821
|
// FIXME: @wangshijun respect all checkoutSession.subscription_data fields
|
|
806
|
-
const setup = getSubscriptionCreateSetup(lineItems, paymentCurrency.id, trialInDays,
|
|
822
|
+
const setup = getSubscriptionCreateSetup(lineItems, paymentCurrency.id, trialInDays, trialEnd);
|
|
807
823
|
subscription = await Subscription.create({
|
|
808
824
|
livemode: !!checkoutSession.livemode,
|
|
809
825
|
currency_id: paymentCurrency.id,
|
|
@@ -877,7 +893,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
877
893
|
lineItems,
|
|
878
894
|
checkoutSession.mode,
|
|
879
895
|
paymentCurrency.id,
|
|
880
|
-
trialInDays > 0 ||
|
|
896
|
+
trialInDays > 0 || trialEnd > now
|
|
881
897
|
);
|
|
882
898
|
const paymentSettings = {
|
|
883
899
|
payment_method_types: checkoutSession.payment_method_types,
|
|
@@ -954,7 +970,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
954
970
|
paymentCurrency,
|
|
955
971
|
lineItems,
|
|
956
972
|
trialInDays,
|
|
957
|
-
|
|
973
|
+
trialEnd
|
|
958
974
|
);
|
|
959
975
|
if (stripeSubscription && subscription?.payment_details?.stripe?.subscription_id === stripeSubscription.id) {
|
|
960
976
|
if (['active', 'trialing'].includes(stripeSubscription.status) && subscription.status === 'incomplete') {
|
|
@@ -3,6 +3,7 @@ import type { CallbackArgs } from '../../libs/auth';
|
|
|
3
3
|
import dayjs from '../../libs/dayjs';
|
|
4
4
|
import logger from '../../libs/logger';
|
|
5
5
|
import { isDelegationSufficientForPayment } from '../../libs/payment';
|
|
6
|
+
import { getSubscriptionTrialSetup } from '../../libs/subscription';
|
|
6
7
|
import { getFastCheckoutAmount } from '../../libs/session';
|
|
7
8
|
import { getTxMetadata } from '../../libs/util';
|
|
8
9
|
import { invoiceQueue } from '../../queues/invoice';
|
|
@@ -39,9 +40,13 @@ export default {
|
|
|
39
40
|
const claims: { [type: string]: [string, object] } = {};
|
|
40
41
|
const now = dayjs().unix();
|
|
41
42
|
const items = checkoutSession.line_items as TLineItemExpanded[];
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
43
|
+
|
|
44
|
+
const { trialEnd, trialInDays } = getSubscriptionTrialSetup(
|
|
45
|
+
checkoutSession.subscription_data as any,
|
|
46
|
+
paymentCurrency.id
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const trialing = trialInDays > 0 || trialEnd > now;
|
|
45
50
|
const billingThreshold = Number(checkoutSession.subscription_data?.billing_threshold_amount || 0);
|
|
46
51
|
const minStakeAmount = Number(checkoutSession.subscription_data?.min_stake_amount || 0);
|
|
47
52
|
const fastCheckoutAmount = getFastCheckoutAmount(items, checkoutSession.mode, paymentCurrency.id, trialing);
|
|
@@ -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
|
|
@@ -3,6 +3,7 @@ import type { CallbackArgs } from '../../libs/auth';
|
|
|
3
3
|
import dayjs from '../../libs/dayjs';
|
|
4
4
|
import logger from '../../libs/logger';
|
|
5
5
|
import { isDelegationSufficientForPayment } from '../../libs/payment';
|
|
6
|
+
import { getSubscriptionTrialSetup } from '../../libs/subscription';
|
|
6
7
|
import { getFastCheckoutAmount } from '../../libs/session';
|
|
7
8
|
import { getTxMetadata } from '../../libs/util';
|
|
8
9
|
import { invoiceQueue } from '../../queues/invoice';
|
|
@@ -40,9 +41,11 @@ export default {
|
|
|
40
41
|
const claims: { [type: string]: [string, object] } = {};
|
|
41
42
|
const now = dayjs().unix();
|
|
42
43
|
const items = checkoutSession.line_items as TLineItemExpanded[];
|
|
43
|
-
const trialInDays =
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
const { trialEnd, trialInDays } = getSubscriptionTrialSetup(
|
|
45
|
+
checkoutSession.subscription_data as any,
|
|
46
|
+
paymentCurrency.id
|
|
47
|
+
);
|
|
48
|
+
const trialing = trialInDays > 0 || trialEnd > now;
|
|
46
49
|
const billingThreshold = Number(checkoutSession.subscription_data?.billing_threshold_amount || 0);
|
|
47
50
|
const minStakeAmount = Number(checkoutSession.subscription_data?.min_stake_amount || 0);
|
|
48
51
|
const fastCheckoutAmount = getFastCheckoutAmount(items, checkoutSession.mode, paymentCurrency.id, trialing);
|
|
@@ -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 });
|
|
1648
|
+
return res.status(400).json({ error: err.message });
|
|
1649
|
+
}
|
|
1650
|
+
});
|
|
1611
1651
|
export default router;
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
getMinRetryMail,
|
|
8
8
|
getSubscriptionCreateSetup,
|
|
9
9
|
getSubscriptionStakeSetup,
|
|
10
|
+
getSubscriptionTrialSetup,
|
|
10
11
|
shouldCancelSubscription,
|
|
11
12
|
} from '../../src/libs/subscription';
|
|
12
13
|
|
|
@@ -183,7 +184,7 @@ describe('getSubscriptionCreateSetup', () => {
|
|
|
183
184
|
);
|
|
184
185
|
});
|
|
185
186
|
|
|
186
|
-
it('should
|
|
187
|
+
it('should trialEnd overwrite trialInDays', () => {
|
|
187
188
|
const items = [
|
|
188
189
|
{
|
|
189
190
|
price: { type: 'recurring', currency_options: currencies, recurring: { interval: 'day', interval_count: '1' } },
|
|
@@ -200,7 +201,7 @@ describe('getSubscriptionCreateSetup', () => {
|
|
|
200
201
|
);
|
|
201
202
|
});
|
|
202
203
|
|
|
203
|
-
it('should calculate trial period when only
|
|
204
|
+
it('should calculate trial period when only trialEnd is provided', () => {
|
|
204
205
|
const items = [
|
|
205
206
|
{
|
|
206
207
|
price: { type: 'recurring', currency_options: currencies, recurring: { interval: 'day', interval_count: '1' } },
|
|
@@ -371,8 +372,8 @@ describe('getSubscriptionStakeSetup', () => {
|
|
|
371
372
|
|
|
372
373
|
it('should calculate staking for recurring metered price type when billingThreshold is 0 #1', () => {
|
|
373
374
|
const result = getSubscriptionStakeSetup(items.slice(2, 3), 'usd', '10');
|
|
374
|
-
expect(result.licensed.toString()).toBe('
|
|
375
|
-
expect(result.metered.toString()).toBe('
|
|
375
|
+
expect(result.licensed.toString()).toBe('10');
|
|
376
|
+
expect(result.metered.toString()).toBe('0');
|
|
376
377
|
});
|
|
377
378
|
|
|
378
379
|
it('should calculate staking for recurring metered price type when billingThreshold is 0 #2', () => {
|
|
@@ -383,7 +384,30 @@ describe('getSubscriptionStakeSetup', () => {
|
|
|
383
384
|
|
|
384
385
|
it('should calculate staking for recurring metered price type when billingThreshold is greater than 0', () => {
|
|
385
386
|
const result = getSubscriptionStakeSetup(items, 'usd', '10');
|
|
386
|
-
expect(result.licensed.toString()).toBe('
|
|
387
|
-
expect(result.metered.toString()).toBe('
|
|
387
|
+
expect(result.licensed.toString()).toBe('10');
|
|
388
|
+
expect(result.metered.toString()).toBe('0');
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
describe('getSubscriptionTrialSetup', () => {
|
|
393
|
+
it('should return trialInDays and trialEnd when data is provided', () => {
|
|
394
|
+
const data: any = { trial_period_days: '10', trial_end: '20', trial_currency: 'USD,EUR' };
|
|
395
|
+
const currencyId = 'USD';
|
|
396
|
+
const result = getSubscriptionTrialSetup(data, currencyId);
|
|
397
|
+
expect(result).toEqual({ trialInDays: 10, trialEnd: 20 });
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('should set trialInDays and trialEnd to 0 if currencyId is not in the trialCurrencyIds list', () => {
|
|
401
|
+
const data: any = { trial_period_days: '10', trial_end: '20', trial_currency: 'USD,EUR' };
|
|
402
|
+
const currencyId = 'JPY'; // currency code not included in the trialCurrencyIds list
|
|
403
|
+
const result = getSubscriptionTrialSetup(data, currencyId);
|
|
404
|
+
expect(result).toEqual({ trialInDays: 0, trialEnd: 0 });
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('should set trialInDays and trialEnd to 0 when values are not provided', () => {
|
|
408
|
+
const data: any = {};
|
|
409
|
+
const currencyId = 'USD';
|
|
410
|
+
const result = getSubscriptionTrialSetup(data, currencyId);
|
|
411
|
+
expect(result).toEqual({ trialInDays: 0, trialEnd: 0 });
|
|
388
412
|
});
|
|
389
413
|
});
|
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.
|
|
17
|
+
version: 1.14.37
|
|
18
18
|
logo: logo.png
|
|
19
19
|
files:
|
|
20
20
|
- dist
|
|
@@ -39,7 +39,6 @@ interfaces:
|
|
|
39
39
|
profileFields:
|
|
40
40
|
- fullName
|
|
41
41
|
- email
|
|
42
|
-
- phone
|
|
43
42
|
- avatar
|
|
44
43
|
allowSwitchProfile: true
|
|
45
44
|
ignoreUrls:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.37",
|
|
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.37",
|
|
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.37",
|
|
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": "645ae0bdbab463eec878af0521eb982b51499639"
|
|
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