payment-kit 1.18.14 → 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.
Files changed (31) hide show
  1. package/api/src/index.ts +2 -0
  2. package/api/src/libs/invoice.ts +5 -3
  3. package/api/src/routes/connect/change-payment.ts +9 -1
  4. package/api/src/routes/connect/change-plan.ts +9 -1
  5. package/api/src/routes/connect/collect-batch.ts +7 -5
  6. package/api/src/routes/connect/recharge-account.ts +124 -0
  7. package/api/src/routes/connect/setup.ts +8 -1
  8. package/api/src/routes/connect/shared.ts +110 -48
  9. package/api/src/routes/connect/subscribe.ts +11 -1
  10. package/api/src/routes/customers.ts +149 -6
  11. package/api/src/routes/invoices.ts +46 -0
  12. package/api/src/routes/payment-currencies.ts +5 -2
  13. package/api/src/routes/subscriptions.ts +0 -3
  14. package/blocklet.yml +1 -1
  15. package/package.json +8 -8
  16. package/src/app.tsx +11 -3
  17. package/src/components/info-card.tsx +3 -1
  18. package/src/components/info-row.tsx +1 -0
  19. package/src/components/invoice/recharge.tsx +85 -56
  20. package/src/components/invoice/table.tsx +7 -1
  21. package/src/components/subscription/portal/actions.tsx +1 -1
  22. package/src/components/subscription/portal/list.tsx +6 -0
  23. package/src/locales/en.tsx +9 -0
  24. package/src/locales/zh.tsx +9 -0
  25. package/src/pages/admin/settings/vault-config/index.tsx +21 -6
  26. package/src/pages/customer/index.tsx +160 -265
  27. package/src/pages/customer/invoice/detail.tsx +24 -16
  28. package/src/pages/customer/invoice/past-due.tsx +45 -23
  29. package/src/pages/customer/recharge/account.tsx +515 -0
  30. package/src/pages/customer/{recharge.tsx → recharge/subscription.tsx} +11 -11
  31. package/src/pages/customer/subscription/embed.tsx +16 -1
@@ -1,24 +1,19 @@
1
- import DID from '@arcblock/ux/lib/DID';
2
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
- import Toast from '@arcblock/ux/lib/Toast';
4
2
  import {
5
3
  CustomerInvoiceList,
6
4
  formatBNStr,
7
5
  formatError,
8
- getCustomerAvatar,
9
6
  getPrefix,
10
- TruncatedText,
11
- useMobile,
12
7
  usePaymentContext,
8
+ OverdueInvoicePayment,
13
9
  } from '@blocklet/payment-react';
14
10
  import type { GroupedBN, TCustomerExpanded } from '@blocklet/payment-types';
