payment-kit 1.14.29 → 1.14.31

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 (59) hide show
  1. package/api/src/index.ts +4 -0
  2. package/api/src/libs/api.ts +23 -0
  3. package/api/src/libs/subscription.ts +32 -0
  4. package/api/src/queues/refund.ts +38 -1
  5. package/api/src/queues/subscription.ts +218 -21
  6. package/api/src/routes/checkout-sessions.ts +5 -0
  7. package/api/src/routes/customers.ts +27 -1
  8. package/api/src/routes/invoices.ts +5 -1
  9. package/api/src/routes/payment-intents.ts +17 -2
  10. package/api/src/routes/payment-links.ts +105 -3
  11. package/api/src/routes/payouts.ts +5 -1
  12. package/api/src/routes/prices.ts +19 -3
  13. package/api/src/routes/pricing-table.ts +79 -2
  14. package/api/src/routes/products.ts +24 -8
  15. package/api/src/routes/refunds.ts +7 -4
  16. package/api/src/routes/subscription-items.ts +5 -1
  17. package/api/src/routes/subscriptions.ts +38 -6
  18. package/api/src/routes/webhook-endpoints.ts +5 -1
  19. package/api/src/store/models/subscription.ts +1 -0
  20. package/api/tests/libs/api.spec.ts +72 -1
  21. package/api/third.d.ts +2 -0
  22. package/blocklet.yml +1 -1
  23. package/package.json +19 -18
  24. package/src/components/customer/form.tsx +53 -0
  25. package/src/components/filter-toolbar.tsx +1 -1
  26. package/src/components/invoice/list.tsx +8 -8
  27. package/src/components/invoice/table.tsx +42 -36
  28. package/src/components/metadata/form.tsx +24 -3
  29. package/src/components/payment-intent/actions.tsx +17 -5
  30. package/src/components/payment-link/after-pay.tsx +46 -4
  31. package/src/components/payouts/list.tsx +1 -1
  32. package/src/components/price/form.tsx +14 -2
  33. package/src/components/pricing-table/payment-settings.tsx +45 -4
  34. package/src/components/product/features.tsx +16 -2
  35. package/src/components/product/form.tsx +28 -4
  36. package/src/components/subscription/actions/cancel.tsx +10 -0
  37. package/src/components/subscription/description.tsx +2 -2
  38. package/src/components/subscription/items/index.tsx +3 -2
  39. package/src/components/subscription/portal/cancel.tsx +12 -1
  40. package/src/components/subscription/portal/list.tsx +169 -145
  41. package/src/hooks/loading.ts +28 -0
  42. package/src/locales/en.tsx +6 -1
  43. package/src/locales/zh.tsx +6 -1
  44. package/src/pages/admin/billing/invoices/detail.tsx +17 -2
  45. package/src/pages/admin/billing/subscriptions/detail.tsx +4 -0
  46. package/src/pages/admin/customers/customers/detail.tsx +4 -0
  47. package/src/pages/admin/customers/customers/index.tsx +1 -1
  48. package/src/pages/admin/payments/intents/detail.tsx +4 -0
  49. package/src/pages/admin/payments/payouts/detail.tsx +4 -0
  50. package/src/pages/admin/payments/refunds/detail.tsx +4 -0
  51. package/src/pages/admin/products/links/detail.tsx +4 -0
  52. package/src/pages/admin/products/prices/detail.tsx +4 -0
  53. package/src/pages/admin/products/pricing-tables/detail.tsx +4 -0
  54. package/src/pages/admin/products/products/detail.tsx +4 -0
  55. package/src/pages/checkout/pricing-table.tsx +9 -3
  56. package/src/pages/customer/index.tsx +28 -17
  57. package/src/pages/customer/invoice/detail.tsx +27 -16
  58. package/src/pages/customer/invoice/past-due.tsx +3 -2
  59. package/src/pages/customer/subscription/detail.tsx +4 -0
@@ -1,11 +1,15 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
2
  import { AddOutlined, DeleteOutlineOutlined } from '@mui/icons-material';
3
3
  import { Box, Button, IconButton, TextField, Typography } from '@mui/material';
4
+ import { get } from 'lodash';
4
5
  import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
