@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.
@@ -1,15 +1,19 @@
1
+ /* eslint-disable @typescript-eslint/indent */
1
2
  import { useEffect, useMemo, useState } from 'react';
2
- import { Button, Typography, Stack, CircularProgress, Alert } from '@mui/material';
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: string;
31
+ subscriptionId?: string;
32
+ customerId?: string;
22
33
  mode?: 'default' | 'custom';
23
- onPaid?: (subscriptionId: string, currencyId: string) => void;
34
+ onPaid?: (id: string, currencyId: string, type: 'subscription' | 'customer') => void;
24
35
  dialogProps?: DialogProps;
25
- inSubscriptionDetail?: boolean;
36
+ detailLinkOptions?: DetailLinkOptions;
37
+ successToast?: boolean;
26
38
  children?: (
27
39
  handlePay: (item: SummaryItem) => void,
28
40
  data: {
29
- subscription: Subscription;
41
+ subscription?: Subscription;
30
42
  summary: { [key: string]: SummaryItem };
31
43
  invoices: Invoice[];
32
- subscriptionUrl: string;
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
- const fetchOverdueInvoices = async (
44
- subscriptionId: string
45
- ): Promise<{
46
- subscription: Subscription;
56
+ type OverdueInvoicesResult = {
57
+ subscription?: Subscription;
47
58
  summary: { [key: string]: SummaryItem };
48
59
  invoices: Invoice[];
49
- }> => {
50
- const res = await api.get(`/api/subscriptions/${subscriptionId}/overdue/invoices`);
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
- inSubscriptionDetail = false,
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 uniqueKey = `${response.subscription_id}-${response.currency_id}`;
92
- if (response.subscription_id === subscriptionId && !processedCurrencies[uniqueKey]) {
93
- Toast.success(t('payment.customer.invoice.paySuccess'));
94
- setPayLoading(false);
95
- setProcessedCurrencies({ ...processedCurrencies, [uniqueKey]: 1 });
96
- refresh().then((res) => {
97
- if (res.invoices?.length === 0) {
98
- setDialogOpen(false);
99
- onPaid(subscriptionId, response.currency_id);
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: { currencyId: currency.id, subscriptionId },
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 (inSubscriptionDetail) {
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 renderPayButton = (item: SummaryItem, props: any) => {
158
- const isPayLoading = payLoading && item.currency.id === selectCurrencyId;
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(subscriptionUrl, '_blank')} {...props}>
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
- <Button variant="contained" color="primary" onClick={() => handlePay(item)} {...props} disabled={isPayLoading}>
168
- {isPayLoading && <CircularProgress size={14} sx={{ mr: 1, color: 'text.lighter' }} />}
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
- </Button>
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 as 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
- {t('payment.subscription.overdue.title', {
222
- // @ts-ignore
223
- name: data?.subscription?.description,
224
- count: data?.invoices?.length,
225
- total: formatAmount(summaryList[0]?.amount, summaryList[0]?.currency?.decimal),
226
- symbol: summaryList[0]?.currency?.symbol,
227
- })}
228
- <br />
229
- {t('payment.subscription.overdue.description')}
230
- <a
231
- href={subscriptionUrl}
232
- target="_blank"
233
- onClick={handleViewDetailClick}
234
- rel="noreferrer"
235
- style={{ color: 'var(--foregrounds-fg-interactive, 0086FF)' }}>
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
- {/* @ts-ignore */}
253
- {t('payment.subscription.overdue.simpleTitle', {
254
- // @ts-ignore
255
- name: data?.subscription?.description,
256
- count: data?.invoices?.length,
257
- })}
258
- <br />
259
- {t('payment.subscription.overdue.description')}
260
- <a
261
- href={subscriptionUrl}
262
- target="_blank"
263
- rel="noreferrer"
264
- onClick={handleViewDetailClick}
265
- style={{ color: 'var(--foregrounds-fg-interactive, 0086FF)' }}>
266
- {t('payment.subscription.overdue.view')}
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
- inSubscriptionDetail: false,
501
+ detailLinkOptions: { enabled: true },
502
+ subscriptionId: undefined,
503
+ customerId: undefined,
504
+ successToast: true,
319
505
  };
320
506
 
321
507
  export default OverdueInvoicePayment;
@@ -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, Hidden, Stack, Typography } from '@mui/material';
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)}&nbsp;
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
- <Status label={invoice.status} color={getInvoiceStatusColor(invoice.status)} />
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
- <Hidden mdDown>
473
- <OpenInNewOutlined fontSize="small" sx={{ color: 'text.secondary' }} />
474
- </Hidden>
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)}&nbsp;
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
- <Hidden mdDown>
490
- <Box flex={2} className="invoice-description" textAlign="right">
491
- <Typography>{invoice.description || invoice.id}</Typography>
492
- </Box>
493
- </Hidden>
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
  }