payment-kit 1.18.17 → 1.18.19

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 (35) hide show
  1. package/api/src/libs/subscription.ts +116 -0
  2. package/api/src/routes/checkout-sessions.ts +28 -1
  3. package/api/src/routes/customers.ts +5 -1
  4. package/api/src/store/migrations/20250318-donate-invoice.ts +45 -0
  5. package/api/tests/libs/subscription.spec.ts +311 -0
  6. package/blocklet.yml +1 -1
  7. package/package.json +9 -9
  8. package/src/components/currency.tsx +11 -4
  9. package/src/components/customer/link.tsx +54 -14
  10. package/src/components/customer/overdraft-protection.tsx +36 -2
  11. package/src/components/info-card.tsx +55 -7
  12. package/src/components/info-row-group.tsx +122 -0
  13. package/src/components/info-row.tsx +14 -1
  14. package/src/components/payouts/portal/list.tsx +7 -2
  15. package/src/components/subscription/items/index.tsx +1 -1
  16. package/src/components/subscription/metrics.tsx +14 -6
  17. package/src/contexts/info-row.tsx +4 -0
  18. package/src/locales/en.tsx +1 -0
  19. package/src/locales/zh.tsx +1 -0
  20. package/src/pages/admin/billing/invoices/detail.tsx +54 -76
  21. package/src/pages/admin/billing/subscriptions/detail.tsx +34 -71
  22. package/src/pages/admin/customers/customers/detail.tsx +41 -64
  23. package/src/pages/admin/payments/intents/detail.tsx +28 -42
  24. package/src/pages/admin/payments/payouts/detail.tsx +27 -36
  25. package/src/pages/admin/payments/refunds/detail.tsx +27 -41
  26. package/src/pages/admin/products/links/detail.tsx +30 -55
  27. package/src/pages/admin/products/prices/detail.tsx +43 -50
  28. package/src/pages/admin/products/pricing-tables/detail.tsx +23 -25
  29. package/src/pages/admin/products/products/detail.tsx +52 -81
  30. package/src/pages/customer/index.tsx +183 -108
  31. package/src/pages/customer/invoice/detail.tsx +49 -50
  32. package/src/pages/customer/payout/detail.tsx +16 -22
  33. package/src/pages/customer/recharge/account.tsx +92 -34
  34. package/src/pages/customer/recharge/subscription.tsx +6 -0
  35. package/src/pages/customer/subscription/detail.tsx +176 -94
@@ -2,16 +2,22 @@ import type { TCustomer } from '@blocklet/payment-types';
2
2
  import { Link } from 'react-router-dom';
3
3
 
4
4
  import { getCustomerAvatar } from '@blocklet/payment-react';
5
+ import DID from '@arcblock/ux/lib/DID';
6
+ import { Box, Typography } from '@mui/material';
5
7
  import InfoCard from '../info-card';
6
8
 
