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.
Files changed (66) hide show
  1. package/api/src/crons/index.ts +1 -1
  2. package/api/src/hooks/pre-start.ts +2 -0
  3. package/api/src/index.ts +2 -0
  4. package/api/src/integrations/arcblock/stake.ts +7 -1
  5. package/api/src/integrations/stripe/resource.ts +1 -1
  6. package/api/src/libs/env.ts +12 -0
  7. package/api/src/libs/event.ts +8 -0
  8. package/api/src/libs/invoice.ts +585 -3
  9. package/api/src/libs/notification/template/subscription-succeeded.ts +1 -2
  10. package/api/src/libs/notification/template/subscription-trial-will-end.ts +2 -2
  11. package/api/src/libs/notification/template/subscription-will-renew.ts +6 -2
  12. package/api/src/libs/notification/template/subscription.overdraft-protection.exhausted.ts +139 -0
  13. package/api/src/libs/overdraft-protection.ts +85 -0
  14. package/api/src/libs/payment.ts +1 -65
  15. package/api/src/libs/queue/index.ts +0 -1
  16. package/api/src/libs/subscription.ts +532 -2
  17. package/api/src/libs/util.ts +4 -0
  18. package/api/src/locales/en.ts +5 -0
  19. package/api/src/locales/zh.ts +5 -0
  20. package/api/src/queues/event.ts +3 -2
  21. package/api/src/queues/invoice.ts +28 -3
  22. package/api/src/queues/notification.ts +25 -3
  23. package/api/src/queues/payment.ts +154 -3
  24. package/api/src/queues/refund.ts +2 -2
  25. package/api/src/queues/subscription.ts +215 -4
  26. package/api/src/queues/webhook.ts +1 -0
  27. package/api/src/routes/connect/change-payment.ts +1 -1
  28. package/api/src/routes/connect/change-plan.ts +1 -1
  29. package/api/src/routes/connect/overdraft-protection.ts +120 -0
  30. package/api/src/routes/connect/recharge.ts +2 -1
  31. package/api/src/routes/connect/setup.ts +1 -1
  32. package/api/src/routes/connect/shared.ts +117 -350
  33. package/api/src/routes/connect/subscribe.ts +1 -1
  34. package/api/src/routes/customers.ts +2 -2
  35. package/api/src/routes/invoices.ts +9 -4
  36. package/api/src/routes/subscriptions.ts +172 -2
  37. package/api/src/store/migrate.ts +9 -10
  38. package/api/src/store/migrations/20240905-index.ts +95 -60
  39. package/api/src/store/migrations/20241203-overdraft-protection.ts +25 -0
  40. package/api/src/store/migrations/20241216-update-overdraft-protection.ts +30 -0
  41. package/api/src/store/models/customer.ts +2 -2
  42. package/api/src/store/models/invoice.ts +7 -0
  43. package/api/src/store/models/lock.ts +7 -0
  44. package/api/src/store/models/subscription.ts +15 -0
  45. package/api/src/store/sequelize.ts +6 -1
  46. package/blocklet.yml +1 -1
  47. package/package.json +23 -23
  48. package/src/components/customer/overdraft-protection.tsx +367 -0
  49. package/src/components/event/list.tsx +3 -4
  50. package/src/components/subscription/actions/cancel.tsx +3 -0
  51. package/src/components/subscription/portal/actions.tsx +324 -77
  52. package/src/components/uploader.tsx +31 -26
  53. package/src/env.d.ts +1 -0
  54. package/src/hooks/subscription.ts +30 -0
  55. package/src/libs/env.ts +4 -0
  56. package/src/locales/en.tsx +41 -0
  57. package/src/locales/zh.tsx +37 -0
  58. package/src/pages/admin/billing/invoices/detail.tsx +16 -15
  59. package/src/pages/customer/index.tsx +7 -2
  60. package/src/pages/customer/invoice/detail.tsx +29 -5
  61. package/src/pages/customer/invoice/past-due.tsx +18 -4
  62. package/src/pages/customer/recharge.tsx +2 -4
  63. package/src/pages/customer/subscription/change-payment.tsx +7 -1
  64. package/src/pages/customer/subscription/detail.tsx +69 -51
  65. package/tsconfig.json +0 -5
  66. package/api/tests/libs/payment.spec.ts +0 -168
@@ -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
- {data.billing_reason === 'stake' && data?.metadata?.payment_details?.arcblock?.tx_hash && (
326
- <InfoRow
327
- label={t('common.stakeTxHash')}
328
- value={
329
- <TxLink
330
- details={{
331
- arcblock: { tx_hash: data.metadata?.payment_details?.arcblock?.tx_hash, payer: '' },
332
- }}
333
- method={data.paymentMethod}
334
- />
335
- }
336
- direction={InfoDirection}
337
- alignItems={InfoAlignItems}
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({ editing: false, loading: false, onlyActive: true });
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 = data.paymentMethod?.type === 'arcblock' && data.billing_reason === 'stake';
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.description}
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 { CustomerInvoiceList, formatError, getPrefix, usePaymentContext } from '@blocklet/payment-react';
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(true);
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}/upcoming`),
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 === '0' ? upcomingRes.data.minExpectedAmount : 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, Tooltip, Typography } from '@mui/material';
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 { joinURL } from 'ufo';
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 fetchSubscriptionDelegation = (id: string): Promise<{ sufficient: boolean }> => {
36
- return api.get(`/api/subscriptions/${id}/delegation`).then((res) => res.data);
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 { connect } = usePaymentContext();
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 { data: delegation = { sufficient: true }, runAsync: runDelegation } = useRequest(() =>
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
- component={Link}
275
- to={`/customer/subscription/${data.id}/change-payment`}>
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 subscription_id={data.id} type="table" include_staking />
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
  }