@blocklet/payment-react 1.18.15 → 1.18.16
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/components/over-due-invoice-payment.d.ts +20 -7
- package/es/components/over-due-invoice-payment.js +213 -74
- package/es/contexts/payment.js +14 -1
- package/es/history/invoice/list.js +28 -10
- package/es/locales/en.js +13 -4
- package/es/locales/zh.js +13 -4
- package/es/payment/form/address.js +63 -65
- package/lib/components/over-due-invoice-payment.d.ts +20 -7
- package/lib/components/over-due-invoice-payment.js +208 -75
- package/lib/contexts/payment.js +14 -1
- package/lib/history/invoice/list.js +50 -20
- package/lib/locales/en.js +13 -4
- package/lib/locales/zh.js +13 -4
- package/lib/payment/form/address.js +46 -50
- package/package.json +8 -7
- package/src/components/over-due-invoice-payment.tsx +267 -81
- package/src/contexts/payment.tsx +15 -1
- package/src/history/invoice/list.tsx +36 -15
- package/src/locales/en.tsx +11 -2
- package/src/locales/zh.tsx +11 -1
- package/src/payment/form/address.tsx +36 -38
|
@@ -1,15 +1,19 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/indent */
|
|
1
2
|
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
-
import { Button, Typography, Stack,
|
|
3
|
+
import { Button, Typography, Stack, Alert, SxProps } from '@mui/material';
|
|
3
4
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
4
5
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
5
6
|
import { joinURL } from 'ufo';
|
|
6
7
|
import type { Invoice, PaymentCurrency, PaymentMethod, Subscription, TInvoiceExpanded } from '@blocklet/payment-types';
|
|
7
8
|
import { useRequest } from 'ahooks';
|
|
8
9
|
import { Dialog } from '@arcblock/ux';
|
|
10
|
+
import { CheckCircle as CheckCircleIcon } from '@mui/icons-material';
|
|
11
|
+
import debounce from 'lodash/debounce';
|
|
9
12
|
import { usePaymentContext } from '../contexts/payment';
|
|
10
13
|
import { formatAmount, formatError, getPrefix } from '../libs/util';
|
|
11
14
|
import { useSubscription } from '../hooks/subscription';
|
|
12
15
|
import api from '../libs/api';
|
|
16
|
+
import LoadingButton from './loading-button';
|
|
13
17
|
|
|
14
18
|
type DialogProps = {
|
|
15
19
|
open?: boolean;
|
|
@@ -17,19 +21,28 @@ type DialogProps = {
|
|
|
17
21
|
title?: string;
|
|
18
22
|
};
|
|
19
23
|
|
|
24
|
+
type DetailLinkOptions = {
|
|
25
|
+
enabled?: boolean;
|
|
26
|
+
onClick?: (e: React.MouseEvent) => void;
|
|
27
|
+
title?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
20
30
|
type Props = {
|
|
21
|
-
subscriptionId
|
|
31
|
+
subscriptionId?: string;
|
|
32
|
+
customerId?: string;
|
|
22
33
|
mode?: 'default' | 'custom';
|
|
23
|
-
onPaid?: (
|
|
34
|
+
onPaid?: (id: string, currencyId: string, type: 'subscription' | 'customer') => void;
|
|
24
35
|
dialogProps?: DialogProps;
|
|
25
|
-
|
|
36
|
+
detailLinkOptions?: DetailLinkOptions;
|
|
37
|
+
successToast?: boolean;
|
|
26
38
|
children?: (
|
|
27
39
|
handlePay: (item: SummaryItem) => void,
|
|
28
40
|
data: {
|
|
29
|
-
subscription
|
|
41
|
+
subscription?: Subscription;
|
|
30
42
|
summary: { [key: string]: SummaryItem };
|
|
31
43
|
invoices: Invoice[];
|
|
32
|
-
|
|
44
|
+
subscriptionCount?: number;
|
|
45
|
+
detailUrl: string;
|
|
33
46
|
}
|
|
34
47
|
) => React.ReactNode;
|
|
35
48
|
};
|
|
@@ -40,24 +53,41 @@ type SummaryItem = {
|
|
|
40
53
|
method: PaymentMethod;
|
|
41
54
|
};
|
|
42
55
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
): Promise<{
|
|
46
|
-
subscription: Subscription;
|
|
56
|
+
type OverdueInvoicesResult = {
|
|
57
|
+
subscription?: Subscription;
|
|
47
58
|
summary: { [key: string]: SummaryItem };
|
|
48
59
|
invoices: Invoice[];
|
|
49
|
-
|
|
50
|
-
|
|
60
|
+
subscriptionCount?: number;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const fetchOverdueInvoices = async (params: {
|
|
64
|
+
subscriptionId?: string;
|
|
65
|
+
customerId?: string;
|
|
66
|
+
}): Promise<OverdueInvoicesResult> => {
|
|
67
|
+
if (!params.subscriptionId && !params.customerId) {
|
|
68
|
+
throw new Error('Either subscriptionId or customerId must be provided');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let url;
|
|
72
|
+
if (params.subscriptionId) {
|
|
73
|
+
url = `/api/subscriptions/${params.subscriptionId}/overdue/invoices`;
|
|
74
|
+
} else {
|
|
75
|
+
url = `/api/customers/${params.customerId}/overdue/invoices`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const res = await api.get(url);
|
|
51
79
|
return res.data;
|
|
52
80
|
};
|
|
53
81
|
|
|
54
82
|
function OverdueInvoicePayment({
|
|
55
83
|
subscriptionId,
|
|
84
|
+
customerId,
|
|
56
85
|
mode = 'default',
|
|
57
86
|
dialogProps = {},
|
|
58
87
|
children,
|
|
59
88
|
onPaid = () => {},
|
|
60
|
-
|
|
89
|
+
detailLinkOptions = { enabled: true },
|
|
90
|
+
successToast = true,
|
|
61
91
|
}: Props) {
|
|
62
92
|
const { t } = useLocaleContext();
|
|
63
93
|
const { connect } = usePaymentContext();
|
|
@@ -65,18 +95,33 @@ function OverdueInvoicePayment({
|
|
|
65
95
|
const [payLoading, setPayLoading] = useState(false);
|
|
66
96
|
const [dialogOpen, setDialogOpen] = useState(dialogProps.open || false);
|
|
67
97
|
const [processedCurrencies, setProcessedCurrencies] = useState<{ [key: string]: number }>({});
|
|
98
|
+
const [paymentStatus, setPaymentStatus] = useState<{ [key: string]: 'success' | 'error' | 'idle' }>({});
|
|
99
|
+
|
|
100
|
+
const sourceType = subscriptionId ? 'subscription' : 'customer';
|
|
101
|
+
const sourceId = subscriptionId || customerId;
|
|
102
|
+
|
|
68
103
|
const {
|
|
69
104
|
data = {
|
|
70
|
-
subscription: {} as Subscription,
|
|
71
105
|
summary: {},
|
|
72
106
|
invoices: [],
|
|
73
|
-
},
|
|
107
|
+
} as OverdueInvoicesResult,
|
|
74
108
|
error,
|
|
75
109
|
loading,
|
|
76
110
|
runAsync: refresh,
|
|
77
|
-
} = useRequest(() => fetchOverdueInvoices(subscriptionId)
|
|
111
|
+
} = useRequest(() => fetchOverdueInvoices({ subscriptionId, customerId }), {
|
|
112
|
+
ready: !!subscriptionId || !!customerId,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const detailUrl = useMemo(() => {
|
|
116
|
+
if (subscriptionId) {
|
|
117
|
+
return joinURL(getPrefix(), `/customer/subscription/${subscriptionId}`);
|
|
118
|
+
}
|
|
119
|
+
if (customerId) {
|
|
120
|
+
return joinURL(getPrefix(), '/customer/invoice/past-due');
|
|
121
|
+
}
|
|
122
|
+
return '';
|
|
123
|
+
}, [subscriptionId, customerId]);
|
|
78
124
|
|
|
79
|
-
const subscriptionUrl = joinURL(getPrefix(), `/customer/subscription/${subscriptionId}`);
|
|
80
125
|
const summaryList = useMemo(() => {
|
|
81
126
|
if (!data?.summary) {
|
|
82
127
|
return [];
|
|
@@ -84,26 +129,47 @@ function OverdueInvoicePayment({
|
|
|
84
129
|
return Object.values(data.summary);
|
|
85
130
|
}, [data?.summary]);
|
|
86
131
|
|
|
132
|
+
const debouncedHandleInvoicePaid = debounce(
|
|
133
|
+
async (currencyId: string) => {
|
|
134
|
+
if (successToast) {
|
|
135
|
+
Toast.close();
|
|
136
|
+
Toast.success(t('payment.customer.invoice.paySuccess'));
|
|
137
|
+
}
|
|
138
|
+
setPayLoading(false);
|
|
139
|
+
const res = await refresh();
|
|
140
|
+
if (res.invoices?.length === 0) {
|
|
141
|
+
setDialogOpen(false);
|
|
142
|
+
onPaid(sourceId as string, currencyId, sourceType as 'subscription' | 'customer');
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
1000,
|
|
146
|
+
{
|
|
147
|
+
leading: false,
|
|
148
|
+
trailing: true,
|
|
149
|
+
maxWait: 5000,
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
|
|
87
153
|
const subscription = useSubscription('events');
|
|
88
154
|
useEffect(() => {
|
|
89
155
|
if (subscription) {
|
|
90
156
|
subscription.on('invoice.paid', ({ response }: { response: TInvoiceExpanded }) => {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
157
|
+
const relevantId = subscriptionId || response.customer_id;
|
|
158
|
+
const uniqueKey = `${relevantId}-${response.currency_id}`;
|
|
159
|
+
|
|
160
|
+
if (
|
|
161
|
+
(subscriptionId && response.subscription_id === subscriptionId) ||
|
|
162
|
+
(customerId && response.customer_id === customerId)
|
|
163
|
+
) {
|
|
164
|
+
if (!processedCurrencies[uniqueKey]) {
|
|
165
|
+
setProcessedCurrencies((prev) => ({ ...prev, [uniqueKey]: 1 }));
|
|
166
|
+
debouncedHandleInvoicePaid(response.currency_id);
|
|
167
|
+
}
|
|
102
168
|
}
|
|
103
169
|
});
|
|
104
170
|
}
|
|
105
171
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
106
|
-
}, [subscription]);
|
|
172
|
+
}, [subscription, subscriptionId, customerId]);
|
|
107
173
|
|
|
108
174
|
const handlePay = (item: SummaryItem) => {
|
|
109
175
|
const { currency, method } = item;
|
|
@@ -116,15 +182,33 @@ function OverdueInvoicePayment({
|
|
|
116
182
|
}
|
|
117
183
|
setSelectCurrencyId(currency.id);
|
|
118
184
|
setPayLoading(true);
|
|
185
|
+
setPaymentStatus((prev) => ({
|
|
186
|
+
...prev,
|
|
187
|
+
[currency.id]: 'idle',
|
|
188
|
+
}));
|
|
189
|
+
|
|
119
190
|
if (['arcblock', 'ethereum', 'base'].includes(method.type)) {
|
|
191
|
+
const extraParams: any = { currencyId: currency.id };
|
|
192
|
+
|
|
193
|
+
if (subscriptionId) {
|
|
194
|
+
extraParams.subscriptionId = subscriptionId;
|
|
195
|
+
} else if (customerId) {
|
|
196
|
+
extraParams.customerId = customerId;
|
|
197
|
+
}
|
|
198
|
+
|
|
120
199
|
connect.open({
|
|
121
200
|
containerEl: undefined as unknown as Element,
|
|
122
201
|
saveConnect: false,
|
|
123
202
|
action: 'collect-batch',
|
|
124
203
|
prefix: joinURL(getPrefix(), '/api/did'),
|
|
125
|
-
extraParams
|
|
204
|
+
extraParams,
|
|
126
205
|
onSuccess: () => {
|
|
127
206
|
connect.close();
|
|
207
|
+
setPayLoading(false);
|
|
208
|
+
setPaymentStatus((prev) => ({
|
|
209
|
+
...prev,
|
|
210
|
+
[currency.id]: 'success',
|
|
211
|
+
}));
|
|
128
212
|
},
|
|
129
213
|
onClose: () => {
|
|
130
214
|
connect.close();
|
|
@@ -132,6 +216,10 @@ function OverdueInvoicePayment({
|
|
|
132
216
|
},
|
|
133
217
|
onError: (err: any) => {
|
|
134
218
|
Toast.error(formatError(err));
|
|
219
|
+
setPaymentStatus((prev) => ({
|
|
220
|
+
...prev,
|
|
221
|
+
[currency.id]: 'error',
|
|
222
|
+
}));
|
|
135
223
|
setPayLoading(false);
|
|
136
224
|
},
|
|
137
225
|
});
|
|
@@ -144,7 +232,10 @@ function OverdueInvoicePayment({
|
|
|
144
232
|
};
|
|
145
233
|
|
|
146
234
|
const handleViewDetailClick = (e: React.MouseEvent) => {
|
|
147
|
-
if (
|
|
235
|
+
if (detailLinkOptions.onClick) {
|
|
236
|
+
e.preventDefault();
|
|
237
|
+
detailLinkOptions.onClick(e);
|
|
238
|
+
} else if (!detailLinkOptions.enabled) {
|
|
148
239
|
e.preventDefault();
|
|
149
240
|
handleClose();
|
|
150
241
|
}
|
|
@@ -154,37 +245,137 @@ function OverdueInvoicePayment({
|
|
|
154
245
|
return null;
|
|
155
246
|
}
|
|
156
247
|
|
|
157
|
-
const
|
|
158
|
-
|
|
248
|
+
const getDetailLinkText = () => {
|
|
249
|
+
if (detailLinkOptions.title) {
|
|
250
|
+
return detailLinkOptions.title;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (subscriptionId) {
|
|
254
|
+
return t('payment.subscription.overdue.view');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return t('payment.customer.pastDue.view');
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const renderPayButton = (
|
|
261
|
+
item: SummaryItem,
|
|
262
|
+
primaryButton = true,
|
|
263
|
+
props: {
|
|
264
|
+
variant?: 'contained' | 'text';
|
|
265
|
+
sx?: SxProps;
|
|
266
|
+
} = {
|
|
267
|
+
variant: 'contained',
|
|
268
|
+
}
|
|
269
|
+
) => {
|
|
270
|
+
const { currency } = item;
|
|
271
|
+
const inProcess = payLoading && selectCurrencyId === currency.id;
|
|
272
|
+
const status = paymentStatus[currency.id] || 'idle';
|
|
273
|
+
|
|
274
|
+
if (status === 'success') {
|
|
275
|
+
return (
|
|
276
|
+
<Button
|
|
277
|
+
// eslint-disable-next-line react/prop-types
|
|
278
|
+
variant={props?.variant || 'contained'}
|
|
279
|
+
size="small"
|
|
280
|
+
{...(primaryButton
|
|
281
|
+
? {}
|
|
282
|
+
: {
|
|
283
|
+
color: 'success',
|
|
284
|
+
startIcon: <CheckCircleIcon />,
|
|
285
|
+
})}>
|
|
286
|
+
{t('payment.subscription.overdue.paid')}
|
|
287
|
+
</Button>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (status === 'error') {
|
|
292
|
+
return (
|
|
293
|
+
<Button variant="contained" size="small" onClick={() => handlePay(item)} {...props}>
|
|
294
|
+
{t('payment.subscription.overdue.retry')}
|
|
295
|
+
</Button>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
159
299
|
if (item.method.type === 'stripe') {
|
|
160
300
|
return (
|
|
161
|
-
<Button variant="contained" color="primary" onClick={() => window.open(
|
|
301
|
+
<Button variant="contained" color="primary" onClick={() => window.open(detailUrl, '_blank')} {...props}>
|
|
162
302
|
{t('payment.subscription.overdue.viewNow')}
|
|
163
303
|
</Button>
|
|
164
304
|
);
|
|
165
305
|
}
|
|
166
306
|
return (
|
|
167
|
-
<
|
|
168
|
-
|
|
307
|
+
<LoadingButton
|
|
308
|
+
variant="contained"
|
|
309
|
+
size="small"
|
|
310
|
+
disabled={inProcess}
|
|
311
|
+
loading={inProcess}
|
|
312
|
+
onClick={() => handlePay(item)}
|
|
313
|
+
{...props}>
|
|
169
314
|
{t('payment.subscription.overdue.payNow')}
|
|
170
|
-
</
|
|
315
|
+
</LoadingButton>
|
|
171
316
|
);
|
|
172
317
|
};
|
|
173
318
|
|
|
319
|
+
const getOverdueTitle = () => {
|
|
320
|
+
if (subscriptionId && data.subscription) {
|
|
321
|
+
if (summaryList.length === 1) {
|
|
322
|
+
return t('payment.subscription.overdue.title', {
|
|
323
|
+
name: data.subscription?.description,
|
|
324
|
+
count: data.invoices?.length,
|
|
325
|
+
total: formatAmount(summaryList[0]?.amount, summaryList[0]?.currency?.decimal),
|
|
326
|
+
symbol: summaryList[0]?.currency?.symbol,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return t('payment.subscription.overdue.simpleTitle', {
|
|
330
|
+
name: data.subscription?.description,
|
|
331
|
+
count: data.invoices?.length,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
if (customerId) {
|
|
335
|
+
if (summaryList.length === 1) {
|
|
336
|
+
return t('payment.customer.overdue.title', {
|
|
337
|
+
subscriptionCount: data.subscriptionCount || 0,
|
|
338
|
+
count: data.invoices?.length,
|
|
339
|
+
total: formatAmount(summaryList[0]?.amount, summaryList[0]?.currency?.decimal),
|
|
340
|
+
symbol: summaryList[0]?.currency?.symbol,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
return t('payment.customer.overdue.simpleTitle', {
|
|
344
|
+
subscriptionCount: data.subscriptionCount || 0,
|
|
345
|
+
count: data.invoices?.length,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return '';
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const getEmptyStateMessage = () => {
|
|
353
|
+
if (subscriptionId && data.subscription) {
|
|
354
|
+
return t('payment.subscription.overdue.empty', {
|
|
355
|
+
name: data.subscription?.description,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
if (customerId) {
|
|
359
|
+
return t('payment.customer.overdue.empty');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return '';
|
|
363
|
+
};
|
|
364
|
+
|
|
174
365
|
if (mode === 'custom' && children && typeof children === 'function') {
|
|
175
366
|
return (
|
|
176
367
|
<Stack>
|
|
177
368
|
{children(handlePay, {
|
|
178
|
-
subscription: data?.subscription
|
|
179
|
-
subscriptionUrl,
|
|
369
|
+
subscription: data?.subscription,
|
|
180
370
|
summary: data?.summary as { [key: string]: SummaryItem },
|
|
181
371
|
invoices: data?.invoices as Invoice[],
|
|
372
|
+
subscriptionCount: data?.subscriptionCount,
|
|
373
|
+
detailUrl,
|
|
182
374
|
})}
|
|
183
375
|
</Stack>
|
|
184
376
|
);
|
|
185
377
|
}
|
|
186
378
|
|
|
187
|
-
// Default mode
|
|
188
379
|
return (
|
|
189
380
|
<Dialog
|
|
190
381
|
PaperProps={{
|
|
@@ -201,12 +392,7 @@ function OverdueInvoicePayment({
|
|
|
201
392
|
<Stack gap={1}>
|
|
202
393
|
{summaryList.length === 0 && (
|
|
203
394
|
<>
|
|
204
|
-
<Alert severity="success">
|
|
205
|
-
{t('payment.subscription.overdue.empty', {
|
|
206
|
-
// @ts-ignore
|
|
207
|
-
name: data?.subscription?.description,
|
|
208
|
-
})}
|
|
209
|
-
</Alert>
|
|
395
|
+
<Alert severity="success">{getEmptyStateMessage()}</Alert>
|
|
210
396
|
<Stack direction="row" justifyContent="flex-end" mt={2}>
|
|
211
397
|
<Button variant="outlined" color="primary" onClick={handleClose} sx={{ width: 'fit-content' }}>
|
|
212
398
|
{/* @ts-ignore */}
|
|
@@ -218,23 +404,21 @@ function OverdueInvoicePayment({
|
|
|
218
404
|
{summaryList.length === 1 && (
|
|
219
405
|
<>
|
|
220
406
|
<Typography color="text.secondary" variant="body1">
|
|
221
|
-
{
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
{t('payment.subscription.overdue.view')}
|
|
237
|
-
</a>
|
|
407
|
+
{getOverdueTitle()}
|
|
408
|
+
{detailLinkOptions.enabled && (
|
|
409
|
+
<>
|
|
410
|
+
<br />
|
|
411
|
+
{t('payment.subscription.overdue.description')}
|
|
412
|
+
<a
|
|
413
|
+
href={detailUrl}
|
|
414
|
+
target="_blank"
|
|
415
|
+
onClick={handleViewDetailClick}
|
|
416
|
+
rel="noreferrer"
|
|
417
|
+
style={{ color: 'var(--foregrounds-fg-interactive, 0086FF)' }}>
|
|
418
|
+
{getDetailLinkText()}
|
|
419
|
+
</a>
|
|
420
|
+
</>
|
|
421
|
+
)}
|
|
238
422
|
</Typography>
|
|
239
423
|
<Stack direction="row" justifyContent="flex-end" gap={2} mt={2}>
|
|
240
424
|
<Button variant="outlined" color="primary" onClick={handleClose}>
|
|
@@ -249,22 +433,21 @@ function OverdueInvoicePayment({
|
|
|
249
433
|
{summaryList.length > 1 && (
|
|
250
434
|
<>
|
|
251
435
|
<Typography color="text.secondary" variant="body1">
|
|
252
|
-
{
|
|
253
|
-
{
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
</a>
|
|
436
|
+
{getOverdueTitle()}
|
|
437
|
+
{detailLinkOptions.enabled && (
|
|
438
|
+
<>
|
|
439
|
+
<br />
|
|
440
|
+
{t('payment.subscription.overdue.description')}
|
|
441
|
+
<a
|
|
442
|
+
href={detailUrl}
|
|
443
|
+
target="_blank"
|
|
444
|
+
rel="noreferrer"
|
|
445
|
+
onClick={handleViewDetailClick}
|
|
446
|
+
style={{ color: 'var(--foregrounds-fg-interactive, 0086FF)' }}>
|
|
447
|
+
{getDetailLinkText()}
|
|
448
|
+
</a>
|
|
449
|
+
</>
|
|
450
|
+
)}
|
|
268
451
|
</Typography>
|
|
269
452
|
<Typography color="text.secondary" variant="body1">
|
|
270
453
|
{t('payment.subscription.overdue.list')}
|
|
@@ -291,7 +474,7 @@ function OverdueInvoicePayment({
|
|
|
291
474
|
currency: item?.currency?.symbol,
|
|
292
475
|
})}
|
|
293
476
|
</Typography>
|
|
294
|
-
{renderPayButton(item, {
|
|
477
|
+
{renderPayButton(item, false, {
|
|
295
478
|
variant: 'text',
|
|
296
479
|
sx: {
|
|
297
480
|
color: 'text.link',
|
|
@@ -315,7 +498,10 @@ OverdueInvoicePayment.defaultProps = {
|
|
|
315
498
|
open: true,
|
|
316
499
|
},
|
|
317
500
|
children: null,
|
|
318
|
-
|
|
501
|
+
detailLinkOptions: { enabled: true },
|
|
502
|
+
subscriptionId: undefined,
|
|
503
|
+
customerId: undefined,
|
|
504
|
+
successToast: true,
|
|
319
505
|
};
|
|
320
506
|
|
|
321
507
|
export default OverdueInvoicePayment;
|
package/src/contexts/payment.tsx
CHANGED
|
@@ -36,6 +36,20 @@ export type PaymentContextProps = {
|
|
|
36
36
|
baseUrl?: string;
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
+
const formatData = (data: any) => {
|
|
40
|
+
if (!data) {
|
|
41
|
+
return {
|
|
42
|
+
paymentMethods: [],
|
|
43
|
+
baseCurrency: {},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
...data,
|
|
48
|
+
paymentMethods: data.paymentMethods || [],
|
|
49
|
+
baseCurrency: data.baseCurrency || {},
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
39
53
|
// @ts-ignore
|
|
40
54
|
const PaymentContext = createContext<PaymentContextType>({ api });
|
|
41
55
|
const { Provider, Consumer } = PaymentContext;
|
|
@@ -95,7 +109,7 @@ function PaymentProvider({ session, connect, children, baseUrl }: PaymentContext
|
|
|
95
109
|
connect,
|
|
96
110
|
prefix,
|
|
97
111
|
livemode: !!livemode,
|
|
98
|
-
settings: data as Settings,
|
|
112
|
+
settings: formatData(data) as Settings,
|
|
99
113
|
getCurrency: (currencyId: string) => getCurrency(currencyId, (data as Settings)?.paymentMethods || []),
|
|
100
114
|
getMethod: (methodId: string) => getMethod(methodId, (data as Settings)?.paymentMethods || []),
|
|
101
115
|
refresh: run,
|
|
@@ -8,7 +8,7 @@ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
|
8
8
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
9
9
|
import type { Paginated, TInvoiceExpanded, TSubscription } from '@blocklet/payment-types';
|
|
10
10
|
import { OpenInNewOutlined } from '@mui/icons-material';
|
|
11
|
-
import { Box, Button, CircularProgress,
|
|
11
|
+
import { Box, Button, CircularProgress, Stack, Typography, Tooltip } from '@mui/material';
|
|
12
12
|
import { styled } from '@mui/system';
|
|
13
13
|
import { useInfiniteScroll, useRequest, useSetState } from 'ahooks';
|
|
14
14
|
import React, { useEffect, useRef, useState } from 'react';
|
|
@@ -186,9 +186,10 @@ const InvoiceTable = React.memo((props: Props & { onPay: (invoiceId: string) =>
|
|
|
186
186
|
options: {
|
|
187
187
|
customBodyRenderLite: (_: string, index: number) => {
|
|
188
188
|
const invoice = data?.list[index] as TInvoiceExpanded;
|
|
189
|
+
const isVoid = invoice.status === 'void';
|
|
189
190
|
return (
|
|
190
191
|
<Box onClick={(e) => handleLinkClick(e, invoice)} sx={linkStyle}>
|
|
191
|
-
<Typography>
|
|
192
|
+
<Typography sx={isVoid ? { textDecoration: 'line-through' } : {}}>
|
|
192
193
|
{formatBNStr(invoice.total, invoice.paymentCurrency.decimal)}
|
|
193
194
|
{invoice.paymentCurrency.symbol}
|
|
194
195
|
</Typography>
|
|
@@ -264,6 +265,8 @@ const InvoiceTable = React.memo((props: Props & { onPay: (invoiceId: string) =>
|
|
|
264
265
|
const invoice = data?.list[index] as TInvoiceExpanded;
|
|
265
266
|
const hidePay = invoice.billing_reason === 'overdraft-protection';
|
|
266
267
|
const { connect } = getInvoiceLink(invoice, action);
|
|
268
|
+
const isVoid = invoice.status === 'void';
|
|
269
|
+
|
|
267
270
|
if (action && !hidePay) {
|
|
268
271
|
return connect ? (
|
|
269
272
|
<Button variant="text" size="small" onClick={() => onPay(invoice.id)} sx={{ color: 'text.link' }}>
|
|
@@ -283,7 +286,15 @@ const InvoiceTable = React.memo((props: Props & { onPay: (invoiceId: string) =>
|
|
|
283
286
|
}
|
|
284
287
|
return (
|
|
285
288
|
<Box onClick={(e) => handleLinkClick(e, invoice)} sx={linkStyle}>
|
|
286
|
-
|
|
289
|
+
{isVoid ? (
|
|
290
|
+
<Tooltip title={t('payment.customer.invoice.noPaymentRequired')} arrow placement="top">
|
|
291
|
+
<span>
|
|
292
|
+
<Status label={invoice.status} color={getInvoiceStatusColor(invoice.status)} />
|
|
293
|
+
</span>
|
|
294
|
+
</Tooltip>
|
|
295
|
+
) : (
|
|
296
|
+
<Status label={invoice.status} color={getInvoiceStatusColor(invoice.status)} />
|
|
297
|
+
)}
|
|
287
298
|
</Box>
|
|
288
299
|
);
|
|
289
300
|
},
|
|
@@ -448,6 +459,7 @@ const InvoiceList = React.memo((props: Props & { onPay: (invoiceId: string) => v
|
|
|
448
459
|
<Typography sx={{ fontWeight: 'bold', color: 'text.secondary', mt: 2, mb: 1 }}>{date}</Typography>
|
|
449
460
|
{invoices.map((invoice) => {
|
|
450
461
|
const { link, connect } = getInvoiceLink(invoice, action);
|
|
462
|
+
const isVoid = invoice.status === 'void';
|
|
451
463
|
return (
|
|
452
464
|
<Stack
|
|
453
465
|
key={invoice.id}
|
|
@@ -469,15 +481,19 @@ const InvoiceList = React.memo((props: Props & { onPay: (invoiceId: string) => v
|
|
|
469
481
|
<Stack direction="row" alignItems="center" spacing={0.5}>
|
|
470
482
|
<Typography component="span">{invoice.number}</Typography>
|
|
471
483
|
{link.external && (
|
|
472
|
-
<
|
|
473
|
-
|
|
474
|
-
|
|
484
|
+
<OpenInNewOutlined
|
|
485
|
+
fontSize="small"
|
|
486
|
+
sx={{
|
|
487
|
+
color: 'text.secondary',
|
|
488
|
+
display: { xs: 'none', md: 'inline-flex' },
|
|
489
|
+
}}
|
|
490
|
+
/>
|
|
475
491
|
)}
|
|
476
492
|
</Stack>
|
|
477
493
|
</a>
|
|
478
494
|
</Box>
|
|
479
495
|
<Box flex={1} textAlign="right">
|
|
480
|
-
<Typography>
|
|
496
|
+
<Typography sx={isVoid ? { textDecoration: 'line-through' } : {}}>
|
|
481
497
|
{formatBNStr(invoice.total, invoice.paymentCurrency.decimal)}
|
|
482
498
|
{invoice.paymentCurrency.symbol}
|
|
483
499
|
</Typography>
|
|
@@ -486,11 +502,13 @@ const InvoiceList = React.memo((props: Props & { onPay: (invoiceId: string) => v
|
|
|
486
502
|
<Typography>{formatToDate(invoice.created_at, locale, 'HH:mm:ss')}</Typography>
|
|
487
503
|
</Box>
|
|
488
504
|
{!action && (
|
|
489
|
-
<
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
505
|
+
<Box
|
|
506
|
+
flex={2}
|
|
507
|
+
className="invoice-description"
|
|
508
|
+
textAlign="right"
|
|
509
|
+
sx={{ display: { xs: 'none', lg: 'inline-flex' } }}>
|
|
510
|
+
<Typography>{invoice.description || invoice.id}</Typography>
|
|
511
|
+
</Box>
|
|
494
512
|
)}
|
|
495
513
|
<Box flex={1} textAlign="right">
|
|
496
514
|
{action ? (
|
|
@@ -514,6 +532,12 @@ const InvoiceList = React.memo((props: Props & { onPay: (invoiceId: string) => v
|
|
|
514
532
|
{t('payment.customer.invoice.pay')}
|
|
515
533
|
</Button>
|
|
516
534
|
)
|
|
535
|
+
) : isVoid ? (
|
|
536
|
+
<Tooltip title={t('payment.customer.invoice.noPaymentRequired')} arrow placement="top">
|
|
537
|
+
<span>
|
|
538
|
+
<Status label={invoice.status} color={getInvoiceStatusColor(invoice.status)} />
|
|
539
|
+
</span>
|
|
540
|
+
</Tooltip>
|
|
517
541
|
) : (
|
|
518
542
|
<Status label={invoice.status} color={getInvoiceStatusColor(invoice.status)} />
|
|
519
543
|
)}
|
|
@@ -601,9 +625,6 @@ CustomerInvoiceList.defaultProps = {
|
|
|
601
625
|
|
|
602
626
|
const Root = styled(Stack)`
|
|
603
627
|
@media (max-width: ${({ theme }) => theme.breakpoints.values.md}px) {
|
|
604
|
-
.invoice-description {
|
|
605
|
-
display: none !important;
|
|
606
|
-
}
|
|
607
628
|
svg.MuiSvgIcon-root {
|
|
608
629
|
display: none !important;
|
|
609
630
|
}
|