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.
Files changed (47) hide show
  1. package/api/src/libs/api.ts +2 -2
  2. package/api/src/libs/session.ts +90 -4
  3. package/api/src/queues/payment.ts +61 -1
  4. package/api/src/routes/checkout-sessions.ts +61 -10
  5. package/api/src/routes/connect/collect.ts +44 -37
  6. package/api/src/routes/connect/pay.ts +40 -29
  7. package/api/src/routes/connect/setup.ts +39 -33
  8. package/api/src/routes/connect/shared.ts +3 -1
  9. package/api/src/routes/donations.ts +157 -0
  10. package/api/src/routes/index.ts +4 -0
  11. package/api/src/routes/payment-intents.ts +2 -2
  12. package/api/src/routes/payment-links.ts +8 -3
  13. package/api/src/routes/payouts.ts +151 -0
  14. package/api/src/routes/products.ts +24 -6
  15. package/api/src/routes/subscriptions.ts +2 -0
  16. package/api/src/routes/usage-records.ts +6 -3
  17. package/api/src/store/migrations/20240408-payout.ts +36 -0
  18. package/api/src/store/models/checkout-session.ts +5 -0
  19. package/api/src/store/models/customer.ts +6 -1
  20. package/api/src/store/models/index.ts +12 -0
  21. package/api/src/store/models/payment-intent.ts +38 -26
  22. package/api/src/store/models/payment-link.ts +8 -1
  23. package/api/src/store/models/payout.ts +243 -0
  24. package/api/src/store/models/types.ts +39 -0
  25. package/api/tests/libs/session.spec.ts +101 -0
  26. package/blocklet.yml +1 -1
  27. package/package.json +22 -21
  28. package/src/components/info-card.tsx +5 -5
  29. package/src/components/invoice/list.tsx +2 -0
  30. package/src/components/invoice/table.tsx +1 -1
  31. package/src/components/payment-intent/list.tsx +2 -0
  32. package/src/components/payouts/actions.tsx +43 -0
  33. package/src/components/payouts/list.tsx +255 -0
  34. package/src/components/refund/list.tsx +2 -0
  35. package/src/components/subscription/list.tsx +2 -0
  36. package/src/libs/util.ts +4 -1
  37. package/src/locales/en.tsx +7 -0
  38. package/src/locales/zh.tsx +6 -0
  39. package/src/pages/admin/customers/customers/index.tsx +2 -2
  40. package/src/pages/admin/payments/index.tsx +7 -0
  41. package/src/pages/admin/payments/intents/detail.tsx +7 -0
  42. package/src/pages/admin/payments/payouts/detail.tsx +204 -0
  43. package/src/pages/admin/payments/payouts/index.tsx +5 -0
  44. package/src/pages/admin/products/links/index.tsx +2 -2
  45. package/src/pages/admin/products/prices/detail.tsx +2 -1
  46. package/src/pages/admin/products/pricing-tables/index.tsx +2 -2
  47. package/src/pages/admin/products/products/index.tsx +2 -2
@@ -230,6 +230,8 @@ export default function InvoiceList({
230
230
  return (
231
231
  <Table
232
232
  data={data.list}
233
+ durable={`__${listKey}__`}
234
+ durableKeys={['searchText']}
233
235
  columns={columns}
234
236
  loading={!data.list}
235
237
  onChange={onTableChange}
@@ -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
+ &nbsp;
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 subscription.items.every((x) => getPriceCurrencyOptions(x.price).length > 1);
190
+ return (
191
+ ['active', 'trialing'].includes(subscription.status) &&
192
+ subscription.items.every((x) => getPriceCurrencyOptions(x.price).length > 1)
193
+ );
191
194
  }
@@ -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',
@@ -203,6 +203,12 @@ export default flat({
203
203
  placeholder: '不向消费者展示',
204
204
  },
205
205
  },
206
+ payout: {
207
+ list: '对外支付',
208
+ name: '对外支付',
209
+ view: '查看对外支付',
210
+ empty: '没有记录',
211
+ },
206
212
  pricingTable: {
207
213
  view: '查看定价表',
208
214
  add: '创建定价表',
@@ -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)``;
@@ -0,0 +1,5 @@
1
+ import PayoutList from '../../../../components/payouts/list';
2
+
3
+ export default function PayoutsList() {
4
+ return <PayoutList />;
5
+ }
@@ -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