payment-kit 1.13.209 → 1.13.211
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/api.ts +2 -2
- package/api/src/libs/session.ts +90 -4
- package/api/src/queues/payment.ts +61 -1
- package/api/src/routes/checkout-sessions.ts +61 -10
- package/api/src/routes/connect/collect.ts +44 -37
- package/api/src/routes/connect/pay.ts +40 -29
- package/api/src/routes/connect/setup.ts +39 -33
- package/api/src/routes/connect/shared.ts +3 -1
- package/api/src/routes/donations.ts +157 -0
- package/api/src/routes/index.ts +4 -0
- package/api/src/routes/payment-intents.ts +2 -2
- package/api/src/routes/payment-links.ts +8 -3
- package/api/src/routes/payouts.ts +151 -0
- package/api/src/routes/products.ts +24 -6
- package/api/src/routes/subscriptions.ts +2 -0
- package/api/src/routes/usage-records.ts +6 -3
- package/api/src/store/migrations/20240408-payout.ts +36 -0
- package/api/src/store/models/checkout-session.ts +5 -0
- package/api/src/store/models/customer.ts +6 -1
- package/api/src/store/models/index.ts +12 -0
- package/api/src/store/models/payment-intent.ts +38 -26
- package/api/src/store/models/payment-link.ts +8 -1
- package/api/src/store/models/payout.ts +243 -0
- package/api/src/store/models/types.ts +39 -0
- package/api/tests/libs/session.spec.ts +101 -0
- package/blocklet.yml +1 -1
- package/package.json +22 -21
- package/src/components/info-card.tsx +5 -5
- package/src/components/invoice/list.tsx +2 -0
- package/src/components/invoice/table.tsx +1 -1
- package/src/components/payment-intent/list.tsx +2 -0
- package/src/components/payouts/actions.tsx +43 -0
- package/src/components/payouts/list.tsx +255 -0
- package/src/components/refund/list.tsx +2 -0
- package/src/components/subscription/list.tsx +2 -0
- package/src/libs/util.ts +4 -1
- package/src/locales/en.tsx +7 -0
- package/src/locales/zh.tsx +6 -0
- package/src/pages/admin/customers/customers/index.tsx +2 -2
- package/src/pages/admin/payments/index.tsx +7 -0
- package/src/pages/admin/payments/intents/detail.tsx +7 -0
- package/src/pages/admin/payments/payouts/detail.tsx +204 -0
- package/src/pages/admin/payments/payouts/index.tsx +5 -0
- package/src/pages/admin/products/links/index.tsx +2 -2
- package/src/pages/admin/products/prices/detail.tsx +2 -1
- package/src/pages/admin/products/pricing-tables/index.tsx +2 -2
- package/src/pages/admin/products/products/index.tsx +2 -2
|
@@ -93,7 +93,7 @@ export default function InvoiceTable({ invoice, simple }: Props) {
|
|
|
93
93
|
</TableCell>
|
|
94
94
|
<TableCell align="right">
|
|
95
95
|
{!line.proration
|
|
96
|
-
? formatAmount(getPriceUintAmountByCurrency(line.price, invoice.paymentCurrency), invoice.paymentCurrency.decimal) // prettier-ignore
|
|
96
|
+
? formatAmount(getPriceUintAmountByCurrency(line.price, invoice.paymentCurrency) || line.amount, invoice.paymentCurrency.decimal) // prettier-ignore
|
|
97
97
|
: ''}
|
|
98
98
|
</TableCell>
|
|
99
99
|
<TableCell align="right">{formatAmount(line.amount, invoice.paymentCurrency.decimal)}</TableCell>
|
|
@@ -203,6 +203,8 @@ export default function PaymentList({ customer_id, invoice_id, features }: ListP
|
|
|
203
203
|
return (
|
|
204
204
|
<Table
|
|
205
205
|
data={data.list || []}
|
|
206
|
+
durable={`__${listKey}__`}
|
|
207
|
+
durableKeys={['searchText']}
|
|
206
208
|
columns={columns}
|
|
207
209
|
loading={!data.list}
|
|
208
210
|
onChange={onTableChange}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
+
import type { TPayoutExpanded } from '@blocklet/payment-types';
|
|
3
|
+
import { useNavigate } from 'react-router-dom';
|
|
4
|
+
import type { LiteralUnion } from 'type-fest';
|
|
5
|
+
|
|
6
|
+
import Actions from '../actions';
|
|
7
|
+
import ClickBoundary from '../click-boundary';
|
|
8
|
+
|
|
9
|
+
type Props = {
|
|
10
|
+
data: TPayoutExpanded;
|
|
11
|
+
variant?: LiteralUnion<'compact' | 'normal', string>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
PayoutActions.defaultProps = {
|
|
15
|
+
variant: 'compact',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default function PayoutActions({ data, variant }: Props) {
|
|
19
|
+
const { t } = useLocaleContext();
|
|
20
|
+
const navigate = useNavigate();
|
|
21
|
+
const actions = [
|
|
22
|
+
{
|
|
23
|
+
label: t('admin.customer.view'),
|
|
24
|
+
handler: () => navigate(`/admin/customers/${data.customer_id}`),
|
|
25
|
+
color: 'primary',
|
|
26
|
+
disabled: false,
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
if (variant === 'compact') {
|
|
30
|
+
actions.push({
|
|
31
|
+
label: t('admin.paymentIntent.view'),
|
|
32
|
+
handler: () => navigate(`/admin/payments/${data.id}`),
|
|
33
|
+
color: 'primary',
|
|
34
|
+
disabled: false,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<ClickBoundary>
|
|
40
|
+
<Actions variant={variant} actions={actions} />
|
|
41
|
+
</ClickBoundary>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/* eslint-disable react/no-unstable-nested-components */
|
|
2
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
|
+
import { Status, api, formatBNStr, formatTime, getPayoutStatusColor } from '@blocklet/payment-react';
|
|
4
|
+
import type { TPayoutExpanded } from '@blocklet/payment-types';
|
|
5
|
+
import { CircularProgress, Typography } from '@mui/material';
|
|
6
|
+
import { useLocalStorageState } from 'ahooks';
|
|
7
|
+
import { useEffect, useState } from 'react';
|
|
8
|
+
import { useNavigate } from 'react-router-dom';
|
|
9
|
+
|
|
10
|
+
import { debounce } from '../../libs/util';
|
|
11
|
+
import CustomerLink from '../customer/link';
|
|
12
|
+
import FilterToolbar from '../filter-toolbar';
|
|
13
|
+
import { useTransitionContext } from '../progress-bar';
|
|
14
|
+
import Table from '../table';
|
|
15
|
+
import PayoutActions from './actions';
|
|
16
|
+
|
|
17
|
+
const fetchData = (params: Record<string, any> = {}): Promise<{ list: TPayoutExpanded[]; count: number }> => {
|
|
18
|
+
const search = new URLSearchParams();
|
|
19
|
+
Object.keys(params).forEach((key) => {
|
|
20
|
+
let v = params[key];
|
|
21
|
+
if (key === 'q') {
|
|
22
|
+
v = Object.entries(v)
|
|
23
|
+
.map((x) => x.join(':'))
|
|
24
|
+
.join(' ');
|
|
25
|
+
}
|
|
26
|
+
search.set(key, String(v));
|
|
27
|
+
});
|
|
28
|
+
return api.get(`/api/payouts?${search.toString()}`).then((res) => res.data);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type SearchProps = {
|
|
32
|
+
status?: string;
|
|
33
|
+
pageSize: number;
|
|
34
|
+
page: number;
|
|
35
|
+
customer_id?: string;
|
|
36
|
+
payment_intent_id?: string;
|
|
37
|
+
q?: any;
|
|
38
|
+
o?: any;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type ListProps = {
|
|
42
|
+
features?: {
|
|
43
|
+
customer?: boolean;
|
|
44
|
+
toolbar?: boolean;
|
|
45
|
+
filter?: boolean;
|
|
46
|
+
footer?: boolean;
|
|
47
|
+
};
|
|
48
|
+
customer_id?: string;
|
|
49
|
+
payment_intent_id?: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const getListKey = (props: ListProps) => {
|
|
53
|
+
if (props.customer_id) {
|
|
54
|
+
return `customer-payouts-${props.customer_id}`;
|
|
55
|
+
}
|
|
56
|
+
if (props.payment_intent_id) {
|
|
57
|
+
return `intent-payouts-${props.payment_intent_id}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return 'payouts';
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
PaymentList.defaultProps = {
|
|
64
|
+
features: {
|
|
65
|
+
customer: true,
|
|
66
|
+
filter: true,
|
|
67
|
+
},
|
|
68
|
+
customer_id: '',
|
|
69
|
+
payment_intent_id: '',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export default function PaymentList({ customer_id, payment_intent_id, features }: ListProps) {
|
|
73
|
+
const { t } = useLocaleContext();
|
|
74
|
+
const navigate = useNavigate();
|
|
75
|
+
|
|
76
|
+
const listKey = getListKey({ customer_id, payment_intent_id });
|
|
77
|
+
const [search, setSearch] = useLocalStorageState<SearchProps>(listKey, {
|
|
78
|
+
defaultValue: {
|
|
79
|
+
status: '',
|
|
80
|
+
customer_id,
|
|
81
|
+
payment_intent_id,
|
|
82
|
+
pageSize: 20,
|
|
83
|
+
page: 1,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const { startTransition } = useTransitionContext();
|
|
88
|
+
const [data, setData] = useState({}) as any;
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
debounce(() => {
|
|
92
|
+
fetchData(search).then((res: any) => {
|
|
93
|
+
setData(res);
|
|
94
|
+
});
|
|
95
|
+
}, 300)();
|
|
96
|
+
}, [search]);
|
|
97
|
+
|
|
98
|
+
if (!data.list) {
|
|
99
|
+
return <CircularProgress />;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const columns = [
|
|
103
|
+
{
|
|
104
|
+
label: t('common.amount'),
|
|
105
|
+
name: 'id',
|
|
106
|
+
align: 'right',
|
|
107
|
+
width: 60,
|
|
108
|
+
options: {
|
|
109
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
110
|
+
const item = data.list[index] as TPayoutExpanded;
|
|
111
|
+
return (
|
|
112
|
+
<Typography component="strong" fontWeight={600}>
|
|
113
|
+
{formatBNStr(item.amount, item?.paymentCurrency.decimal)}
|
|
114
|
+
|
|
115
|
+
{item?.paymentCurrency.symbol}
|
|
116
|
+
</Typography>
|
|
117
|
+
);
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
label: t('common.status'),
|
|
123
|
+
name: 'status',
|
|
124
|
+
width: 60,
|
|
125
|
+
options: {
|
|
126
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
127
|
+
const item = data.list[index] as TPayoutExpanded;
|
|
128
|
+
return <Status label={item.status} color={getPayoutStatusColor(item.status)} />;
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
label: t('common.description'),
|
|
134
|
+
name: 'description',
|
|
135
|
+
options: {
|
|
136
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
137
|
+
const item = data.list[index] as TPayoutExpanded;
|
|
138
|
+
return item.description || item.id;
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
label: t('common.createdAt'),
|
|
144
|
+
name: 'created_at',
|
|
145
|
+
options: {
|
|
146
|
+
sort: true,
|
|
147
|
+
customBodyRender: (e: string) => {
|
|
148
|
+
return formatTime(e);
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
label: t('common.updatedAt'),
|
|
154
|
+
name: 'updated_at',
|
|
155
|
+
options: {
|
|
156
|
+
sort: true,
|
|
157
|
+
customBodyRender: (e: string) => {
|
|
158
|
+
return formatTime(e);
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
label: t('common.actions'),
|
|
164
|
+
name: '',
|
|
165
|
+
options: {
|
|
166
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
167
|
+
const item = data.list[index] as TPayoutExpanded;
|
|
168
|
+
return <PayoutActions data={item} />;
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
if (features?.customer) {
|
|
175
|
+
columns.splice(3, 0, {
|
|
176
|
+
label: t('common.customer'),
|
|
177
|
+
name: 'customer_id',
|
|
178
|
+
options: {
|
|
179
|
+
filter: true,
|
|
180
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
181
|
+
const item = data.list[index] as TPayoutExpanded;
|
|
182
|
+
return item.customer ? <CustomerLink customer={item.customer} /> : item.destination;
|
|
183
|
+
},
|
|
184
|
+
} as any,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const onTableChange = ({ page, rowsPerPage }: any) => {
|
|
189
|
+
if (search!.pageSize !== rowsPerPage) {
|
|
190
|
+
setSearch((x) => ({ ...x, pageSize: rowsPerPage, page: 1 }));
|
|
191
|
+
} else if (search!.page !== page + 1) {
|
|
192
|
+
// @ts-ignore
|
|
193
|
+
setSearch((x) => ({ ...x, page: page + 1 }));
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<Table
|
|
199
|
+
data={data.list || []}
|
|
200
|
+
columns={columns}
|
|
201
|
+
loading={!data.list}
|
|
202
|
+
onChange={onTableChange}
|
|
203
|
+
options={{
|
|
204
|
+
count: data.count,
|
|
205
|
+
page: search!.page - 1,
|
|
206
|
+
rowsPerPage: search!.pageSize,
|
|
207
|
+
onSearchChange: (text: string) => {
|
|
208
|
+
if (text) {
|
|
209
|
+
setSearch({
|
|
210
|
+
q: {
|
|
211
|
+
'like-description': text,
|
|
212
|
+
'like-metadata': text,
|
|
213
|
+
},
|
|
214
|
+
pageSize: 100,
|
|
215
|
+
page: 1,
|
|
216
|
+
});
|
|
217
|
+
} else {
|
|
218
|
+
setSearch({
|
|
219
|
+
status: '',
|
|
220
|
+
customer_id,
|
|
221
|
+
payment_intent_id,
|
|
222
|
+
pageSize: 100,
|
|
223
|
+
page: 1,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
onColumnSortChange(_: any, order: any) {
|
|
228
|
+
setSearch({
|
|
229
|
+
...search!,
|
|
230
|
+
q: search!.q || {},
|
|
231
|
+
o: order,
|
|
232
|
+
});
|
|
233
|
+
},
|
|
234
|
+
onRowClick: (_: any, { dataIndex }: any) => {
|
|
235
|
+
const item = data.list[dataIndex] as TPayoutExpanded;
|
|
236
|
+
startTransition(() => {
|
|
237
|
+
navigate(`/admin/payments/${item.id}`);
|
|
238
|
+
});
|
|
239
|
+
},
|
|
240
|
+
}}
|
|
241
|
+
toolbar={features?.toolbar}
|
|
242
|
+
footer={features?.footer}
|
|
243
|
+
title={
|
|
244
|
+
features?.filter && (
|
|
245
|
+
<FilterToolbar
|
|
246
|
+
setSearch={setSearch}
|
|
247
|
+
search={search}
|
|
248
|
+
status={['paid', 'pending', 'failed', 'canceled']}
|
|
249
|
+
currency
|
|
250
|
+
/>
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
/>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
@@ -193,6 +193,8 @@ export default function RefundList({ customer_id, invoice_id, subscription_id, f
|
|
|
193
193
|
return (
|
|
194
194
|
<Table
|
|
195
195
|
data={data.list}
|
|
196
|
+
durable={`__${listKey}__`}
|
|
197
|
+
durableKeys={['searchText']}
|
|
196
198
|
columns={columns}
|
|
197
199
|
loading={!data.list}
|
|
198
200
|
onChange={onTableChange}
|
|
@@ -188,6 +188,8 @@ export default function SubscriptionList({ customer_id, features, status }: List
|
|
|
188
188
|
columns={columns}
|
|
189
189
|
loading={!data.list}
|
|
190
190
|
onChange={onTableChange}
|
|
191
|
+
durable={`__${listKey}__`}
|
|
192
|
+
durableKeys={['searchText']}
|
|
191
193
|
options={{
|
|
192
194
|
count: data.count,
|
|
193
195
|
page: search!.page - 1,
|
package/src/libs/util.ts
CHANGED
|
@@ -187,5 +187,8 @@ export const debounce = (fun: Function, wait: number) => {
|
|
|
187
187
|
};
|
|
188
188
|
|
|
189
189
|
export function canChangePaymentMethod(subscription: TSubscriptionExpanded) {
|
|
190
|
-
return
|
|
190
|
+
return (
|
|
191
|
+
['active', 'trialing'].includes(subscription.status) &&
|
|
192
|
+
subscription.items.every((x) => getPriceCurrencyOptions(x.price).length > 1)
|
|
193
|
+
);
|
|
191
194
|
}
|
package/src/locales/en.tsx
CHANGED
|
@@ -29,6 +29,7 @@ export default flat({
|
|
|
29
29
|
webhooks: 'Webhooks',
|
|
30
30
|
events: 'Events',
|
|
31
31
|
refunds: 'Refunds',
|
|
32
|
+
payouts: 'Payouts',
|
|
32
33
|
logs: 'Logs',
|
|
33
34
|
passports: 'Passports',
|
|
34
35
|
details: 'Details',
|
|
@@ -239,6 +240,12 @@ export default flat({
|
|
|
239
240
|
refund: 'Refund payment',
|
|
240
241
|
received: 'Received',
|
|
241
242
|
},
|
|
243
|
+
payout: {
|
|
244
|
+
list: 'Payouts',
|
|
245
|
+
name: 'Payout',
|
|
246
|
+
view: 'View payout',
|
|
247
|
+
empty: 'No payout',
|
|
248
|
+
},
|
|
242
249
|
paymentMethod: {
|
|
243
250
|
_name: 'Payment Method',
|
|
244
251
|
type: 'Type',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -110,8 +110,8 @@ export default function CustomersList() {
|
|
|
110
110
|
|
|
111
111
|
return (
|
|
112
112
|
<Table
|
|
113
|
-
durable={listKey}
|
|
114
|
-
durableKeys={['page', 'rowsPerPage']}
|
|
113
|
+
durable={`__${listKey}__`}
|
|
114
|
+
durableKeys={['page', 'rowsPerPage', 'searchText']}
|
|
115
115
|
data={data.list}
|
|
116
116
|
columns={columns}
|
|
117
117
|
options={{
|
|
@@ -8,10 +8,12 @@ import { useTransitionContext } from '../../../components/progress-bar';
|
|
|
8
8
|
|
|
9
9
|
const PaymentIntentDetail = React.lazy(() => import('./intents/detail'));
|
|
10
10
|
const RefundDetail = React.lazy(() => import('./refunds/detail'));
|
|
11
|
+
const PayoutDetail = React.lazy(() => import('./payouts/detail'));
|
|
11
12
|
|
|
12
13
|
const pages = {
|
|
13
14
|
intents: React.lazy(() => import('./intents')),
|
|
14
15
|
refunds: React.lazy(() => import('./refunds')),
|
|
16
|
+
payouts: React.lazy(() => import('./payouts')),
|
|
15
17
|
};
|
|
16
18
|
|
|
17
19
|
export default function PaymentIndex() {
|
|
@@ -24,6 +26,10 @@ export default function PaymentIndex() {
|
|
|
24
26
|
return <PaymentIntentDetail id={page} />;
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
if (page.startsWith('po_')) {
|
|
30
|
+
return <PayoutDetail id={page} />;
|
|
31
|
+
}
|
|
32
|
+
|
|
27
33
|
if (page.startsWith('re_')) {
|
|
28
34
|
return <RefundDetail id={page} />;
|
|
29
35
|
}
|
|
@@ -39,6 +45,7 @@ export default function PaymentIndex() {
|
|
|
39
45
|
const tabs = [
|
|
40
46
|
{ label: t('admin.paymentIntent.list'), value: 'intents' },
|
|
41
47
|
{ label: t('admin.refunds'), value: 'refunds' },
|
|
48
|
+
{ label: t('admin.payouts'), value: 'payouts' },
|
|
42
49
|
];
|
|
43
50
|
|
|
44
51
|
return (
|
|
@@ -27,6 +27,7 @@ import InfoRow from '../../../../components/info-row';
|
|
|
27
27
|
import MetadataEditor from '../../../../components/metadata/editor';
|
|
28
28
|
import MetadataList from '../../../../components/metadata/list';
|
|
29
29
|
import PaymentIntentActions from '../../../../components/payment-intent/actions';
|
|
30
|
+
import PayoutList from '../../../../components/payouts/list';
|
|
30
31
|
import SectionHeader from '../../../../components/section/header';
|
|
31
32
|
|
|
32
33
|
const fetchData = (id: string): Promise<TPaymentIntentExpanded> => {
|
|
@@ -199,6 +200,12 @@ export default function PaymentIntentDetail(props: { id: string }) {
|
|
|
199
200
|
)}
|
|
200
201
|
</Stack>
|
|
201
202
|
</Box>
|
|
203
|
+
<Box className="section">
|
|
204
|
+
<SectionHeader title={t('admin.payouts')} />
|
|
205
|
+
<Box className="section-body">
|
|
206
|
+
<PayoutList features={{ toolbar: false }} payment_intent_id={data.id} />
|
|
207
|
+
</Box>
|
|
208
|
+
</Box>
|
|
202
209
|
<Box className="section">
|
|
203
210
|
<SectionHeader title={t('admin.events')} />
|
|
204
211
|
<Box className="section-body">
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/* eslint-disable react/no-unstable-nested-components */
|
|
2
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
|
+
import Toast from '@arcblock/ux/lib/Toast';
|
|
4
|
+
import {
|
|
5
|
+
Amount,
|
|
6
|
+
Status,
|
|
7
|
+
TxLink,
|
|
8
|
+
api,
|
|
9
|
+
formatBNStr,
|
|
10
|
+
formatError,
|
|
11
|
+
formatTime,
|
|
12
|
+
getPayoutStatusColor,
|
|
13
|
+
} from '@blocklet/payment-react';
|
|
14
|
+
import type { TPayoutExpanded } from '@blocklet/payment-types';
|
|
15
|
+
import { ArrowBackOutlined, Edit, InfoOutlined } from '@mui/icons-material';
|
|
16
|
+
import { Alert, Box, Button, CircularProgress, Stack, Tooltip, Typography } from '@mui/material';
|
|
17
|
+
import { styled } from '@mui/system';
|
|
18
|
+
import { useRequest, useSetState } from 'ahooks';
|
|
19
|
+
import { Link } from 'react-router-dom';
|
|
20
|
+
|
|
21
|
+
import Copyable from '../../../../components/copyable';
|
|
22
|
+
import Currency from '../../../../components/currency';
|
|
23
|
+
import CustomerLink from '../../../../components/customer/link';
|
|
24
|
+
import EventList from '../../../../components/event/list';
|
|
25
|
+
import InfoMetric from '../../../../components/info-metric';
|
|
26
|
+
import InfoRow from '../../../../components/info-row';
|
|
27
|
+
import MetadataEditor from '../../../../components/metadata/editor';
|
|
28
|
+
import MetadataList from '../../../../components/metadata/list';
|
|
29
|
+
import SectionHeader from '../../../../components/section/header';
|
|
30
|
+
|
|
31
|
+
const fetchData = (id: string): Promise<TPayoutExpanded> => {
|
|
32
|
+
return api.get(`/api/payouts/${id}`).then((res) => res.data);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default function PayoutDetail(props: { id: string }) {
|
|
36
|
+
const { t } = useLocaleContext();
|
|
37
|
+
const [state, setState] = useSetState({
|
|
38
|
+
adding: {
|
|
39
|
+
price: false,
|
|
40
|
+
},
|
|
41
|
+
editing: {
|
|
42
|
+
metadata: false,
|
|
43
|
+
product: false,
|
|
44
|
+
},
|
|
45
|
+
loading: {
|
|
46
|
+
metadata: false,
|
|
47
|
+
price: false,
|
|
48
|
+
product: false,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const { loading, error, data, runAsync } = useRequest(() => fetchData(props.id));
|
|
53
|
+
|
|
54
|
+
if (error) {
|
|
55
|
+
return <Alert severity="error">{error.message}</Alert>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (loading || !data) {
|
|
59
|
+
return <CircularProgress />;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const createUpdater = (key: string) => async (updates: TPayoutExpanded) => {
|
|
63
|
+
try {
|
|
64
|
+
setState((prev) => ({ loading: { ...prev.loading, [key]: true } }));
|
|
65
|
+
await api.put(`/api/payouts/${props.id}`, updates).then((res) => res.data);
|
|
66
|
+
Toast.success(t('common.saved'));
|
|
67
|
+
runAsync();
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error(err);
|
|
70
|
+
Toast.error(formatError(err));
|
|
71
|
+
} finally {
|
|
72
|
+
setState((prev) => ({ loading: { ...prev.loading, [key]: false } }));
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const onUpdateMetadata = createUpdater('metadata');
|
|
77
|
+
|
|
78
|
+
const currency = data.paymentCurrency;
|
|
79
|
+
const total = [formatBNStr(data?.amount, currency.decimal), currency.symbol].join(' ');
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Root direction="column" spacing={4} mb={4}>
|
|
83
|
+
<Box>
|
|
84
|
+
<Stack className="page-header" direction="row" justifyContent="space-between" alignItems="center">
|
|
85
|
+
<Link to="/admin/payments/payouts">
|
|
86
|
+
<Stack direction="row" alignItems="center" sx={{ fontWeight: 'normal' }}>
|
|
87
|
+
<ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
|
|
88
|
+
<Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
|
|
89
|
+
{t('admin.payouts')}
|
|
90
|
+
</Typography>
|
|
91
|
+
</Stack>
|
|
92
|
+
</Link>
|
|
93
|
+
<Copyable text={props.id} style={{ marginLeft: 4 }} />
|
|
94
|
+
</Stack>
|
|
95
|
+
<Box mt={2}>
|
|
96
|
+
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
|
97
|
+
<Stack direction="row" alignItems="center">
|
|
98
|
+
<Amount amount={total} sx={{ my: 0, fontSize: '2rem', lineHeight: '1rem' }} />
|
|
99
|
+
<Status label={data.status} color={getPayoutStatusColor(data.status)} sx={{ ml: 2 }} />
|
|
100
|
+
</Stack>
|
|
101
|
+
</Stack>
|
|
102
|
+
<Stack
|
|
103
|
+
className="section-body"
|
|
104
|
+
direction="row"
|
|
105
|
+
spacing={3}
|
|
106
|
+
justifyContent="flex-start"
|
|
107
|
+
flexWrap="wrap"
|
|
108
|
+
sx={{ pt: 2, mt: 2, borderTop: '1px solid #eee' }}>
|
|
109
|
+
<InfoMetric label={t('common.createdAt')} value={formatTime(data.created_at)} divider />
|
|
110
|
+
<InfoMetric label={t('common.updatedAt')} value={formatTime(data.updated_at)} divider />
|
|
111
|
+
</Stack>
|
|
112
|
+
</Box>
|
|
113
|
+
</Box>
|
|
114
|
+
<Box className="section">
|
|
115
|
+
<SectionHeader title={t('admin.details')} />
|
|
116
|
+
<Stack>
|
|
117
|
+
<InfoRow label={t('common.amount')} value={total} />
|
|
118
|
+
<InfoRow
|
|
119
|
+
label={t('common.status')}
|
|
120
|
+
value={
|
|
121
|
+
<Stack direction="row" alignItems="center" spacing={1}>
|
|
122
|
+
<Status label={data.status} color={getPayoutStatusColor(data.status)} />
|
|
123
|
+
{data.last_attempt_error && (
|
|
124
|
+
<Tooltip title={<pre>{JSON.stringify(data.last_attempt_error, null, 2)}</pre>}>
|
|
125
|
+
<InfoOutlined fontSize="small" color="error" />
|
|
126
|
+
</Tooltip>
|
|
127
|
+
)}
|
|
128
|
+
</Stack>
|
|
129
|
+
}
|
|
130
|
+
/>
|
|
131
|
+
<InfoRow label={t('common.description')} value={data.description} />
|
|
132
|
+
<InfoRow label={t('common.createdAt')} value={formatTime(data.created_at)} />
|
|
133
|
+
<InfoRow label={t('common.updatedAt')} value={formatTime(data.updated_at)} />
|
|
134
|
+
<InfoRow
|
|
135
|
+
label={t('common.customer')}
|
|
136
|
+
value={data.customer ? <CustomerLink customer={data.customer} /> : data.destination}
|
|
137
|
+
/>
|
|
138
|
+
</Stack>
|
|
139
|
+
</Box>
|
|
140
|
+
<Box className="section">
|
|
141
|
+
<SectionHeader title={t('admin.paymentMethod._name')} />
|
|
142
|
+
<Stack>
|
|
143
|
+
<InfoRow label={t('common.id')} value={data.paymentMethod.id} />
|
|
144
|
+
<InfoRow label={t('admin.paymentMethod.type')} value={data.paymentMethod.type} />
|
|
145
|
+
<InfoRow
|
|
146
|
+
label={t('admin.paymentMethod._name')}
|
|
147
|
+
value={<Currency logo={data.paymentMethod.logo} name={data.paymentMethod.name} />}
|
|
148
|
+
/>
|
|
149
|
+
<InfoRow
|
|
150
|
+
label={t('admin.paymentCurrency.name')}
|
|
151
|
+
value={<Currency logo={data.paymentCurrency.logo} name={data.paymentCurrency.symbol} />}
|
|
152
|
+
/>
|
|
153
|
+
<InfoRow
|
|
154
|
+
label={t(`common.${data.payment_details?.arcblock?.type || 'transfer'}TxHash`)}
|
|
155
|
+
value={<TxLink details={data.payment_details as any} method={data.paymentMethod} />}
|
|
156
|
+
/>
|
|
157
|
+
</Stack>
|
|
158
|
+
</Box>
|
|
159
|
+
<Box className="section">
|
|
160
|
+
<SectionHeader title={t('common.metadata.label')}>
|
|
161
|
+
<Button
|
|
162
|
+
variant="outlined"
|
|
163
|
+
color="inherit"
|
|
164
|
+
size="small"
|
|
165
|
+
disabled={state.editing.metadata}
|
|
166
|
+
onClick={() => setState((prev) => ({ editing: { ...prev.editing, metadata: true } }))}>
|
|
167
|
+
<Edit fontSize="small" sx={{ mr: 0.5 }} />
|
|
168
|
+
{t('common.metadata.edit')}
|
|
169
|
+
</Button>
|
|
170
|
+
</SectionHeader>
|
|
171
|
+
<Box className="section-body">
|
|
172
|
+
{!state.editing.metadata && <MetadataList data={data.metadata} />}
|
|
173
|
+
{state.editing.metadata && (
|
|
174
|
+
<MetadataEditor
|
|
175
|
+
data={data}
|
|
176
|
+
loading={state.loading.metadata}
|
|
177
|
+
onSave={onUpdateMetadata}
|
|
178
|
+
onCancel={() => setState((prev) => ({ editing: { ...prev.editing, metadata: false } }))}
|
|
179
|
+
/>
|
|
180
|
+
)}
|
|
181
|
+
</Box>
|
|
182
|
+
</Box>
|
|
183
|
+
<Box className="section">
|
|
184
|
+
<SectionHeader title={t('admin.connections')} />
|
|
185
|
+
<Stack>
|
|
186
|
+
{data.payment_intent_id && (
|
|
187
|
+
<InfoRow
|
|
188
|
+
label={t('admin.paymentIntent.name')}
|
|
189
|
+
value={<Link to={`/admin/payments/${data.paymentIntent.id}`}>{data.paymentIntent.id}</Link>}
|
|
190
|
+
/>
|
|
191
|
+
)}
|
|
192
|
+
</Stack>
|
|
193
|
+
</Box>
|
|
194
|
+
<Box className="section">
|
|
195
|
+
<SectionHeader title={t('admin.events')} />
|
|
196
|
+
<Box className="section-body">
|
|
197
|
+
<EventList features={{ toolbar: false }} object_id={data.id} />
|
|
198
|
+
</Box>
|
|
199
|
+
</Box>
|
|
200
|
+
</Root>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const Root = styled(Stack)``;
|
|
@@ -128,8 +128,8 @@ function PaymentLinks() {
|
|
|
128
128
|
|
|
129
129
|
return (
|
|
130
130
|
<Table
|
|
131
|
-
durable={listKey}
|
|
132
|
-
durableKeys={['page', 'rowsPerPage']}
|
|
131
|
+
durable={`__${listKey}__`}
|
|
132
|
+
durableKeys={['page', 'rowsPerPage', 'searchText']}
|
|
133
133
|
title={
|
|
134
134
|
<div className="table-toolbar-left">
|
|
135
135
|
<ToggleButtonGroup
|