payment-kit 1.13.73 → 1.13.74
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/{schedule → crons}/base.ts +1 -1
- package/api/src/index.ts +7 -7
- package/api/src/integrations/stripe/handlers/customer.ts +24 -0
- package/api/src/integrations/stripe/handlers/index.ts +4 -0
- package/api/src/integrations/stripe/handlers/payment-intent.ts +1 -1
- package/api/src/integrations/stripe/resource.ts +1 -1
- package/api/src/libs/audit.ts +34 -28
- package/api/src/libs/payment.ts +26 -0
- package/api/src/libs/queue/index.ts +18 -1
- package/api/src/libs/queue/store.ts +6 -5
- package/api/src/libs/session.ts +13 -12
- package/api/src/libs/subscription.ts +26 -0
- package/api/src/libs/util.ts +5 -1
- package/api/src/{jobs → queues}/checkout-session.ts +11 -0
- package/api/src/{jobs → queues}/invoice.ts +15 -6
- package/api/src/{jobs → queues}/payment.ts +182 -30
- package/api/src/{jobs → queues}/subscription.ts +36 -104
- package/api/src/{jobs → queues}/webhook.ts +2 -0
- package/api/src/routes/checkout-sessions.ts +68 -19
- package/api/src/routes/connect/collect.ts +2 -2
- package/api/src/routes/connect/pay.ts +1 -1
- package/api/src/routes/connect/setup.ts +2 -2
- package/api/src/routes/connect/shared.ts +94 -45
- package/api/src/routes/connect/subscribe.ts +3 -3
- package/api/src/routes/pricing-table.ts +2 -0
- package/api/src/routes/subscription-items.ts +1 -1
- package/api/src/routes/subscriptions.ts +434 -13
- package/api/src/store/migrate.ts +0 -1
- package/api/src/store/migrations/20231204-subupdate.ts +50 -0
- package/api/src/store/models/checkout-session.ts +4 -0
- package/api/src/store/models/customer.ts +52 -15
- package/api/src/store/models/invoice-item.ts +6 -1
- package/api/src/store/models/invoice.ts +41 -22
- package/api/src/store/models/payment-intent.ts +4 -0
- package/api/src/store/models/setup-intent.ts +4 -0
- package/api/src/store/models/subscription-item.ts +0 -4
- package/api/src/store/models/subscription.ts +77 -44
- package/api/src/store/models/types.ts +1 -0
- package/api/src/store/sequelize.ts +6 -0
- package/api/third.d.ts +2 -0
- package/blocklet.yml +1 -1
- package/jest.config.js +14 -0
- package/package.json +24 -19
- package/src/components/blockchain/tx.tsx +20 -11
- package/src/components/checkout/form/index.tsx +1 -1
- package/src/components/invoice/table.tsx +58 -19
- package/src/components/layout/admin.tsx +17 -5
- package/src/components/portal/invoice/list.tsx +12 -8
- package/src/components/portal/subscription/list.tsx +114 -77
- package/src/components/subscription/status.tsx +21 -19
- package/src/global.css +4 -0
- package/src/locales/en.tsx +14 -1
- package/src/locales/zh.tsx +14 -0
- package/src/pages/admin/customers/customers/detail.tsx +47 -3
- package/src/pages/admin/overview.tsx +21 -1
- package/src/pages/admin/payments/intents/detail.tsx +12 -3
- package/src/pages/customer/invoice.tsx +15 -1
- package/src/pages/customer/subscription/index.tsx +9 -2
- package/tests/api/libs/subscription.spec.ts +45 -0
- /package/api/src/{schedule → crons}/index.ts +0 -0
- /package/api/src/{schedule → crons}/interface/diff.ts +0 -0
- /package/api/src/{schedule → crons}/subscription-trail-will-end.ts +0 -0
- /package/api/src/{schedule → crons}/subscription-will-renew.ts +0 -0
- /package/api/src/{jobs → queues}/event.ts +0 -0
- /package/api/src/{jobs → queues}/notification.ts +0 -0
|
@@ -4,6 +4,7 @@ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
|
4
4
|
import Dashboard from '@blocklet/ui-react/lib/Dashboard';
|
|
5
5
|
import { Avatar, Button, Stack, Typography } from '@mui/material';
|
|
6
6
|
import { styled } from '@mui/system';
|
|
7
|
+
import { useEffect, useState } from 'react';
|
|
7
8
|
|
|
8
9
|
import { useSessionContext } from '../../contexts/session';
|
|
9
10
|
|
|
@@ -54,12 +55,27 @@ const Root = styled(Dashboard)`
|
|
|
54
55
|
export default function Layout(props: any) {
|
|
55
56
|
const { t } = useLocaleContext();
|
|
56
57
|
const { session } = useSessionContext();
|
|
58
|
+
const [showLogin, setShowLogin] = useState(false);
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const timer = setTimeout(() => {
|
|
62
|
+
if (!session.user) {
|
|
63
|
+
setShowLogin(true);
|
|
64
|
+
}
|
|
65
|
+
}, 500);
|
|
66
|
+
|
|
67
|
+
return () => clearTimeout(timer);
|
|
68
|
+
});
|
|
57
69
|
|
|
58
70
|
const handleLogin = () => {
|
|
59
71
|
session.login();
|
|
60
72
|
};
|
|
61
73
|
|
|
62
|
-
if (
|
|
74
|
+
if (session.user) {
|
|
75
|
+
return <Root {...props} footerProps={{ className: 'dashboard-footer' }} />;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (showLogin) {
|
|
63
79
|
return (
|
|
64
80
|
<Center>
|
|
65
81
|
<Stack maxWidth="sm" direction="column" alignItems="center" spacing={3}>
|
|
@@ -75,9 +91,5 @@ export default function Layout(props: any) {
|
|
|
75
91
|
);
|
|
76
92
|
}
|
|
77
93
|
|
|
78
|
-
if (session.user) {
|
|
79
|
-
return <Root {...props} footerProps={{ className: 'dashboard-footer' }} />;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
94
|
return null;
|
|
83
95
|
}
|
|
@@ -42,7 +42,7 @@ export default function CustomerInvoiceList({ customer_id }: Props) {
|
|
|
42
42
|
const { data, loadMore, loadingMore, loading } = useInfiniteScroll<Paginated<TInvoiceExpanded>>(
|
|
43
43
|
(d) => {
|
|
44
44
|
const page = d ? Math.ceil(d.list.length / pageSize) + 1 : 1;
|
|
45
|
-
return fetchData({ page, pageSize, status: 'open,paid', customer_id });
|
|
45
|
+
return fetchData({ page, pageSize, status: 'open,paid,uncollectible', customer_id });
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
reloadDeps: [customer_id],
|
|
@@ -54,7 +54,11 @@ export default function CustomerInvoiceList({ customer_id }: Props) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
if (data && data.list.length === 0) {
|
|
57
|
-
return
|
|
57
|
+
return (
|
|
58
|
+
<Typography color="text.secondary">
|
|
59
|
+
{customer_id ? t('customer.invoice.empty') : t('admin.invoice.empty')}
|
|
60
|
+
</Typography>
|
|
61
|
+
);
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
const hasMore = data && data.list.length < data.count;
|
|
@@ -77,27 +81,27 @@ export default function CustomerInvoiceList({ customer_id }: Props) {
|
|
|
77
81
|
gap={{
|
|
78
82
|
xs: 0.5,
|
|
79
83
|
sm: 1.5,
|
|
80
|
-
md:
|
|
84
|
+
md: 3,
|
|
81
85
|
}}
|
|
82
86
|
flexWrap="nowrap">
|
|
83
|
-
<Box flex={
|
|
87
|
+
<Box flex={3}>
|
|
84
88
|
<Link to={`/customer/invoice/${invoice.id}`}>
|
|
85
89
|
<Typography component="span">{invoice.number}</Typography>
|
|
86
90
|
</Link>
|
|
87
91
|
</Box>
|
|
88
|
-
<Box flex={
|
|
92
|
+
<Box flex={3}>
|
|
89
93
|
<Typography>{formatToDate(invoice.created_at)}</Typography>
|
|
90
94
|
</Box>
|
|
91
95
|
<Box flex={1}>
|
|
92
|
-
<Typography>
|
|
96
|
+
<Typography textAlign="right">
|
|
93
97
|
{fromUnitToToken(invoice.total, invoice.paymentCurrency.decimal)}
|
|
94
98
|
{invoice.paymentCurrency.symbol}
|
|
95
99
|
</Typography>
|
|
96
100
|
</Box>
|
|
97
|
-
<Box flex={
|
|
101
|
+
<Box flex={2}>
|
|
98
102
|
<Status label={invoice.status} color={getInvoiceStatusColor(invoice.status)} />
|
|
99
103
|
</Box>
|
|
100
|
-
<Box flex={
|
|
104
|
+
<Box flex={4}>
|
|
101
105
|
<Typography>{invoice.description || invoice.id}</Typography>
|
|
102
106
|
</Box>
|
|
103
107
|
</Stack>
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
3
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
4
4
|
import type { Paginated, TSubscriptionExpanded } from '@did-pay/types';
|
|
5
|
-
import { ScheduleOutlined } from '@mui/icons-material';
|
|
6
5
|
import { Avatar, AvatarGroup, Box, Button, CircularProgress, Stack, StackProps, Typography } from '@mui/material';
|
|
7
6
|
import { useInfiniteScroll, useSetState } from 'ahooks';
|
|
8
7
|
import { FormProvider, useForm, useFormContext } from 'react-hook-form';
|
|
8
|
+
import { useNavigate } from 'react-router-dom';
|
|
9
9
|
|
|
10
10
|
import api from '../../../libs/api';
|
|
11
11
|
import {
|
|
@@ -35,14 +35,52 @@ type Props = {
|
|
|
35
35
|
|
|
36
36
|
const pageSize = 4;
|
|
37
37
|
|
|
38
|
+
export const getSubscriptionTimeSummary = (subscription: TSubscriptionExpanded) => {
|
|
39
|
+
const lines = [`Started on ${formatToDate(subscription.start_date * 1000)}`];
|
|
40
|
+
if (subscription.status === 'active' || subscription.status === 'trialing') {
|
|
41
|
+
if (subscription.cancel_at) {
|
|
42
|
+
lines.push(`will cancel on ${formatToDate(subscription.cancel_at * 1000)}`);
|
|
43
|
+
} else if (subscription.cancel_at_period_end) {
|
|
44
|
+
lines.push(`will cancel on ${formatToDate(subscription.current_period_end * 1000)}`);
|
|
45
|
+
} else {
|
|
46
|
+
lines.push(`will renew on ${formatToDate(subscription.current_period_end * 1000)}`);
|
|
47
|
+
}
|
|
48
|
+
} else if (subscription.status === 'past_due') {
|
|
49
|
+
lines.push(`will cancel on ${formatToDate(subscription.current_period_end * 1000)}`);
|
|
50
|
+
} else if (subscription.status === 'canceled') {
|
|
51
|
+
lines.push(`canceled on ${formatToDate(subscription.canceled_at * 1000)}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return lines.join(', ');
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const getSubscriptionAction = (subscription: TSubscriptionExpanded) => {
|
|
58
|
+
if (subscription.status === 'active' || subscription.status === 'trialing') {
|
|
59
|
+
if (subscription.cancel_at) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (subscription.cancel_at_period_end) {
|
|
63
|
+
return { action: 'recover', color: 'secondary' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { action: 'cancel', color: 'error' };
|
|
67
|
+
}
|
|
68
|
+
if (subscription.status === 'past_due') {
|
|
69
|
+
return { action: 'pastDue', color: 'primary' };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return null;
|
|
73
|
+
};
|
|
74
|
+
|
|
38
75
|
export function CurrentSubscriptionsInner({ id, onChange, onClickSubscription, ...rest }: Props) {
|
|
39
76
|
const { t } = useLocaleContext();
|
|
40
77
|
const { reset, getValues } = useFormContext();
|
|
78
|
+
const navigate = useNavigate();
|
|
41
79
|
|
|
42
80
|
const { data, loadMore, loadingMore, loading } = useInfiniteScroll<Paginated<TSubscriptionExpanded>>(
|
|
43
81
|
(d) => {
|
|
44
82
|
const page = d ? Math.ceil(d.list.length / pageSize) + 1 : 1;
|
|
45
|
-
return fetchData({ page, pageSize, status: 'active,trialing,paused', customer_id: id });
|
|
83
|
+
return fetchData({ page, pageSize, status: 'active,trialing,paused,past_due,canceled', customer_id: id });
|
|
46
84
|
},
|
|
47
85
|
{
|
|
48
86
|
reloadDeps: [id],
|
|
@@ -99,89 +137,88 @@ export function CurrentSubscriptionsInner({ id, onChange, onClickSubscription, .
|
|
|
99
137
|
|
|
100
138
|
return (
|
|
101
139
|
<Stack direction="column" spacing={4} sx={{ mt: 2 }}>
|
|
102
|
-
{data.list.map((subscription) =>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
140
|
+
{data.list.map((subscription) => {
|
|
141
|
+
const action = getSubscriptionAction(subscription);
|
|
142
|
+
return (
|
|
143
|
+
<Stack
|
|
144
|
+
key={subscription.id}
|
|
145
|
+
direction="row"
|
|
146
|
+
justifyContent="space-between"
|
|
147
|
+
gap={{
|
|
148
|
+
xs: 1,
|
|
149
|
+
sm: 3,
|
|
150
|
+
}}
|
|
151
|
+
flexWrap="wrap">
|
|
152
|
+
<Stack direction="column" spacing={0.5} onClick={() => onClickSubscription(subscription)} {...rest}>
|
|
153
|
+
<Stack direction="row" spacing={1} alignItems="center">
|
|
154
|
+
<AvatarGroup max={3}>
|
|
155
|
+
{subscription.items.map((item) =>
|
|
156
|
+
item.price.product.images.length > 0 ? (
|
|
157
|
+
// @ts-ignore
|
|
158
|
+
<Avatar
|
|
159
|
+
key={item.price.product_id}
|
|
160
|
+
src={item.price.product.images[0]}
|
|
161
|
+
alt={item.price.product.name}
|
|
162
|
+
variant="rounded"
|
|
163
|
+
sx={size}
|
|
164
|
+
/>
|
|
165
|
+
) : (
|
|
166
|
+
<Avatar key={item.price.product_id} variant="rounded" sx={size}>
|
|
167
|
+
{item.price.product.name.slice(0, 1)}
|
|
168
|
+
</Avatar>
|
|
169
|
+
)
|
|
170
|
+
)}
|
|
171
|
+
</AvatarGroup>
|
|
172
|
+
<Stack direction="column" spacing={0.5}>
|
|
173
|
+
<Stack direction="row" spacing={2} alignItems="center">
|
|
174
|
+
<Typography variant="body1" fontWeight={600}>
|
|
175
|
+
{formatSubscriptionProduct(subscription.items)}
|
|
176
|
+
</Typography>
|
|
177
|
+
<Status
|
|
178
|
+
size="small"
|
|
179
|
+
sx={{ height: 18 }}
|
|
180
|
+
label={subscription.status}
|
|
181
|
+
color={getSubscriptionStatusColor(subscription.status)}
|
|
124
182
|
/>
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
</AvatarGroup>
|
|
132
|
-
<Stack direction="column" spacing={0.5}>
|
|
133
|
-
<Stack direction="row" spacing={2} alignItems="center">
|
|
134
|
-
<Typography variant="body1" fontWeight={600}>
|
|
135
|
-
{formatSubscriptionProduct(subscription.items)}
|
|
183
|
+
</Stack>
|
|
184
|
+
<Typography variant="subtitle1" fontWeight={500}>
|
|
185
|
+
{
|
|
186
|
+
// @ts-ignore
|
|
187
|
+
formatPrice(subscription.items[0].price, subscription.paymentCurrency)
|
|
188
|
+
}
|
|
136
189
|
</Typography>
|
|
137
|
-
<Status
|
|
138
|
-
size="small"
|
|
139
|
-
sx={{ height: 18 }}
|
|
140
|
-
label={subscription.status}
|
|
141
|
-
color={getSubscriptionStatusColor(subscription.status)}
|
|
142
|
-
/>
|
|
143
190
|
</Stack>
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
191
|
+
</Stack>
|
|
192
|
+
<Stack direction="row" spacing={2} alignItems="center">
|
|
193
|
+
<Typography variant="body1" color="text.secondary">
|
|
194
|
+
{getSubscriptionTimeSummary(subscription)}
|
|
149
195
|
</Typography>
|
|
150
196
|
</Stack>
|
|
151
197
|
</Stack>
|
|
152
|
-
<Stack direction="
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
198
|
+
<Stack direction="column" alignItems="flex-end" spacing={1}>
|
|
199
|
+
{action && (
|
|
200
|
+
<Button
|
|
201
|
+
variant="outlined"
|
|
202
|
+
// @ts-ignore
|
|
203
|
+
color={action.color}
|
|
204
|
+
size="small"
|
|
205
|
+
onClick={() => {
|
|
206
|
+
if (action.action === 'pastDue') {
|
|
207
|
+
navigate(`/customer/invoice/${subscription.latest_invoice_id}?action=pay`);
|
|
208
|
+
} else {
|
|
209
|
+
setState({
|
|
210
|
+
action: action.action,
|
|
211
|
+
subscription: subscription.id,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}}>
|
|
215
|
+
{t(`customer.${action.action}.button`)}
|
|
216
|
+
</Button>
|
|
217
|
+
)}
|
|
158
218
|
</Stack>
|
|
159
219
|
</Stack>
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
variant="outlined"
|
|
163
|
-
color={subscription.cancel_at_period_end ? 'inherit' : 'error'}
|
|
164
|
-
size="small"
|
|
165
|
-
onClick={() =>
|
|
166
|
-
setState({
|
|
167
|
-
action: subscription.cancel_at_period_end ? 'recover' : 'cancel',
|
|
168
|
-
subscription: subscription.id,
|
|
169
|
-
})
|
|
170
|
-
}>
|
|
171
|
-
{t(`customer.${subscription.cancel_at_period_end ? 'recover' : 'cancel'}.button`)}
|
|
172
|
-
</Button>
|
|
173
|
-
{subscription.cancel_at_period_end && (
|
|
174
|
-
<Stack direction="row" alignItems="center" spacing={0.5}>
|
|
175
|
-
<ScheduleOutlined fontSize="small" sx={{ color: 'text.secondary' }} />
|
|
176
|
-
<Typography component="span" variant="body1" color="text.secondary">
|
|
177
|
-
will {subscription.cancel_at_period_end ? 'cancel' : 'renew'} on{' '}
|
|
178
|
-
{formatToDate(subscription.current_period_end * 1000)}
|
|
179
|
-
</Typography>
|
|
180
|
-
</Stack>
|
|
181
|
-
)}
|
|
182
|
-
</Stack>
|
|
183
|
-
</Stack>
|
|
184
|
-
))}
|
|
220
|
+
);
|
|
221
|
+
})}
|
|
185
222
|
<Box>
|
|
186
223
|
{hasMore && (
|
|
187
224
|
<Button variant="text" type="button" color="inherit" onClick={loadMore} disabled={loadingMore}>
|
|
@@ -13,26 +13,28 @@ export default function SubscriptionStatus({
|
|
|
13
13
|
[key: string]: any;
|
|
14
14
|
}) {
|
|
15
15
|
const { t } = useLocaleContext();
|
|
16
|
-
if (subscription.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
if (subscription.status === 'active' || subscription.status === 'trialing') {
|
|
17
|
+
if (subscription.cancel_at_period_end && subscription.current_period_end > Date.now() / 1000) {
|
|
18
|
+
return (
|
|
19
|
+
<Status
|
|
20
|
+
icon={<AccessTimeOutlined />}
|
|
21
|
+
label={t('admin.subscription.cancel.will', { date: formatToDate(subscription.current_period_end * 1000) })}
|
|
22
|
+
color="default"
|
|
23
|
+
{...rest}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
if (subscription.cancel_at && subscription.cancel_at >= Date.now() / 1000) {
|
|
29
|
+
return (
|
|
30
|
+
<Status
|
|
31
|
+
icon={<AccessTimeOutlined />}
|
|
32
|
+
label={t('admin.subscription.cancel.will', { date: formatToDate(subscription.cancel_at * 1000) })}
|
|
33
|
+
color="default"
|
|
34
|
+
{...rest}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
if (subscription.pause_collection) {
|
package/src/global.css
CHANGED
package/src/locales/en.tsx
CHANGED
|
@@ -18,6 +18,7 @@ export default flat({
|
|
|
18
18
|
name: 'Name',
|
|
19
19
|
amount: 'Amount',
|
|
20
20
|
total: 'Total',
|
|
21
|
+
subtotal: 'Subtotal',
|
|
21
22
|
status: 'Status',
|
|
22
23
|
livemode: 'Test mode',
|
|
23
24
|
afterTime: 'After {time}',
|
|
@@ -408,6 +409,7 @@ export default flat({
|
|
|
408
409
|
title: 'Cancel subscription',
|
|
409
410
|
required: 'Custom cancel time is required',
|
|
410
411
|
will: 'Cancels on {date}',
|
|
412
|
+
done: 'Canceled',
|
|
411
413
|
at: {
|
|
412
414
|
title: 'Cancel',
|
|
413
415
|
now: 'Immediately ({date})',
|
|
@@ -455,6 +457,7 @@ export default flat({
|
|
|
455
457
|
email: 'Email',
|
|
456
458
|
phone: 'Phone',
|
|
457
459
|
invoicePrefix: 'Invoice Prefix',
|
|
460
|
+
balance: 'Balance ({currency})',
|
|
458
461
|
address: {
|
|
459
462
|
label: 'Address',
|
|
460
463
|
country: 'Country',
|
|
@@ -592,6 +595,9 @@ export default flat({
|
|
|
592
595
|
other: 'Other reasons',
|
|
593
596
|
},
|
|
594
597
|
},
|
|
598
|
+
pastDue: {
|
|
599
|
+
button: 'Pay',
|
|
600
|
+
},
|
|
595
601
|
recover: {
|
|
596
602
|
button: 'Renew',
|
|
597
603
|
title: 'Renew your subscription',
|
|
@@ -601,12 +607,19 @@ export default flat({
|
|
|
601
607
|
summary: 'Summary',
|
|
602
608
|
details: 'Details',
|
|
603
609
|
download: 'Download PDF',
|
|
610
|
+
unitPrice: 'Unit Price',
|
|
611
|
+
amountPaid: 'Amount Paid',
|
|
612
|
+
amountDue: 'Amount Due',
|
|
613
|
+
amountApplied: 'Applied Balance',
|
|
604
614
|
pay: 'Pay this invoice',
|
|
615
|
+
paySuccess: 'You have successfully paid the invoice',
|
|
616
|
+
payError: 'Failed to paid the invoice',
|
|
617
|
+
empty: 'Seems you do not have any payments here',
|
|
605
618
|
},
|
|
606
|
-
|
|
607
619
|
subscriptions: {
|
|
608
620
|
title: 'Manage subscriptions',
|
|
609
621
|
current: 'Current subscriptions',
|
|
622
|
+
empty: 'Seems you do not have any subscriptions here',
|
|
610
623
|
},
|
|
611
624
|
},
|
|
612
625
|
});
|
package/src/locales/zh.tsx
CHANGED
|
@@ -18,6 +18,7 @@ export default flat({
|
|
|
18
18
|
login: '登录以访问此页面',
|
|
19
19
|
amount: '金额',
|
|
20
20
|
total: '总计',
|
|
21
|
+
subtotal: '小计',
|
|
21
22
|
status: '状态',
|
|
22
23
|
livemode: '测试模式',
|
|
23
24
|
afterTime: '在{time}后',
|
|
@@ -399,6 +400,7 @@ export default flat({
|
|
|
399
400
|
title: '取消订阅',
|
|
400
401
|
required: '必须指定自定义取消时间',
|
|
401
402
|
will: '于{date}取消',
|
|
403
|
+
done: '已取消',
|
|
402
404
|
at: {
|
|
403
405
|
title: '取消',
|
|
404
406
|
now: '立即取消({date})',
|
|
@@ -446,6 +448,7 @@ export default flat({
|
|
|
446
448
|
email: '电子邮件',
|
|
447
449
|
phone: '电话',
|
|
448
450
|
invoicePrefix: '发票前缀',
|
|
451
|
+
balance: '余额 ({currency})',
|
|
449
452
|
address: {
|
|
450
453
|
label: '地址',
|
|
451
454
|
country: '国家',
|
|
@@ -583,16 +586,27 @@ export default flat({
|
|
|
583
586
|
title: '续订您的订阅',
|
|
584
587
|
description: '您的订阅将不再被取消,将在{date}续订',
|
|
585
588
|
},
|
|
589
|
+
pastDue: {
|
|
590
|
+
button: '续费',
|
|
591
|
+
},
|
|
586
592
|
invoice: {
|
|
587
593
|
summary: '摘要',
|
|
588
594
|
details: '详情',
|
|
589
595
|
download: '下载PDF',
|
|
596
|
+
unitPrice: '单价',
|
|
597
|
+
amountPaid: '已支付',
|
|
598
|
+
amountDue: '待支付',
|
|
599
|
+
amountApplied: '余额变更',
|
|
590
600
|
pay: '支付此发票',
|
|
601
|
+
paySuccess: '支付成功',
|
|
602
|
+
payError: '支付失败',
|
|
603
|
+
empty: '你没有任何支付',
|
|
591
604
|
},
|
|
592
605
|
|
|
593
606
|
subscriptions: {
|
|
594
607
|
title: '订阅管理',
|
|
595
608
|
current: '当前订阅',
|
|
609
|
+
empty: '你还没有任何订阅',
|
|
596
610
|
},
|
|
597
611
|
},
|
|
598
612
|
});
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
import DidAddress from '@arcblock/ux/lib/DID';
|
|
3
3
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
4
4
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
5
|
-
import type { TCustomerExpanded } from '@did-pay/types';
|
|
5
|
+
import type { TCustomerExpanded, TPaymentMethodExpanded } from '@did-pay/types';
|
|
6
6
|
import { ArrowBackOutlined, Edit } from '@mui/icons-material';
|
|
7
7
|
import { Alert, Box, Button, CircularProgress, Stack, Typography } from '@mui/material';
|
|
8
8
|
import { styled } from '@mui/system';
|
|
9
|
+
import { fromUnitToToken } from '@ocap/util';
|
|
9
10
|
import { useRequest, useSetState } from 'ahooks';
|
|
10
11
|
import { isEmpty } from 'lodash';
|
|
11
12
|
import { FlagEmoji } from 'react-international-phone';
|
|
@@ -22,6 +23,7 @@ import MetadataEditor from '../../../../components/metadata/editor';
|
|
|
22
23
|
import PaymentList from '../../../../components/payment-intent/list';
|
|
23
24
|
import SectionHeader from '../../../../components/section/header';
|
|
24
25
|
import SubscriptionList from '../../../../components/subscription/list';
|
|
26
|
+
import { useSettingsContext } from '../../../../contexts/settings';
|
|
25
27
|
import api from '../../../../libs/api';
|
|
26
28
|
import { formatError, formatTime } from '../../../../libs/util';
|
|
27
29
|
|
|
@@ -29,9 +31,43 @@ const fetchData = (id: string): Promise<TCustomerExpanded> => {
|
|
|
29
31
|
return api.get(`/api/customers/${id}`).then((res) => res.data);
|
|
30
32
|
};
|
|
31
33
|
|
|
34
|
+
function getTokenBalances(customer: TCustomerExpanded, paymentMethods: TPaymentMethodExpanded[]) {
|
|
35
|
+
const tokens: { currency: string; balance: string }[] = [];
|
|
36
|
+
// @ts-ignore
|
|
37
|
+
const currencies = paymentMethods.reduce((acc, x) => acc.concat(x.payment_currencies), []);
|
|
38
|
+
|
|
39
|
+
if (customer.balance) {
|
|
40
|
+
const currency = currencies.find((x: any) => x.symbol === 'USD');
|
|
41
|
+
tokens.push({
|
|
42
|
+
// @ts-ignore
|
|
43
|
+
currency: currency.symbol,
|
|
44
|
+
// @ts-ignore
|
|
45
|
+
balance: fromUnitToToken(customer.balance, currency.decimal),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (customer.token_balance) {
|
|
50
|
+
Object.keys(customer.token_balance).forEach((currencyId) => {
|
|
51
|
+
const currency = currencies.find((x: any) => x.id === currencyId);
|
|
52
|
+
// @ts-ignore
|
|
53
|
+
if (currency && customer.token_balance[currencyId] !== '0') {
|
|
54
|
+
tokens.push({
|
|
55
|
+
// @ts-ignore
|
|
56
|
+
currency: currency.symbol,
|
|
57
|
+
// @ts-ignore
|
|
58
|
+
balance: fromUnitToToken(customer.token_balance[currencyId], currency.decimal),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return tokens;
|
|
65
|
+
}
|
|
66
|
+
|
|
32
67
|
export default function CustomerDetail(props: { id: string }) {
|
|
33
68
|
const { t } = useLocaleContext();
|
|
34
69
|
const navigate = useNavigate();
|
|
70
|
+
const { settings } = useSettingsContext();
|
|
35
71
|
const [state, setState] = useSetState({
|
|
36
72
|
adding: {
|
|
37
73
|
price: false,
|
|
@@ -92,6 +128,8 @@ export default function CustomerDetail(props: { id: string }) {
|
|
|
92
128
|
}
|
|
93
129
|
};
|
|
94
130
|
|
|
131
|
+
const tokenBalances = getTokenBalances(data, settings.paymentMethods);
|
|
132
|
+
|
|
95
133
|
return (
|
|
96
134
|
<Root direction="column" spacing={4} sx={{ mb: 4 }}>
|
|
97
135
|
<Box>
|
|
@@ -123,8 +161,14 @@ export default function CustomerDetail(props: { id: string }) {
|
|
|
123
161
|
<InfoMetric label={t('common.createdAt')} value={formatTime(data.created_at)} divider />
|
|
124
162
|
<InfoMetric label={t('common.updatedAt')} value={formatTime(data.updated_at)} divider />
|
|
125
163
|
<InfoMetric label={t('admin.customer.spent')} value={0} divider />
|
|
126
|
-
|
|
127
|
-
|
|
164
|
+
{tokenBalances.map((x) => (
|
|
165
|
+
<InfoMetric
|
|
166
|
+
key={x.currency}
|
|
167
|
+
label={t('admin.customer.balance', { currency: x.currency })}
|
|
168
|
+
value={x.balance}
|
|
169
|
+
divider
|
|
170
|
+
/>
|
|
171
|
+
))}
|
|
128
172
|
</Stack>
|
|
129
173
|
</Box>
|
|
130
174
|
</Box>
|
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
+
import { Button, Stack } from '@mui/material';
|
|
3
|
+
import { Link } from 'react-router-dom';
|
|
4
|
+
|
|
1
5
|
export default function Overview() {
|
|
2
|
-
|
|
6
|
+
const { t } = useLocaleContext();
|
|
7
|
+
return (
|
|
8
|
+
<Stack direction="row" spacing={2}>
|
|
9
|
+
<Button component={Link} variant="outlined" to="/admin/products">
|
|
10
|
+
{t('admin.products')}
|
|
11
|
+
</Button>
|
|
12
|
+
<Button component={Link} variant="outlined" to="/admin/payments">
|
|
13
|
+
{t('admin.payments')}
|
|
14
|
+
</Button>
|
|
15
|
+
<Button component={Link} variant="outlined" to="/admin/customers">
|
|
16
|
+
{t('admin.customers')}
|
|
17
|
+
</Button>
|
|
18
|
+
<Button component={Link} variant="outlined" to="/admin/billing/subscriptions">
|
|
19
|
+
{t('admin.subscriptions')}
|
|
20
|
+
</Button>
|
|
21
|
+
</Stack>
|
|
22
|
+
);
|
|
3
23
|
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
3
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
4
4
|
import type { TPaymentIntentExpanded } from '@did-pay/types';
|
|
5
|
-
import { ArrowBackOutlined, Edit } from '@mui/icons-material';
|
|
6
|
-
import { Alert, Box, Button, CircularProgress, Stack, Typography } from '@mui/material';
|
|
5
|
+
import { ArrowBackOutlined, Edit, InfoOutlined } from '@mui/icons-material';
|
|
6
|
+
import { Alert, Box, Button, CircularProgress, Stack, Tooltip, Typography } from '@mui/material';
|
|
7
7
|
import { styled } from '@mui/system';
|
|
8
8
|
import { fromUnitToToken } from '@ocap/util';
|
|
9
9
|
import { useRequest, useSetState } from 'ahooks';
|
|
@@ -118,7 +118,16 @@ export default function PaymentIntentDetail(props: { id: string }) {
|
|
|
118
118
|
<InfoRow label={t('common.amount')} value={amount} />
|
|
119
119
|
<InfoRow
|
|
120
120
|
label={t('common.status')}
|
|
121
|
-
value={
|
|
121
|
+
value={
|
|
122
|
+
<Stack direction="row" alignItems="center" spacing={1}>
|
|
123
|
+
<Status label={data.status} color={getPaymentIntentStatusColor(data.status)} />
|
|
124
|
+
{data.last_payment_error && (
|
|
125
|
+
<Tooltip title={<pre>{JSON.stringify(data.last_payment_error, null, 2)}</pre>}>
|
|
126
|
+
<InfoOutlined fontSize="small" color="error" />
|
|
127
|
+
</Tooltip>
|
|
128
|
+
)}
|
|
129
|
+
</Stack>
|
|
130
|
+
}
|
|
122
131
|
/>
|
|
123
132
|
<InfoRow label={t('common.description')} value={data.description} />
|
|
124
133
|
<InfoRow label={t('common.statementDescriptor')} value={data.statement_descriptor} />
|