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.
- package/api/src/routes/subscriptions.ts +14 -2
- package/blocklet.yml +1 -1
- package/package.json +4 -4
- package/src/components/subscription/portal/list.tsx +166 -143
- package/src/hooks/loading.ts +28 -0
|
@@ -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
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.14.
|
|
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.
|
|
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.
|
|
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": "
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
168
|
+
alignItems: {
|
|
169
|
+
xs: 'flex-start',
|
|
170
|
+
lg: 'center',
|
|
186
171
|
},
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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;
|