payment-kit 1.16.17 → 1.16.18
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/crons/index.ts +1 -1
- package/api/src/hooks/pre-start.ts +2 -0
- package/api/src/index.ts +2 -0
- package/api/src/integrations/arcblock/stake.ts +7 -1
- package/api/src/integrations/stripe/resource.ts +1 -1
- package/api/src/libs/env.ts +12 -0
- package/api/src/libs/event.ts +8 -0
- package/api/src/libs/invoice.ts +585 -3
- package/api/src/libs/notification/template/subscription-succeeded.ts +1 -2
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +2 -2
- package/api/src/libs/notification/template/subscription-will-renew.ts +6 -2
- package/api/src/libs/notification/template/subscription.overdraft-protection.exhausted.ts +139 -0
- package/api/src/libs/overdraft-protection.ts +85 -0
- package/api/src/libs/payment.ts +1 -65
- package/api/src/libs/queue/index.ts +0 -1
- package/api/src/libs/subscription.ts +532 -2
- package/api/src/libs/util.ts +4 -0
- package/api/src/locales/en.ts +5 -0
- package/api/src/locales/zh.ts +5 -0
- package/api/src/queues/event.ts +3 -2
- package/api/src/queues/invoice.ts +28 -3
- package/api/src/queues/notification.ts +25 -3
- package/api/src/queues/payment.ts +154 -3
- package/api/src/queues/refund.ts +2 -2
- package/api/src/queues/subscription.ts +215 -4
- package/api/src/queues/webhook.ts +1 -0
- package/api/src/routes/connect/change-payment.ts +1 -1
- package/api/src/routes/connect/change-plan.ts +1 -1
- package/api/src/routes/connect/overdraft-protection.ts +120 -0
- package/api/src/routes/connect/recharge.ts +2 -1
- package/api/src/routes/connect/setup.ts +1 -1
- package/api/src/routes/connect/shared.ts +117 -350
- package/api/src/routes/connect/subscribe.ts +1 -1
- package/api/src/routes/customers.ts +2 -2
- package/api/src/routes/invoices.ts +9 -4
- package/api/src/routes/subscriptions.ts +172 -2
- package/api/src/store/migrate.ts +9 -10
- package/api/src/store/migrations/20240905-index.ts +95 -60
- package/api/src/store/migrations/20241203-overdraft-protection.ts +25 -0
- package/api/src/store/migrations/20241216-update-overdraft-protection.ts +30 -0
- package/api/src/store/models/customer.ts +2 -2
- package/api/src/store/models/invoice.ts +7 -0
- package/api/src/store/models/lock.ts +7 -0
- package/api/src/store/models/subscription.ts +15 -0
- package/api/src/store/sequelize.ts +6 -1
- package/blocklet.yml +1 -1
- package/package.json +23 -23
- package/src/components/customer/overdraft-protection.tsx +367 -0
- package/src/components/event/list.tsx +3 -4
- package/src/components/subscription/actions/cancel.tsx +3 -0
- package/src/components/subscription/portal/actions.tsx +324 -77
- package/src/components/uploader.tsx +31 -26
- package/src/env.d.ts +1 -0
- package/src/hooks/subscription.ts +30 -0
- package/src/libs/env.ts +4 -0
- package/src/locales/en.tsx +41 -0
- package/src/locales/zh.tsx +37 -0
- package/src/pages/admin/billing/invoices/detail.tsx +16 -15
- package/src/pages/customer/index.tsx +7 -2
- package/src/pages/customer/invoice/detail.tsx +29 -5
- package/src/pages/customer/invoice/past-due.tsx +18 -4
- package/src/pages/customer/recharge.tsx +2 -4
- package/src/pages/customer/subscription/change-payment.tsx +7 -1
- package/src/pages/customer/subscription/detail.tsx +69 -51
- package/tsconfig.json +0 -5
- package/api/tests/libs/payment.spec.ts +0 -168
package/src/locales/zh.tsx
CHANGED
|
@@ -22,6 +22,8 @@ export default flat({
|
|
|
22
22
|
latinOnly: '至少包含一个字母,并且不能包含中文字符和特殊字符如 <, >、"、’ 或 \\',
|
|
23
23
|
loading: '加载中...',
|
|
24
24
|
rechargeTime: '充值时间',
|
|
25
|
+
submit: '提交',
|
|
26
|
+
custom: '自定义',
|
|
25
27
|
},
|
|
26
28
|
admin: {
|
|
27
29
|
balances: '余额',
|
|
@@ -631,5 +633,40 @@ export default flat({
|
|
|
631
633
|
success: '授权成功',
|
|
632
634
|
error: '授权失败',
|
|
633
635
|
},
|
|
636
|
+
overdraftProtection: {
|
|
637
|
+
title: '透支保护',
|
|
638
|
+
setting: '透支保护设置',
|
|
639
|
+
tip: '为避免因扣费失败中断服务,您可以通过质押开启透支保护。按时付款不会收取额外费用,请及时付清账单,若可用质押不足或者超期未付,我们将从质押中扣除并收取服务费',
|
|
640
|
+
enabled: '已启用',
|
|
641
|
+
disabled: '未启用',
|
|
642
|
+
returnRemaining: '退还剩余质押',
|
|
643
|
+
returnRemainingTip: '退还剩余质押后,透支保护将自动关闭,请确认操作。',
|
|
644
|
+
applyRemainingSuccess: '质押退还申请成功',
|
|
645
|
+
remaining: '您当前剩余可用质押:{amount} {symbol}, 每周期预计需质押:{estimateAmount} {symbol}。',
|
|
646
|
+
noRemaining: '当前无质押,为确保透支保护功能的正常使用,请至少质押 {estimateAmount} {symbol}。',
|
|
647
|
+
remainingNotEnough:
|
|
648
|
+
'当前存在未支付的账单,总计 {due} {symbol},如果不支付,您当前剩余质押将无法覆盖下期账单,剩余可用质押:{unused} {symbol},请质押至少 {min} {symbol}。',
|
|
649
|
+
due: '请先支付欠款',
|
|
650
|
+
insufficient: '额度不足,下期账单将无法使用透支保护',
|
|
651
|
+
insufficientTip: '透支保护额度不足,请尽快质押保证透支保护功能的正常使用。',
|
|
652
|
+
intervals: '个周期',
|
|
653
|
+
estimatedDuration: '预计可用 {duration} {unit}',
|
|
654
|
+
rule: '规则:N * ( P + Fee )',
|
|
655
|
+
ruleTip: 'N 为周期数, P 为订阅账单费用, Fee 为透支保护服务费用,单次费用为 {gas} {symbol}',
|
|
656
|
+
min: '质押金额不得小于 {min} {symbol}',
|
|
657
|
+
settingSuccess: '透支保护设置成功',
|
|
658
|
+
settingError: '透支保护设置失败',
|
|
659
|
+
keepStake: '不退还质押',
|
|
660
|
+
returnStake: '退还剩余质押',
|
|
661
|
+
stake: '质押',
|
|
662
|
+
address: '质押账户',
|
|
663
|
+
total: '总质押:{total} {symbol},',
|
|
664
|
+
disableConfirm: '您当前有未支付的账单,请先付清账单。',
|
|
665
|
+
},
|
|
666
|
+
unpaidInvoicesWarning: '您当前有未支付的账单,请先付清账单。',
|
|
667
|
+
unpaidInvoicesWarningTip: '您当前有未支付的账单,请及时付清。',
|
|
668
|
+
invoice: {
|
|
669
|
+
relatedInvoice: '关联账单',
|
|
670
|
+
},
|
|
634
671
|
},
|
|
635
672
|
});
|
|
@@ -322,21 +322,22 @@ export default function InvoiceDetail(props: { id: string }) {
|
|
|
322
322
|
alignItems={InfoAlignItems}
|
|
323
323
|
/>
|
|
324
324
|
)}
|
|
325
|
-
{
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
325
|
+
{['stake', 'stake_overdraft_protection'].includes(data.billing_reason) &&
|
|
326
|
+
data?.metadata?.payment_details?.arcblock?.tx_hash && (
|
|
327
|
+
<InfoRow
|
|
328
|
+
label={t('common.stakeTxHash')}
|
|
329
|
+
value={
|
|
330
|
+
<TxLink
|
|
331
|
+
details={{
|
|
332
|
+
arcblock: { tx_hash: data.metadata?.payment_details?.arcblock?.tx_hash, payer: '' },
|
|
333
|
+
}}
|
|
334
|
+
method={data.paymentMethod}
|
|
335
|
+
/>
|
|
336
|
+
}
|
|
337
|
+
direction={InfoDirection}
|
|
338
|
+
alignItems={InfoAlignItems}
|
|
339
|
+
/>
|
|
340
|
+
)}
|
|
340
341
|
{data.subscription && (
|
|
341
342
|
<InfoRow
|
|
342
343
|
label={t('admin.subscription.name')}
|
|
@@ -97,7 +97,12 @@ export default function CustomerHome() {
|
|
|
97
97
|
const currencies = flatten(settings.paymentMethods.map((method) => method.payment_currencies));
|
|
98
98
|
|
|
99
99
|
const { livemode, setLivemode } = usePaymentContext();
|
|
100
|
-
const [state, setState] = useSetState({
|
|
100
|
+
const [state, setState] = useSetState({
|
|
101
|
+
editing: false,
|
|
102
|
+
loading: false,
|
|
103
|
+
onlyActive: true,
|
|
104
|
+
setOverdraftProtection: false,
|
|
105
|
+
});
|
|
101
106
|
const navigate = useNavigate();
|
|
102
107
|
const { isMobile } = useMobile('lg');
|
|
103
108
|
const { isPending, startTransition } = useTransitionContext();
|
|
@@ -207,7 +212,7 @@ export default function CustomerHome() {
|
|
|
207
212
|
id={data.id}
|
|
208
213
|
onlyActive={state.onlyActive}
|
|
209
214
|
changeActive={(v) => setState({ onlyActive: v })}
|
|
210
|
-
status={state.onlyActive ? 'active,trialing' : 'active,trialing,paused,past_due,canceled'}
|
|
215
|
+
status={state.onlyActive ? 'active,trialing,past_due' : 'active,trialing,paused,past_due,canceled'}
|
|
211
216
|
style={{
|
|
212
217
|
cursor: 'pointer',
|
|
213
218
|
}}
|
|
@@ -9,7 +9,9 @@ import {
|
|
|
9
9
|
formatAmount,
|
|
10
10
|
formatError,
|
|
11
11
|
formatTime,
|
|
12
|
+
getInvoiceDescriptionAndReason,
|
|
12
13
|
getInvoiceStatusColor,
|
|
14
|
+
getPrefix,
|
|
13
15
|
usePaymentContext,
|
|
14
16
|
} from '@blocklet/payment-react';
|
|
15
17
|
import type { TInvoiceExpanded } from '@blocklet/payment-types';
|
|
@@ -20,6 +22,7 @@ import { useRequest, useSetState } from 'ahooks';
|
|
|
20
22
|
import { useEffect } from 'react';
|
|
21
23
|
import { Link, useParams, useSearchParams } from 'react-router-dom';
|
|
22
24
|
|
|
25
|
+
import { joinURL } from 'ufo';
|
|
23
26
|
import { useSessionContext } from '../../../contexts/session';
|
|
24
27
|
import Currency from '../../../components/currency';
|
|
25
28
|
import CustomerLink from '../../../components/customer/link';
|
|
@@ -30,14 +33,14 @@ import { goBackOrFallback } from '../../../libs/util';
|
|
|
30
33
|
import CustomerRefundList from '../refund/list';
|
|
31
34
|
import InfoMetric from '../../../components/info-metric';
|
|
32
35
|
|
|
33
|
-
const fetchData = (id: string): Promise<TInvoiceExpanded> => {
|
|
36
|
+
const fetchData = (id: string): Promise<TInvoiceExpanded & { relatedInvoice?: TInvoiceExpanded }> => {
|
|
34
37
|
return api.get(`/api/invoices/${id}`).then((res) => res.data);
|
|
35
38
|
};
|
|
36
39
|
|
|
37
40
|
const InfoDirection = 'column';
|
|
38
41
|
const InfoAlignItems = 'flex-start';
|
|
39
42
|
export default function CustomerInvoiceDetail() {
|
|
40
|
-
const { t } = useLocaleContext();
|
|
43
|
+
const { t, locale } = useLocaleContext();
|
|
41
44
|
const [searchParams] = useSearchParams();
|
|
42
45
|
const { connect } = usePaymentContext();
|
|
43
46
|
const params = useParams<{ id: string }>();
|
|
@@ -108,9 +111,11 @@ export default function CustomerInvoiceDetail() {
|
|
|
108
111
|
return <CircularProgress />;
|
|
109
112
|
}
|
|
110
113
|
|
|
111
|
-
const isStake =
|
|
114
|
+
const isStake =
|
|
115
|
+
data.paymentMethod?.type === 'arcblock' && ['stake', 'stake_overdraft_protection'].includes(data.billing_reason);
|
|
112
116
|
const isSlashStake = data.paymentMethod?.type === 'arcblock' && data.billing_reason.includes('slash_stake');
|
|
113
117
|
|
|
118
|
+
const hidePayButton = data.billing_reason === 'overdraft_protection';
|
|
114
119
|
const paymentDetails = data.paymentIntent?.payment_details || data.metadata?.payment_details;
|
|
115
120
|
return (
|
|
116
121
|
<InvoiceDetailRoot direction="column" spacing={3} sx={{ my: 2 }}>
|
|
@@ -127,7 +132,7 @@ export default function CustomerInvoiceDetail() {
|
|
|
127
132
|
</Stack>
|
|
128
133
|
<Stack direction="row" spacing={1} justifyContent="flex-end" alignItems="center">
|
|
129
134
|
{['open', 'paid', 'uncollectible'].includes(data.status) && <Download data={data} />}
|
|
130
|
-
{['open', 'uncollectible'].includes(data.status) && (
|
|
135
|
+
{['open', 'uncollectible'].includes(data.status) && !hidePayButton && (
|
|
131
136
|
<Button
|
|
132
137
|
variant="outlined"
|
|
133
138
|
color="primary"
|
|
@@ -227,7 +232,7 @@ export default function CustomerInvoiceDetail() {
|
|
|
227
232
|
}}>
|
|
228
233
|
<InfoRow
|
|
229
234
|
label={t('admin.invoice.description')}
|
|
230
|
-
value={data
|
|
235
|
+
value={getInvoiceDescriptionAndReason(data, locale)?.description}
|
|
231
236
|
direction={InfoDirection}
|
|
232
237
|
alignItems={InfoAlignItems}
|
|
233
238
|
/>
|
|
@@ -303,6 +308,25 @@ export default function CustomerInvoiceDetail() {
|
|
|
303
308
|
alignItems={InfoAlignItems}
|
|
304
309
|
/>
|
|
305
310
|
)}
|
|
311
|
+
{data?.relatedInvoice && (
|
|
312
|
+
<InfoRow
|
|
313
|
+
label={t('customer.invoice.relatedInvoice')}
|
|
314
|
+
value={
|
|
315
|
+
<Typography
|
|
316
|
+
variant="body1"
|
|
317
|
+
color="text.link"
|
|
318
|
+
sx={{ cursor: 'pointer' }}
|
|
319
|
+
component="a"
|
|
320
|
+
onClick={() => {
|
|
321
|
+
window.open(joinURL(getPrefix(), `/customer/invoice/${data.relatedInvoice?.id}`), '_self');
|
|
322
|
+
}}>
|
|
323
|
+
{data.relatedInvoice?.number}
|
|
324
|
+
</Typography>
|
|
325
|
+
}
|
|
326
|
+
direction={InfoDirection}
|
|
327
|
+
alignItems={InfoAlignItems}
|
|
328
|
+
/>
|
|
329
|
+
)}
|
|
306
330
|
</Stack>
|
|
307
331
|
</Box>
|
|
308
332
|
{!isSlashStake && (
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
2
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
CustomerInvoiceList,
|
|
5
|
+
formatError,
|
|
6
|
+
getPrefix,
|
|
7
|
+
getQueryParams,
|
|
8
|
+
usePaymentContext,
|
|
9
|
+
} from '@blocklet/payment-react';
|
|
4
10
|
import type { TCustomerExpanded } from '@blocklet/payment-types';
|
|
5
11
|
import { ArrowBackOutlined } from '@mui/icons-material';
|
|
6
12
|
import { Alert, Box, Button, CircularProgress, Stack, Typography } from '@mui/material';
|
|
@@ -25,7 +31,7 @@ export default function CustomerInvoicePastDue() {
|
|
|
25
31
|
const { events } = useSessionContext();
|
|
26
32
|
const { connect } = usePaymentContext();
|
|
27
33
|
const [params] = useSearchParams();
|
|
28
|
-
const [alertVisible, setAlertVisible] = useState(
|
|
34
|
+
const [alertVisible, setAlertVisible] = useState(false);
|
|
29
35
|
|
|
30
36
|
const { loading, error, data, runAsync } = useRequest(fetchData);
|
|
31
37
|
|
|
@@ -62,7 +68,6 @@ export default function CustomerInvoicePastDue() {
|
|
|
62
68
|
extraParams: { subscriptionId, currencyId },
|
|
63
69
|
onSuccess: () => {
|
|
64
70
|
connect.close();
|
|
65
|
-
window.location.reload();
|
|
66
71
|
},
|
|
67
72
|
onClose: () => {
|
|
68
73
|
connect.close();
|
|
@@ -73,9 +78,18 @@ export default function CustomerInvoicePastDue() {
|
|
|
73
78
|
});
|
|
74
79
|
};
|
|
75
80
|
|
|
76
|
-
const onTableDataChange = (tableData: any) => {
|
|
81
|
+
const onTableDataChange = (tableData: any, prevData: any) => {
|
|
77
82
|
if (isEmpty(tableData) || tableData?.count === 0) {
|
|
78
83
|
setAlertVisible(false);
|
|
84
|
+
if (prevData?.count > 0) {
|
|
85
|
+
// paid all invoices
|
|
86
|
+
const referer = getQueryParams(window.location.href)?.referer;
|
|
87
|
+
if (referer) {
|
|
88
|
+
window.location.replace(referer);
|
|
89
|
+
} else {
|
|
90
|
+
goBackOrFallback('/customer');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
79
93
|
return;
|
|
80
94
|
}
|
|
81
95
|
setAlertVisible(true);
|
|
@@ -98,7 +98,7 @@ export default function RechargePage() {
|
|
|
98
98
|
const [subscriptionRes, payerTokenRes, upcomingRes] = await Promise.all([
|
|
99
99
|
api.get(`/api/subscriptions/${subscriptionId}`),
|
|
100
100
|
api.get(`/api/subscriptions/${subscriptionId}/payer-token`),
|
|
101
|
-
api.get(`/api/subscriptions/${subscriptionId}/
|
|
101
|
+
api.get(`/api/subscriptions/${subscriptionId}/cycle-amount`),
|
|
102
102
|
]);
|
|
103
103
|
setSubscription(subscriptionRes.data);
|
|
104
104
|
setPayerValue(payerTokenRes.data);
|
|
@@ -106,9 +106,7 @@ export default function RechargePage() {
|
|
|
106
106
|
// Calculate preset amounts
|
|
107
107
|
const getCycleAmount = (cycles: number) =>
|
|
108
108
|
fromUnitToToken(
|
|
109
|
-
new BN(upcomingRes.data.amount
|
|
110
|
-
.mul(new BN(cycles))
|
|
111
|
-
.toString(),
|
|
109
|
+
new BN(upcomingRes.data.amount).mul(new BN(cycles)).toString(),
|
|
112
110
|
upcomingRes.data?.currency?.decimal
|
|
113
111
|
);
|
|
114
112
|
setPresetAmounts([
|
|
@@ -33,6 +33,7 @@ import { joinURL } from 'ufo';
|
|
|
33
33
|
import SectionHeader from '../../../components/section/header';
|
|
34
34
|
import SubscriptionDescription from '../../../components/subscription/description';
|
|
35
35
|
import { goBackOrFallback } from '../../../libs/util';
|
|
36
|
+
import { useUnpaidInvoicesCheckForSubscription } from '../../../hooks/subscription';
|
|
36
37
|
|
|
37
38
|
const fetchData = async (id: string): Promise<{ subscription: TSubscriptionExpanded; customer: TCustomer }> => {
|
|
38
39
|
const [subscription, customer] = await Promise.all([
|
|
@@ -78,6 +79,7 @@ function CustomerSubscriptionChangePayment({ subscription, customer, onComplete
|
|
|
78
79
|
const navigate = useNavigate();
|
|
79
80
|
const [searchParams] = useSearchParams();
|
|
80
81
|
const { settings, connect } = usePaymentContext();
|
|
82
|
+
const { checkUnpaidInvoices } = useUnpaidInvoicesCheckForSubscription(subscription.id);
|
|
81
83
|
const { control, setValue, handleSubmit } = useFormContext();
|
|
82
84
|
|
|
83
85
|
const [state, setState] = useSetState<{
|
|
@@ -195,7 +197,11 @@ function CustomerSubscriptionChangePayment({ subscription, customer, onComplete
|
|
|
195
197
|
}
|
|
196
198
|
};
|
|
197
199
|
|
|
198
|
-
const onConfirm = () => {
|
|
200
|
+
const onConfirm = async () => {
|
|
201
|
+
const result = await checkUnpaidInvoices();
|
|
202
|
+
if (result) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
199
205
|
handleSubmit(onSubmit)();
|
|
200
206
|
};
|
|
201
207
|
|
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
/* eslint-disable react/no-unstable-nested-components */
|
|
2
2
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
|
-
import {
|
|
4
|
-
CustomerInvoiceList,
|
|
5
|
-
TxLink,
|
|
6
|
-
api,
|
|
7
|
-
formatError,
|
|
8
|
-
formatTime,
|
|
9
|
-
getPrefix,
|
|
10
|
-
hasDelegateTxHash,
|
|
11
|
-
usePaymentContext,
|
|
12
|
-
} from '@blocklet/payment-react';
|
|
3
|
+
import { CustomerInvoiceList, TxLink, api, formatTime, hasDelegateTxHash, useMobile } from '@blocklet/payment-react';
|
|
13
4
|
import type { TSubscriptionExpanded } from '@blocklet/payment-types';
|
|
14
5
|
import { ArrowBackOutlined } from '@mui/icons-material';
|
|
15
|
-
import { Alert, Box, Button, CircularProgress, Divider, Stack,
|
|
6
|
+
import { Alert, Box, Button, CircularProgress, Divider, Stack, Typography } from '@mui/material';
|
|
16
7
|
import { useRequest } from 'ahooks';
|
|
17
8
|
import { Link, useNavigate, useParams } from 'react-router-dom';
|
|
18
|
-
import Toast from '@arcblock/ux/lib/Toast';
|
|
19
9
|
import { styled } from '@mui/system';
|
|
20
|
-
import {
|
|
10
|
+
import { BN } from '@ocap/util';
|
|
21
11
|
import Currency from '../../../components/currency';
|
|
22
12
|
import CustomerLink from '../../../components/customer/link';
|
|
23
13
|
import InfoRow from '../../../components/info-row';
|
|
@@ -27,15 +17,18 @@ import SubscriptionMetrics from '../../../components/subscription/metrics';
|
|
|
27
17
|
import SubscriptionActions from '../../../components/subscription/portal/actions';
|
|
28
18
|
import { canChangePaymentMethod } from '../../../libs/util';
|
|
29
19
|
import { useSessionContext } from '../../../contexts/session';
|
|
20
|
+
import InfoMetric from '../../../components/info-metric';
|
|
21
|
+
import { useUnpaidInvoicesCheckForSubscription } from '../../../hooks/subscription';
|
|
30
22
|
|
|
31
23
|
const fetchData = (id: string | undefined): Promise<TSubscriptionExpanded> => {
|
|
32
24
|
return api.get(`/api/subscriptions/${id}`).then((res) => res.data);
|
|
33
25
|
};
|
|
34
26
|
|
|
35
|
-
const
|
|
36
|
-
|
|
27
|
+
const fetchOverdraftProtection = (
|
|
28
|
+
id: string
|
|
29
|
+
): Promise<{ enabled: boolean; remaining: string; unused: string; upcoming: { amount: string }; gas: string }> => {
|
|
30
|
+
return api.get(`/api/subscriptions/${id}/overdraft-protection`).then((res) => res.data);
|
|
37
31
|
};
|
|
38
|
-
|
|
39
32
|
const InfoDirection = 'column';
|
|
40
33
|
const InfoAlignItems = 'flex-start';
|
|
41
34
|
|
|
@@ -43,35 +36,20 @@ export default function CustomerSubscriptionDetail() {
|
|
|
43
36
|
const { id } = useParams() as { id: string };
|
|
44
37
|
const navigate = useNavigate();
|
|
45
38
|
const { t } = useLocaleContext();
|
|
39
|
+
const { isMobile } = useMobile();
|
|
46
40
|
const { session } = useSessionContext();
|
|
47
41
|
const { loading, error, data, refresh } = useRequest(() => fetchData(id));
|
|
48
|
-
const {
|
|
42
|
+
const { hasUnpaid, checkUnpaidInvoices } = useUnpaidInvoicesCheckForSubscription(id);
|
|
43
|
+
const {
|
|
44
|
+
data: overdraftProtection = null,
|
|
45
|
+
loading: overdraftProtectionLoading,
|
|
46
|
+
run: refreshOverdraftProtection,
|
|
47
|
+
} = useRequest(() => fetchOverdraftProtection(id), {
|
|
48
|
+
ready: ['active', 'trialing', 'past_due'].includes(data?.status || ''),
|
|
49
|
+
});
|
|
49
50
|
|
|
50
|
-
const
|
|
51
|
-
fetchSubscriptionDelegation(id)
|
|
52
|
-
);
|
|
53
|
-
const noDelegation = delegation && typeof delegation === 'object' && !delegation.sufficient;
|
|
51
|
+
const enableOverdraftProtection = !!overdraftProtection?.enabled;
|
|
54
52
|
|
|
55
|
-
const handleDelegate = () => {
|
|
56
|
-
connect.open({
|
|
57
|
-
containerEl: undefined as unknown as Element,
|
|
58
|
-
saveConnect: false,
|
|
59
|
-
action: 'delegation',
|
|
60
|
-
prefix: joinURL(getPrefix(), '/api/did'),
|
|
61
|
-
extraParams: { subscriptionId: id },
|
|
62
|
-
onSuccess: () => {
|
|
63
|
-
connect.close();
|
|
64
|
-
Toast.success(t('customer.delegation.success'));
|
|
65
|
-
runDelegation();
|
|
66
|
-
},
|
|
67
|
-
onClose: () => {
|
|
68
|
-
connect.close();
|
|
69
|
-
},
|
|
70
|
-
onError: (err: any) => {
|
|
71
|
-
Toast.error(formatError(err));
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
};
|
|
75
53
|
if (data?.customer?.did && session?.user?.did && data.customer.did !== session.user.did) {
|
|
76
54
|
return <Alert severity="error">You do not have permission to access other customer data</Alert>;
|
|
77
55
|
}
|
|
@@ -83,9 +61,37 @@ export default function CustomerSubscriptionDetail() {
|
|
|
83
61
|
return <CircularProgress />;
|
|
84
62
|
}
|
|
85
63
|
|
|
64
|
+
const showOverdraftProtection =
|
|
65
|
+
data?.paymentMethod?.type === 'arcblock' &&
|
|
66
|
+
!overdraftProtectionLoading &&
|
|
67
|
+
!!overdraftProtection &&
|
|
68
|
+
['active', 'trialing', 'past_due'].includes(data.status);
|
|
69
|
+
|
|
70
|
+
const renderOverdraftProtectionLabel = () => {
|
|
71
|
+
const enabled = data?.overdraft_protection?.enabled;
|
|
72
|
+
const insufficient =
|
|
73
|
+
!enableOverdraftProtection ||
|
|
74
|
+
overdraftProtection.unused === '0' ||
|
|
75
|
+
new BN(overdraftProtection.unused).lt(
|
|
76
|
+
new BN(overdraftProtection.upcoming?.amount).add(new BN(overdraftProtection.gas))
|
|
77
|
+
);
|
|
78
|
+
if (!enabled) {
|
|
79
|
+
return t('customer.overdraftProtection.disabled');
|
|
80
|
+
}
|
|
81
|
+
if (enabled && insufficient) {
|
|
82
|
+
return <Typography color="error">{t('customer.overdraftProtection.insufficient')}</Typography>;
|
|
83
|
+
}
|
|
84
|
+
return t('customer.overdraftProtection.enabled');
|
|
85
|
+
};
|
|
86
|
+
|
|
86
87
|
return (
|
|
87
88
|
<Root>
|
|
88
89
|
<Box>
|
|
90
|
+
{hasUnpaid && (
|
|
91
|
+
<Alert severity="error" sx={{ mt: 2 }}>
|
|
92
|
+
{t('customer.unpaidInvoicesWarningTip')}
|
|
93
|
+
</Alert>
|
|
94
|
+
)}
|
|
89
95
|
<Stack
|
|
90
96
|
className="page-header"
|
|
91
97
|
direction="row"
|
|
@@ -103,18 +109,17 @@ export default function CustomerSubscriptionDetail() {
|
|
|
103
109
|
</Typography>
|
|
104
110
|
</Stack>
|
|
105
111
|
<Stack direction="row" gap={1}>
|
|
106
|
-
{noDelegation && ['active', 'trialing', 'past_due'].includes(data?.status) && (
|
|
107
|
-
<Tooltip title={t('customer.delegation.title')}>
|
|
108
|
-
<Button variant="outlined" color="primary" onClick={() => handleDelegate()}>
|
|
109
|
-
{t('customer.delegation.btn')}
|
|
110
|
-
</Button>
|
|
111
|
-
</Tooltip>
|
|
112
|
-
)}
|
|
113
112
|
<SubscriptionActions
|
|
114
113
|
subscription={data}
|
|
115
114
|
onChange={() => refresh()}
|
|
116
115
|
showExtra
|
|
116
|
+
showDelegation
|
|
117
|
+
showOverdraftProtection={{
|
|
118
|
+
show: showOverdraftProtection,
|
|
119
|
+
onChange: () => refreshOverdraftProtection(),
|
|
120
|
+
}}
|
|
117
121
|
showRecharge
|
|
122
|
+
mode={isMobile ? 'menu' : 'btn'}
|
|
118
123
|
actionProps={{
|
|
119
124
|
cancel: {
|
|
120
125
|
variant: 'outlined',
|
|
@@ -183,6 +188,9 @@ export default function CustomerSubscriptionDetail() {
|
|
|
183
188
|
},
|
|
184
189
|
}}>
|
|
185
190
|
<SubscriptionMetrics subscription={data} />
|
|
191
|
+
{showOverdraftProtection && (
|
|
192
|
+
<InfoMetric label={t('customer.overdraftProtection.title')} value={renderOverdraftProtectionLabel()} />
|
|
193
|
+
)}
|
|
186
194
|
</Stack>
|
|
187
195
|
</Box>
|
|
188
196
|
</Box>
|
|
@@ -271,8 +279,13 @@ export default function CustomerSubscriptionDetail() {
|
|
|
271
279
|
variant="text"
|
|
272
280
|
sx={{ color: 'text.link' }}
|
|
273
281
|
size="small"
|
|
274
|
-
|
|
275
|
-
|
|
282
|
+
onClick={async () => {
|
|
283
|
+
const result = await checkUnpaidInvoices();
|
|
284
|
+
if (result) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
navigate(`/customer/subscription/${data.id}/change-payment`);
|
|
288
|
+
}}>
|
|
276
289
|
{t('payment.customer.subscriptions.changePayment')}
|
|
277
290
|
</Button>
|
|
278
291
|
)}
|
|
@@ -338,7 +351,12 @@ export default function CustomerSubscriptionDetail() {
|
|
|
338
351
|
{t('customer.invoiceHistory')}
|
|
339
352
|
</Typography>
|
|
340
353
|
<Box className="section-body">
|
|
341
|
-
<CustomerInvoiceList
|
|
354
|
+
<CustomerInvoiceList
|
|
355
|
+
subscription_id={data.id}
|
|
356
|
+
type="table"
|
|
357
|
+
include_staking
|
|
358
|
+
status="open,paid,uncollectible,void"
|
|
359
|
+
/>
|
|
342
360
|
</Box>
|
|
343
361
|
</Box>
|
|
344
362
|
</Root>
|
package/tsconfig.json
CHANGED
|
@@ -98,10 +98,5 @@
|
|
|
98
98
|
/* Completeness */
|
|
99
99
|
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
|
100
100
|
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
|
|
101
|
-
// "baseUrl": "./",
|
|
102
|
-
"paths": {
|
|
103
|
-
// "@app/*": ["./src/*"],
|
|
104
|
-
// "@api/*": ["./api/src/*"],
|
|
105
|
-
}
|
|
106
101
|
}
|
|
107
102
|
}
|