@blocklet/payment-react 1.13.123 → 1.13.125
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/es/api.js +0 -1
- package/es/components/confirm.d.ts +14 -0
- package/es/components/confirm.js +31 -0
- package/es/components/pricing-table.js +77 -67
- package/es/contexts/payment.d.ts +4 -2
- package/es/index.d.ts +4 -1
- package/es/index.js +7 -1
- package/es/locales/en.js +5 -1
- package/es/locales/zh.js +4 -0
- package/es/payment/form/index.js +2 -2
- package/es/payment/index.js +3 -3
- package/es/portal/invoice/list.d.ts +6 -0
- package/es/portal/invoice/list.js +84 -0
- package/es/portal/payment/list.d.ts +6 -0
- package/es/portal/payment/list.js +84 -0
- package/es/util.d.ts +4 -0
- package/es/util.js +56 -0
- package/lib/api.js +0 -1
- package/lib/components/confirm.d.ts +14 -0
- package/lib/components/confirm.js +49 -0
- package/lib/components/pricing-table.js +143 -135
- package/lib/contexts/payment.d.ts +4 -2
- package/lib/index.d.ts +4 -1
- package/lib/index.js +24 -0
- package/lib/locales/en.js +5 -1
- package/lib/locales/zh.js +4 -0
- package/lib/payment/form/index.js +2 -2
- package/lib/payment/index.js +3 -3
- package/lib/portal/invoice/list.d.ts +6 -0
- package/lib/portal/invoice/list.js +150 -0
- package/lib/portal/payment/list.d.ts +6 -0
- package/lib/portal/payment/list.js +149 -0
- package/lib/util.d.ts +4 -0
- package/lib/util.js +62 -1
- package/package.json +3 -3
- package/src/api.ts +1 -1
- package/src/components/confirm.tsx +39 -0
- package/src/components/pricing-table.tsx +125 -113
- package/src/contexts/payment.tsx +2 -2
- package/src/index.ts +6 -0
- package/src/locales/en.tsx +5 -1
- package/src/locales/zh.tsx +4 -0
- package/src/payment/form/index.tsx +5 -3
- package/src/payment/index.tsx +3 -3
- package/src/portal/invoice/list.tsx +122 -0
- package/src/portal/payment/list.tsx +120 -0
- package/src/util.ts +60 -0
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
ToggleButtonGroup,
|
|
18
18
|
Typography,
|
|
19
19
|
} from '@mui/material';
|
|
20
|
+
import { styled } from '@mui/system';
|
|
20
21
|
import { useSetState } from 'ahooks';
|
|
21
22
|
import { useEffect } from 'react';
|
|
22
23
|
|
|
@@ -83,123 +84,134 @@ export default function PricingTable({ table, alignItems, interval, mode, onSele
|
|
|
83
84
|
}
|
|
84
85
|
};
|
|
85
86
|
|
|
87
|
+
const Root = styled(Box)`
|
|
88
|
+
@media (max-width: ${({ theme }) => theme.breakpoints.values.sm}px) {
|
|
89
|
+
.price-table-item {
|
|
90
|
+
width: 90% !important;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
`;
|
|
86
94
|
return (
|
|
87
|
-
<
|
|
88
|
-
direction="column"
|
|
89
|
-
alignItems={alignItems === 'center' ? 'center' : 'flex-start'}
|
|
90
|
-
sx={{
|
|
91
|
-
pt: {
|
|
92
|
-
xs: 4,
|
|
93
|
-
sm: 2,
|
|
94
|
-
},
|
|
95
|
-
gap: {
|
|
96
|
-
xs: 3,
|
|
97
|
-
sm: mode === 'select' ? 3 : 5,
|
|
98
|
-
},
|
|
99
|
-
}}>
|
|
100
|
-
{Object.keys(recurring).length > 1 && (
|
|
101
|
-
<ToggleButtonGroup
|
|
102
|
-
color="primary"
|
|
103
|
-
value={state.interval}
|
|
104
|
-
onChange={(_, value) => {
|
|
105
|
-
if (value !== null) {
|
|
106
|
-
setState({ interval: value });
|
|
107
|
-
}
|
|
108
|
-
}}
|
|
109
|
-
exclusive>
|
|
110
|
-
{Object.keys(recurring).map((x) => (
|
|
111
|
-
<ToggleButton key={x} value={x} sx={{ textTransform: 'capitalize' }}>
|
|
112
|
-
{formatRecurring(recurring[x] as PriceRecurring, true, '', locale)}
|
|
113
|
-
</ToggleButton>
|
|
114
|
-
))}
|
|
115
|
-
</ToggleButtonGroup>
|
|
116
|
-
)}
|
|
95
|
+
<Root>
|
|
117
96
|
<Stack
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
97
|
+
direction="column"
|
|
98
|
+
alignItems={alignItems === 'center' ? 'center' : 'flex-start'}
|
|
99
|
+
sx={{
|
|
100
|
+
pt: {
|
|
101
|
+
xs: 4,
|
|
102
|
+
sm: 2,
|
|
103
|
+
},
|
|
104
|
+
gap: {
|
|
105
|
+
xs: 3,
|
|
106
|
+
sm: mode === 'select' ? 3 : 5,
|
|
107
|
+
},
|
|
108
|
+
}}>
|
|
109
|
+
{Object.keys(recurring).length > 1 && (
|
|
110
|
+
<ToggleButtonGroup
|
|
111
|
+
color="primary"
|
|
112
|
+
value={state.interval}
|
|
113
|
+
onChange={(_, value) => {
|
|
114
|
+
if (value !== null) {
|
|
115
|
+
setState({ interval: value });
|
|
116
|
+
}
|
|
117
|
+
}}
|
|
118
|
+
exclusive>
|
|
119
|
+
{Object.keys(recurring).map((x) => (
|
|
120
|
+
<ToggleButton key={x} value={x} sx={{ textTransform: 'capitalize' }}>
|
|
121
|
+
{formatRecurring(recurring[x] as PriceRecurring, true, '', locale)}
|
|
122
|
+
</ToggleButton>
|
|
123
|
+
))}
|
|
124
|
+
</ToggleButtonGroup>
|
|
125
|
+
)}
|
|
126
|
+
<Stack
|
|
127
|
+
flexWrap="wrap"
|
|
128
|
+
direction="row"
|
|
129
|
+
gap="calc(10px + 3%)"
|
|
130
|
+
justifyContent={alignItems === 'center' ? 'center' : 'flex-start'}>
|
|
131
|
+
{grouped[state.interval as string]?.map(
|
|
132
|
+
(x: TPricingTableItem & { is_selected?: boolean; is_disabled?: boolean }) => {
|
|
133
|
+
let action = x.subscription_data?.trial_period_days
|
|
134
|
+
? t('payment.checkout.try')
|
|
135
|
+
: t('payment.checkout.subscription');
|
|
136
|
+
if (mode === 'select') {
|
|
137
|
+
action = x.is_selected ? t('payment.checkout.selected') : t('payment.checkout.select');
|
|
138
|
+
}
|
|
129
139
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
140
|
+
return (
|
|
141
|
+
<Fade key={x.price_id} in>
|
|
142
|
+
<Stack
|
|
143
|
+
padding={4}
|
|
144
|
+
spacing={2}
|
|
145
|
+
direction="column"
|
|
146
|
+
alignItems="center"
|
|
147
|
+
className="price-table-item"
|
|
148
|
+
sx={{
|
|
149
|
+
width: '30%',
|
|
150
|
+
cursor: 'pointer',
|
|
151
|
+
borderWidth: '1px',
|
|
152
|
+
borderStyle: 'solid',
|
|
153
|
+
borderColor: mode === 'select' && x.is_selected ? 'primary.main' : '#eee',
|
|
154
|
+
borderRadius: 1,
|
|
155
|
+
transition: 'border-color 0.3s ease 0s, box-shadow 0.3s ease 0s',
|
|
156
|
+
boxShadow: '0 4px 8px rgba(0, 0, 0, 20%)',
|
|
157
|
+
'&:hover': {
|
|
158
|
+
borderColor: mode === 'select' && x.is_selected ? 'primary.main' : '#ddd',
|
|
159
|
+
boxShadow: '0 8px 16px rgba(0, 0, 0, 20%)',
|
|
160
|
+
},
|
|
161
|
+
}}>
|
|
162
|
+
<Box textAlign="center">
|
|
163
|
+
<Stack direction="row" alignItems="center" spacing={1}>
|
|
164
|
+
<Typography variant="h5" color="text.primary" fontWeight={600}>
|
|
165
|
+
{x.product.name}
|
|
166
|
+
</Typography>
|
|
167
|
+
{x.is_highlight && <Chip label={x.highlight_text} color="default" size="small" />}
|
|
168
|
+
</Stack>
|
|
169
|
+
<Typography color="text.secondary">{x.product.description}</Typography>
|
|
170
|
+
</Box>
|
|
171
|
+
<Stack direction="row" alignItems="center" spacing={1}>
|
|
172
|
+
<Amount amount={formatPriceAmount(x.price, table.currency, x.product.unit_label)} />
|
|
173
|
+
<Stack direction="column" alignItems="flex-start">
|
|
174
|
+
<Typography component="span" color="text.secondary" fontSize="0.8rem">
|
|
175
|
+
{t('payment.checkout.per')}
|
|
176
|
+
</Typography>
|
|
177
|
+
<Typography component="span" color="text.secondary" fontSize="0.8rem">
|
|
178
|
+
{formatRecurring(x.price.recurring as PriceRecurring, false, '', locale)}
|
|
179
|
+
</Typography>
|
|
180
|
+
</Stack>
|
|
181
|
+
</Stack>
|
|
182
|
+
<LoadingButton
|
|
183
|
+
fullWidth
|
|
184
|
+
size="large"
|
|
185
|
+
variant={x.is_highlight || x.is_selected ? 'contained' : 'outlined'}
|
|
186
|
+
color={x.is_highlight || x.is_selected ? 'primary' : 'info'}
|
|
187
|
+
sx={{ fontSize: '1.2rem' }}
|
|
188
|
+
loading={state.loading === x.price_id}
|
|
189
|
+
disabled={x.is_disabled}
|
|
190
|
+
onClick={() => handleSelect(x.price_id)}>
|
|
191
|
+
{action}
|
|
192
|
+
</LoadingButton>
|
|
193
|
+
{x.product.features.length > 0 && (
|
|
194
|
+
<Box>
|
|
195
|
+
<Typography>{t('payment.checkout.include')}</Typography>
|
|
196
|
+
<List dense>
|
|
197
|
+
{x.product.features.map((f: any) => (
|
|
198
|
+
<ListItem key={f.name} disableGutters disablePadding>
|
|
199
|
+
<ListItemIcon sx={{ minWidth: 25 }}>
|
|
200
|
+
<CheckOutlined color="success" fontSize="small" />
|
|
201
|
+
</ListItemIcon>
|
|
202
|
+
<ListItemText primary={f.name} />
|
|
203
|
+
</ListItem>
|
|
204
|
+
))}
|
|
205
|
+
</List>
|
|
206
|
+
</Box>
|
|
207
|
+
)}
|
|
157
208
|
</Stack>
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
<Typography component="span" color="text.secondary" fontSize="0.8rem">
|
|
164
|
-
{t('payment.checkout.per')}
|
|
165
|
-
</Typography>
|
|
166
|
-
<Typography component="span" color="text.secondary" fontSize="0.8rem">
|
|
167
|
-
{formatRecurring(x.price.recurring as PriceRecurring, false, '', locale)}
|
|
168
|
-
</Typography>
|
|
169
|
-
</Stack>
|
|
170
|
-
</Stack>
|
|
171
|
-
<LoadingButton
|
|
172
|
-
fullWidth
|
|
173
|
-
size="large"
|
|
174
|
-
loadingPosition="end"
|
|
175
|
-
variant={x.is_highlight || x.is_selected ? 'contained' : 'outlined'}
|
|
176
|
-
color={x.is_highlight || x.is_selected ? 'primary' : 'info'}
|
|
177
|
-
sx={{ fontSize: '1.2rem' }}
|
|
178
|
-
loading={state.loading === x.price_id}
|
|
179
|
-
disabled={x.is_disabled}
|
|
180
|
-
onClick={() => handleSelect(x.price_id)}>
|
|
181
|
-
{action}
|
|
182
|
-
</LoadingButton>
|
|
183
|
-
{x.product.features.length > 0 && (
|
|
184
|
-
<Box>
|
|
185
|
-
<Typography>{t('payment.checkout.include')}</Typography>
|
|
186
|
-
<List dense>
|
|
187
|
-
{x.product.features.map((f: any) => (
|
|
188
|
-
<ListItem key={f.name} disableGutters disablePadding>
|
|
189
|
-
<ListItemIcon sx={{ minWidth: 25 }}>
|
|
190
|
-
<CheckOutlined color="success" fontSize="small" />
|
|
191
|
-
</ListItemIcon>
|
|
192
|
-
<ListItemText primary={f.name} />
|
|
193
|
-
</ListItem>
|
|
194
|
-
))}
|
|
195
|
-
</List>
|
|
196
|
-
</Box>
|
|
197
|
-
)}
|
|
198
|
-
</Stack>
|
|
199
|
-
</Fade>
|
|
200
|
-
);
|
|
201
|
-
})}
|
|
209
|
+
</Fade>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
)}
|
|
213
|
+
</Stack>
|
|
202
214
|
</Stack>
|
|
203
|
-
</
|
|
215
|
+
</Root>
|
|
204
216
|
);
|
|
205
217
|
}
|
package/src/contexts/payment.tsx
CHANGED
|
@@ -16,8 +16,8 @@ export interface Settings {
|
|
|
16
16
|
|
|
17
17
|
export type PaymentContextType = {
|
|
18
18
|
livemode: boolean;
|
|
19
|
-
session: any;
|
|
20
|
-
connect:
|
|
19
|
+
session: import('@arcblock/did-connect/lib/types').SessionContext['session'] & { user: any };
|
|
20
|
+
connect: import('@arcblock/did-connect/lib/types').SessionContext['connectApi'];
|
|
21
21
|
prefix: string;
|
|
22
22
|
settings: Settings;
|
|
23
23
|
refresh: () => void;
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import api from './api';
|
|
2
2
|
import CheckoutForm from './checkout/form';
|
|
3
3
|
import CheckoutTable from './checkout/table';
|
|
4
|
+
import ConfirmDialog from './components/confirm';
|
|
4
5
|
import FormInput from './components/input';
|
|
5
6
|
import Livemode from './components/livemode';
|
|
6
7
|
import PricingTable from './components/pricing-table';
|
|
@@ -11,6 +12,8 @@ import Amount from './payment/amount';
|
|
|
11
12
|
import PhoneInput from './payment/form/phone';
|
|
12
13
|
import Payment from './payment/index';
|
|
13
14
|
import ProductSkeleton from './payment/product-skeleton';
|
|
15
|
+
import CustomerInvoiceList from './portal/invoice/list';
|
|
16
|
+
import CustomerPaymentList from './portal/payment/list';
|
|
14
17
|
|
|
15
18
|
export * from './util';
|
|
16
19
|
export * from './contexts/payment';
|
|
@@ -25,10 +28,13 @@ export {
|
|
|
25
28
|
Status,
|
|
26
29
|
Livemode,
|
|
27
30
|
Switch,
|
|
31
|
+
ConfirmDialog,
|
|
28
32
|
CheckoutForm,
|
|
29
33
|
CheckoutTable,
|
|
30
34
|
Payment,
|
|
31
35
|
PricingTable,
|
|
32
36
|
ProductSkeleton,
|
|
33
37
|
Amount,
|
|
38
|
+
CustomerInvoiceList,
|
|
39
|
+
CustomerPaymentList,
|
|
34
40
|
};
|
package/src/locales/en.tsx
CHANGED
|
@@ -150,6 +150,7 @@ export default flat({
|
|
|
150
150
|
},
|
|
151
151
|
},
|
|
152
152
|
customer: {
|
|
153
|
+
payments: 'Payment History',
|
|
153
154
|
invoices: 'Invoice History',
|
|
154
155
|
details: 'Billing Details',
|
|
155
156
|
update: 'Update Information',
|
|
@@ -206,7 +207,10 @@ export default flat({
|
|
|
206
207
|
pay: 'Pay this invoice',
|
|
207
208
|
paySuccess: 'You have successfully paid the invoice',
|
|
208
209
|
payError: 'Failed to paid the invoice',
|
|
209
|
-
empty: 'Seems you do not have any
|
|
210
|
+
empty: 'Seems you do not have any invoice here',
|
|
211
|
+
},
|
|
212
|
+
payment: {
|
|
213
|
+
empty: 'Seems you do not have any payment here',
|
|
210
214
|
},
|
|
211
215
|
subscriptions: {
|
|
212
216
|
title: 'Manage subscriptions',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -147,6 +147,7 @@ export default flat({
|
|
|
147
147
|
},
|
|
148
148
|
},
|
|
149
149
|
customer: {
|
|
150
|
+
payments: '支付历史',
|
|
150
151
|
invoices: '发票历史',
|
|
151
152
|
details: '计费详情',
|
|
152
153
|
update: '更新客户信息',
|
|
@@ -202,6 +203,9 @@ export default flat({
|
|
|
202
203
|
pay: '支付此发票',
|
|
203
204
|
paySuccess: '支付成功',
|
|
204
205
|
payError: '支付失败',
|
|
206
|
+
empty: '你没有任何发票',
|
|
207
|
+
},
|
|
208
|
+
payment: {
|
|
205
209
|
empty: '你没有任何支付',
|
|
206
210
|
},
|
|
207
211
|
subscriptions: {
|
|
@@ -225,10 +225,11 @@ export default function PaymentForm({
|
|
|
225
225
|
if (result.data.balance?.sufficient || result.data.delegation?.sufficient) {
|
|
226
226
|
await handleConnected();
|
|
227
227
|
} else {
|
|
228
|
+
// @FIXME: 需要考虑如何正确地适配前端组件的使用 @wangshijun
|
|
228
229
|
connect.open({
|
|
230
|
+
containerEl: undefined as unknown as Element,
|
|
229
231
|
action: checkoutSession.mode,
|
|
230
|
-
prefix: joinURL(getPrefix(), '/api/did'),
|
|
231
|
-
timeout: 5 * 60 * 1000,
|
|
232
|
+
prefix: joinURL(window.location.origin, getPrefix(), '/api/did'),
|
|
232
233
|
extraParams: { checkoutSessionId: checkoutSession.id },
|
|
233
234
|
onSuccess: async () => {
|
|
234
235
|
connect.close();
|
|
@@ -242,7 +243,7 @@ export default function PaymentForm({
|
|
|
242
243
|
setState({ submitting: false, paying: false });
|
|
243
244
|
onError(err);
|
|
244
245
|
},
|
|
245
|
-
});
|
|
246
|
+
} as any);
|
|
246
247
|
}
|
|
247
248
|
}
|
|
248
249
|
if (['stripe'].includes(method.type)) {
|
|
@@ -266,6 +267,7 @@ export default function PaymentForm({
|
|
|
266
267
|
if (session?.user) {
|
|
267
268
|
handleSubmit(onSubmit)();
|
|
268
269
|
} else {
|
|
270
|
+
// @ts-ignored
|
|
269
271
|
session?.login({
|
|
270
272
|
onSuccess: onUserLoggedIn,
|
|
271
273
|
extraParams: {},
|
package/src/payment/index.tsx
CHANGED
|
@@ -350,18 +350,18 @@ export const Root = styled(Box)<{ mode: LiteralUnion<'standalone' | 'inline' | '
|
|
|
350
350
|
}
|
|
351
351
|
|
|
352
352
|
.cko-payment-card-unselect {
|
|
353
|
-
border: 2px solid #
|
|
353
|
+
border: 2px solid #ddd;
|
|
354
354
|
padding: 5px 10px;
|
|
355
355
|
margin: 5px 0;
|
|
356
356
|
cursor: pointer;
|
|
357
357
|
}
|
|
358
358
|
|
|
359
359
|
.cko-payment-card:nth-child(odd) {
|
|
360
|
-
margin-right:
|
|
360
|
+
margin-right: 16px;
|
|
361
361
|
}
|
|
362
362
|
|
|
363
363
|
.cko-payment-card-unselect:nth-child(odd) {
|
|
364
|
-
margin-right:
|
|
364
|
+
margin-right: 16px;
|
|
365
365
|
}
|
|
366
366
|
|
|
367
367
|
.cko-payment-card::after {
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/* eslint-disable react/no-unstable-nested-components */
|
|
2
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
|
+
import type { Paginated, TInvoiceExpanded } from '@blocklet/payment-types';
|
|
4
|
+
import { Box, Button, CircularProgress, Stack, Typography } from '@mui/material';
|
|
5
|
+
import { fromUnitToToken } from '@ocap/util';
|
|
6
|
+
import { useInfiniteScroll } from 'ahooks';
|
|
7
|
+
|
|
8
|
+
import api from '../../api';
|
|
9
|
+
import Status from '../../components/status';
|
|
10
|
+
import { formatToDate, getInvoiceStatusColor } from '../../util';
|
|
11
|
+
|
|
12
|
+
const groupByDate = (items: TInvoiceExpanded[]) => {
|
|
13
|
+
const grouped: { [key: string]: TInvoiceExpanded[] } = {};
|
|
14
|
+
items.forEach((item) => {
|
|
15
|
+
const date = new Date(item.created_at).toLocaleDateString();
|
|
16
|
+
if (!grouped[date]) {
|
|
17
|
+
grouped[date] = [];
|
|
18
|
+
}
|
|
19
|
+
grouped[date]?.push(item);
|
|
20
|
+
});
|
|
21
|
+
return grouped;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const fetchData = (params: Record<string, any> = {}): Promise<Paginated<TInvoiceExpanded>> => {
|
|
25
|
+
const search = new URLSearchParams();
|
|
26
|
+
Object.keys(params).forEach((key) => {
|
|
27
|
+
search.set(key, String(params[key]));
|
|
28
|
+
});
|
|
29
|
+
return api.get(`/api/invoices?${search.toString()}`).then((res: any) => res.data);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type Props = {
|
|
33
|
+
customer_id: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const pageSize = 10;
|
|
37
|
+
|
|
38
|
+
export default function CustomerInvoiceList({ customer_id }: Props) {
|
|
39
|
+
const { t } = useLocaleContext();
|
|
40
|
+
|
|
41
|
+
const { data, loadMore, loadingMore, loading } = useInfiniteScroll<Paginated<TInvoiceExpanded>>(
|
|
42
|
+
(d) => {
|
|
43
|
+
const page = d ? Math.ceil(d.list.length / pageSize) + 1 : 1;
|
|
44
|
+
return fetchData({ page, pageSize, status: 'open,paid,uncollectible', customer_id });
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
reloadDeps: [customer_id],
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (loading || !data) {
|
|
52
|
+
return <CircularProgress />;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (data && data.list.length === 0) {
|
|
56
|
+
return <Typography color="text.secondary">{t('payment.customer.invoice.empty')}</Typography>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const hasMore = data && data.list.length < data.count;
|
|
60
|
+
|
|
61
|
+
const grouped = groupByDate(data.list as any);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Stack direction="column" gap={1} sx={{ mt: 1 }}>
|
|
65
|
+
{Object.entries(grouped).map(([date, invoices]) => (
|
|
66
|
+
<Box key={date}>
|
|
67
|
+
<Typography sx={{ fontWeight: 'bold', color: 'text.secondary', mt: 2, mb: 1 }}>{date}</Typography>
|
|
68
|
+
{invoices.map((invoice) => (
|
|
69
|
+
<Stack
|
|
70
|
+
key={invoice.id}
|
|
71
|
+
direction={{
|
|
72
|
+
xs: 'column',
|
|
73
|
+
sm: 'row',
|
|
74
|
+
}}
|
|
75
|
+
sx={{ my: 1 }}
|
|
76
|
+
gap={{
|
|
77
|
+
xs: 0.5,
|
|
78
|
+
sm: 1.5,
|
|
79
|
+
md: 3,
|
|
80
|
+
}}
|
|
81
|
+
flexWrap="nowrap">
|
|
82
|
+
<Box flex={3}>
|
|
83
|
+
<a href={`/customer/invoice/${invoice.id}`}>
|
|
84
|
+
<Typography component="span">{invoice.number}</Typography>
|
|
85
|
+
</a>
|
|
86
|
+
</Box>
|
|
87
|
+
<Box flex={3}>
|
|
88
|
+
<Typography>{formatToDate(invoice.created_at)}</Typography>
|
|
89
|
+
</Box>
|
|
90
|
+
<Box flex={2}>
|
|
91
|
+
<Typography textAlign="right">
|
|
92
|
+
{fromUnitToToken(invoice.total, invoice.paymentCurrency.decimal)}
|
|
93
|
+
{invoice.paymentCurrency.symbol}
|
|
94
|
+
</Typography>
|
|
95
|
+
</Box>
|
|
96
|
+
<Box flex={2}>
|
|
97
|
+
<Status label={invoice.status} color={getInvoiceStatusColor(invoice.status)} />
|
|
98
|
+
</Box>
|
|
99
|
+
<Box flex={4}>
|
|
100
|
+
<Typography>{invoice.description || invoice.id}</Typography>
|
|
101
|
+
</Box>
|
|
102
|
+
</Stack>
|
|
103
|
+
))}
|
|
104
|
+
</Box>
|
|
105
|
+
))}
|
|
106
|
+
<Box>
|
|
107
|
+
{hasMore && (
|
|
108
|
+
<Button variant="text" type="button" color="inherit" onClick={loadMore} disabled={loadingMore}>
|
|
109
|
+
{loadingMore
|
|
110
|
+
? t('common.loadingMore', { resource: t('payment.customer.invoices') })
|
|
111
|
+
: t('common.loadMore', { resource: t('payament.customer.invoices') })}
|
|
112
|
+
</Button>
|
|
113
|
+
)}
|
|
114
|
+
{!hasMore && data.count > pageSize && (
|
|
115
|
+
<Typography color="text.secondary">
|
|
116
|
+
{t('common.noMore', { resource: t('payment.customer.invoices') })}
|
|
117
|
+
</Typography>
|
|
118
|
+
)}
|
|
119
|
+
</Box>
|
|
120
|
+
</Stack>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/* eslint-disable react/no-unstable-nested-components */
|
|
2
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
|
+
import type { Paginated, TPaymentIntentExpanded } from '@blocklet/payment-types';
|
|
4
|
+
import { Box, Button, CircularProgress, Stack, Typography } from '@mui/material';
|
|
5
|
+
import { fromUnitToToken } from '@ocap/util';
|
|
6
|
+
import { useInfiniteScroll } from 'ahooks';
|
|
7
|
+
|
|
8
|
+
import api from '../../api';
|
|
9
|
+
import Status from '../../components/status';
|
|
10
|
+
import { formatToDate, getPaymentIntentStatusColor } from '../../util';
|
|
11
|
+
|
|
12
|
+
const groupByDate = (items: TPaymentIntentExpanded[]) => {
|
|
13
|
+
const grouped: { [key: string]: TPaymentIntentExpanded[] } = {};
|
|
14
|
+
items.forEach((item) => {
|
|
15
|
+
const date = new Date(item.created_at).toLocaleDateString();
|
|
16
|
+
if (!grouped[date]) {
|
|
17
|
+
grouped[date] = [];
|
|
18
|
+
}
|
|
19
|
+
grouped[date]?.push(item);
|
|
20
|
+
});
|
|
21
|
+
return grouped;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const fetchData = (params: Record<string, any> = {}): Promise<Paginated<TPaymentIntentExpanded>> => {
|
|
25
|
+
const search = new URLSearchParams();
|
|
26
|
+
Object.keys(params).forEach((key) => {
|
|
27
|
+
search.set(key, String(params[key]));
|
|
28
|
+
});
|
|
29
|
+
return api.get(`/api/payment-intents?${search.toString()}`).then((res: any) => res.data);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type Props = {
|
|
33
|
+
customer_id: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const pageSize = 10;
|
|
37
|
+
|
|
38
|
+
export default function CustomerPaymentList({ customer_id }: Props) {
|
|
39
|
+
const { t } = useLocaleContext();
|
|
40
|
+
|
|
41
|
+
const { data, loadMore, loadingMore, loading } = useInfiniteScroll<Paginated<TPaymentIntentExpanded>>(
|
|
42
|
+
(d) => {
|
|
43
|
+
const page = d ? Math.ceil(d.list.length / pageSize) + 1 : 1;
|
|
44
|
+
return fetchData({ page, pageSize, customer_id });
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
reloadDeps: [customer_id],
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (loading || !data) {
|
|
52
|
+
return <CircularProgress />;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (data && data.list.length === 0) {
|
|
56
|
+
return <Typography color="text.secondary">{t('payment.customer.payment.empty')}</Typography>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const hasMore = data && data.list.length < data.count;
|
|
60
|
+
|
|
61
|
+
const grouped = groupByDate(data.list as any);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Stack direction="column" gap={1} sx={{ mt: 1 }}>
|
|
65
|
+
{Object.entries(grouped).map(([date, payments]) => (
|
|
66
|
+
<Box key={date}>
|
|
67
|
+
<Typography sx={{ fontWeight: 'bold', color: 'text.secondary', mt: 2, mb: 1 }}>{date}</Typography>
|
|
68
|
+
{payments.map((item) => (
|
|
69
|
+
<Stack
|
|
70
|
+
key={item.id}
|
|
71
|
+
direction={{
|
|
72
|
+
xs: 'column',
|
|
73
|
+
sm: 'row',
|
|
74
|
+
}}
|
|
75
|
+
sx={{ my: 1 }}
|
|
76
|
+
gap={{
|
|
77
|
+
xs: 0.5,
|
|
78
|
+
sm: 1.5,
|
|
79
|
+
md: 3,
|
|
80
|
+
}}
|
|
81
|
+
flexWrap="nowrap">
|
|
82
|
+
<Box flex={3} sx={{ minWidth: '220px' }}>
|
|
83
|
+
<Typography component="span">{item.id}</Typography>
|
|
84
|
+
</Box>
|
|
85
|
+
<Box flex={3}>
|
|
86
|
+
<Typography>{formatToDate(item.created_at)}</Typography>
|
|
87
|
+
</Box>
|
|
88
|
+
<Box flex={2}>
|
|
89
|
+
<Typography textAlign="right">
|
|
90
|
+
{fromUnitToToken(item.amount, item.paymentCurrency.decimal)}
|
|
91
|
+
{item.paymentCurrency.symbol}
|
|
92
|
+
</Typography>
|
|
93
|
+
</Box>
|
|
94
|
+
<Box flex={3}>
|
|
95
|
+
<Status label={item.status} color={getPaymentIntentStatusColor(item.status)} />
|
|
96
|
+
</Box>
|
|
97
|
+
<Box flex={4}>
|
|
98
|
+
<Typography>{item.description || item.id}</Typography>
|
|
99
|
+
</Box>
|
|
100
|
+
</Stack>
|
|
101
|
+
))}
|
|
102
|
+
</Box>
|
|
103
|
+
))}
|
|
104
|
+
<Box>
|
|
105
|
+
{hasMore && (
|
|
106
|
+
<Button variant="text" type="button" color="inherit" onClick={loadMore} disabled={loadingMore}>
|
|
107
|
+
{loadingMore
|
|
108
|
+
? t('common.loadingMore', { resource: t('payment.customer.payments') })
|
|
109
|
+
: t('common.loadMore', { resource: t('payment.customer.payments') })}
|
|
110
|
+
</Button>
|
|
111
|
+
)}
|
|
112
|
+
{!hasMore && data.count > pageSize && (
|
|
113
|
+
<Typography color="text.secondary">
|
|
114
|
+
{t('common.noMore', { resource: t('payment.customer.payments') })}
|
|
115
|
+
</Typography>
|
|
116
|
+
)}
|
|
117
|
+
</Box>
|
|
118
|
+
</Stack>
|
|
119
|
+
);
|
|
120
|
+
}
|