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.
- package/api/src/libs/notification/template/customer-revenue-succeeded.ts +254 -0
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +12 -11
- package/api/src/libs/payment.ts +47 -2
- package/api/src/libs/payout.ts +24 -0
- package/api/src/libs/util.ts +83 -1
- package/api/src/locales/en.ts +16 -1
- package/api/src/locales/zh.ts +28 -12
- package/api/src/queues/notification.ts +23 -1
- package/api/src/routes/invoices.ts +42 -5
- package/api/src/routes/payment-intents.ts +14 -1
- package/api/src/routes/payment-links.ts +17 -0
- package/api/src/routes/payouts.ts +103 -8
- package/api/src/store/migrations/20250206-update-donation-products.ts +56 -0
- package/api/src/store/models/payout.ts +6 -2
- package/api/src/store/models/types.ts +2 -0
- package/blocklet.yml +1 -1
- package/package.json +4 -4
- package/public/methods/default.png +0 -0
- package/src/app.tsx +10 -0
- package/src/components/customer/link.tsx +11 -2
- package/src/components/customer/overdraft-protection.tsx +2 -2
- package/src/components/info-card.tsx +6 -5
- package/src/components/invoice/table.tsx +4 -0
- package/src/components/payouts/list.tsx +17 -2
- package/src/components/payouts/portal/list.tsx +192 -0
- package/src/components/subscription/items/actions.tsx +1 -2
- package/src/libs/util.ts +42 -1
- package/src/locales/en.tsx +10 -0
- package/src/locales/zh.tsx +10 -0
- package/src/pages/admin/billing/invoices/detail.tsx +21 -0
- package/src/pages/admin/payments/payouts/detail.tsx +65 -4
- package/src/pages/customer/index.tsx +12 -25
- package/src/pages/customer/invoice/detail.tsx +27 -3
- package/src/pages/customer/payout/detail.tsx +264 -0
- package/src/pages/customer/recharge.tsx +2 -2
- 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
|
+
|
|
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
|
+
}
|
package/src/locales/en.tsx
CHANGED
|
@@ -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
|
});
|
package/src/locales/zh.tsx
CHANGED
|
@@ -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 = (
|
|
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={
|
|
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 = (
|
|
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
|
-
{!
|
|
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
|
<>
|