payment-kit 1.15.33 → 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 (63) hide show
  1. package/api/src/integrations/stripe/handlers/setup-intent.ts +3 -1
  2. package/api/src/integrations/stripe/handlers/subscription.ts +2 -8
  3. package/api/src/integrations/stripe/resource.ts +0 -11
  4. package/api/src/libs/invoice.ts +202 -1
  5. package/api/src/libs/notification/template/subscription-canceled.ts +15 -2
  6. package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -1
  7. package/api/src/libs/notification/template/subscription-renewed.ts +1 -1
  8. package/api/src/libs/notification/template/subscription-trial-will-end.ts +9 -5
  9. package/api/src/libs/notification/template/subscription-will-canceled.ts +9 -5
  10. package/api/src/libs/notification/template/subscription-will-renew.ts +10 -12
  11. package/api/src/libs/payment.ts +3 -2
  12. package/api/src/libs/refund.ts +4 -0
  13. package/api/src/libs/subscription.ts +58 -14
  14. package/api/src/queues/invoice.ts +1 -0
  15. package/api/src/queues/payment.ts +3 -1
  16. package/api/src/queues/refund.ts +9 -8
  17. package/api/src/queues/subscription.ts +111 -40
  18. package/api/src/routes/checkout-sessions.ts +22 -6
  19. package/api/src/routes/connect/change-payment.ts +51 -34
  20. package/api/src/routes/connect/change-plan.ts +25 -3
  21. package/api/src/routes/connect/recharge.ts +28 -3
  22. package/api/src/routes/connect/setup.ts +27 -6
  23. package/api/src/routes/connect/shared.ts +223 -1
  24. package/api/src/routes/connect/subscribe.ts +25 -3
  25. package/api/src/routes/customers.ts +2 -2
  26. package/api/src/routes/invoices.ts +27 -105
  27. package/api/src/routes/payment-links.ts +3 -0
  28. package/api/src/routes/refunds.ts +22 -1
  29. package/api/src/routes/subscriptions.ts +112 -21
  30. package/api/src/routes/webhook-attempts.ts +14 -1
  31. package/api/src/store/models/invoice.ts +3 -1
  32. package/blocklet.yml +1 -1
  33. package/package.json +4 -4
  34. package/src/app.tsx +3 -1
  35. package/src/components/invoice/list.tsx +83 -31
  36. package/src/components/invoice/recharge.tsx +244 -0
  37. package/src/components/payment-intent/actions.tsx +2 -1
  38. package/src/components/payment-link/actions.tsx +6 -6
  39. package/src/components/payment-link/item.tsx +53 -18
  40. package/src/components/pricing-table/actions.tsx +14 -3
  41. package/src/components/pricing-table/payment-settings.tsx +1 -1
  42. package/src/components/refund/actions.tsx +43 -1
  43. package/src/components/refund/list.tsx +1 -1
  44. package/src/components/subscription/actions/cancel.tsx +10 -7
  45. package/src/components/subscription/metrics.tsx +1 -1
  46. package/src/components/subscription/portal/actions.tsx +22 -1
  47. package/src/components/subscription/portal/list.tsx +1 -0
  48. package/src/components/webhook/attempts.tsx +19 -121
  49. package/src/components/webhook/request-info.tsx +139 -0
  50. package/src/locales/en.tsx +4 -0
  51. package/src/locales/zh.tsx +8 -0
  52. package/src/pages/admin/billing/invoices/detail.tsx +15 -0
  53. package/src/pages/admin/billing/invoices/index.tsx +1 -1
  54. package/src/pages/admin/billing/subscriptions/detail.tsx +12 -4
  55. package/src/pages/admin/customers/customers/detail.tsx +1 -0
  56. package/src/pages/admin/payments/refunds/detail.tsx +2 -2
  57. package/src/pages/admin/products/links/create.tsx +4 -1
  58. package/src/pages/customer/index.tsx +1 -1
  59. package/src/pages/customer/invoice/detail.tsx +34 -14
  60. package/src/pages/customer/recharge.tsx +45 -35
  61. package/src/pages/customer/subscription/change-plan.tsx +8 -1
  62. package/src/pages/customer/subscription/detail.tsx +12 -22
  63. package/src/pages/customer/subscription/embed.tsx +3 -1
@@ -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
  });
@@ -322,6 +322,21 @@ export default function InvoiceDetail(props: { id: string }) {
322
322
  alignItems={InfoAlignItems}
323
323
  />
324
324
  )}
