payment-kit 1.18.0 → 1.18.1

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/customer-revenue-succeeded.ts +254 -0
  2. package/api/src/libs/notification/template/customer-reward-succeeded.ts +12 -11
  3. package/api/src/libs/payment.ts +47 -2
  4. package/api/src/libs/payout.ts +24 -0
  5. package/api/src/libs/util.ts +83 -1
  6. package/api/src/locales/en.ts +16 -1
  7. package/api/src/locales/zh.ts +28 -12
  8. package/api/src/queues/notification.ts +23 -1
  9. package/api/src/routes/invoices.ts +42 -5
  10. package/api/src/routes/payment-intents.ts +14 -1
  11. package/api/src/routes/payment-links.ts +17 -0
  12. package/api/src/routes/payouts.ts +103 -8
  13. package/api/src/store/migrations/20250206-update-donation-products.ts +56 -0
  14. package/api/src/store/models/payout.ts +6 -2
  15. package/api/src/store/models/types.ts +2 -0
  16. package/blocklet.yml +1 -1
  17. package/package.json +4 -4
  18. package/public/methods/default.png +0 -0
  19. package/src/app.tsx +10 -0
  20. package/src/components/customer/link.tsx +11 -2
  21. package/src/components/customer/overdraft-protection.tsx +2 -2
  22. package/src/components/info-card.tsx +6 -5
  23. package/src/components/invoice/table.tsx +4 -0
  24. package/src/components/payouts/list.tsx +17 -2
  25. package/src/components/payouts/portal/list.tsx +192 -0
  26. package/src/components/subscription/items/actions.tsx +1 -2
  27. package/src/libs/util.ts +42 -1
  28. package/src/locales/en.tsx +10 -0
  29. package/src/locales/zh.tsx +10 -0
  30. package/src/pages/admin/billing/invoices/detail.tsx +21 -0
  31. package/src/pages/admin/payments/payouts/detail.tsx +65 -4
  32. package/src/pages/customer/index.tsx +12 -25
  33. package/src/pages/customer/invoice/detail.tsx +27 -3
  34. package/src/pages/customer/payout/detail.tsx +264 -0
  35. package/src/pages/customer/recharge.tsx +2 -2
  36. package/vite.config.ts +1 -0
