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.
Files changed (36) hide show
  1. package/api/src/libs/notification/template/subscription-canceled.ts +4 -0
  2. package/api/src/libs/refund.ts +4 -0
  3. package/api/src/libs/subscription.ts +25 -0
  4. package/api/src/queues/subscription.ts +2 -2
  5. package/api/src/routes/checkout-sessions.ts +2 -2
  6. package/api/src/routes/connect/recharge.ts +28 -3
  7. package/api/src/routes/connect/shared.ts +88 -0
  8. package/api/src/routes/customers.ts +2 -2
  9. package/api/src/routes/invoices.ts +5 -1
  10. package/api/src/routes/payment-links.ts +3 -0
  11. package/api/src/routes/refunds.ts +22 -1
  12. package/api/src/routes/subscriptions.ts +47 -5
  13. package/api/src/routes/webhook-attempts.ts +14 -1
  14. package/api/src/store/models/invoice.ts +2 -1
  15. package/blocklet.yml +1 -1
  16. package/package.json +4 -4
  17. package/src/app.tsx +3 -1
  18. package/src/components/invoice/list.tsx +40 -11
  19. package/src/components/invoice/recharge.tsx +244 -0
  20. package/src/components/payment-intent/actions.tsx +2 -1
  21. package/src/components/payment-link/actions.tsx +6 -6
  22. package/src/components/payment-link/item.tsx +53 -18
  23. package/src/components/pricing-table/actions.tsx +14 -3
  24. package/src/components/refund/actions.tsx +43 -1
  25. package/src/components/refund/list.tsx +1 -1
  26. package/src/components/subscription/portal/actions.tsx +22 -1
  27. package/src/components/subscription/portal/list.tsx +1 -0
  28. package/src/components/webhook/attempts.tsx +19 -121
  29. package/src/components/webhook/request-info.tsx +139 -0
  30. package/src/locales/en.tsx +4 -0
  31. package/src/locales/zh.tsx +8 -0
  32. package/src/pages/admin/payments/refunds/detail.tsx +2 -2
  33. package/src/pages/admin/products/links/create.tsx +4 -1
  34. package/src/pages/customer/invoice/detail.tsx +6 -0
  35. package/src/pages/customer/recharge.tsx +45 -35
  36. 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, InfoOutlined } from '@mui/icons-material';
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 InfoCard from '../info-card';
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?: { avatar: string; email: string; did: string } };
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<TWebhookAttemptExpanded>(null);
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
- <Typography variant="h6">Request</Typography>
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
- {/* @ts-ignore */}
187
- <InfoOutlined
188
- fontSize="small"
189
- onClick={handleClick}
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
+ }
@@ -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
  });
@@ -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) => setCurrent(v)}
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
- window.location.reload();
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, goBackOrFallback } from '../../../libs/util';
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={() => goBackOrFallback('/customer')}
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