payment-kit 1.14.29 → 1.14.30

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.
@@ -6,6 +6,8 @@ import isObject from 'lodash/isObject';
6
6
  import pick from 'lodash/pick';
7
7
  import uniq from 'lodash/uniq';
8
8
 
9
+ import { literal } from 'sequelize';
10
+ import type { Literal } from 'sequelize/types/utils';
9
11
  import { ensureStripeCustomer, ensureStripePrice, ensureStripeSubscription } from '../integrations/stripe/resource';
10
12
  import { createListParamSchema, getWhereFromKvQuery, getWhereFromQuery } from '../libs/api';
11
13
  import dayjs from '../libs/dayjs';
@@ -72,10 +74,12 @@ const schema = createListParamSchema<{
72
74
  status?: string;
73
75
  customer_id?: string;
74
76
  customer_did?: string;
77
+ activeFirst?: boolean;
75
78
  }>({
76
79
  status: Joi.string().empty(''),
77
80
  customer_id: Joi.string().empty(''),
78
81
  customer_did: Joi.string().empty(''),
82
+ activeFirst: Joi.boolean().optional(),
79
83
  });
80
84
  router.get('/', authMine, async (req, res) => {
81
85
  const { page, pageSize, status, livemode, ...query } = await schema.validateAsync(req.query, {
@@ -113,10 +117,19 @@ router.get('/', authMine, async (req, res) => {
113
117
  where[key] = query[key];
114
118
  });
115
119
 
120
+ const order: [Literal | string, 'ASC' | 'DESC'][] = [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']];
121
+
122
+ if (query.activeFirst) {
123
+ order.unshift([
124
+ literal("CASE status WHEN 'active' THEN 1 WHEN 'trialing' THEN 2 WHEN 'past_due' THEN 3 ELSE 4 END"),
125
+ 'ASC',
126
+ ]);
127
+ }
128
+
116
129
  try {
117
130
  const { rows: list, count } = await Subscription.findAndCountAll({
118
131
  where,
119
- order: [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']],
132
+ order,
120
133
  offset: (page - 1) * pageSize,
121
134
  limit: pageSize,
122
135
  include: [
@@ -128,7 +141,6 @@ router.get('/', authMine, async (req, res) => {
128
141
  // https://github.com/sequelize/sequelize/issues/9481
129
142
  distinct: true,
130
143
  });
131
-
132
144
  const products = (await Product.findAll()).map((x) => x.toJSON());
133
145
  const prices = (await Price.findAll()).map((x) => x.toJSON());
134
146
  const docs = list.map((x) => x.toJSON());
package/blocklet.yml CHANGED
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.14.29
17
+ version: 1.14.30
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.14.29",
3
+ "version": "1.14.30",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -52,7 +52,7 @@
52
52
  "@arcblock/validator": "^1.18.128",
53
53
  "@blocklet/js-sdk": "1.16.28",
54
54
  "@blocklet/logger": "1.16.28",
55
- "@blocklet/payment-react": "1.14.29",
55
+ "@blocklet/payment-react": "1.14.30",
56
56
  "@blocklet/sdk": "1.16.28",
57
57
  "@blocklet/ui-react": "^2.10.16",
58
58
  "@blocklet/uploader": "^0.1.20",
@@ -118,7 +118,7 @@
118
118
  "devDependencies": {
119
119
  "@abtnode/types": "1.16.28",
120
120
  "@arcblock/eslint-config-ts": "^0.3.2",
121
- "@blocklet/payment-types": "1.14.29",
121
+ "@blocklet/payment-types": "1.14.30",
122
122
  "@types/cookie-parser": "^1.4.7",
123
123
  "@types/cors": "^2.8.17",
124
124
  "@types/debug": "^4.1.12",
@@ -160,5 +160,5 @@
160
160
  "parser": "typescript"
161
161
  }
162
162
  },
163
- "gitHead": "104f4189d9ef96c1e332ffa90824ebd3063c989d"
163
+ "gitHead": "f98dee7bce684f81f4e060b942efaa9ad8730b55"
164
164
  }
@@ -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,196 @@ 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
 
65
+ const showLoadingMore = useDelayedLoading(loadingMore);
66
+
61
67
  if (loading || !data) {
62
68
  return <CircularProgress />;
63
69
  }
64
70
 
65
- if (data && data.list.length === 0) {
66
- return <Typography color="text.secondary">{t('payment.customer.subscriptions.empty')}</Typography>;
67
- }
68
-
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
+ <Box
79
+ ref={listRef}
80
+ sx={{
81
+ maxHeight: {
82
+ xs: '100%',
83
+ md: '450px',
84
+ },
85
+ overflowY: 'auto',
86
+ }}>
87
+ {data.list.map((subscription) => {
88
+ return (
89
+ <Stack
90
+ key={subscription.id}
91
+ direction="row"
92
+ justifyContent="space-between"
93
+ gap={{
94
+ xs: 1,
95
+ sm: 2,
96
+ }}
97
+ sx={{
98
+ padding: 1.5,
99
+ background: 'var(--backgrounds-bg-subtle, #F9FAFB)',
100
+ '&:hover': {
101
+ backgroundColor: 'grey.50',
102
+ transition: 'background-color 200ms linear',
103
+ cursor: 'pointer',
104
+ },
105
+ }}
106
+ flexWrap="wrap">
107
+ <Stack direction="column" flex={1} spacing={0.5} {...rest}>
108
+ <Stack
109
+ direction={isMobile ? 'column' : 'row'}
110
+ spacing={1}
111
+ alignItems={isMobile ? 'flex-start' : 'center'}
112
+ flexWrap="wrap"
113
+ justifyContent="space-between"
114
+ onClick={() => onClickSubscription(subscription)}>
115
+ <Stack direction="row" spacing={1.5}>
116
+ <AvatarGroup max={3}>
117
+ {subscription.items.map((item) =>
118
+ item.price.product.images.length > 0 ? (
119
+ // @ts-ignore
120
+ <Avatar
121
+ key={item.price.product_id}
122
+ src={item.price.product.images[0]}
123
+ alt={item.price.product.name}
124
+ variant="rounded"
125
+ sx={size}
126
+ />
127
+ ) : (
128
+ <Avatar key={item.price.product_id} variant="rounded" sx={size}>
129
+ {item.price.product.name.slice(0, 1)}
130
+ </Avatar>
131
+ )
132
+ )}
133
+ </AvatarGroup>
134
+ <Stack
135
+ direction="column"
136
+ spacing={0.5}
137
+ sx={{
138
+ '.MuiTypography-body1': {
139
+ fontSize: '16px',
140
+ },
141
+ }}>
142
+ <SubscriptionDescription subscription={subscription} hideSubscription variant="body1" />
143
+ <SubscriptionStatus
144
+ subscription={subscription}
145
+ sx={{ height: 18, width: 'fit-content' }}
146
+ size="small"
147
+ />
148
+ </Stack>
149
+ </Stack>
150
+ <Stack>
151
+ <Typography variant="subtitle1" fontWeight={500} fontSize={16}>
152
+ {
108
153
  // @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
- />
154
+ formatPrice(subscription.items[0].price, subscription.paymentCurrency)
155
+ }
156
+ </Typography>
138
157
  </Stack>
139
158
  </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',
159
+ <Stack
160
+ gap={1}
161
+ justifyContent="space-between"
162
+ flexWrap="wrap"
163
+ sx={{
164
+ flexDirection: {
165
+ xs: 'column',
166
+ lg: 'row',
182
167
  },
183
- recover: {
184
- variant: 'outlined',
185
- color: 'info',
168
+ alignItems: {
169
+ xs: 'flex-start',
170
+ lg: 'center',
186
171
  },
187
- pastDue: {
188
- variant: 'outlined',
189
- color: 'primary',
190
- },
191
- }}
192
- />
172
+ }}>
173
+ <Box
174
+ component="div"
175
+ onClick={() => onClickSubscription(subscription)}
176
+ sx={{ display: 'flex', gap: 0.5, flexDirection: isMobile ? 'column' : 'row' }}>
177
+ {getSubscriptionTimeSummary(subscription)
178
+ .split(',')
179
+ .map((x) => (
180
+ <Typography key={x} variant="body1" color="text.secondary">
181
+ {x}
182
+ </Typography>
183
+ ))}
184
+ </Box>
185
+ <SubscriptionActions
186
+ subscription={subscription}
187
+ onChange={(v) => {
188
+ reload();
189
+ if (onChange) {
190
+ onChange(v);
191
+ }
192
+ }}
193
+ actionProps={{
194
+ cancel: {
195
+ variant: 'outlined',
196
+ color: 'primary',
197
+ },
198
+ recover: {
199
+ variant: 'outlined',
200
+ color: 'info',
201
+ },
202
+ pastDue: {
203
+ variant: 'outlined',
204
+ color: 'primary',
205
+ },
206
+ }}
207
+ />
208
+ </Stack>
193
209
  </Stack>
194
210
  </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>
205
- )}
206
- {!hasMore && data.count > pageSize && (
207
- <Typography color="text.secondary">
208
- {t('common.noMore', { resource: t('admin.subscriptions') })}
209
- </Typography>
211
+ );
212
+ })}
213
+ {hasMore && !isMobile && showLoadingMore && (
214
+ <Box alignItems="center" gap={0.5} display="flex" mt={0.5}>
215
+ {t('common.loadingMore', { resource: t('admin.subscriptions') })}
216
+ </Box>
210
217
  )}
211
218
  </Box>
219
+ {isMobile && (
220
+ <Box>
221
+ {hasMore && (
222
+ <Button variant="text" type="button" color="inherit" onClick={loadMore} disabled={loadingMore}>
223
+ {loadingMore
224
+ ? t('common.loadingMore', { resource: t('admin.subscriptions') })
225
+ : t('common.loadMore', { resource: t('admin.subscriptions') })}
226
+ </Button>
227
+ )}
228
+ {!hasMore && data.count > pageSize && (
229
+ <Typography color="text.secondary">
230
+ {t('common.noMore', { resource: t('admin.subscriptions') })}
231
+ </Typography>
232
+ )}
233
+ </Box>
234
+ )}
212
235
  </>
213
236
  ) : (
214
237
  <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;