15
- import { ExpandMore } from '@mui/icons-material';
11
+ import { ExpandMore, AccountBalanceWalletOutlined } from '@mui/icons-material';
16
12
  import {
17
13
  Avatar,
18
14
  Box,
19
15
  Button,
20
16
  FormControl,
21
- Grid,
22
17
  MenuItem,
23
18
  Select,
24
19
  Skeleton,
@@ -31,13 +26,10 @@ import type { SelectChangeEvent } from '@mui/material/Select';
31
26
  import { styled, SxProps } from '@mui/system';
32
27
  import { useRequest, useSetState } from 'ahooks';
33
28
  import { flatten, isEmpty } from 'lodash';
34
- import { memo, useEffect, useMemo, useState } from 'react';
35
- import { FlagEmoji, defaultCountries, parseCountry } from 'react-international-phone';
29
+ import { memo, useEffect, useState } from 'react';
36
30
  import { useNavigate } from 'react-router-dom';
37
31
  import { joinURL } from 'ufo';
38
32
 
39
- import EditCustomer from '../../components/customer/edit';
40
- import InfoRow from '../../components/info-row';
41
33
  import { useTransitionContext } from '../../components/progress-bar';
42
34
  import CurrentSubscriptions from '../../components/subscription/portal/list';
43
35
  import { useSessionContext } from '../../contexts/session';
@@ -58,6 +50,7 @@ const CurrencyCard = memo(
58
50
  data,
59
51
  currency,
60
52
  sx,
53
+ type,
61
54
  }: {
62
55
  label: string;
63
56
  data: {
@@ -66,16 +59,35 @@ const CurrencyCard = memo(
66
59
  currency: {
67
60
  id: string;
68
61
  decimal: number;
62
+ payment_method_id: string;
69
63
  };
70
64
  sx: SxProps;
65
+ type: 'balance' | 'spent' | 'stake' | 'refund' | 'due';
71
66
  }) => {
67
+ const navigate = useNavigate();
68
+ const { settings } = usePaymentContext();
69
+ const method = settings?.paymentMethods?.find((m) => m.id === currency.payment_method_id);
70
+ const { t } = useLocaleContext();
72
71
  const safeData = data || {};
73
72
  const value = formatBNStr(safeData[currency?.id], currency?.decimal, 6, false);
73
+ const hideTypes = ['stake', 'refund', 'due'];
74
+ if (hideTypes.includes(type) && (!safeData[currency?.id] || safeData[currency?.id] === '0')) {
75
+ return null;
76
+ }
77
+
78
+ const handleCardClick = () => {
79
+ if (type === 'balance' && currency?.id) {
80
+ navigate(`/customer/recharge/${currency.id}`);
81
+ }
82
+ };
83
+
74
84
  return (
75
85
  <Box
76
86
  sx={{
77
87
  padding: '12px 16px',
78
88
  borderRadius: 'var(--radius-m, 8px)',
89
+ transition: 'all 0.2s ease-in-out',
90
+ position: 'relative',
79
91
  ...sx,
80
92
  }}>
81
93
  <Typography variant="h4" gutterBottom>
@@ -84,6 +96,26 @@ const CurrencyCard = memo(
84
96
  <Typography variant="h3" gutterBottom>
85
97
  {value}
86
98
  </Typography>
99
+ {type === 'balance' && method?.type === 'arcblock' && (
100
+ <Tooltip title={t('customer.recharge.rechargeTooltip')} arrow placement="top">
101
+ <Box
102
+ onClick={handleCardClick}
103
+ sx={{
104
+ position: 'absolute',
105
+ top: '12px',
106
+ right: '12px',
107
+ display: 'flex',
108
+ alignItems: 'center',
109
+ justifyContent: 'center',
110
+ borderRadius: '50%',
111
+ padding: '4px',
112
+ zIndex: 1,
113
+ cursor: 'pointer',
114
+ }}>
115
+ <AccountBalanceWalletOutlined fontSize="small" />
116
+ </Box>
117
+ </Tooltip>
118
+ )}
87
119
  </Box>
88
120
  );
89
121
  }
@@ -104,58 +136,19 @@ function SummaryCardSkeleton() {
104
136
  return (
105
137
  <Box className="base-card section section-summary">
106
138
  <Skeleton variant="text" width={150} height={32} sx={{ mb: 2 }} />
107
- <Stack gap={1}>
108
- <Stack direction="row" spacing={2}>
109
- <Skeleton variant="rectangular" height={88} width="100%" />
110
- <Skeleton variant="rectangular" height={88} width="100%" />
111
- </Stack>
112
- <Stack direction="row" spacing={2}>
113
- <Skeleton variant="rectangular" height={88} width="100%" />
114
- <Skeleton variant="rectangular" height={88} width="100%" />
115
- </Stack>
116
- <Stack direction="row">
117
- <Skeleton variant="rectangular" height={88} width="calc(50% - 8px)" />
118
- </Stack>
119
- </Stack>
120
- </Box>
121
- );
122
- }
123
-
124
- function DetailCardSkeleton() {
125
- return (
126
- <Box className="base-card section section-detail">
127
- <Box className="section-header" display="flex" justifyContent="space-between" alignItems="center" mb={2}>
128
- <Skeleton variant="text" width={150} height={32} />
129
- <Skeleton variant="text" width={80} height={20} />
130
- </Box>
131
- <Stack direction="row" spacing={2} mb={3}>
132
- <Skeleton variant="circular" width={48} height={48} sx={{ minWidth: '48px' }} />
133
- <Box width="100%">
134
- <Skeleton variant="text" width="80%" />
135
- <Skeleton variant="text" width="60%" />
136
- </Box>
137
- </Stack>
138
- <Stack spacing={1}>
139
- {[...Array(7)].map((_, i) => (
140
- // eslint-disable-next-line react/no-array-index-key
141
- <Stack key={i} direction="column" spacing={0.5}>
142
- <Skeleton variant="text" width="30%" />
143
- <Skeleton variant="text" width="70%" />
144
- </Stack>
145
- ))}
146
- </Stack>
139
+ <Skeleton variant="rectangular" height={88} width="100%" />
147
140
  </Box>
148
141
  );
149
142
  }
150
143
 
151
144
  export default function CustomerHome() {
152
145
  const { t } = useLocaleContext();
153
- const { events, session } = useSessionContext();
146
+ const { events } = useSessionContext();
154
147
  const { settings } = usePaymentContext();
155
148
  const [currency, setCurrency] = useState(settings?.baseCurrency);
156
149
  const [subscriptionLoading, setSubscriptionLoading] = useState(false);
157
150
  const currencies = flatten(
158
- settings.paymentMethods.map((method) =>
151
+ settings.paymentMethods?.map((method) =>
159
152
  (method.payment_currencies || []).map((c) => ({
160
153
  ...c,
161
154
  methodName: method.name,
@@ -171,8 +164,8 @@ export default function CustomerHome() {
171
164
  setOverdraftProtection: false,
172
165
  });
173
166
  const navigate = useNavigate();
174
- const { isMobile } = useMobile('lg');
175
167
  const [subscriptionStatus, setSubscriptionStatus] = useState(false);
168
+ const [hasSubscriptions, setHasSubscriptions] = useState(true);
176
169
  const { startTransition } = useTransitionContext();
177
170
  const {
178
171
  data,
@@ -184,11 +177,7 @@ export default function CustomerHome() {
184
177
  });
185
178
 
186
179
  const loadingCard = loading || !data;
187
-
188
- const countryDetail = useMemo(() => {
189
- const item = defaultCountries.find((v) => v[1] === data?.address?.country);
190
- return item ? parseCountry(item) : { name: '' };
191
- }, [data]);
180
+ const [showOverduePayment, setShowOverduePayment] = useState(false);
192
181
 
193
182
  useEffect(() => {
194
183
  runAsync();
@@ -221,20 +210,6 @@ export default function CustomerHome() {
221
210
  );
222
211
  }
223
212
 
224
- const onUpdateInfo = async (updates: TCustomerExpanded) => {
225
- try {
226
- setState({ loading: true });
227
- await api.put(`/api/customers/${data?.id}`, updates).then((res) => res.data);
228
- Toast.success(t('common.saved'));
229
- runAsync();
230
- } catch (err) {
231
- console.error(err);
232
- Toast.error(formatError(err));
233
- } finally {
234
- setState({ loading: false });
235
- }
236
- };
237
-
238
213
  const onToggleActive = (e: SelectChangeEvent) => {
239
214
  setSubscriptionLoading(true);
240
215
  setState({ onlyActive: e.target.value === 'active' });
@@ -247,58 +222,58 @@ export default function CustomerHome() {
247
222
  setCurrency(newCurrency);
248
223
  };
249
224
 
250
- const SubscriptionCard = loadingCard ? (
251
- <CardSkeleton height={200} />
252
- ) : (
253
- <Box className="base-card section section-subscription">
254
- <Box className="section-header">
255
- <Typography variant="h3">{t('admin.subscription.name')}</Typography>
256
- {subscriptionStatus && (
257
- <FormControl
258
- sx={{
259
- '.MuiInputBase-root': {
260
- background: 'none',
261
- border: 'none',
262
- },
263
- '.MuiOutlinedInput-notchedOutline': {
264
- border: 'none',
265
- },
266
- }}>
267
- <Select
268
- value={state.onlyActive ? 'active' : ''}
269
- onChange={onToggleActive}
270
- displayEmpty
271
- IconComponent={ExpandMore}
272
- inputProps={{ 'aria-label': 'Without label' }}>
273
- <MenuItem value="">All</MenuItem>
274
- <MenuItem value="active">Active</MenuItem>
275
- </Select>
276
- </FormControl>
277
- )}
278
- </Box>
279
- <Box className="section-body">
280
- {subscriptionLoading ? (
281
- <Box>{t('common.loading')}</Box>
282
- ) : (
283
- <CurrentSubscriptions
284
- id={data?.id}
285
- onlyActive={state.onlyActive}
286
- changeActive={(v) => setState({ onlyActive: v })}
287
- status={state.onlyActive ? 'active,trialing,past_due' : 'active,trialing,paused,past_due,canceled'}
288
- style={{
289
- cursor: 'pointer',
290
- }}
291
- onClickSubscription={(subscription) => {
292
- startTransition(() => {
293
- navigate(`/customer/subscription/${subscription.id}`);
294
- });
295
- }}
296
- setStatusState={setSubscriptionStatus}
297
- />
298
- )}
225
+ const SubscriptionCard =
226
+ loadingCard || !hasSubscriptions ? null : (
227
+ <Box className="base-card section section-subscription">
228
+ <Box className="section-header">
229
+ <Typography variant="h3">{t('admin.subscription.name')}</Typography>
230
+ {subscriptionStatus && (
231
+ <FormControl
232
+ sx={{
233
+ '.MuiInputBase-root': {
234
+ background: 'none',
235
+ border: 'none',
236
+ },
237
+ '.MuiOutlinedInput-notchedOutline': {
238
+ border: 'none',
239
+ },
240
+ }}>
241
+ <Select
242
+ value={state.onlyActive ? 'active' : ''}
243
+ onChange={onToggleActive}
244
+ displayEmpty
245
+ IconComponent={ExpandMore}
246
+ inputProps={{ 'aria-label': 'Without label' }}>
247
+ <MenuItem value="">All</MenuItem>
248
+ <MenuItem value="active">Active</MenuItem>
249
+ </Select>
250
+ </FormControl>
251
+ )}
252
+ </Box>
253
+ <Box className="section-body">
254
+ {subscriptionLoading ? (
255
+ <Box>{t('common.loading')}</Box>
256
+ ) : (
257
+ <CurrentSubscriptions
258
+ id={data?.id}
259
+ onlyActive={state.onlyActive}
260
+ changeActive={(v) => setState({ onlyActive: v })}
261
+ status={state.onlyActive ? 'active,trialing,past_due' : 'active,trialing,paused,past_due,canceled'}
262
+ style={{
263
+ cursor: 'pointer',
264
+ }}
265
+ onClickSubscription={(subscription) => {
266
+ startTransition(() => {
267
+ navigate(`/customer/subscription/${subscription.id}`);
268
+ });
269
+ }}
270
+ setStatusState={setSubscriptionStatus}
271
+ setHasSubscriptions={setHasSubscriptions}
272
+ />
273
+ )}
274
+ </Box>
299
275
  </Box>
300
- </Box>
301
- );
276
+ );
302
277
 
303
278
  const SummaryCard = loadingCard ? (
304
279
  <SummaryCardSkeleton />
@@ -345,11 +320,14 @@ export default function CustomerHome() {
345
320
  </Box>
346
321
  <Stack
347
322
  className="section-body"
323
+ flexDirection="row"
348
324
  sx={{
349
- display: 'grid',
350
- gridTemplateColumns: '1fr 1fr',
351
325
  gap: 2,
352
326
  mt: 1.5,
327
+ flexWrap: 'wrap',
328
+ '>div': {
329
+ flex: 1,
330
+ },
353
331
  }}>
354
332
  <CurrencyCard
355
333
  label={t('admin.customer.summary.balance')}
@@ -359,30 +337,35 @@ export default function CustomerHome() {
359
337
  background: 'var(--tags-tag-orange-bg, #B7FEE3)',
360
338
  color: 'var(--tags-tag-orange-text, #007C52)',
361
339
  }}
340
+ type="balance"
362
341
  />
363
342
  <CurrencyCard
364
343
  label={t('admin.customer.summary.spent')}
365
344
  data={data?.summary?.paid || emptyObject}
366
345
  currency={currency}
367
346
  sx={{ background: 'var(--tags-tag-green-bg, #B7FEE3)', color: 'var(--tags-tag-green-text, #007C52)' }}
347
+ type="spent"
368
348
  />
369
349
  <CurrencyCard
370
350
  label={t('admin.customer.summary.stake')}
371
351
  data={data?.summary?.stake || emptyObject}
372
352
  currency={currency}
373
353
  sx={{ background: 'var(--tags-tag-blue-bg, #D2ECFF)', color: 'var(--tags-tag-blue-text, #0051E9)' }}
354
+ type="stake"
374
355
  />
375
356
  <CurrencyCard
376
357
  label={t('admin.customer.summary.refund')}
377
358
  data={data?.summary?.refund || emptyObject}
378
359
  currency={currency}
379
360
  sx={{ background: 'var(--tags-tag-purple-bg, #EFE9FF)', color: 'var(--tags-tag-purple-text, #007C52)' }}
361
+ type="refund"
380
362
  />
381
363
  <CurrencyCard
382
364
  label={t('admin.customer.summary.due')}
383
365
  data={data?.summary?.due || emptyObject}
384
366
  currency={currency}
385
367
  sx={{ background: 'var(--tags-tag-red-bg, #FFE2E6)', color: 'var(--tags-tag-red-text, #E40031)' }}
368
+ type="due"
386
369
  />
387
370
  </Stack>
388
371
  </>
@@ -418,121 +401,6 @@ export default function CustomerHome() {
418
401
  </Box>
419
402
  );
420
403
 
421
- const DetailCard = loadingCard ? (
422
- <DetailCardSkeleton />
423
- ) : (
424
- <Box className="base-card section section-detail">
425
- <Box className="section-header" sx={{ mb: 2 }}>
426
- <Typography variant="h3">{t('payment.customer.details')}</Typography>
427
- <Button
428
- variant="text"
429
- sx={{ color: 'text.link' }}
430
- disabled={state.editing || state.loading}
431
- onClick={() => setState({ editing: true })}>
432
- {t('common.edit')}
433
- </Button>
434
- </Box>
435
- <Box display="flex" alignItems="center" gap={1} sx={{ mb: 3 }}>
436
- <Avatar
437
- title={data?.name}
438
- src={getCustomerAvatar(data?.did, data?.updated_at ? new Date(data.updated_at).toISOString() : '', 48)}
439
- variant="circular"
440
- sx={{ width: 48, height: 48 }}
441
- />
442
- <Box sx={{ minWidth: 0, flexGrow: 1 }}>
443
- <Typography variant="h4" gutterBottom>
444
- {data?.name || t('common.none')}
445
- </Typography>
446
- <Typography variant="body2" color="text.secondary">
447
- {(data?.did || session.user?.did) && (
448
- <DID
449
- did={data?.did || session.user?.did || ''}
450
- copyable
451
- showQrcode
452
- chainId={livemode ? 'main' : 'beta'}
453
- />
454
- )}
455
- </Typography>
456
- </Box>
457
- </Box>
458
- <Stack>
459
- <InfoRow
460
- label={t('admin.customer.phone')}
461
- value={data?.phone}
462
- sizes={[1, 1]}
463
- alignItems="normal"
464
- direction="column"
465
- />
466
- <InfoRow
467
- label={t('admin.customer.email')}
468
- value={data?.email}
469
- sizes={[1, 1]}
470
- alignItems="normal"
471
- direction="column"
472
- />
473
- <InfoRow
474
- label={t('admin.customer.address.country')}
475
- value={
476
- data?.address?.country ? (
477
- <Box display="flex" alignItems="center" flexWrap="nowrap" gap={0.5} sx={{ cursor: 'pointer' }}>
478
- <FlagEmoji iso2={data.address?.country} style={{ display: 'flex', width: 24 }} />
479
- <Typography>{countryDetail?.name}</Typography>
480
- </Box>
481
- ) : (
482
- t('common.none')
483
- )
484
- }
485
- sizes={[1, 1]}
486
- alignItems="normal"
487
- direction="column"
488
- />
489
- <InfoRow
490
- label={t('admin.customer.address.state')}
491
- value={data?.address?.state}
492
- sizes={[1, 1]}
493
- alignItems="normal"
494
- direction="column"
495
- />
496
- <InfoRow
497
- label={t('admin.customer.address.city')}
498
- value={data?.address?.city && <TruncatedText text={data.address?.city} maxLength={280} useWidth />}
499
- sizes={[1, 1]}
500
- alignItems="normal"
501
- direction="column"
502
- />
503
- <InfoRow
504
- label={t('admin.customer.address.line1')}
505
- value={data?.address?.line1 && <TruncatedText text={data.address?.line1} maxLength={280} useWidth />}
506
- sizes={[1, 1]}
507
- alignItems="normal"
508
- direction="column"
509
- />
510
- <InfoRow
511
- label={t('admin.customer.address.line2')}
512
- value={data?.address?.line2 && <TruncatedText text={data.address?.line2} maxLength={280} useWidth />}
513
- sizes={[1, 1]}
514
- alignItems="normal"
515
- direction="column"
516
- />
517
- <InfoRow
518
- label={t('admin.customer.address.postal_code')}
519
- value={data?.address?.postal_code}
520
- sizes={[1, 1]}
521
- alignItems="normal"
522
- direction="column"
523
- />
524
- </Stack>
525
- {state.editing && (
526
- <EditCustomer
527
- data={data}
528
- loading={state.loading}
529
- onSave={onUpdateInfo}
530
- onCancel={() => setState({ editing: false })}
531
- />
532
- )}
533
- </Box>
534
- );
535
-
536
404
  const RevenueCard = loadingCard ? (
537
405
  <CardSkeleton height={200} />
538
406
  ) : (
@@ -551,31 +419,57 @@ export default function CustomerHome() {
551
419
  {data?.error}
552
420
  </Alert>
553
421
  )}
554
- {isMobile ? (
555
- <Root>
556
- {SummaryCard}
557
- {SubscriptionCard}
558
- {InvoiceCard}
559
- {RevenueCard}
560
- {DetailCard}
561
- </Root>
562
- ) : (
563
- <Grid container spacing={2.5}>
564
- <Grid item xs={12} md={8}>
565
- <Stack direction="column" spacing={2.5}>
566
- {SubscriptionCard}
567
- {InvoiceCard}
568
- {RevenueCard}
569
- </Stack>
570
- </Grid>
571
- <Grid item xs={12} md={4}>
572
- <Stack direction="column" spacing={3}>
573
- {SummaryCard}
574
- {DetailCard}
575
- </Stack>
576
- </Grid>
577
- </Grid>
422
+ {isEmpty(data?.summary?.due) === false && (
423
+ <Alert
424
+ severity="error"
425
+ sx={{
426
+ mb: 2,
427
+ '.MuiAlert-action': {
428
+ alignItems: 'center',
429
+ },
430
+ }}
431
+ action={
432
+ <Button
433
+ color="inherit"
434
+ size="small"
435
+ variant="outlined"
436
+ onClick={() => setShowOverduePayment(true)}
437
+ sx={{ whiteSpace: 'nowrap' }}>
438
+ {t('customer.pastDue.payNow')}
439
+ </Button>
440
+ }>
441
+ {t('customer.pastDue.alert')}
442
+ </Alert>
443
+ )}
444
+ {showOverduePayment && data && (
445
+ <OverdueInvoicePayment
446
+ customerId={data.id}
447
+ onPaid={() => {
448
+ setShowOverduePayment(false);
449
+ runAsync();
450
+ }}
451
+ successToast={false}
452
+ detailLinkOptions={{
453
+ enabled: true,
454
+ onClick: () => {
455
+ setShowOverduePayment(false);
456
+ navigate('/customer/invoice/past-due');
457
+ },
458
+ }}
459
+ dialogProps={{
460
+ open: showOverduePayment,
461
+ onClose: () => setShowOverduePayment(false),
462
+ title: t('customer.pastDue.title'),
463
+ }}
464
+ />
578
465
  )}
466
+ <Root>
467
+ {SummaryCard}
468
+ {SubscriptionCard}
469
+ {InvoiceCard}
470
+ {RevenueCard}
471
+ {/* {DetailCard} */}
472
+ </Root>
579
473
  </Content>
580
474
  );
581
475
  }
@@ -603,6 +497,7 @@ const Content = styled(Stack)`
603
497
  `;
604
498
 
605
499
  const Root = styled(Stack)`
500
+ gap: 24px;
606
501
  @media (max-width: ${({ theme }) => theme.breakpoints.values.xl}px) {
607
502
  padding: 0px;
608
503
  gap: 0;
@@ -17,7 +17,7 @@ import {
17
17
  } from '@blocklet/payment-react';
18
18
  import type { TCheckoutSession, TInvoiceExpanded, TPaymentLink } from '@blocklet/payment-types';
19
19
  import { ArrowBackOutlined } from '@mui/icons-material';
20
- import { Alert, Box, Button, CircularProgress, Divider, Stack, Typography } from '@mui/material';
20
+ import { Alert, Box, Button, CircularProgress, Divider, Stack, Tooltip, Typography } from '@mui/material';
21
21
  import { styled } from '@mui/system';
22
22
  import { useRequest, useSetState } from 'ahooks';
23
23
  import { useEffect } from 'react';
@@ -204,9 +204,31 @@ export default function CustomerInvoiceDetail() {
204
204
  }}>
205
205
  <InfoMetric
206
206
  label={t('common.status')}
207
- value={<Status label={data.status} color={getInvoiceStatusColor(data.status)} />}
207
+ value={
208
+ <Tooltip
209
+ title={data.status === 'void' ? t('payment.customer.invoice.noPaymentRequired') : ''}
210
+ arrow
211
+ placement="top">
212
+ <span>
213
+ <Status label={data.status} color={getInvoiceStatusColor(data.status)} />
214
+ </span>
215
+ </Tooltip>
216
+ }
208
217
  divider
209
218
  />
219
+ {data.subscription && (
220
+ <InfoMetric
221
+ label={t('admin.subscription.name')}
222
+ value={
223
+ <Link to={`/customer/subscription/${data.subscription.id}`}>
224
+ <Typography variant="body1" color="text.link">
225
+ {data.subscription.description || data.subscription.id}
226
+ </Typography>
227
+ </Link>
228
+ }
229
+ divider
230
+ />
231
+ )}
210
232
  <InfoMetric label={t('common.createdAt')} value={formatTime(data.created_at)} divider />
211
233
  {data.period_start > 0 && data.period_end > 0 && (
212
234
  <InfoMetric
@@ -301,20 +323,6 @@ export default function CustomerInvoiceDetail() {
301
323
  alignItems={InfoAlignItems}
302
324
  />
303
325
  )}
304
- {data.subscription && (
305
- <InfoRow
306
- label={t('admin.subscription.name')}
307
- value={
308
- <Link to={`/customer/subscription/${data.subscription.id}`}>
309
- <Typography variant="body1" color="text.link">
310
- {data.subscription.description || data.subscription.id}
311
- </Typography>
312
- </Link>
313
- }
314
- direction={InfoDirection}
315
- alignItems={InfoAlignItems}
316
- />
317
- )}
318
326
  {data?.relatedInvoice && (
319
327
  <InfoRow
320
328
  label={t('customer.invoice.relatedInvoice')}