325
+ {data.billing_reason === 'stake' && data?.metadata?.payment_details?.arcblock?.tx_hash && (
326
+ <InfoRow
327
+ label={t('common.stakeTxHash')}
328
+ value={
329
+ <TxLink
330
+ details={{
331
+ arcblock: { tx_hash: data.metadata?.payment_details?.arcblock?.tx_hash, payer: '' },
332
+ }}
333
+ method={data.paymentMethod}
334
+ />
335
+ }
336
+ direction={InfoDirection}
337
+ alignItems={InfoAlignItems}
338
+ />
339
+ )}
325
340
  {data.subscription && (
326
341
  <InfoRow
327
342
  label={t('admin.subscription.name')}
@@ -1,5 +1,5 @@
1
1
  import InvoiceList from '../../../../components/invoice/list';
2
2
 
3
3
  export default function InvoicesList() {
4
- return <InvoiceList features={{ customer: true, toolbar: true, filter: true }} />;
4
+ return <InvoiceList features={{ customer: true, toolbar: true, filter: true }} include_staking />;
5
5
  }
@@ -1,7 +1,15 @@
1
1
  /* eslint-disable react/no-unstable-nested-components */
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import Toast from '@arcblock/ux/lib/Toast';
4
- import { TxLink, api, formatError, formatSubscriptionProduct, formatTime, useMobile } from '@blocklet/payment-react';
4
+ import {
5
+ TxLink,
6
+ api,
7
+ formatError,
8
+ formatSubscriptionProduct,
9
+ formatTime,
10
+ useMobile,
11
+ hasDelegateTxHash,
12
+ } from '@blocklet/payment-react';
5
13
  import type { TProduct, TSubscriptionExpanded } from '@blocklet/payment-types';
6
14
  import { ArrowBackOutlined } from '@mui/icons-material';
7
15
  import { Alert, Box, Button, CircularProgress, Divider, Stack, Typography } from '@mui/material';
@@ -268,7 +276,7 @@ export default function SubscriptionDetail(props: { id: string }) {
268
276
  direction={InfoDirection}
269
277
  alignItems={InfoAlignItems}
270
278
  />
271
- {(data.payment_details?.arcblock?.tx_hash || data.payment_details?.ethereum?.tx_hash) && (
279
+ {data.payment_details && hasDelegateTxHash(data.payment_details, data.paymentMethod) && (
272
280
  <InfoRow
273
281
  label={t('common.delegateTxHash')}
274
282
  value={<TxLink details={data.payment_details} method={data.paymentMethod} />}
@@ -276,7 +284,7 @@ export default function SubscriptionDetail(props: { id: string }) {
276
284
  alignItems={InfoAlignItems}
277
285
  />
278
286
  )}
279
- {data.payment_details?.arcblock?.staking?.tx_hash && (
287
+ {data.paymentMethod?.type === 'arcblock' && data.payment_details?.arcblock?.staking?.tx_hash && (
280
288
  <InfoRow
281
289
  label={t('common.stakeTxHash')}
282
290
  value={
@@ -322,7 +330,7 @@ export default function SubscriptionDetail(props: { id: string }) {
322
330
  <Box className="section">
323
331
  <SectionHeader title={t('admin.invoices')} />
324
332
  <Box className="section-body">
325
- <InvoiceList features={{ customer: true, toolbar: false }} subscription_id={data.id} />
333
+ <InvoiceList features={{ customer: true, toolbar: false }} subscription_id={data.id} include_staking />
326
334
  </Box>
327
335
  </Box>
328
336
  <Divider />
@@ -367,6 +367,7 @@ export default function CustomerDetail(props: { id: string }) {
367
367
  features={{ customer: false, toolbar: false }}
368
368
  customer_id={data.customer.id}
369
369
  status={['open', 'paid', 'uncollectible', 'draft', 'void'].join(',')}
370
+ include_staking
370
371
  />
371
372
  </Box>
372
373
  </Box>
@@ -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
  />
@@ -318,7 +318,7 @@ export default function CustomerHome() {
318
318
  )}
319
319
  </Box>
320
320
  <Box className="section-body">
321
- <CustomerInvoiceList customer_id={data.id} type="table" />
321
+ <CustomerInvoiceList customer_id={data.id} type="table" include_staking />
322
322
  </Box>
323
323
  </Box>
324
324
  );
@@ -6,6 +6,7 @@ import {
6
6
  TxGas,
7
7
  TxLink,
8
8
  api,
9
+ formatAmount,
9
10
  formatError,
10
11
  formatTime,
11
12
  getInvoiceStatusColor,
@@ -19,6 +20,7 @@ import { useRequest, useSetState } from 'ahooks';
19
20
  import { useEffect } from 'react';
20
21
  import { Link, useParams, useSearchParams } from 'react-router-dom';
21
22
 
23
+ import { useSessionContext } from '../../../contexts/session';
22
24
  import Currency from '../../../components/currency';
23
25
  import CustomerLink from '../../../components/customer/link';
24
26
  import InfoRow from '../../../components/info-row';
@@ -46,6 +48,7 @@ export default function CustomerInvoiceDetail() {
46
48
 
47
49
  const { loading, error, data, runAsync } = useRequest(() => fetchData(params.id as string));
48
50
  const action = searchParams.get('action');
51
+ const { session } = useSessionContext();
49
52
 
50
53
  const onPay = () => {
51
54
  setState({ paying: true });
@@ -93,6 +96,10 @@ export default function CustomerInvoiceDetail() {
93
96
  // eslint-disable-next-line react-hooks/exhaustive-deps
94
97
  }, [error]);
95
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
+
96
103
  if (error) {
97
104
  return <Alert severity="error">{formatError(error)}</Alert>;
98
105
  }
@@ -101,9 +108,10 @@ export default function CustomerInvoiceDetail() {
101
108
  return <CircularProgress />;
102
109
  }
103
110
 
104
- const isSlash =
105
- data.paymentMethod?.type === 'arcblock' && data.paymentIntent?.payment_details?.arcblock?.type === 'slash';
111
+ const isStake = data.paymentMethod?.type === 'arcblock' && data.billing_reason === 'stake';
112
+ const isSlashStake = data.paymentMethod?.type === 'arcblock' && data.billing_reason.includes('slash_stake');
106
113
 
114
+ const paymentDetails = data.paymentIntent?.payment_details || data.metadata?.payment_details;
107
115
  return (
108
116
  <InvoiceDetailRoot direction="column" spacing={3} sx={{ my: 2 }}>
109
117
  <Stack direction="row" justifyContent="space-between">
@@ -263,12 +271,20 @@ export default function CustomerInvoiceDetail() {
263
271
  alignItems={InfoAlignItems}
264
272
  />
265
273
  )}
266
- {data.paymentIntent && data.paymentIntent.payment_details && (
274
+ {paymentDetails && (
267
275
  <InfoRow
268
- label={t(`common.${data.paymentIntent.payment_details?.arcblock?.type || 'transfer'}TxHash`)}
269
- value={
270
- <TxLink details={data.paymentIntent.payment_details} method={data.paymentMethod} mode="customer" />
276
+ label={
277
+ isStake ? t('common.stakeTxHash') : t(`common.${paymentDetails?.arcblock?.type || 'transfer'}TxHash`)
271
278
  }
279
+ value={<TxLink details={paymentDetails} method={data.paymentMethod} mode="customer" />}
280
+ direction={InfoDirection}
281
+ alignItems={InfoAlignItems}
282
+ />
283
+ )}
284
+ {(isStake || isSlashStake) && (
285
+ <InfoRow
286
+ label={isSlashStake ? t('common.slashStakeAmount') : t('common.stakeAmount')}
287
+ value={`${formatAmount(data.total, data.paymentCurrency.decimal)} ${data.paymentCurrency.symbol}`}
272
288
  direction={InfoDirection}
273
289
  alignItems={InfoAlignItems}
274
290
  />
@@ -289,15 +305,19 @@ export default function CustomerInvoiceDetail() {
289
305
  )}
290
306
  </Stack>
291
307
  </Box>
292
- {!isSlash && (
308
+ {!isSlashStake && (
293
309
  <>
294
- <Divider />
295
- <Box className="section">
296
- <Typography variant="h3" mb={1.5} className="section-header">
297
- {t('payment.customer.products')}
298
- </Typography>
299
- <InvoiceTable invoice={data} simple />
300
- </Box>
310
+ {!isStake && (
311
+ <>
312
+ <Divider />
313
+ <Box className="section">
314
+ <Typography variant="h3" mb={1.5} className="section-header">
315
+ {t('payment.customer.products')}
316
+ </Typography>
317
+ <InvoiceTable invoice={data} simple />
318
+ </Box>
319
+ </>
320
+ )}
301
321
  <Divider />
302
322
  <Box className="section">
303
323
  <Typography variant="h3" className="section-header">