payment-kit 1.15.34 → 1.15.35
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/libs/notification/template/subscription-canceled.ts +4 -0
- package/api/src/libs/refund.ts +4 -0
- package/api/src/libs/subscription.ts +25 -0
- package/api/src/queues/subscription.ts +2 -2
- package/api/src/routes/checkout-sessions.ts +2 -2
- package/api/src/routes/connect/recharge.ts +28 -3
- package/api/src/routes/connect/shared.ts +88 -0
- package/api/src/routes/customers.ts +2 -2
- package/api/src/routes/invoices.ts +5 -1
- package/api/src/routes/payment-links.ts +3 -0
- package/api/src/routes/refunds.ts +22 -1
- package/api/src/routes/subscriptions.ts +47 -5
- package/api/src/routes/webhook-attempts.ts +14 -1
- package/api/src/store/models/invoice.ts +2 -1
- package/blocklet.yml +1 -1
- package/package.json +4 -4
- package/src/app.tsx +3 -1
- package/src/components/invoice/list.tsx +40 -11
- package/src/components/invoice/recharge.tsx +244 -0
- package/src/components/payment-intent/actions.tsx +2 -1
- package/src/components/payment-link/actions.tsx +6 -6
- package/src/components/payment-link/item.tsx +53 -18
- package/src/components/pricing-table/actions.tsx +14 -3
- package/src/components/refund/actions.tsx +43 -1
- package/src/components/refund/list.tsx +1 -1
- package/src/components/subscription/portal/actions.tsx +22 -1
- package/src/components/subscription/portal/list.tsx +1 -0
- package/src/components/webhook/attempts.tsx +19 -121
- package/src/components/webhook/request-info.tsx +139 -0
- package/src/locales/en.tsx +4 -0
- package/src/locales/zh.tsx +8 -0
- package/src/pages/admin/payments/refunds/detail.tsx +2 -2
- package/src/pages/admin/products/links/create.tsx +4 -1
- package/src/pages/customer/invoice/detail.tsx +6 -0
- package/src/pages/customer/recharge.tsx +45 -35
- package/src/pages/customer/subscription/detail.tsx +8 -18
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import CodeBlock from '@arcblock/ux/lib/CodeBlock';
|
|
3
3
|
import { api, formatTime } from '@blocklet/payment-react';
|
|
4
4
|
import type { Paginated, TEvent, TWebhookAttemptExpanded } from '@blocklet/payment-types';
|
|
5
|
-
import { CheckCircleOutlined, ErrorOutlined
|
|
5
|
+
import { CheckCircleOutlined, ErrorOutlined } from '@mui/icons-material';
|
|
6
6
|
import {
|
|
7
7
|
Box,
|
|
8
8
|
Button,
|
|
@@ -15,15 +15,13 @@ import {
|
|
|
15
15
|
ListSubheader,
|
|
16
16
|
Stack,
|
|
17
17
|
Typography,
|
|
18
|
-
Popper,
|
|
19
|
-
Paper,
|
|
20
18
|
} from '@mui/material';
|
|
21
19
|
import { useInfiniteScroll } from 'ahooks';
|
|
22
20
|
import React, { useEffect, useState } from 'react';
|
|
23
21
|
|
|
24
22
|
import { isEmpty } from 'lodash';
|
|
25
23
|
import { isSuccessAttempt } from '../../libs/util';
|
|
26
|
-
import
|
|
24
|
+
import RequestInfoPopper, { RequestType } from './request-info';
|
|
27
25
|
|
|
28
26
|
const fetchData = (params: Record<string, any> = {}): Promise<Paginated<TWebhookAttemptExpanded>> => {
|
|
29
27
|
const search = new URLSearchParams();
|
|
@@ -48,7 +46,7 @@ const groupAttemptsByDate = (attempts: TWebhookAttemptExpanded[]) => {
|
|
|
48
46
|
type Props = {
|
|
49
47
|
event_id?: string;
|
|
50
48
|
webhook_endpoint_id?: string;
|
|
51
|
-
event?: TEvent & { requestInfo?:
|
|
49
|
+
event?: TEvent & { requestInfo?: RequestInfo };
|
|
52
50
|
};
|
|
53
51
|
|
|
54
52
|
WebhookAttempts.defaultProps = {
|
|
@@ -72,7 +70,9 @@ export default function WebhookAttempts({ event_id, webhook_endpoint_id, event }
|
|
|
72
70
|
const attempts = data?.list || [];
|
|
73
71
|
|
|
74
72
|
// @ts-ignore
|
|
75
|
-
const [selected, setSelected] = useState<
|
|
73
|
+
const [selected, setSelected] = useState<
|
|
74
|
+
(TWebhookAttemptExpanded & { event: TEvent & { requestInfo?: RequestInfo } }) | null
|
|
75
|
+
>(null);
|
|
76
76
|
const groupedAttempts = groupAttemptsByDate(attempts);
|
|
77
77
|
|
|
78
78
|
useEffect(() => {
|
|
@@ -85,35 +85,6 @@ export default function WebhookAttempts({ event_id, webhook_endpoint_id, event }
|
|
|
85
85
|
setSelected(attempt);
|
|
86
86
|
};
|
|
87
87
|
|
|
88
|
-
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
|
89
|
-
|
|
90
|
-
const handleClick = (e: React.MouseEvent<HTMLElement>) => {
|
|
91
|
-
setAnchorEl(anchorEl ? null : e.currentTarget);
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
useEffect(() => {
|
|
95
|
-
const handleClickOutside = (e: MouseEvent) => {
|
|
96
|
-
if (anchorEl && !anchorEl.contains(e.target as Node) && !(e.target as Element).closest('.popper-content')) {
|
|
97
|
-
setAnchorEl(null);
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
const handleScroll = (e: Event) => {
|
|
102
|
-
// @ts-ignore
|
|
103
|
-
if (anchorEl && !e.target?.closest('.popper-content')) {
|
|
104
|
-
setAnchorEl(null);
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
document.addEventListener('click', handleClickOutside);
|
|
109
|
-
window.addEventListener('scroll', handleScroll, true);
|
|
110
|
-
|
|
111
|
-
return () => {
|
|
112
|
-
document.removeEventListener('click', handleClickOutside);
|
|
113
|
-
window.removeEventListener('scroll', handleScroll, true);
|
|
114
|
-
};
|
|
115
|
-
}, [anchorEl]);
|
|
116
|
-
|
|
117
88
|
if (loading) {
|
|
118
89
|
return <CircularProgress />;
|
|
119
90
|
}
|
|
@@ -172,7 +143,14 @@ export default function WebhookAttempts({ event_id, webhook_endpoint_id, event }
|
|
|
172
143
|
<CodeBlock language="json">{JSON.stringify(selected.response_body, null, 2)}</CodeBlock>
|
|
173
144
|
</Box>
|
|
174
145
|
<Box>
|
|
175
|
-
<
|
|
146
|
+
<Stack direction="row" alignItems="center" spacing={1}>
|
|
147
|
+
<Typography variant="h6">Request</Typography>
|
|
148
|
+
<RequestInfoPopper
|
|
149
|
+
// @ts-ignore
|
|
150
|
+
requestInfo={selected?.event?.requestInfo}
|
|
151
|
+
request={selected?.event.request as RequestType}
|
|
152
|
+
/>
|
|
153
|
+
</Stack>
|
|
176
154
|
{/* @ts-ignore */}
|
|
177
155
|
<CodeBlock language="json">{JSON.stringify(selected.event, null, 2)}</CodeBlock>
|
|
178
156
|
</Box>
|
|
@@ -182,91 +160,11 @@ export default function WebhookAttempts({ event_id, webhook_endpoint_id, event }
|
|
|
182
160
|
<Box>
|
|
183
161
|
<Stack direction="row" alignItems="center" spacing={1}>
|
|
184
162
|
<Typography variant="h6">Event Data</Typography>
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
sx={{
|
|
191
|
-
color: 'text.secondary',
|
|
192
|
-
opacity: 0.6,
|
|
193
|
-
cursor: 'pointer',
|
|
194
|
-
}}
|
|
195
|
-
/>
|
|
196
|
-
<Popper
|
|
197
|
-
open={Boolean(anchorEl)}
|
|
198
|
-
anchorEl={anchorEl}
|
|
199
|
-
placement="right"
|
|
200
|
-
sx={{
|
|
201
|
-
zIndex: 1000,
|
|
202
|
-
'@media (max-width: 600px)': {
|
|
203
|
-
'& .MuiPaper-root': {
|
|
204
|
-
width: 'calc(100vw - 32px)',
|
|
205
|
-
maxWidth: 'none',
|
|
206
|
-
},
|
|
207
|
-
},
|
|
208
|
-
}}
|
|
209
|
-
modifiers={[
|
|
210
|
-
{
|
|
211
|
-
name: 'preventOverflow',
|
|
212
|
-
options: {
|
|
213
|
-
boundary: window,
|
|
214
|
-
altAxis: true,
|
|
215
|
-
padding: 16,
|
|
216
|
-
},
|
|
217
|
-
},
|
|
218
|
-
{
|
|
219
|
-
name: 'flip',
|
|
220
|
-
options: {
|
|
221
|
-
fallbackPlacements: ['bottom'],
|
|
222
|
-
},
|
|
223
|
-
},
|
|
224
|
-
{
|
|
225
|
-
name: 'matchWidth',
|
|
226
|
-
enabled: true,
|
|
227
|
-
fn: ({ state }) => {
|
|
228
|
-
if (window.innerWidth <= 600) {
|
|
229
|
-
state.styles.popper = {
|
|
230
|
-
...state.styles.popper,
|
|
231
|
-
width: 'calc(100vw - 32px)',
|
|
232
|
-
maxWidth: 'none',
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
return state;
|
|
236
|
-
},
|
|
237
|
-
},
|
|
238
|
-
]}>
|
|
239
|
-
<Paper
|
|
240
|
-
className="popper-content"
|
|
241
|
-
elevation={3}
|
|
242
|
-
sx={{
|
|
243
|
-
p: 2,
|
|
244
|
-
border: '1px solid',
|
|
245
|
-
borderColor: 'divider',
|
|
246
|
-
maxWidth: 300,
|
|
247
|
-
'@media (max-width: 600px)': {
|
|
248
|
-
maxWidth: 'none',
|
|
249
|
-
margin: '0 auto',
|
|
250
|
-
},
|
|
251
|
-
}}>
|
|
252
|
-
{event.requestInfo ? (
|
|
253
|
-
<>
|
|
254
|
-
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
|
|
255
|
-
Requested by:
|
|
256
|
-
</Typography>
|
|
257
|
-
<InfoCard
|
|
258
|
-
logo={event.requestInfo.avatar}
|
|
259
|
-
name={event.requestInfo.email}
|
|
260
|
-
description={event.requestInfo.did || event.request.requested_by}
|
|
261
|
-
size={40}
|
|
262
|
-
/>
|
|
263
|
-
</>
|
|
264
|
-
) : (
|
|
265
|
-
<Typography>Requested by: {event.request?.requested_by || 'system'}</Typography>
|
|
266
|
-
)}
|
|
267
|
-
</Paper>
|
|
268
|
-
</Popper>
|
|
269
|
-
</>
|
|
163
|
+
<RequestInfoPopper
|
|
164
|
+
// @ts-ignore
|
|
165
|
+
requestInfo={event.requestInfo}
|
|
166
|
+
request={event.request as RequestType}
|
|
167
|
+
/>
|
|
270
168
|
</Stack>
|
|
271
169
|
{/* @ts-ignore */}
|
|
272
170
|
<CodeBlock language="json">{JSON.stringify(event.data, null, 2)}</CodeBlock>
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { InfoOutlined } from '@mui/icons-material';
|
|
2
|
+
import { Popper, Paper, Typography } from '@mui/material';
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import InfoCard from '../info-card';
|
|
5
|
+
|
|
6
|
+
export type RequestInfo = {
|
|
7
|
+
avatar: string;
|
|
8
|
+
email: string;
|
|
9
|
+
did: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type RequestType = {
|
|
13
|
+
requested_by: string;
|
|
14
|
+
};
|
|
15
|
+
type Props = {
|
|
16
|
+
requestInfo?: RequestInfo;
|
|
17
|
+
request?: RequestType;
|
|
18
|
+
};
|
|
19
|
+
RequestInfoPopper.defaultProps = {
|
|
20
|
+
requestInfo: null,
|
|
21
|
+
request: null,
|
|
22
|
+
};
|
|
23
|
+
export default function RequestInfoPopper({ requestInfo, request }: Props) {
|
|
24
|
+
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
|
25
|
+
|
|
26
|
+
const handleClick = (e: React.MouseEvent<HTMLElement>) => {
|
|
27
|
+
setAnchorEl(anchorEl ? null : e.currentTarget);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
32
|
+
if (anchorEl && !anchorEl.contains(e.target as Node) && !(e.target as Element).closest('.popper-content')) {
|
|
33
|
+
setAnchorEl(null);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const handleScroll = (e: Event) => {
|
|
38
|
+
if (anchorEl && !(e.target as Element)?.closest('.popper-content')) {
|
|
39
|
+
setAnchorEl(null);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
document.addEventListener('click', handleClickOutside);
|
|
44
|
+
window.addEventListener('scroll', handleScroll, true);
|
|
45
|
+
|
|
46
|
+
return () => {
|
|
47
|
+
document.removeEventListener('click', handleClickOutside);
|
|
48
|
+
window.removeEventListener('scroll', handleScroll, true);
|
|
49
|
+
};
|
|
50
|
+
}, [anchorEl]);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<>
|
|
54
|
+
<InfoOutlined
|
|
55
|
+
fontSize="small"
|
|
56
|
+
// @ts-ignore
|
|
57
|
+
onClick={(e) => handleClick(e)}
|
|
58
|
+
sx={{
|
|
59
|
+
color: 'text.secondary',
|
|
60
|
+
opacity: 0.6,
|
|
61
|
+
cursor: 'pointer',
|
|
62
|
+
}}
|
|
63
|
+
/>
|
|
64
|
+
<Popper
|
|
65
|
+
open={Boolean(anchorEl)}
|
|
66
|
+
anchorEl={anchorEl}
|
|
67
|
+
placement="right"
|
|
68
|
+
sx={{
|
|
69
|
+
zIndex: 1000,
|
|
70
|
+
'@media (max-width: 600px)': {
|
|
71
|
+
'& .MuiPaper-root': {
|
|
72
|
+
width: 'calc(100vw - 32px)',
|
|
73
|
+
maxWidth: 'none',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
}}
|
|
77
|
+
modifiers={[
|
|
78
|
+
{
|
|
79
|
+
name: 'preventOverflow',
|
|
80
|
+
options: {
|
|
81
|
+
boundary: window,
|
|
82
|
+
altAxis: true,
|
|
83
|
+
padding: 16,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'flip',
|
|
88
|
+
options: {
|
|
89
|
+
fallbackPlacements: ['bottom'],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'matchWidth',
|
|
94
|
+
enabled: true,
|
|
95
|
+
fn: ({ state }) => {
|
|
96
|
+
if (window.innerWidth <= 600) {
|
|
97
|
+
state.styles.popper = {
|
|
98
|
+
...state.styles.popper,
|
|
99
|
+
width: 'calc(100vw - 32px)',
|
|
100
|
+
maxWidth: 'none',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return state;
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
]}>
|
|
107
|
+
<Paper
|
|
108
|
+
className="popper-content"
|
|
109
|
+
elevation={3}
|
|
110
|
+
sx={{
|
|
111
|
+
p: 2,
|
|
112
|
+
border: '1px solid',
|
|
113
|
+
borderColor: 'divider',
|
|
114
|
+
maxWidth: 360,
|
|
115
|
+
'@media (max-width: 600px)': {
|
|
116
|
+
maxWidth: 'none',
|
|
117
|
+
margin: '0 auto',
|
|
118
|
+
},
|
|
119
|
+
}}>
|
|
120
|
+
{requestInfo ? (
|
|
121
|
+
<>
|
|
122
|
+
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
|
|
123
|
+
Requested by:
|
|
124
|
+
</Typography>
|
|
125
|
+
<InfoCard
|
|
126
|
+
logo={requestInfo.avatar}
|
|
127
|
+
name={requestInfo.email}
|
|
128
|
+
description={requestInfo.did || request?.requested_by}
|
|
129
|
+
size={40}
|
|
130
|
+
/>
|
|
131
|
+
</>
|
|
132
|
+
) : (
|
|
133
|
+
<Typography>Requested by: {request?.requested_by || 'system'}</Typography>
|
|
134
|
+
)}
|
|
135
|
+
</Paper>
|
|
136
|
+
</Popper>
|
|
137
|
+
</>
|
|
138
|
+
);
|
|
139
|
+
}
|
package/src/locales/en.tsx
CHANGED
|
@@ -22,6 +22,7 @@ export default flat({
|
|
|
22
22
|
latinOnly:
|
|
23
23
|
'At least one letter and cannot include Chinese characters and special characters such as <, >、"、’ or \\',
|
|
24
24
|
loading: 'Loading...',
|
|
25
|
+
rechargeTime: 'Recharge Time',
|
|
25
26
|
},
|
|
26
27
|
admin: {
|
|
27
28
|
balances: 'Balances',
|
|
@@ -252,11 +253,13 @@ export default flat({
|
|
|
252
253
|
label: 'Name',
|
|
253
254
|
placeholder: 'Not consumer facing',
|
|
254
255
|
},
|
|
256
|
+
adjustableQuantityError: 'Minimum must be less than maximum',
|
|
255
257
|
},
|
|
256
258
|
pricingTable: {
|
|
257
259
|
view: 'View pricing table',
|
|
258
260
|
add: 'Create pricing table',
|
|
259
261
|
save: 'Create',
|
|
262
|
+
openLink: 'Open URL',
|
|
260
263
|
copyLink: 'Copy URL',
|
|
261
264
|
saved: 'Pricing table successfully saved',
|
|
262
265
|
edit: 'Edit pricing table',
|
|
@@ -628,6 +631,7 @@ export default flat({
|
|
|
628
631
|
custom: 'Custom',
|
|
629
632
|
estimatedDuration: '{duration} {unit} est.',
|
|
630
633
|
intervals: 'intervals',
|
|
634
|
+
history: 'Recharge History',
|
|
631
635
|
},
|
|
632
636
|
},
|
|
633
637
|
});
|
package/src/locales/zh.tsx
CHANGED
|
@@ -21,6 +21,7 @@ export default flat({
|
|
|
21
21
|
invalidCharacters: '无效字符',
|
|
22
22
|
latinOnly: '至少包含一个字母,并且不能包含中文字符和特殊字符如 <, >、"、’ 或 \\',
|
|
23
23
|
loading: '加载中...',
|
|
24
|
+
rechargeTime: '充值时间',
|
|
24
25
|
},
|
|
25
26
|
admin: {
|
|
26
27
|
balances: '余额',
|
|
@@ -245,6 +246,7 @@ export default flat({
|
|
|
245
246
|
label: '名称',
|
|
246
247
|
placeholder: '不向消费者展示',
|
|
247
248
|
},
|
|
249
|
+
adjustableQuantityError: '最小数量必须小于最大数量',
|
|
248
250
|
},
|
|
249
251
|
payout: {
|
|
250
252
|
list: '对外支付',
|
|
@@ -257,6 +259,7 @@ export default flat({
|
|
|
257
259
|
view: '查看定价表',
|
|
258
260
|
add: '创建定价表',
|
|
259
261
|
save: '创建',
|
|
262
|
+
openLink: '打开URL',
|
|
260
263
|
copyLink: '复制URL',
|
|
261
264
|
saved: '定价表已成功保存',
|
|
262
265
|
edit: '编辑定价表',
|
|
@@ -283,6 +286,10 @@ export default flat({
|
|
|
283
286
|
attention: '失败的付款',
|
|
284
287
|
refundError: '退款申请失败',
|
|
285
288
|
refundSuccess: '退款申请已成功创建',
|
|
289
|
+
cancelRefund: '取消退款',
|
|
290
|
+
refundCanceled: '取消退款成功',
|
|
291
|
+
refundCanceledError: '取消退款失败',
|
|
292
|
+
refundCanceledTip: '您确定要取消退款申请吗?取消后,退款将不再进行。',
|
|
286
293
|
refundForm: {
|
|
287
294
|
reason: '退款原因',
|
|
288
295
|
amount: '退款金额',
|
|
@@ -616,6 +623,7 @@ export default flat({
|
|
|
616
623
|
estimatedDuration: '预计可用 {duration} {unit}',
|
|
617
624
|
custom: '自定义',
|
|
618
625
|
intervals: '个周期',
|
|
626
|
+
history: '充值记录',
|
|
619
627
|
},
|
|
620
628
|
},
|
|
621
629
|
});
|
|
@@ -104,7 +104,7 @@ export default function RefundDetail(props: { id: string }) {
|
|
|
104
104
|
{t('admin.refunds')}
|
|
105
105
|
</Typography>
|
|
106
106
|
</Stack>
|
|
107
|
-
<RefundActions data={data} variant="normal" />
|
|
107
|
+
<RefundActions data={data} variant="normal" onChange={() => runAsync()} />
|
|
108
108
|
</Stack>
|
|
109
109
|
<Box
|
|
110
110
|
mt={4}
|
|
@@ -234,7 +234,7 @@ export default function RefundDetail(props: { id: string }) {
|
|
|
234
234
|
value={
|
|
235
235
|
<Stack direction="row" alignItems="center" spacing={1}>
|
|
236
236
|
<Status label={data.status} color={getRefundStatusColor(data.status)} />
|
|
237
|
-
{data.last_attempt_error && (
|
|
237
|
+
{data.last_attempt_error && data.status !== 'canceled' && (
|
|
238
238
|
<Tooltip
|
|
239
239
|
title={
|
|
240
240
|
<pre style={{ whiteSpace: 'break-spaces' }}>
|
|
@@ -35,6 +35,7 @@ export default function CreatePaymentLink() {
|
|
|
35
35
|
|
|
36
36
|
const methods = useForm<PaymentLink>({
|
|
37
37
|
shouldUnregister: false,
|
|
38
|
+
mode: 'onChange',
|
|
38
39
|
defaultValues: {
|
|
39
40
|
name: '',
|
|
40
41
|
line_items: [],
|
|
@@ -160,7 +161,9 @@ export default function CreatePaymentLink() {
|
|
|
160
161
|
// @ts-ignore
|
|
161
162
|
current={current}
|
|
162
163
|
// @ts-ignore
|
|
163
|
-
onChange={(v: string) =>
|
|
164
|
+
onChange={(v: string) => {
|
|
165
|
+
methods.handleSubmit(() => setCurrent(v))();
|
|
166
|
+
}}
|
|
164
167
|
style={{ width: '100%' }}
|
|
165
168
|
scrollButtons="auto"
|
|
166
169
|
/>
|
|
@@ -20,6 +20,7 @@ import { useRequest, useSetState } from 'ahooks';
|
|
|
20
20
|
import { useEffect } from 'react';
|
|
21
21
|
import { Link, useParams, useSearchParams } from 'react-router-dom';
|
|
22
22
|
|
|
23
|
+
import { useSessionContext } from '../../../contexts/session';
|
|
23
24
|
import Currency from '../../../components/currency';
|
|
24
25
|
import CustomerLink from '../../../components/customer/link';
|
|
25
26
|
import InfoRow from '../../../components/info-row';
|
|
@@ -47,6 +48,7 @@ export default function CustomerInvoiceDetail() {
|
|
|
47
48
|
|
|
48
49
|
const { loading, error, data, runAsync } = useRequest(() => fetchData(params.id as string));
|
|
49
50
|
const action = searchParams.get('action');
|
|
51
|
+
const { session } = useSessionContext();
|
|
50
52
|
|
|
51
53
|
const onPay = () => {
|
|
52
54
|
setState({ paying: true });
|
|
@@ -94,6 +96,10 @@ export default function CustomerInvoiceDetail() {
|
|
|
94
96
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
95
97
|
}, [error]);
|
|
96
98
|
|
|
99
|
+
if (data?.customer?.did && session?.user?.did && data.customer.did !== session.user.did) {
|
|
100
|
+
return <Alert severity="error">You do not have permission to access other customer data</Alert>;
|
|
101
|
+
}
|
|
102
|
+
|
|
97
103
|
if (error) {
|
|
98
104
|
return <Alert severity="error">{formatError(error)}</Alert>;
|
|
99
105
|
}
|
|
@@ -30,12 +30,14 @@ import { joinURL } from 'ufo';
|
|
|
30
30
|
import type { TSubscriptionExpanded } from '@blocklet/payment-types';
|
|
31
31
|
import { ArrowBackOutlined } from '@mui/icons-material';
|
|
32
32
|
import { BN, fromUnitToToken } from '@ocap/util';
|
|
33
|
+
import RechargeList from '../../components/invoice/recharge';
|
|
33
34
|
import SubscriptionDescription from '../../components/subscription/description';
|
|
34
35
|
import InfoRow from '../../components/info-row';
|
|
35
36
|
import Currency from '../../components/currency';
|
|
36
37
|
import SubscriptionMetrics from '../../components/subscription/metrics';
|
|
37
38
|
import { goBackOrFallback } from '../../libs/util';
|
|
38
39
|
import CustomerLink from '../../components/customer/link';
|
|
40
|
+
import { useSessionContext } from '../../contexts/session';
|
|
39
41
|
|
|
40
42
|
const Root = styled(Stack)(({ theme }) => ({
|
|
41
43
|
marginBottom: theme.spacing(3),
|
|
@@ -70,6 +72,7 @@ export default function RechargePage() {
|
|
|
70
72
|
} | null>(null);
|
|
71
73
|
const [customAmount, setCustomAmount] = useState(false);
|
|
72
74
|
const [presetAmounts, setPresetAmounts] = useState<Array<{ amount: string; cycles: number }>>([]);
|
|
75
|
+
const { session } = useSessionContext();
|
|
73
76
|
|
|
74
77
|
const {
|
|
75
78
|
paymentCurrency,
|
|
@@ -84,42 +87,41 @@ export default function RechargePage() {
|
|
|
84
87
|
|
|
85
88
|
// @ts-ignore
|
|
86
89
|
const receiveAddress = paymentDetails?.[paymentMethod?.type]?.payer;
|
|
90
|
+
const fetchData = async () => {
|
|
91
|
+
try {
|
|
92
|
+
setLoading(true);
|
|
93
|
+
const [subscriptionRes, payerTokenRes, upcomingRes] = await Promise.all([
|
|
94
|
+
api.get(`/api/subscriptions/${subscriptionId}`),
|
|
95
|
+
api.get(`/api/subscriptions/${subscriptionId}/payer-token`),
|
|
96
|
+
api.get(`/api/subscriptions/${subscriptionId}/upcoming`),
|
|
97
|
+
]);
|
|
98
|
+
setSubscription(subscriptionRes.data);
|
|
99
|
+
setPayerValue(payerTokenRes.data);
|
|
100
|
+
|
|
101
|
+
// Calculate preset amounts
|
|
102
|
+
const getCycleAmount = (cycles: number) =>
|
|
103
|
+
fromUnitToToken(
|
|
104
|
+
new BN(upcomingRes.data.amount === '0' ? upcomingRes.data.minExpectedAmount : upcomingRes.data.amount)
|
|
105
|
+
.mul(new BN(cycles))
|
|
106
|
+
.toString(),
|
|
107
|
+
upcomingRes.data?.currency?.decimal
|
|
108
|
+
);
|
|
109
|
+
setPresetAmounts([
|
|
110
|
+
{ amount: getCycleAmount(1), cycles: 1 },
|
|
111
|
+
{ amount: getCycleAmount(2), cycles: 2 },
|
|
112
|
+
{ amount: getCycleAmount(3), cycles: 3 },
|
|
113
|
+
{ amount: getCycleAmount(5), cycles: 5 },
|
|
114
|
+
{ amount: getCycleAmount(10), cycles: 10 },
|
|
115
|
+
]);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
setError(err instanceof Error ? err.message : t('common.fetchError'));
|
|
118
|
+
console.error(err);
|
|
119
|
+
} finally {
|
|
120
|
+
setLoading(false);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
87
123
|
|
|
88
124
|
useEffect(() => {
|
|
89
|
-
const fetchData = async () => {
|
|
90
|
-
try {
|
|
91
|
-
setLoading(true);
|
|
92
|
-
const [subscriptionRes, payerTokenRes, upcomingRes] = await Promise.all([
|
|
93
|
-
api.get(`/api/subscriptions/${subscriptionId}`),
|
|
94
|
-
api.get(`/api/subscriptions/${subscriptionId}/payer-token`),
|
|
95
|
-
api.get(`/api/subscriptions/${subscriptionId}/upcoming`),
|
|
96
|
-
]);
|
|
97
|
-
setSubscription(subscriptionRes.data);
|
|
98
|
-
setPayerValue(payerTokenRes.data);
|
|
99
|
-
|
|
100
|
-
// Calculate preset amounts
|
|
101
|
-
const getCycleAmount = (cycles: number) =>
|
|
102
|
-
fromUnitToToken(
|
|
103
|
-
new BN(upcomingRes.data.amount === '0' ? upcomingRes.data.minExpectedAmount : upcomingRes.data.amount)
|
|
104
|
-
.mul(new BN(cycles))
|
|
105
|
-
.toString(),
|
|
106
|
-
upcomingRes.data?.currency?.decimal
|
|
107
|
-
);
|
|
108
|
-
setPresetAmounts([
|
|
109
|
-
{ amount: getCycleAmount(1), cycles: 1 },
|
|
110
|
-
{ amount: getCycleAmount(2), cycles: 2 },
|
|
111
|
-
{ amount: getCycleAmount(3), cycles: 3 },
|
|
112
|
-
{ amount: getCycleAmount(5), cycles: 5 },
|
|
113
|
-
{ amount: getCycleAmount(10), cycles: 10 },
|
|
114
|
-
]);
|
|
115
|
-
} catch (err) {
|
|
116
|
-
setError(err instanceof Error ? err.message : t('common.fetchError'));
|
|
117
|
-
console.error(err);
|
|
118
|
-
} finally {
|
|
119
|
-
setLoading(false);
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
|
|
123
125
|
fetchData();
|
|
124
126
|
}, [subscriptionId]);
|
|
125
127
|
|
|
@@ -135,7 +137,7 @@ export default function RechargePage() {
|
|
|
135
137
|
onSuccess: () => {
|
|
136
138
|
connect.close();
|
|
137
139
|
Toast.success(t('customer.recharge.success'));
|
|
138
|
-
|
|
140
|
+
fetchData();
|
|
139
141
|
},
|
|
140
142
|
onClose: () => {
|
|
141
143
|
connect.close();
|
|
@@ -181,6 +183,9 @@ export default function RechargePage() {
|
|
|
181
183
|
return <Alert severity="info">{t('common.dataNotFound')}</Alert>;
|
|
182
184
|
}
|
|
183
185
|
|
|
186
|
+
if (subscription?.customer?.did && session?.user?.did && subscription.customer.did !== session.user.did) {
|
|
187
|
+
return <Alert severity="error">You do not have permission to access other customer data</Alert>;
|
|
188
|
+
}
|
|
184
189
|
const currentBalance = formatBNStr(payerValue?.token || '0', paymentCurrency?.decimal, 6, false);
|
|
185
190
|
|
|
186
191
|
const supportRecharge = ['arcblock', 'ethereum'].includes(paymentMethod?.type || '');
|
|
@@ -414,6 +419,11 @@ export default function RechargePage() {
|
|
|
414
419
|
<Alert severity="info">{t('customer.recharge.unsupported')}</Alert>
|
|
415
420
|
)}
|
|
416
421
|
</Box>
|
|
422
|
+
<Divider />
|
|
423
|
+
<Typography variant="h2" gutterBottom>
|
|
424
|
+
{t('customer.recharge.history')}
|
|
425
|
+
</Typography>
|
|
426
|
+
<RechargeList subscription_id={subscriptionId} currency_id={paymentCurrency?.id} />
|
|
417
427
|
</Root>
|
|
418
428
|
);
|
|
419
429
|
}
|
|
@@ -15,7 +15,8 @@ import SubscriptionDescription from '../../../components/subscription/descriptio
|
|
|
15
15
|
import SubscriptionItemList from '../../../components/subscription/items';
|
|
16
16
|
import SubscriptionMetrics from '../../../components/subscription/metrics';
|
|
17
17
|
import SubscriptionActions from '../../../components/subscription/portal/actions';
|
|
18
|
-
import { canChangePaymentMethod
|
|
18
|
+
import { canChangePaymentMethod } from '../../../libs/util';
|
|
19
|
+
import { useSessionContext } from '../../../contexts/session';
|
|
19
20
|
|
|
20
21
|
const fetchData = (id: string | undefined): Promise<TSubscriptionExpanded> => {
|
|
21
22
|
return api.get(`/api/subscriptions/${id}`).then((res) => res.data);
|
|
@@ -24,19 +25,15 @@ const fetchData = (id: string | undefined): Promise<TSubscriptionExpanded> => {
|
|
|
24
25
|
const InfoDirection = 'column';
|
|
25
26
|
const InfoAlignItems = 'flex-start';
|
|
26
27
|
|
|
27
|
-
const supportRecharge = (subscription: TSubscriptionExpanded) => {
|
|
28
|
-
return (
|
|
29
|
-
['active', 'trialing', 'past_due'].includes(subscription?.status) &&
|
|
30
|
-
['arcblock', 'ethereum'].includes(subscription?.paymentMethod?.type)
|
|
31
|
-
);
|
|
32
|
-
};
|
|
33
|
-
|
|
34
28
|
export default function CustomerSubscriptionDetail() {
|
|
35
29
|
const { id } = useParams() as { id: string };
|
|
36
30
|
const navigate = useNavigate();
|
|
37
31
|
const { t } = useLocaleContext();
|
|
32
|
+
const { session } = useSessionContext();
|
|
38
33
|
const { loading, error, data, refresh } = useRequest(() => fetchData(id));
|
|
39
|
-
|
|
34
|
+
if (data?.customer?.did && session?.user?.did && data.customer.did !== session.user.did) {
|
|
35
|
+
return <Alert severity="error">You do not have permission to access other customer data</Alert>;
|
|
36
|
+
}
|
|
40
37
|
if (error) {
|
|
41
38
|
return <Alert severity="error">{error.message}</Alert>;
|
|
42
39
|
}
|
|
@@ -56,7 +53,7 @@ export default function CustomerSubscriptionDetail() {
|
|
|
56
53
|
sx={{ position: 'relative', mt: '16px' }}>
|
|
57
54
|
<Stack
|
|
58
55
|
direction="row"
|
|
59
|
-
onClick={() =>
|
|
56
|
+
onClick={() => navigate('/customer', { replace: true })}
|
|
60
57
|
alignItems="center"
|
|
61
58
|
sx={{ fontWeight: 'normal', cursor: 'pointer' }}>
|
|
62
59
|
<ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
|
|
@@ -69,6 +66,7 @@ export default function CustomerSubscriptionDetail() {
|
|
|
69
66
|
subscription={data}
|
|
70
67
|
onChange={() => refresh()}
|
|
71
68
|
showExtra
|
|
69
|
+
showRecharge
|
|
72
70
|
actionProps={{
|
|
73
71
|
cancel: {
|
|
74
72
|
variant: 'outlined',
|
|
@@ -84,14 +82,6 @@ export default function CustomerSubscriptionDetail() {
|
|
|
84
82
|
},
|
|
85
83
|
}}
|
|
86
84
|
/>
|
|
87
|
-
{supportRecharge(data) && (
|
|
88
|
-
<Button
|
|
89
|
-
variant="outlined"
|
|
90
|
-
color="primary"
|
|
91
|
-
onClick={() => navigate(`/customer/subscription/${data.id}/recharge`)}>
|
|
92
|
-
{t('customer.recharge.title')}
|
|
93
|
-
</Button>
|
|
94
|
-
)}
|
|
95
85
|
</Stack>
|
|
96
86
|
</Stack>
|
|
97
87
|
<Box
|