@@ -0,0 +1,192 @@
1
+ /* eslint-disable react/no-unstable-nested-components */
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import { api, formatBNStr, formatTime, Table, useDefaultPageSize } from '@blocklet/payment-react';
4
+ import type { TCustomer, TPaymentIntentExpanded, TPayoutExpanded } from '@blocklet/payment-types';
5
+ import { Box, Typography } from '@mui/material';
6
+ import { useLocalStorageState } from 'ahooks';
7
+ import { useEffect, useState } from 'react';
8
+ import { Link } from 'react-router-dom';
9
+
10
+ import { styled } from '@mui/system';
11
+ import { debounce } from '../../../libs/util';
12
+ import CustomerLink from '../../customer/link';
13
+
14
+ const fetchData = (
15
+ params: Record<string, any> = {}
16
+ ): Promise<{
17
+ list: TPayoutExpanded &
18
+ {
19
+ paymentIntent: TPaymentIntentExpanded & { customer: TCustomer };
20
+ }[];
21
+ count: number;
22
+ }> => {
23
+ const search = new URLSearchParams();
24
+ Object.keys(params).forEach((key) => {
25
+ let v = params[key];
26
+ if (key === 'q') {
27
+ v = Object.entries(v)
28
+ .map((x) => x.join(':'))
29
+ .join(' ');
30
+ }
31
+ search.set(key, String(v));
32
+ });
33
+ return api.get(`/api/payouts/mine?${search.toString()}`).then((res) => res.data);
34
+ };
35
+
36
+ type SearchProps = {
37
+ status?: string;
38
+ pageSize: number;
39
+ page: number;
40
+ currency_id?: string;
41
+ customer_id?: string;
42
+ q?: any;
43
+ o?: any;
44
+ };
45
+
46
+ type ListProps = {
47
+ status?: string;
48
+ customer_id?: string;
49
+ currency_id?: string;
50
+ };
51
+
52
+ const getListKey = (props: ListProps) => {
53
+ if (props.customer_id) {
54
+ return `customer-payouts-${props.customer_id}`;
55
+ }
56
+ return 'payouts-mine';
57
+ };
58
+
59
+ CustomerRevenueList.defaultProps = {
60
+ status: '',
61
+ currency_id: '',
62
+ customer_id: '',
63
+ };
64
+
65
+ export default function CustomerRevenueList({ currency_id, status, customer_id }: ListProps) {
66
+ const { t } = useLocaleContext();
67
+
68
+ const listKey = getListKey({ customer_id });
69
+ const defaultPageSize = useDefaultPageSize(10);
70
+ const [search, setSearch] = useLocalStorageState<SearchProps>(listKey, {
71
+ defaultValue: {
72
+ status: status as string,
73
+ customer_id,
74
+ currency_id,
75
+ pageSize: defaultPageSize,
76
+ page: 1,
77
+ },
78
+ });
79
+
80
+ const [data, setData] = useState({
81
+ list: [],
82
+ count: 0,
83
+ }) as any;
84
+
85
+ useEffect(() => {
86
+ debounce(() => {
87
+ fetchData(search).then((res: any) => {
88
+ setData(res);
89
+ });
90
+ }, 300)();
91
+ }, [search]);
92
+
93
+ const columns = [
94
+ {
95
+ label: t('common.amount'),
96
+ name: 'id',
97
+ align: 'right',
98
+ width: 80,
99
+ options: {
100
+ customBodyRenderLite: (_: string, index: number) => {
101
+ const item = data.list[index] as TPayoutExpanded;
102
+ return (
103
+ <Link to={`/customer/payout/${item.id}`}>
104
+ <Typography component="strong" fontWeight={600}>
105
+ {formatBNStr(item.amount, item?.paymentCurrency.decimal)}
106
+ &nbsp;
107
+ {item?.paymentCurrency.symbol}
108
+ </Typography>
109
+ </Link>
110
+ );
111
+ },
112
+ },
113
+ },
114
+ {
115
+ label: t('customer.payout.payer'),
116
+ name: 'customer_id',
117
+ options: {
118
+ customBodyRenderLite: (_: string, index: number) => {
119
+ const item = data.list[index] as TPayoutExpanded & {
120
+ paymentIntent: TPaymentIntentExpanded & { customer: TCustomer };
121
+ };
122
+ // @ts-ignore
123
+ return item?.paymentIntent?.customer ? (
124
+ <CustomerLink customer={item.paymentIntent.customer} linkTo={`/customer/payout/${item.id}`} />
125
+ ) : (
126
+ t('common.none')
127
+ );
128
+ },
129
+ } as any,
130
+ },
131
+ {
132
+ label: t('common.createdAt'),
133
+ name: 'created_at',
134
+ options: {
135
+ customBodyRenderLite: (_: string, index: number) => {
136
+ const item = data.list[index] as TPayoutExpanded;
137
+ return <Link to={`/customer/payout/${item.id}`}>{formatTime(item.created_at)}</Link>;
138
+ },
139
+ },
140
+ },
141
+ ];
142
+
143
+ const onTableChange = ({ page, rowsPerPage }: any) => {
144
+ if (search!.pageSize !== rowsPerPage) {
145
+ setSearch((x) => ({ ...x, pageSize: rowsPerPage, page: 1 }));
146
+ } else if (search!.page !== page + 1) {
147
+ // @ts-ignore
148
+ setSearch((x) => ({ ...x, page: page + 1 }));
149
+ }
150
+ };
151
+
152
+ return (
153
+ <Root>
154
+ <Table
155
+ hasRowLink
156
+ durable={`__${listKey}__`}
157
+ data={data.list || []}
158
+ columns={columns}
159
+ loading={!data.list}
160
+ onChange={onTableChange}
161
+ options={{
162
+ count: data.count,
163
+ page: search!.page - 1,
164
+ rowsPerPage: search!.pageSize,
165
+ }}
166
+ toolbar={false}
167
+ showMobile={false}
168
+ mobileTDFlexDirection="row"
169
+ emptyNodeText={t('customer.payout.empty')}
170
+ sx={{ mt: 2 }}
171
+ />
172
+ </Root>
173
+ );
174
+ }
175
+
176
+ const Root = styled(Box)`
177
+ @media (max-width: ${({ theme }) => theme.breakpoints.values.md}px) {
178
+ .MuiTable-root > .MuiTableBody-root > .MuiTableRow-root > td.MuiTableCell-root {
179
+ > div {
180
+ width: fit-content;
181
+ flex: inherit;
182
+ font-size: 14px;
183
+ .info-card {
184
+ align-items: flex-end;
185
+ }
186
+ }
187
+ }
188
+ .invoice-summary {
189
+ padding-right: 20px;
190
+ }
191
+ }
192
+ `;
@@ -12,7 +12,6 @@ type Props = {
12
12
  export default function LineItemActions(props: Props) {
13
13
  const { t } = useLocaleContext();
14
14
  const navigate = useNavigate();
15
-
16
15
  return (
17
16
  <ClickBoundary>
18
17
  <Actions
@@ -24,7 +23,7 @@ export default function LineItemActions(props: Props) {
24
23
  },
25
24
  {
26
25
  label: t('admin.product.view'),
27
- handler: () => navigate(`/admin/products/${props.data.price.product_id}`),
26
+ handler: () => navigate(`/admin/products/${props.data.product_id || props.data.price.product_id}`),
28
27
  color: 'primary',
29
28
  },
30
29
  ]}
package/src/libs/util.ts CHANGED
@@ -19,8 +19,9 @@ import { hexToNumber } from '@ocap/util';
19
19
  import { isEmpty, isObject } from 'lodash';
20
20
  import cloneDeep from 'lodash/cloneDeep';
21
21
  import isEqual from 'lodash/isEqual';
22
- import { joinURL } from 'ufo';
22
+ import { joinURL, withQuery } from 'ufo';
23
23
 
24
+ import type { LiteralUnion } from 'type-fest';
24
25
  import { t } from '../locales/index';
25
26
 
26
27
  export const formatProductPrice = (
@@ -323,3 +324,43 @@ export function getInvoiceUsageReportStartEnd(invoice: TInvoiceExpanded, showPre
323
324
  usageReportRange.end = invoice.period_end - offset;
324
325
  return usageReportRange;
325
326
  }
327
+
328
+ export function getCustomerProfileUrl({
329
+ locale = 'en',
330
+ userDid,
331
+ }: {
332
+ locale: LiteralUnion<'en' | 'zh', string>;
333
+ userDid: string;
334
+ }) {
335
+ return joinURL(
336
+ getPrefix(),
337
+ withQuery('.well-known/service/user', {
338
+ locale,
339
+ did: userDid,
340
+ })
341
+ );
342
+ }
343
+
344
+ export function getAppInfo(address: string): { name: string; avatar: string; type: string; url: string } | null {
345
+ const blockletJson = window.blocklet;
346
+ if (blockletJson) {
347
+ if (blockletJson?.appId === address) {
348
+ return {
349
+ name: blockletJson?.appName,
350
+ avatar: blockletJson?.appLogo,
351
+ type: 'dapp',
352
+ url: blockletJson?.appUrl,
353
+ };
354
+ }
355
+ const appInfo = blockletJson?.componentMountPoints?.find((x: any) => x.appId === address);
356
+ if (appInfo) {
357
+ return {
358
+ name: appInfo.name || '',
359
+ avatar: joinURL(getPrefix(), `.well-known/service/blocklet/logo-bundle/${appInfo.did}`),
360
+ type: 'dapp',
361
+ url: joinURL(getPrefix(), appInfo.mountPoint),
362
+ };
363
+ }
364
+ }
365
+ return null;
366
+ }
@@ -26,6 +26,7 @@ export default flat({
26
26
  submit: 'Submit',
27
27
  custom: 'Custom',
28
28
  estimatedDuration: '{duration} est.',
29
+ detail: 'Detail',
29
30
  },
30
31
  admin: {
31
32
  balances: 'Balances',
@@ -734,6 +735,15 @@ export default flat({
734
735
  unpaidInvoicesWarningTip: 'You currently have unpaid invoices, please settle your invoices promptly.',
735
736
  invoice: {
736
737
  relatedInvoice: 'Related Invoice',
738
+ donation: 'Donation',
739
+ },
740
+ payout: {
741
+ empty: 'No Payout',
742
+ payer: 'Payer',
743
+ receiver: 'Receiver',
744
+ payTxHash: 'Payment TxHash',
745
+ viewReference: 'View Reference',
746
+ title: 'Revenue',
737
747
  },
738
748
  },
739
749
  });
@@ -25,6 +25,7 @@ export default flat({
25
25
  submit: '提交',
26
26
  custom: '自定义',
27
27
  estimatedDuration: '预计可用 {duration}',
28
+ detail: '详情',
28
29
  },
29
30
  admin: {
30
31
  balances: '余额',
@@ -715,6 +716,15 @@ export default flat({
715
716
  unpaidInvoicesWarningTip: '您当前有未支付的账单,请及时付清。',
716
717
  invoice: {
717
718
  relatedInvoice: '关联账单',
719
+ donation: '打赏记录',
720
+ },
721
+ payout: {
722
+ empty: '没有收款记录',
723
+ payer: '付款方',
724
+ receiver: '收款方',
725
+ payTxHash: '交易详情',
726
+ viewReference: '查看打赏原文',
727
+ title: '收款记录',
718
728
  },
719
729
  },
720
730
  });
@@ -2,6 +2,7 @@
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import Toast from '@arcblock/ux/lib/Toast';
4
4
  import {
5
+ PaymentBeneficiaries,
5
6
  Status,
6
7
  TxGas,
7
8
  TxLink,
@@ -19,6 +20,7 @@ import { styled } from '@mui/system';
19
20
  import { useRequest, useSetState } from 'ahooks';
20
21
  import { Link } from 'react-router-dom';
21
22
 
23
+ import { isEmpty } from 'lodash';
22
24
  import Copyable from '../../../../components/copyable';
23
25
  import Currency from '../../../../components/currency';
24
26
  import CustomerLink from '../../../../components/customer/link';
@@ -99,6 +101,8 @@ export default function InvoiceDetail(props: { id: string }) {
99
101
  }
100
102
  return desc;
101
103
  };
104
+ // @ts-ignore
105
+ const isDonation = data?.checkoutSession?.submit_type === 'donate';
102
106
  return (
103
107
  <Root direction="column" spacing={2.5} sx={{ mb: 4 }}>
104
108
  <Box>
@@ -360,6 +364,23 @@ export default function InvoiceDetail(props: { id: string }) {
360
364
  </Box>
361
365
  </Box>
362
366
  <Divider />
367
+ {isDonation && !isEmpty(data.paymentIntent?.beneficiaries) && (
368
+ <>
369
+ <Box className="section">
370
+ <Typography variant="h3" className="section-header">
371
+ {t('customer.invoice.donation')}
372
+ </Typography>
373
+ </Box>
374
+ <Box sx={{ maxWidth: 800 }}>
375
+ <PaymentBeneficiaries
376
+ data={data?.paymentIntent?.beneficiaries as any}
377
+ currency={data.paymentCurrency}
378
+ totalAmount={data?.amount_paid}
379
+ />
380
+ </Box>
381
+ <Divider />
382
+ </>
383
+ )}
363
384
  <Box className="section">
364
385
  <SectionHeader title={t('admin.payments')} />
365
386
  <Box className="section-body">
@@ -10,16 +10,18 @@ import {
10
10
  formatBNStr,
11
11
  formatError,
12
12
  formatTime,
13
+ getCustomerAvatar,
13
14
  getPayoutStatusColor,
14
15
  useMobile,
15
16
  } from '@blocklet/payment-react';
16
- import type { TPayoutExpanded } from '@blocklet/payment-types';
17
+ import type { TCustomer, TPayoutExpanded } from '@blocklet/payment-types';
17
18
  import { ArrowBackOutlined, InfoOutlined } from '@mui/icons-material';
18
19
  import { Alert, Avatar, Box, Button, CircularProgress, Divider, Stack, Tooltip, Typography } from '@mui/material';
19
20
  import { styled } from '@mui/system';
20
21
  import { useRequest, useSetState } from 'ahooks';
21
22
  import { Link } from 'react-router-dom';
22
23
 
24
+ import DID from '@arcblock/ux/lib/DID';
23
25
  import Copyable from '../../../../components/copyable';
24
26
  import CustomerLink from '../../../../components/customer/link';
25
27
  import EventList from '../../../../components/event/list';
@@ -28,9 +30,16 @@ import InfoRow from '../../../../components/info-row';
28
30
  import MetadataEditor from '../../../../components/metadata/editor';
29
31
  import MetadataList from '../../../../components/metadata/list';
30
32
  import SectionHeader from '../../../../components/section/header';
31
- import { goBackOrFallback } from '../../../../libs/util';
33
+ import { getAppInfo, getCustomerProfileUrl, goBackOrFallback } from '../../../../libs/util';
34
+ import InfoCard from '../../../../components/info-card';
32
35
 
33
- const fetchData = (id: string): Promise<TPayoutExpanded> => {
36
+ const fetchData = (
37
+ id: string
38
+ ): Promise<
39
+ TPayoutExpanded & {
40
+ paymentIntent?: { customer: TCustomer };
41
+ }
42
+ > => {
34
43
  return api.get(`/api/payouts/${id}`).then((res) => res.data);
35
44
  };
36
45
 
@@ -88,6 +97,21 @@ export default function PayoutDetail(props: { id: string }) {
88
97
  setState((prev) => ({ editing: { ...prev.editing, metadata: true } }));
89
98
  };
90
99
 
100
+ const { paymentIntent } = data || {};
101
+
102
+ const renderCustomer = () => {
103
+ if (data.customer) {
104
+ return <CustomerLink customer={data.customer} />;
105
+ }
106
+ const appInfo = getAppInfo(data.destination);
107
+ if (appInfo) {
108
+ return (
109
+ <InfoCard name={appInfo.name} description={<DID did={data.destination} />} logo={appInfo.avatar} size={40} />
110
+ );
111
+ }
112
+ return data.destination;
113
+ };
114
+
91
115
  return (
92
116
  <Root direction="column" spacing={2.5} mb={4}>
93
117
  <Box>
@@ -170,6 +194,43 @@ export default function PayoutDetail(props: { id: string }) {
170
194
  value={<Status label={data.status} color={getPayoutStatusColor(data.status)} />}
171
195
  divider
172
196
  />
197
+ <InfoMetric
198
+ label={t('customer.payout.payer')}
199
+ value={
200
+ <InfoCard
201
+ logo={getCustomerAvatar(
202
+ paymentIntent?.customer?.did,
203
+ paymentIntent?.customer?.updated_at
204
+ ? new Date(paymentIntent?.customer?.updated_at).toISOString()
205
+ : '',
206
+ 48
207
+ )}
208
+ name={
209
+ <Typography
210
+ variant="subtitle2"
211
+ sx={{
212
+ cursor: 'pointer',
213
+ '&:hover': {
214
+ color: 'text.link',
215
+ },
216
+ }}
217
+ onClick={() => {
218
+ const url = getCustomerProfileUrl({
219
+ userDid: paymentIntent?.customer?.did,
220
+ locale: 'zh',
221
+ });
222
+ window.open(url, '_blank');
223
+ }}>
224
+ {paymentIntent?.customer?.name} ({paymentIntent?.customer?.email})
225
+ </Typography>
226
+ }
227
+ description={<DID did={paymentIntent?.customer?.did} />}
228
+ size={40}
229
+ variant="rounded"
230
+ />
231
+ }
232
+ divider
233
+ />
173
234
  {/* <InfoMetric label={t('common.createdAt')} value={formatTime(data.created_at)} divider /> */}
174
235
  {/* <InfoMetric label={t('common.updatedAt')} value={formatTime(data.updated_at)} divider /> */}
175
236
  </Stack>
@@ -262,7 +323,7 @@ export default function PayoutDetail(props: { id: string }) {
262
323
  />
263
324
  <InfoRow
264
325
  label={t('common.customer')}
265
- value={data.customer ? <CustomerLink customer={data.customer} /> : data.destination}
326
+ value={renderCustomer()}
266
327
  direction={InfoDirection}
267
328
  alignItems={InfoAlignItems}
268
329
  />
@@ -41,6 +41,7 @@ import ProgressBar, { useTransitionContext } from '../../components/progress-bar
41
41
  import CurrentSubscriptions from '../../components/subscription/portal/list';
42
42
  import { useSessionContext } from '../../contexts/session';
43
43
  import api from '../../libs/api';
44
+ import CustomerRevenueList from '../../components/payouts/portal/list';
44
45
 
45
46
  type Result = TCustomerExpanded & { summary: { [key: string]: GroupedBN }; error?: string };
46
47
 
@@ -449,6 +450,15 @@ export default function CustomerHome() {
449
450
  </Box>
450
451
  );
451
452
 
453
+ const RevenueCard = (
454
+ <Box className="base-card section section-revenue">
455
+ <Box className="section-header">
456
+ <Typography variant="h3">{t('customer.payout.title')}</Typography>
457
+ </Box>
458
+ <CustomerRevenueList />
459
+ </Box>
460
+ );
461
+
452
462
  return (
453
463
  <Content>
454
464
  <ProgressBar pending={isPending} />
@@ -457,6 +467,7 @@ export default function CustomerHome() {
457
467
  {SummaryCard}
458
468
  {SubscriptionCard}
459
469
  {InvoiceCard}
470
+ {RevenueCard}
460
471
  {DetailCard}
461
472
  </Root>
462
473
  ) : (
@@ -465,6 +476,7 @@ export default function CustomerHome() {
465
476
  <Stack direction="column" spacing={2.5}>
466
477
  {SubscriptionCard}
467
478
  {InvoiceCard}
479
+ {RevenueCard}
468
480
  </Stack>
469
481
  </Grid>
470
482
  <Grid item xs={12} md={4}>
@@ -503,33 +515,8 @@ const Content = styled(Stack)`
503
515
  `;
504
516
 
505
517
  const Root = styled(Stack)`
506
- display: grid;
507
- grid-template-columns: 5fr 2fr;
508
- grid-auto-rows: minmax(min-content, max-content);
509
- grid-gap: 20px;
510
- grid-template-areas:
511
- 'subscription summary'
512
- 'invoice detail';
513
- .section-summary {
514
- grid-area: summary;
515
- }
516
- .section-detail {
517
- grid-area: detail;
518
- }
519
- .section-invoice {
520
- grid-area: invoice;
521
- }
522
- .section-subscription {
523
- grid-area: subscription;
524
- }
525
518
  @media (max-width: ${({ theme }) => theme.breakpoints.values.xl}px) {
526
519
  padding: 0px;
527
- grid-template-columns: 1fr;
528
- grid-template-areas:
529
- 'summary'
530
- 'subscription'
531
- 'invoice'
532
- 'detail';
533
520
  gap: 0;
534
521
  .base-card {
535
522
  border: none;
@@ -13,8 +13,9 @@ import {
13
13
  getInvoiceStatusColor,
14
14
  getPrefix,
15
15
  usePaymentContext,
16
+ PaymentBeneficiaries,
16
17
  } from '@blocklet/payment-react';
17
- import type { TInvoiceExpanded } from '@blocklet/payment-types';
18
+ import type { TCheckoutSession, TInvoiceExpanded, TPaymentLink } from '@blocklet/payment-types';
18
19
  import { ArrowBackOutlined } from '@mui/icons-material';
19
20
  import { Alert, Box, Button, CircularProgress, Divider, Stack, Typography } from '@mui/material';
20
21
  import { styled } from '@mui/system';
@@ -23,6 +24,7 @@ import { useEffect } from 'react';
23
24
  import { Link, useParams, useSearchParams } from 'react-router-dom';
24
25
 
25
26
  import { joinURL } from 'ufo';
27
+ import { isEmpty } from 'lodash';
26
28
  import { useSessionContext } from '../../../contexts/session';
27
29
  import Currency from '../../../components/currency';
28
30
  import CustomerLink from '../../../components/customer/link';
@@ -33,7 +35,11 @@ import { goBackOrFallback } from '../../../libs/util';
33
35
  import CustomerRefundList from '../refund/list';
34
36
  import InfoMetric from '../../../components/info-metric';
35
37
 
36
- const fetchData = (id: string): Promise<TInvoiceExpanded & { relatedInvoice?: TInvoiceExpanded }> => {
38
+ const fetchData = (
39
+ id: string
40
+ ): Promise<
41
+ TInvoiceExpanded & { relatedInvoice?: TInvoiceExpanded; checkoutSession: TCheckoutSession; paymentLink: TPaymentLink }
42
+ > => {
37
43
  return api.get(`/api/invoices/${id}`).then((res) => res.data);
38
44
  };
39
45
 
@@ -52,6 +58,7 @@ export default function CustomerInvoiceDetail() {
52
58
  const { loading, error, data, runAsync } = useRequest(() => fetchData(params.id as string));
53
59
  const action = searchParams.get('action');
54
60
  const { session } = useSessionContext();
61
+ const isDonation = data?.checkoutSession?.submit_type === 'donate';
55
62
 
56
63
  const onPay = () => {
57
64
  setState({ paying: true });
@@ -329,7 +336,24 @@ export default function CustomerInvoiceDetail() {
329
336
  )}
330
337
  </Stack>
331
338
  </Box>
332
- {!isSlashStake && (
339
+ {isDonation && !isEmpty(data.paymentIntent?.beneficiaries) && (
340
+ <>
341
+ <Divider />
342
+ <Box className="section">
343
+ <Typography variant="h3" className="section-header">
344
+ {t('customer.invoice.donation')}
345
+ </Typography>
346
+ </Box>
347
+ <Box sx={{ maxWidth: 800 }}>
348
+ <PaymentBeneficiaries
349
+ data={data?.paymentIntent?.beneficiaries as any}
350
+ currency={data.paymentCurrency}
351
+ totalAmount={data?.amount_paid}
352
+ />
353
+ </Box>
354
+ </>
355
+ )}
356
+ {!isSlashStake && !isDonation && (
333
357
  <>
334
358
  {!isStake && (
335
359
  <>