5
6
 
6
7
  export default function MetadataForm() {
7
8
  const { t } = useLocaleContext();
8
- const { control } = useFormContext();
9
+ const {
10
+ control,
11
+ formState: { errors },
12
+ } = useFormContext();
9
13
  const features = useFieldArray({ control, name: 'features' });
10
14
  return (
11
15
  <Box sx={{ width: 1 }}>
@@ -13,7 +17,17 @@ export default function MetadataForm() {
13
17
  {features.fields.map((feature, index) => (
14
18
  <Box key={feature.id} mt={2} sx={{ width: 1 }}>
15
19
  <Controller
16
- render={({ field }) => <TextField {...field} sx={{ width: '80%' }} size="small" />}
20
+ render={({ field }) => (
21
+ <TextField
22
+ {...field}
23
+ sx={{ width: '80%' }}
24
+ size="small"
25
+ inputProps={{ maxLength: 64 }}
26
+ error={!!get(errors, field.name)}
27
+ helperText={get(errors, field.name)?.message as string}
28
+ />
29
+ )}
30
+ rules={{ maxLength: { value: 64, message: t('common.maxLength', { len: 64 }) } }}
17
31
  name={`features.${index}.name`}
18
32
  control={control}
19
33
  />
@@ -38,16 +38,29 @@ export default function ProductForm(props: Props) {
38
38
  <Stack spacing={2} flex={2} alignItems="flex-start">
39
39
  <FormInput
40
40
  name="name"
41
- rules={{ required: t('admin.product.name.required') }}
41
+ rules={{
42
+ required: t('admin.product.name.required'),
43
+ maxLength: {
44
+ value: 64,
45
+ message: t('common.maxLength', { len: 64 }),
46
+ },
47
+ }}
42
48
  label={t('admin.product.name.label')}
43
49
  placeholder={t('admin.product.name.placeholder')}
44
50
  error={!!formState.errors.name}
45
51
  helperText={formState.errors.name?.message as string}
46
52
  autoFocus
53
+ inputProps={{ maxLength: 64 }}
47
54
  />
48
55
  <FormInput
49
56
  name="description"
50
- rules={{ required: t('admin.product.description.required') }}
57
+ rules={{
58
+ required: t('admin.product.description.required'),
59
+ maxLength: {
60
+ value: 256,
61
+ message: t('common.maxLength', { len: 256 }),
62
+ },
63
+ }}
51
64
  label={t('admin.product.description.label')}
52
65
  placeholder={t('admin.product.description.placeholder')}
53
66
  error={!!formState.errors.description}
@@ -55,11 +68,22 @@ export default function ProductForm(props: Props) {
55
68
  multiline
56
69
  minRows={2}
57
70
  maxRows={4}
71
+ inputProps={{ maxLength: 256 }}
58
72
  />
59
73
  <Collapse trigger={t('admin.product.additional')}>
60
74
  <Stack spacing={2} alignItems="flex-start">
61
- <FormInput name="statement_descriptor" label={t('admin.product.statement_descriptor.label')} />
62
- <FormInput name="unit_label" label={t('admin.product.unit_label.label')} />
75
+ <FormInput
76
+ name="statement_descriptor"
77
+ label={t('admin.product.statement_descriptor.label')}
78
+ rules={{ maxLength: { value: 32, message: t('common.maxLength', { len: 32 }) } }}
79
+ inputProps={{ maxLength: 32 }}
80
+ />
81
+ <FormInput
82
+ name="unit_label"
83
+ label={t('admin.product.unit_label.label')}
84
+ rules={{ maxLength: { value: 32, message: t('common.maxLength', { len: 32 }) } }}
85
+ inputProps={{ maxLength: 32 }}
86
+ />
63
87
  {!props.simple && <ProductFeatures />}
64
88
  {!props.simple && <MetadataForm title={t('common.metadata.label')} />}
65
89
  </Stack>
@@ -147,6 +147,16 @@ export default function SubscriptionCancelForm({ data }: { data: TSubscriptionEx
147
147
  symbol,
148
148
  })}
149
149
  />
150
+ <FormControlLabel
151
+ value="slash"
152
+ disabled={loading || !staking}
153
+ onClick={() => !(loading || !staking) && setValue('cancel.staking', 'slash')}
154
+ control={<Radio checked={stakingType === 'slash'} />}
155
+ label={t('admin.subscription.cancel.staking.slash', {
156
+ unused: formatAmount(staking?.return_amount || '0', decimal),
157
+ symbol,
158
+ })}
159
+ />
150
160
  </RadioGroup>
151
161
  </Stack>
152
162
  </>
@@ -1,4 +1,4 @@
1
- import { formatSubscriptionProduct, useMobile } from '@blocklet/payment-react';
1
+ import { formatSubscriptionProduct, TruncatedText, useMobile } from '@blocklet/payment-react';
2
2
  import type { TSubscriptionExpanded } from '@blocklet/payment-types';
3
3
  import { InfoOutlined } from '@mui/icons-material';
4
4
  import { Stack, Tooltip, Typography } from '@mui/material';
@@ -15,7 +15,7 @@ export default function SubscriptionDescription({ subscription, variant, hideSub
15
15
  return (
16
16
  <Stack direction="row" alignItems="center" spacing={1}>
17
17
  <Typography variant={variant} fontWeight={600} className="subscription-description">
18
- {subscription.description}
18
+ <TruncatedText text={subscription.description} maxLength={80} useWidth />
19
19
  </Typography>
20
20
  {!hideSubscription && !isMobile && (
21
21
  <Tooltip title={formatSubscriptionProduct(subscription.items)}>
@@ -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 { formatPrice, Table } from '@blocklet/payment-react';
3
+ import { formatPrice, Table, TruncatedText, useMobile } from '@blocklet/payment-react';
4
4
  import type { TPaymentCurrency, TSubscriptionItemExpanded } from '@blocklet/payment-types';
5
5
  import { Avatar, Stack, Typography } from '@mui/material';
6
6
 
@@ -22,6 +22,7 @@ const size = { width: 48, height: 48 };
22
22
 
23
23
  export default function SubscriptionItemList({ data, currency, mode }: ListProps) {
24
24
  const { t } = useLocaleContext();
25
+ const { isMobile } = useMobile();
25
26
  const columns = [
26
27
  {
27
28
  label: t('admin.subscription.product'),
@@ -53,7 +54,7 @@ export default function SubscriptionItemList({ data, currency, mode }: ListProps
53
54
  </Avatar>
54
55
  )}
55
56
  <Typography color="text.primary" fontWeight={600}>
56
- {item?.price.product.name}
57
+ <TruncatedText text={item?.price.product.name} maxLength={isMobile ? 20 : 50} useWidth />
57
58
  {mode === 'customer' ? '' : ` - ${item?.price_id}`}
58
59
  </Typography>
59
60
  <Typography color="text.secondary" whiteSpace="nowrap">
@@ -11,7 +11,9 @@ export default function CustomerCancelForm({ data }: { data: TSubscriptionExpand
11
11
  return (
12
12
  <Stack direction="column" spacing={1} alignItems="flex-start">
13
13
  <Typography>
14
- {t('payment.customer.cancel.description', { date: formatTime(data.current_period_end * 1000) })}
14
+ {data.status === 'trialing'
15
+ ? t('payment.customer.cancel.trialDescription')
16
+ : t('payment.customer.cancel.description', { date: formatTime(data.current_period_end * 1000) })}
15
17
  </Typography>
16
18
  <Controller
17
19
  name="cancel.feedback"
@@ -66,6 +68,12 @@ export default function CustomerCancelForm({ data }: { data: TSubscriptionExpand
66
68
  <Controller
67
69
  name="cancel.comment"
68
70
  control={control}
71
+ rules={{
72
+ maxLength: {
73
+ value: 200,
74
+ message: t('common.maxLength', { len: 200 }),
75
+ },
76
+ }}
69
77
  render={({ field }) => (
70
78
  <TextField
71
79
  variant="outlined"
@@ -76,6 +84,9 @@ export default function CustomerCancelForm({ data }: { data: TSubscriptionExpand
76
84
  maxRows={4}
77
85
  placeholder={t('payment.customer.cancel.comment')}
78
86
  {...field}
87
+ inputProps={{
88
+ maxLength: 200,
89
+ }}
79
90
  />
80
91
  )}
81
92
  />
@@ -1,21 +1,16 @@
1
1
  /* eslint-disable react/no-unstable-nested-components */
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import Empty from '@arcblock/ux/lib/Empty';
4
- import {
5
- Status,
6
- api,
7
- formatPrice,
8
- getSubscriptionStatusColor,
9
- getSubscriptionTimeSummary,
10
- useMobile,
11
- formatSubscriptionStatus,
12
- } from '@blocklet/payment-react';
4
+ import { api, formatPrice, getSubscriptionTimeSummary, useMobile } from '@blocklet/payment-react';
13
5
  import type { Paginated, TSubscriptionExpanded } from '@blocklet/payment-types';
14
6
  import { Avatar, AvatarGroup, Box, Button, CircularProgress, Stack, StackProps, Typography } from '@mui/material';
15
7
  import { useInfiniteScroll } from 'ahooks';
16
8
 
9
+ import { useRef } from 'react';
17
10
  import SubscriptionDescription from '../description';
18
11
  import SubscriptionActions from './actions';
12
+ import SubscriptionStatus from '../status';
13
+ import useDelayedLoading from '../../../hooks/loading';
19
14
 
20
15
  const fetchData = (params: Record<string, any> = {}): Promise<Paginated<TSubscriptionExpanded>> => {
21
16
  const search = new URLSearchParams();
@@ -47,168 +42,197 @@ export default function CurrentSubscriptions({
47
42
  }: Props) {
48
43
  const { t } = useLocaleContext();
49
44
  const { isMobile } = useMobile();
45
+ const listRef = useRef<HTMLDivElement | null>(null);
50
46
 
51
- const { data, loadMore, loadingMore, loading } = useInfiniteScroll<Paginated<TSubscriptionExpanded>>(
47
+ const { data, loadMore, loadingMore, loading, reload } = useInfiniteScroll<Paginated<TSubscriptionExpanded>>(
52
48
  (d) => {
53
49
  const page = d ? Math.ceil(d.list.length / pageSize) + 1 : 1;
54
- return fetchData({ page, pageSize, status, customer_id: id });
50
+ return fetchData({ page, pageSize, status, customer_id: id, activeFirst: true });
55
51
  },
56
52
  {
57
53
  reloadDeps: [id, status],
54
+ ...(isMobile
55
+ ? {}
56
+ : {
57
+ target: listRef,
58
+ isNoMore: (d) => {
59
+ return d?.list?.length === 0 || (d?.list?.length ?? 0) >= (d?.count ?? 0);
60
+ },
61
+ }),
58
62
  }
59
63
  );
60
64
 
61
- if (loading || !data) {
62
- return <CircularProgress />;
63
- }
65
+ const showLoadingMore = useDelayedLoading(loadingMore);
64
66
 
65
- if (data && data.list.length === 0) {
66
- return <Typography color="text.secondary">{t('payment.customer.subscriptions.empty')}</Typography>;
67
+ if (!data || loading) {
68
+ return <CircularProgress />;
67
69
  }
68
70
 
69
- const hasMore = data && data.list.length < data.count;
71
+ const hasMore = data && data.list?.length < data.count;
70
72
  const size = { width: 48, height: 48 };
71
73
 
72
74
  return (
73
75
  <Stack direction="column" spacing={2} sx={{ mt: 2 }}>
74
76
  {data.list?.length > 0 ? (
75
77
  <>
76
- {data.list.map((subscription) => {
77
- return (
78
- <Stack
79
- key={subscription.id}
80
- direction="row"
81
- justifyContent="space-between"
82
- gap={{
83
- xs: 1,
84
- sm: 2,
85
- }}
86
- sx={{
87
- padding: 1.5,
88
- background: 'var(--backgrounds-bg-subtle, #F9FAFB)',
89
- '&:hover': {
90
- backgroundColor: 'grey.50',
91
- transition: 'background-color 200ms linear',
92
- cursor: 'pointer',
93
- },
94
- }}
95
- flexWrap="wrap">
96
- <Stack direction="column" flex={1} spacing={0.5} {...rest}>
97
- <Stack
98
- direction={isMobile ? 'column' : 'row'}
99
- spacing={1}
100
- alignItems={isMobile ? 'flex-start' : 'center'}
101
- flexWrap="wrap"
102
- justifyContent="space-between"
103
- onClick={() => onClickSubscription(subscription)}>
104
- <Stack direction="row" spacing={1.5}>
105
- <AvatarGroup max={3}>
106
- {subscription.items.map((item) =>
107
- item.price.product.images.length > 0 ? (
78
+ <Stack
79
+ ref={listRef}
80
+ spacing={2}
81
+ sx={{
82
+ maxHeight: {
83
+ xs: '100%',
84
+ md: '500px',
85
+ },
86
+ overflowY: 'auto',
87
+ }}>
88
+ {data.list.map((subscription) => {
89
+ return (
90
+ <Stack
91
+ key={subscription.id}
92
+ direction="row"
93
+ justifyContent="space-between"
94
+ gap={{
95
+ xs: 1,
96
+ sm: 2,
97
+ }}
98
+ sx={{
99
+ padding: 1.5,
100
+ background: 'var(--backgrounds-bg-subtle, #F9FAFB)',
101
+ '&:hover': {
102
+ backgroundColor: 'var(--backgrounds-bg-highlight, #eff6ff)',
103
+ transition: 'background-color 200ms linear',
104
+ cursor: 'pointer',
105
+ },
106
+ }}
107
+ flexWrap="wrap">
108
+ <Stack direction="column" flex={1} spacing={0.5} {...rest}>
109
+ <Stack
110
+ direction={isMobile ? 'column' : 'row'}
111
+ spacing={1}
112
+ alignItems={isMobile ? 'flex-start' : 'center'}
113
+ flexWrap="wrap"
114
+ justifyContent="space-between"
115
+ onClick={() => onClickSubscription(subscription)}>
116
+ <Stack direction="row" spacing={1.5}>
117
+ <AvatarGroup max={3}>
118
+ {subscription.items.map((item) =>
119
+ item.price.product.images.length > 0 ? (
120
+ // @ts-ignore
121
+ <Avatar
122
+ key={item.price.product_id}
123
+ src={item.price.product.images[0]}
124
+ alt={item.price.product.name}
125
+ variant="rounded"
126
+ sx={size}
127
+ />
128
+ ) : (
129
+ <Avatar key={item.price.product_id} variant="rounded" sx={size}>
130
+ {item.price.product.name.slice(0, 1)}
131
+ </Avatar>
132
+ )
133
+ )}
134
+ </AvatarGroup>
135
+ <Stack
136
+ direction="column"
137
+ spacing={0.5}
138
+ sx={{
139
+ '.MuiTypography-body1': {
140
+ fontSize: '16px',
141
+ },
142
+ }}>
143
+ <SubscriptionDescription subscription={subscription} hideSubscription variant="body1" />
144
+ <SubscriptionStatus
145
+ subscription={subscription}
146
+ sx={{ height: 18, width: 'fit-content' }}
147
+ size="small"
148
+ />
149
+ </Stack>
150
+ </Stack>
151
+ <Stack>
152
+ <Typography variant="subtitle1" fontWeight={500} fontSize={16}>
153
+ {
108
154
  // @ts-ignore
109
- <Avatar
110
- key={item.price.product_id}
111
- src={item.price.product.images[0]}
112
- alt={item.price.product.name}
113
- variant="rounded"
114
- sx={size}
115
- />
116
- ) : (
117
- <Avatar key={item.price.product_id} variant="rounded" sx={size}>
118
- {item.price.product.name.slice(0, 1)}
119
- </Avatar>
120
- )
121
- )}
122
- </AvatarGroup>
123
- <Stack
124
- direction="column"
125
- spacing={0.5}
126
- sx={{
127
- '.MuiTypography-body1': {
128
- fontSize: '16px',
129
- },
130
- }}>
131
- <SubscriptionDescription subscription={subscription} hideSubscription variant="body1" />
132
- <Status
133
- size="small"
134
- sx={{ height: 18, width: 'fit-content' }}
135
- label={formatSubscriptionStatus(subscription.status)}
136
- color={getSubscriptionStatusColor(subscription.status)}
137
- />
155
+ formatPrice(subscription.items[0].price, subscription.paymentCurrency)
156
+ }
157
+ </Typography>
138
158
  </Stack>
139
159
  </Stack>
140
- <Stack>
141
- <Typography variant="subtitle1" fontWeight={500} fontSize={16}>
142
- {
143
- // @ts-ignore
144
- formatPrice(subscription.items[0].price, subscription.paymentCurrency)
145
- }
146
- </Typography>
147
- </Stack>
148
- </Stack>
149
- <Stack
150
- gap={1}
151
- justifyContent="space-between"
152
- flexWrap="wrap"
153
- sx={{
154
- flexDirection: {
155
- xs: 'column',
156
- lg: 'row',
157
- },
158
- alignItems: {
159
- xs: 'flex-start',
160
- lg: 'center',
161
- },
162
- }}>
163
- <Box
164
- component="div"
165
- onClick={() => onClickSubscription(subscription)}
166
- sx={{ display: 'flex', gap: 0.5, flexDirection: isMobile ? 'column' : 'row' }}>
167
- {getSubscriptionTimeSummary(subscription)
168
- .split(',')
169
- .map((x) => (
170
- <Typography key={x} variant="body1" color="text.secondary">
171
- {x}
172
- </Typography>
173
- ))}
174
- </Box>
175
- <SubscriptionActions
176
- subscription={subscription}
177
- onChange={onChange}
178
- actionProps={{
179
- cancel: {
180
- variant: 'outlined',
181
- color: 'primary',
160
+ <Stack
161
+ gap={1}
162
+ justifyContent="space-between"
163
+ flexWrap="wrap"
164
+ sx={{
165
+ flexDirection: {
166
+ xs: 'column',
167
+ lg: 'row',
182
168
  },
183
- recover: {
184
- variant: 'outlined',
185
- color: 'info',
169
+ alignItems: {
170
+ xs: 'flex-start',
171
+ lg: 'center',
186
172
  },
187
- pastDue: {
188
- variant: 'outlined',
189
- color: 'primary',
190
- },
191
- }}
192
- />
173
+ }}>
174
+ <Box
175
+ component="div"
176
+ onClick={() => onClickSubscription(subscription)}
177
+ sx={{ display: 'flex', gap: 0.5, flexDirection: isMobile ? 'column' : 'row' }}>
178
+ {getSubscriptionTimeSummary(subscription)
179
+ .split(',')
180
+ .map((x) => (
181
+ <Typography key={x} variant="body1" color="text.secondary">
182
+ {x}
183
+ </Typography>
184
+ ))}
185
+ </Box>
186
+ <SubscriptionActions
187
+ subscription={subscription}
188
+ onChange={(v) => {
189
+ reload();
190
+ if (onChange) {
191
+ onChange(v);
192
+ }
193
+ }}
194
+ actionProps={{
195
+ cancel: {
196
+ variant: 'outlined',
197
+ color: 'primary',
198
+ },
199
+ recover: {
200
+ variant: 'outlined',
201
+ color: 'info',
202
+ },
203
+ pastDue: {
204
+ variant: 'outlined',
205
+ color: 'primary',
206
+ },
207
+ }}
208
+ />
209
+ </Stack>
193
210
  </Stack>
194
211
  </Stack>
195
- </Stack>
196
- );
197
- })}
198
- <Box>
199
- {hasMore && (
200
- <Button variant="text" type="button" color="inherit" onClick={loadMore} disabled={loadingMore}>
201
- {loadingMore
202
- ? t('common.loadingMore', { resource: t('admin.subscriptions') })
203
- : t('common.loadMore', { resource: t('admin.subscriptions') })}
204
- </Button>
212
+ );
213
+ })}
214
+ {hasMore && !isMobile && showLoadingMore && (
215
+ <Box alignItems="center" gap={0.5} display="flex" mt={0.5}>
216
+ {t('common.loadingMore', { resource: t('admin.subscriptions') })}
217
+ </Box>
205
218
  )}
206
- {!hasMore && data.count > pageSize && (
207
- <Typography color="text.secondary">
208
- {t('common.noMore', { resource: t('admin.subscriptions') })}
209
- </Typography>
210
- )}
211
- </Box>
219
+ </Stack>
220
+ {isMobile && (
221
+ <Box>
222
+ {hasMore && (
223
+ <Button variant="text" type="button" color="inherit" onClick={loadMore} disabled={loadingMore}>
224
+ {loadingMore
225
+ ? t('common.loadingMore', { resource: t('admin.subscriptions') })
226
+ : t('common.loadMore', { resource: t('admin.subscriptions') })}
227
+ </Button>
228
+ )}
229
+ {!hasMore && data.count > pageSize && (
230
+ <Typography color="text.secondary">
231
+ {t('common.noMore', { resource: t('admin.subscriptions') })}
232
+ </Typography>
233
+ )}
234
+ </Box>
235
+ )}
212
236
  </>
213
237
  ) : (
214
238
  <Empty>
@@ -0,0 +1,28 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+
3
+ function useDelayedLoading(loading: boolean, delay: number = 300) {
4
+ const [showLoading, setShowLoading] = useState(false);
5
+ const delayTimeout = useRef<NodeJS.Timeout | null>(null);
6
+
7
+ useEffect(() => {
8
+ if (delayTimeout.current) {
9
+ clearTimeout(delayTimeout.current);
10
+ }
11
+ if (loading) {
12
+ delayTimeout.current = setTimeout(() => {
13
+ setShowLoading(true);
14
+ }, delay);
15
+ } else {
16
+ setShowLoading(false);
17
+ }
18
+ return () => {
19
+ if (delayTimeout.current) {
20
+ clearTimeout(delayTimeout.current);
21
+ }
22
+ };
23
+ }, [loading, delay]);
24
+
25
+ return showLoading;
26
+ }
27
+
28
+ export default useDelayedLoading;
@@ -16,6 +16,9 @@ export default flat({
16
16
  add: 'Add',
17
17
  fullscreen: 'Fullscreen',
18
18
  exit: 'Exit',
19
+ maxLength: 'Max {len} characters',
20
+ minLength: 'Min {len} characters',
21
+ loading: 'Loading...',
19
22
  },
20
23
  admin: {
21
24
  balances: 'Balances',
@@ -459,8 +462,9 @@ export default flat({
459
462
  },
460
463
  staking: {
461
464
  title: 'Stake',
462
- none: 'No return',
465
+ none: 'No return or slash',
463
466
  proration: 'Return Remaining Stake {unused}{symbol}',
467
+ slash: 'Slash Remaining Stake {unused}{symbol}',
464
468
  },
465
469
  },
466
470
  pause: {
@@ -592,6 +596,7 @@ export default flat({
592
596
  payments: 'No Payments',
593
597
  prices: 'No Prices',
594
598
  pricing: 'You haven’t added any prices you can add it',
599
+ summary: 'No Summary',
595
600
  },
596
601
  customer: {
597
602
  subscription: {
@@ -16,6 +16,9 @@ export default flat({
16
16
  add: '添加',
17
17
  fullscreen: '全屏',
18
18
  exit: '退出',
19
+ maxLength: '最多输入{len}个字符',
20
+ minLength: '最少输入{len}个字符',
21
+ loading: '加载中...',
19
22
  },
20
23
  admin: {
21
24
  balances: '余额',
@@ -450,8 +453,9 @@ export default flat({
450
453
  },
451
454
  staking: {
452
455
  title: '质押',
453
- none: '不退还质押',
456
+ none: '不退还 / 罚没质押',
454
457
  proration: '退还剩余部分 {unused}{symbol}',
458
+ slash: '罚没剩余部分 {unused}{symbol}',
455
459
  },
456
460
  },
457
461
  pause: {
@@ -582,6 +586,7 @@ export default flat({
582
586
  payments: '没有付款记录',
583
587
  prices: '没有价格',
584
588
  pricing: '您还没有设置定价,您可以添加它',
589
+ summary: '没有摘要',
585
590
  },
586
591
  customer: {
587
592
  subscription: {