7
9
  export default function CustomerLink({
8
10
  customer,
9
11
  linked,
10
12
  linkTo,
13
+ size,
14
+ tooltip,
11
15
  }: {
12
16
  customer: TCustomer;
13
17
  linked?: boolean;
14
18
  linkTo?: string;
19
+ size?: 'default' | 'small';
20
+ tooltip?: boolean;
15
21
  }) {
16
22
  if (!customer) {
17
23
  return null;
@@ -19,29 +25,63 @@ export default function CustomerLink({
19
25
  if (linked) {
20
26
  return (
21
27
  <Link to={linkTo || `/admin/customers/${customer.id}`}>
22
- <InfoCard
23
- logo={getCustomerAvatar(
24
- customer?.did,
25
- customer?.updated_at ? new Date(customer.updated_at).toISOString() : '',
26
- 48
27
- )}
28
- name={customer.email}
29
- description={`${customer.did.slice(0, 6)}...${customer.did.slice(-6)}`}
30
- />
28
+ <Box sx={{ '.info-card-wrapper': { cursor: 'pointer' }, '.info-card': { minWidth: 0 } }}>
29
+ {/* @ts-ignore */}
30
+ <InfoCard
31
+ logo={getCustomerAvatar(
32
+ customer?.did,
33
+ customer?.updated_at ? new Date(customer.updated_at).toISOString() : '',
34
+ size === 'small' ? 24 : 48
35
+ )}
36
+ name={
37
+ <Typography
38
+ sx={{ maxWidth: 208, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
39
+ className="customer-link-name">
40
+ {customer.name || customer.email}
41
+ </Typography>
42
+ }
43
+ {...(size === 'small'
44
+ ? { tooltip: tooltip ? <DID did={customer?.did} /> : false, size: 24 }
45
+ : {
46
+ description: <DID did={customer?.did} compact responsive={false} sx={{ whiteSpace: 'nowrap' }} />,
47
+ size: 48,
48
+ })}
49
+ />
50
+ </Box>
31
51
  </Link>
32
52
  );
33
53
  }
34
54
 
35
55
  return (
36
- <InfoCard
37
- logo={getCustomerAvatar(customer.did, customer.updated_at ? new Date(customer.updated_at).toISOString() : '', 48)}
38
- name={customer.email}
39
- description={<span style={{ wordBreak: 'break-all' }}>{customer?.did}</span>}
40
- />
56
+ <Box sx={{ '.info-card': { minWidth: 0 } }}>
57
+ {/* @ts-ignore */}
58
+ <InfoCard
59
+ logo={getCustomerAvatar(
60
+ customer.did,
61
+ customer.updated_at ? new Date(customer.updated_at).toISOString() : '',
62
+ size === 'small' ? 24 : 48
63
+ )}
64
+ name={
65
+ <Typography
66
+ sx={{ maxWidth: 320, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
67
+ className="customer-link-name">
68
+ {customer.name || customer.email}
69
+ </Typography>
70
+ }
71
+ {...(size === 'small'
72
+ ? { tooltip: tooltip ? <DID did={customer?.did} /> : false, size: 24 }
73
+ : {
74
+ description: <DID did={customer?.did} compact responsive={false} sx={{ whiteSpace: 'nowrap' }} />,
75
+ size: 48,
76
+ })}
77
+ />
78
+ </Box>
41
79
  );
42
80
  }
43
81
 
44
82
  CustomerLink.defaultProps = {
45
83
  linked: true,
46
84
  linkTo: '',
85
+ size: 'default',
86
+ tooltip: true,
47
87
  };
@@ -26,7 +26,7 @@ import { useRequest } from 'ahooks';
26
26
  import { BN, fromTokenToUnit, fromUnitToToken } from '@ocap/util';
27
27
  import type { TPaymentCurrency, TSubscriptionExpanded } from '@blocklet/payment-types';
28
28
  import { joinURL } from 'ufo';
29
- import { OpenInNewOutlined } from '@mui/icons-material';
29
+ import { HelpOutline, OpenInNewOutlined } from '@mui/icons-material';
30
30
  import Currency from '../currency';
31
31
  import { formatSmartDuration, TimeUnit } from '../../libs/dayjs';
32
32
 
@@ -228,7 +228,41 @@ export default function OverdraftProtectionDialog({
228
228
  maxWidth="sm"
229
229
  fullWidth
230
230
  className="base-dialog"
231
- title={t('customer.overdraftProtection.setting')}
231
+ title={
232
+ <Stack direction="row" alignItems="center" spacing={1}>
233
+ <Typography variant="h6" component="span">
234
+ {t('customer.overdraftProtection.setting')}
235
+ </Typography>
236
+ <Link
237
+ href="https://www.arcblock.io/content/blog/en/payment-kit-v117-sub-guard#listen-to-the-audio-overview"
238
+ target="_blank"
239
+ rel="noopener noreferrer"
240
+ underline="hover"
241
+ sx={{
242
+ display: 'flex',
243
+ alignItems: 'center',
244
+ gap: 0.5,
245
+ fontSize: '14px',
246
+ fontWeight: 'normal',
247
+ ml: 1,
248
+ color: 'text.link',
249
+ '&:hover': { color: 'primary.main' },
250
+ ...(isMobile ? { fontSize: '12px' } : {}),
251
+ }}>
252
+ <Tooltip title={t('customer.overdraftProtection.learnMore')} placement="top">
253
+ <HelpOutline
254
+ fontSize="small"
255
+ sx={{
256
+ fontSize: '16px',
257
+ color: 'text.secondary',
258
+ cursor: 'pointer',
259
+ '&:hover': { color: 'primary.main' },
260
+ }}
261
+ />
262
+ </Tooltip>
263
+ </Link>
264
+ </Stack>
265
+ }
232
266
  actions={
233
267
  <Stack direction="row" spacing={2}>
234
268
  <Button variant="outlined" onClick={onCancel}>
@@ -1,5 +1,5 @@
1
1
  import { getWordBreakStyle } from '@blocklet/payment-react';
2
- import { Avatar, Stack, SxProps, Typography } from '@mui/material';
2
+ import { Avatar, Stack, SxProps, Typography, Tooltip } from '@mui/material';
3
3
  import type { LiteralUnion } from 'type-fest';
4
4
 
5
5
  type Props = {
@@ -11,13 +11,20 @@ type Props = {
11
11
  sx?: SxProps;
12
12
  className?: string;
13
13
  logoName?: string;
14
+ tooltip?: string | React.ReactNode;
14
15
  };
15
16
 
16
17
  export default function InfoCard(props: Props) {
17
18
  const dimensions = { width: props.size, height: props.size, ...props.sx };
18
19
  const avatarName = typeof props.name === 'string' ? props.name : props.logo;
19
- return (
20
- <Stack direction="row" alignItems="center" spacing={1} className={`info-card-wrapper ${props.className}`}>
20
+
21
+ const cardContent = (
22
+ <Stack
23
+ direction="row"
24
+ alignItems="center"
25
+ spacing={1}
26
+ className={`info-card-wrapper ${props.className}`}
27
+ sx={{ cursor: props.tooltip ? 'pointer' : 'default' }}>
21
28
  {props.logo ? (
22
29
  <Avatar src={props.logo} alt={props.logoName ?? avatarName} variant={props.variant as any} sx={dimensions} />
23
30
  ) : (
@@ -30,16 +37,56 @@ export default function InfoCard(props: Props) {
30
37
  alignItems="flex-start"
31
38
  justifyContent="space-around"
32
39
  className="info-card"
33
- sx={{ wordBreak: getWordBreakStyle(props.name), minWidth: 140 }}>
40
+ sx={{
41
+ wordBreak: getWordBreakStyle(props.name),
42
+ minWidth: 140,
43
+ }}>
34
44
  <Typography variant="body1" color="text.primary" component="div">
35
45
  {props.name}
36
46
  </Typography>
37
- <Typography variant="subtitle1" color="text.secondary">
38
- {props.description}
39
- </Typography>
47
+ {props.description && (
48
+ <Typography variant="subtitle1" color="text.secondary">
49
+ {props.description}
50
+ </Typography>
51
+ )}
40
52
  </Stack>
41
53
  </Stack>
42
54
  );
55
+
56
+ if (props.tooltip) {
57
+ return (
58
+ <Tooltip
59
+ title={props.tooltip}
60
+ placement="top-start"
61
+ componentsProps={{
62
+ tooltip: {
63
+ sx: {
64
+ bgcolor: (theme) => (theme.palette.mode === 'dark' ? '#2a2e37' : '#ffffff'),
65
+ color: (theme) => (theme.palette.mode === 'dark' ? '#ffffff' : theme.palette.text.primary),
66
+ boxShadow: '0 2px 10px rgba(0, 0, 0, 0.15)',
67
+ borderRadius: 2,
68
+ padding: '10px 16px',
69
+ fontSize: 14,
70
+ maxWidth: 400,
71
+ minWidth: 240,
72
+ wordBreak: 'break-word',
73
+ border: (theme) => (theme.palette.mode === 'dark' ? '1px solid #3a3f48' : '1px solid #e0e0e0'),
74
+ '& .MuiTooltip-arrow': {
75
+ color: (theme) => (theme.palette.mode === 'dark' ? '#2a2e37' : '#ffffff'),
76
+ '&::before': {
77
+ border: (theme) => (theme.palette.mode === 'dark' ? '1px solid #3a3f48' : '1px solid #e0e0e0'),
78
+ backgroundColor: (theme) => (theme.palette.mode === 'dark' ? '#2a2e37' : '#ffffff'),
79
+ },
80
+ },
81
+ },
82
+ },
83
+ }}>
84
+ {cardContent}
85
+ </Tooltip>
86
+ );
87
+ }
88
+
89
+ return cardContent;
43
90
  }
44
91
 
45
92
  InfoCard.defaultProps = {
@@ -49,4 +96,5 @@ InfoCard.defaultProps = {
49
96
  sx: {},
50
97
  className: '',
51
98
  logoName: '',
99
+ tooltip: false,
52
100
  };
@@ -0,0 +1,122 @@
1
+ import { Box, Skeleton, SxProps } from '@mui/material';
2
+ import React, {
3
+ useEffect,
4
+ useRef,
5
+ useState,
6
+ Children,
7
+ cloneElement,
8
+ isValidElement,
9
+ ReactElement,
10
+ useLayoutEffect,
11
+ } from 'react';
12
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
13
+ import { InfoRowGroupContext } from '../contexts/info-row';
14
+
15
+ type InfoRowGroupProps = {
16
+ children: React.ReactNode;
17
+ loading?: boolean;
18
+ skeletonHeight?: number;
19
+ sx?: SxProps;
20
+ };
21
+
22
+ function InfoRowGroup({ children, loading = false, skeletonHeight = 24, sx = {} }: InfoRowGroupProps) {
23
+ const [maxLabelWidth, setMaxLabelWidth] = useState<number>(0);
24
+ const [measurementStage, setMeasurementStage] = useState<'measuring' | 'complete'>('measuring');
25
+ const containerRef = useRef<HTMLDivElement>(null);
26
+ const { locale } = useLocaleContext();
27
+
28
+ const resetMeasurement = () => {
29
+ setMeasurementStage('measuring');
30
+ setMaxLabelWidth(0);
31
+ };
32
+
33
+ useLayoutEffect(() => {
34
+ if (loading || !containerRef.current) return;
35
+
36
+ if (measurementStage === 'measuring' || maxLabelWidth === 0) {
37
+ const labelElements = containerRef.current.querySelectorAll('.info-row-label');
38
+
39
+ if (labelElements.length > 0) {
40
+ let maxWidth = 0;
41
+ labelElements.forEach((label) => {
42
+ const width = (label as HTMLElement).offsetWidth;
43
+ if (width > maxWidth) {
44
+ maxWidth = width;
45
+ }
46
+ });
47
+
48
+ maxWidth += 16;
49
+
50
+ if (maxWidth > 0) {
51
+ setMaxLabelWidth(maxWidth);
52
+ requestAnimationFrame(() => {
53
+ setMeasurementStage('complete');
54
+ });
55
+ }
56
+ }
57
+ }
58
+ }, [children, loading, measurementStage, locale]); // 添加locale作为依赖项
59
+
60
+ useEffect(() => {
61
+ resetMeasurement();
62
+ }, [locale]);
63
+
64
+ // Create skeleton rows for loading state
65
+ const renderSkeletons = () => {
66
+ // Count the number of InfoRow children
67
+ let count = 0;
68
+ Children.forEach(children, (child) => {
69
+ if (isValidElement(child) && child.type && (child.type as any).displayName === 'InfoRow') {
70
+ count++;
71
+ }
72
+ });
73
+
74
+ return Array(count)
75
+ .fill(0)
76
+ .map((_, index) => (
77
+ <Box key={`skeleton-row-${count - index}`} sx={{ display: 'flex', gap: 2 }}>
78
+ <Skeleton width="30%" height={skeletonHeight} />
79
+ <Skeleton width="65%" height={skeletonHeight} />
80
+ </Box>
81
+ ));
82
+ };
83
+
84
+ // Clone children with calculated width
85
+ const renderChildren = () => {
86
+ return Children.map(children, (child) => {
87
+ if (isValidElement(child) && child.type && (child.type as any).displayName === 'InfoRow') {
88
+ const childProps = {
89
+ ...child.props,
90
+ sx: {
91
+ ...child.props.sx,
92
+ visibility: measurementStage === 'measuring' ? 'hidden' : 'visible',
93
+ '.info-row-label': {
94
+ width: maxLabelWidth > 0 ? `${maxLabelWidth}px` : 'auto',
95
+ minWidth: maxLabelWidth > 0 ? `${maxLabelWidth}px` : 'auto',
96
+ whiteSpace: 'nowrap',
97
+ transition: 'width 0.3s ease-out, min-width 0.3s ease-out',
98
+ },
99
+ },
100
+ };
101
+ return cloneElement(child as ReactElement, childProps);
102
+ }
103
+ return child;
104
+ });
105
+ };
106
+
107
+ return (
108
+ <InfoRowGroupContext.Provider value>
109
+ <Box ref={containerRef} sx={{ ...sx }}>
110
+ {loading ? renderSkeletons() : renderChildren()}
111
+ </Box>
112
+ </InfoRowGroupContext.Provider>
113
+ );
114
+ }
115
+
116
+ InfoRowGroup.defaultProps = {
117
+ loading: false,
118
+ skeletonHeight: 24,
119
+ sx: {},
120
+ };
121
+
122
+ export default InfoRowGroup;
@@ -1,8 +1,10 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
2
  import { Box, Stack, SxProps } from '@mui/material';
3
+ import { useContext } from 'react';
3
4
  import type { ReactNode } from 'react';
4
5
  import { getWordBreakStyle } from '@blocklet/payment-react';
5
6
  import { isEmptyExceptNumber } from '../libs/util';
7
+ import { InfoRowGroupContext } from '../contexts/info-row';
6
8
 
7
9
  type Props = {
8
10
  label: string | ReactNode;
@@ -27,6 +29,7 @@ export default function InfoRow(props: Props) {
27
29
  const { t } = useLocaleContext();
28
30
  const isNone = isEmptyExceptNumber(props.value);
29
31
  const sizes = props.sizes || [1, 3];
32
+ const inGroup = useContext(InfoRowGroupContext);
30
33
  return (
31
34
  <Stack
32
35
  direction={props.direction || 'row'}
@@ -47,11 +50,18 @@ export default function InfoRow(props: Props) {
47
50
  flex={sizes[0]}
48
51
  className="info-row-label"
49
52
  color="text.primary"
50
- fontWeight={500}
51
53
  fontSize={14}
52
54
  sx={{
53
55
  flex: props.direction === 'column' ? 'none' : sizes[0],
54
56
  mb: props.direction === 'column' ? 0.5 : 0,
57
+ ...(inGroup && {
58
+ flex: 'none',
59
+ whiteSpace: 'nowrap',
60
+ }),
61
+ fontWeight: {
62
+ xs: 500,
63
+ md: 400,
64
+ },
55
65
  }}>
56
66
  {props.label}
57
67
  </Box>
@@ -68,3 +78,6 @@ export default function InfoRow(props: Props) {
68
78
  </Stack>
69
79
  );
70
80
  }
81
+
82
+ // Add display name for identification
83
+ InfoRow.displayName = 'InfoRow';
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable react/no-unstable-nested-components */
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
- import { api, formatBNStr, formatTime, Table, useDefaultPageSize } from '@blocklet/payment-react';
3
+ import { api, formatBNStr, formatTime, Table, useDefaultPageSize, useMobile } from '@blocklet/payment-react';
4
4
  import type { TCustomer, TPaymentIntentExpanded, TPayoutExpanded } from '@blocklet/payment-types';
5
5
  import { Box, Typography } from '@mui/material';
6
6
  import { useLocalStorageState } from 'ahooks';
@@ -64,6 +64,7 @@ CustomerRevenueList.defaultProps = {
64
64
 
65
65
  export default function CustomerRevenueList({ currency_id, status, customer_id }: ListProps) {
66
66
  const { t } = useLocaleContext();
67
+ const { isMobile } = useMobile('sm');
67
68
 
68
69
  const listKey = getListKey({ customer_id });
69
70
  const defaultPageSize = useDefaultPageSize(10);
@@ -121,7 +122,11 @@ export default function CustomerRevenueList({ currency_id, status, customer_id }
121
122
  };
122
123
  // @ts-ignore
123
124
  return item?.paymentIntent?.customer ? (
124
- <CustomerLink customer={item.paymentIntent.customer} linkTo={`/customer/payout/${item.id}`} />
125
+ <CustomerLink
126
+ customer={item.paymentIntent.customer}
127
+ linkTo={`/customer/payout/${item.id}`}
128
+ size={isMobile ? 'small' : 'default'}
129
+ />
125
130
  ) : (
126
131
  t('common.none')
127
132
  );
@@ -123,7 +123,7 @@ export default function SubscriptionItemList({ data, currency, mode }: ListProps
123
123
  },
124
124
  },
125
125
  mode === 'admin' && {
126
- label: '',
126
+ label: t('common.actions'),
127
127
  name: '',
128
128
  align: 'center',
129
129
  options: {
@@ -6,7 +6,7 @@ import { useRequest } from 'ahooks';
6
6
  import { Button, Stack, Typography, Tooltip, Avatar, Box, CircularProgress, Skeleton } from '@mui/material';
7
7
  import { BN } from '@ocap/util';
8
8
  import { useNavigate } from 'react-router-dom';
9
- import { ArrowForward, InfoOutlined } from '@mui/icons-material';
9
+ import { AccountBalanceWalletOutlined, ArrowForward } from '@mui/icons-material';
10
10
  import InfoMetric from '../info-metric';
11
11
  import SubscriptionStatus from './status';
12
12
 
@@ -59,7 +59,12 @@ export default function SubscriptionMetrics({ subscription, showBalance = true }
59
59
  }
60
60
 
61
61
  return (
62
- <Stack flexDirection="row" alignItems="center" gap={0.5} sx={{ fontSize: '16px', fontWeight: 500 }}>
62
+ <Stack
63
+ flexDirection="row"
64
+ alignItems="center"
65
+ gap={0.5}
66
+ sx={{ fontSize: '16px', fontWeight: 500, cursor: 'pointer', '&:hover': { color: 'primary.main' } }}
67
+ onClick={() => navigate(`/customer/subscription/${subscription.id}/recharge`)}>
63
68
  <Avatar
64
69
  src={subscription.paymentCurrency?.logo}
65
70
  sx={{ width: 16, height: 16 }}
@@ -67,9 +72,7 @@ export default function SubscriptionMetrics({ subscription, showBalance = true }
67
72
  />
68
73
  <Box display="flex" alignItems="baseline">
69
74
  {formatBNStr(payerValue?.token, subscription.paymentCurrency.decimal)}
70
- <Typography sx={{ fontSize: '14px', color: 'text.secondary', ml: 0.5 }}>
71
- {subscription.paymentCurrency.symbol}
72
- </Typography>
75
+ <Typography sx={{ fontSize: '14px', ml: 0.5 }}>{subscription.paymentCurrency.symbol}</Typography>
73
76
  </Box>
74
77
  </Stack>
75
78
  );
@@ -105,13 +108,18 @@ export default function SubscriptionMetrics({ subscription, showBalance = true }
105
108
  {payerLoading ? <Skeleton width={120} /> : payerValue?.paymentAddress}
106
109
  </Typography>
107
110
  }
111
+ placement="top"
108
112
  arrow>
109
- <InfoOutlined
113
+ <AccountBalanceWalletOutlined
110
114
  sx={{
111
115
  fontSize: '16px',
112
116
  color: 'text.secondary',
113
117
  cursor: 'pointer',
114
118
  '&:hover': { color: 'primary.main' },
119
+ display: {
120
+ xs: 'none',
121
+ md: 'block',
122
+ },
115
123
  }}
116
124
  />
117
125
  </Tooltip>
@@ -0,0 +1,4 @@
1
+ import { createContext } from 'react';
2
+
3
+ // Create shared context for InfoRowGroup that InfoRow can use
4
+ export const InfoRowGroupContext = createContext(false);
@@ -809,6 +809,7 @@ export default flat({
809
809
  overdraftProtection: {
810
810
  title: 'SubGuard™',
811
811
  setting: 'Set SubGuard™',
812
+ learnMore: 'Click to learn more about SubGuard™',
812
813
  tip: 'To avoid service interruption due to unpaid invoices, you can enable SubGuard™ by staking. Timely payment will not incur additional fees. Please settle your invoices promptly. If your available stake is insufficient or payment is overdue, we will deduct the amount from your stake and charge a service fee.',
813
814
  enabled: 'Enabled',
814
815
  disabled: 'Disabled',
@@ -786,6 +786,7 @@ export default flat({
786
786
  overdraftProtection: {
787
787
  title: '订阅守护',
788
788
  setting: '配置订阅守护',
789
+ learnMore: '点击了解更多关于订阅守护',
789
790
  tip: '为避免因扣费失败中断服务,您可以通过质押开启订阅守护。按时付款不会收取额外费用,请及时付清账单,若可用质押不足或者超期未付,我们将从质押中扣除并收取服务费',
790
791
  enabled: '已启用',
791
792
  disabled: '未启用',