payment-kit 1.18.15 → 1.18.17

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 (37) hide show
  1. package/api/src/index.ts +2 -0
  2. package/api/src/libs/invoice.ts +5 -3
  3. package/api/src/libs/notification/template/customer-reward-succeeded.ts +32 -14
  4. package/api/src/libs/session.ts +9 -1
  5. package/api/src/libs/util.ts +12 -4
  6. package/api/src/routes/checkout-sessions.ts +286 -120
  7. package/api/src/routes/connect/change-payment.ts +9 -1
  8. package/api/src/routes/connect/change-plan.ts +9 -1
  9. package/api/src/routes/connect/collect-batch.ts +7 -5
  10. package/api/src/routes/connect/pay.ts +1 -1
  11. package/api/src/routes/connect/recharge-account.ts +124 -0
  12. package/api/src/routes/connect/setup.ts +8 -1
  13. package/api/src/routes/connect/shared.ts +175 -54
  14. package/api/src/routes/connect/subscribe.ts +11 -1
  15. package/api/src/routes/customers.ts +150 -7
  16. package/api/src/routes/donations.ts +1 -1
  17. package/api/src/routes/invoices.ts +47 -1
  18. package/api/src/routes/subscriptions.ts +0 -3
  19. package/blocklet.yml +2 -1
  20. package/package.json +16 -16
  21. package/src/app.tsx +11 -3
  22. package/src/components/info-card.tsx +6 -2
  23. package/src/components/info-row.tsx +1 -0
  24. package/src/components/invoice/recharge.tsx +85 -56
  25. package/src/components/invoice/table.tsx +7 -1
  26. package/src/components/subscription/portal/actions.tsx +1 -1
  27. package/src/components/subscription/portal/list.tsx +6 -0
  28. package/src/locales/en.tsx +9 -0
  29. package/src/locales/zh.tsx +9 -0
  30. package/src/pages/admin/payments/payouts/detail.tsx +16 -5
  31. package/src/pages/customer/index.tsx +226 -284
  32. package/src/pages/customer/invoice/detail.tsx +24 -16
  33. package/src/pages/customer/invoice/past-due.tsx +46 -23
  34. package/src/pages/customer/payout/detail.tsx +16 -5
  35. package/src/pages/customer/recharge/account.tsx +513 -0
  36. package/src/pages/customer/{recharge.tsx → recharge/subscription.tsx} +22 -19
  37. package/src/pages/customer/subscription/embed.tsx +16 -1
@@ -1,24 +1,27 @@
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 {
12
+ ExpandMore,
13
+ AccountBalanceWalletOutlined,
14
+ AddOutlined,
15
+ CreditCardOutlined,
16
+ AccountBalanceOutlined,
17
+ InfoOutlined,
18
+ AssignmentReturnOutlined,
19
+ } from '@mui/icons-material';
16
20
  import {
17
21
  Avatar,
18
22
  Box,
19
23
  Button,
20
24
  FormControl,
21
- Grid,
22
25
  MenuItem,
23
26
  Select,
24
27
  Skeleton,
@@ -28,16 +31,13 @@ import {
28
31
  Alert,
29
32
  } from '@mui/material';
30
33
  import type { SelectChangeEvent } from '@mui/material/Select';
31
- import { styled, SxProps } from '@mui/system';
34
+ import { styled } from '@mui/system';
32
35
  import { useRequest, useSetState } from 'ahooks';
33
36
  import { flatten, isEmpty } from 'lodash';
34
- import { memo, useEffect, useMemo, useState } from 'react';
35
- import { FlagEmoji, defaultCountries, parseCountry } from 'react-international-phone';
37
+ import { memo, useEffect, useState } from 'react';
36
38
  import { useNavigate } from 'react-router-dom';
37
39
  import { joinURL } from 'ufo';
38
40
 
39
- import EditCustomer from '../../components/customer/edit';
40
- import InfoRow from '../../components/info-row';
41
41
  import { useTransitionContext } from '../../components/progress-bar';
42
42
  import CurrentSubscriptions from '../../components/subscription/portal/list';
43
43
  import { useSessionContext } from '../../contexts/session';
@@ -57,33 +57,80 @@ const CurrencyCard = memo(
57
57
  label,
58
58
  data,
59
59
  currency,
60
- sx,
60
+ type,
61
+ icon,
61
62
  }: {
62
63
  label: string;
63
64
  data: {
64
65
  [key: string]: string;
65
66
  };
67
+ icon: any;
66
68
  currency: {
67
69
  id: string;
68
70
  decimal: number;
71
+ payment_method_id: string;
72
+ symbol: string;
69
73
  };
70
- sx: SxProps;
74
+ type: 'balance' | 'spent' | 'stake' | 'refund' | 'due';
71
75
  }) => {
76
+ const navigate = useNavigate();
77
+ const { settings } = usePaymentContext();
78
+ const method = settings?.paymentMethods?.find((m) => m.id === currency.payment_method_id);
79
+ const { t } = useLocaleContext();
72
80
  const safeData = data || {};
73
81
  const value = formatBNStr(safeData[currency?.id], currency?.decimal, 6, false);
82
+ const hideTypes = ['stake', 'refund', 'due'];
83
+ if (hideTypes.includes(type) && (!safeData[currency?.id] || safeData[currency?.id] === '0')) {
84
+ return null;
85
+ }
86
+
87
+ const handleCardClick = () => {
88
+ if (type === 'balance' && currency?.id) {
89
+ navigate(`/customer/recharge/${currency.id}`);
90
+ }
91
+ };
92
+
74
93
  return (
75
94
  <Box
76
95
  sx={{
77
- padding: '12px 16px',
78
- borderRadius: 'var(--radius-m, 8px)',
79
- ...sx,
96
+ transition: 'all 0.2s ease-in-out',
97
+ position: 'relative',
98
+ borderRight: '1px solid var(--stroke-border-base, #EFF1F5)',
80
99
  }}>
81
- <Typography variant="h4" gutterBottom>
100
+ <Typography
101
+ variant="body1"
102
+ gutterBottom
103
+ sx={{ color: 'text.secondary', display: 'flex', alignItems: 'center', gap: 1 }}>
104
+ {icon}
82
105
  {label}
83
106
  </Typography>
84
- <Typography variant="h3" gutterBottom>
85
- {value}
107
+ <Typography variant="h3" gutterBottom sx={{ color: 'text.primary', whiteSpace: 'nowrap' }}>
108
+ {value} {currency.symbol}
86
109
  </Typography>
110
+ {type === 'balance' && method?.type === 'arcblock' && (
111
+ <Tooltip title={t('customer.recharge.rechargeTooltip')} arrow placement="top">
112
+ <Box
113
+ onClick={handleCardClick}
114
+ sx={{
115
+ position: 'absolute',
116
+ top: 0,
117
+ right: '12px',
118
+ display: 'flex',
119
+ alignItems: 'center',
120
+ justifyContent: 'center',
121
+ borderRadius: '50%',
122
+ padding: '4px',
123
+ zIndex: 1,
124
+ cursor: 'pointer',
125
+ color: 'text.secondary',
126
+ '&:hover': {
127
+ color: 'text.link',
128
+ },
129
+ }}>
130
+ <AddOutlined fontSize="small" />
131
+ </Box>
132
+ </Tooltip>
133
+ )}
87
134
  </Box>
88
135
  );
89
136
  }
@@ -104,58 +151,19 @@ function SummaryCardSkeleton() {
104
151
  return (
105
152
  <Box className="base-card section section-summary">
106
153
  <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>
154
+ <Skeleton variant="rectangular" height={88} width="100%" />
147
155
  </Box>
148
156
  );
149
157
  }
150
158
 
151
159
  export default function CustomerHome() {
152
160
  const { t } = useLocaleContext();
153
- const { events, session } = useSessionContext();
161
+ const { events } = useSessionContext();
154
162
  const { settings } = usePaymentContext();
155
163
  const [currency, setCurrency] = useState(settings?.baseCurrency);
156
164
  const [subscriptionLoading, setSubscriptionLoading] = useState(false);
157
165
  const currencies = flatten(
158
- settings.paymentMethods.map((method) =>
166
+ settings.paymentMethods?.map((method) =>
159
167
  (method.payment_currencies || []).map((c) => ({
160
168
  ...c,
161
169
  methodName: method.name,
@@ -171,8 +179,8 @@ export default function CustomerHome() {
171
179
  setOverdraftProtection: false,
172
180
  });
173
181
  const navigate = useNavigate();
174
- const { isMobile } = useMobile('lg');
175
182
  const [subscriptionStatus, setSubscriptionStatus] = useState(false);
183
+ const [hasSubscriptions, setHasSubscriptions] = useState(false);
176
184
  const { startTransition } = useTransitionContext();
177
185
  const {
178
186
  data,
@@ -181,14 +189,28 @@ export default function CustomerHome() {
181
189
  loading = true,
182
190
  } = useRequest(fetchData, {
183
191
  manual: true,
192
+ onSuccess: async (res) => {
193
+ if (!hasSubscriptions) {
194
+ const search = new URLSearchParams();
195
+ const searchParams = {
196
+ page: 1,
197
+ pageSize: 5,
198
+ showTotalCount: true,
199
+ customer_id: res.id,
200
+ };
201
+ Object.entries(searchParams).forEach(([key, value]) => {
202
+ search.set(key, String(value));
203
+ });
204
+ const { data: subscriptions } = await api.get(`/api/subscriptions?${search.toString()}`);
205
+ if (subscriptions.totalCount > 0 || subscriptions.count > 0) {
206
+ setHasSubscriptions(true);
207
+ }
208
+ }
209
+ },
184
210
  });
185
211
 
186
212
  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]);
213
+ const [showOverduePayment, setShowOverduePayment] = useState(false);
192
214
 
193
215
  useEffect(() => {
194
216
  runAsync();
@@ -221,20 +243,6 @@ export default function CustomerHome() {
221
243
  );
222
244
  }
223
245
 
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
246
  const onToggleActive = (e: SelectChangeEvent) => {
239
247
  setSubscriptionLoading(true);
240
248
  setState({ onlyActive: e.target.value === 'active' });
@@ -247,58 +255,58 @@ export default function CustomerHome() {
247
255
  setCurrency(newCurrency);
248
256
  };
249
257
 
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
- )}
258
+ const SubscriptionCard =
259
+ loadingCard || !hasSubscriptions ? null : (
260
+ <Box className="base-card section section-subscription">
261
+ <Box className="section-header">
262
+ <Typography variant="h3">{t('admin.subscription.name')}</Typography>
263
+ {subscriptionStatus && (
264
+ <FormControl
265
+ sx={{
266
+ '.MuiInputBase-root': {
267
+ background: 'none',
268
+ border: 'none',
269
+ },
270
+ '.MuiOutlinedInput-notchedOutline': {
271
+ border: 'none',
272
+ },
273
+ }}>
274
+ <Select
275
+ value={state.onlyActive ? 'active' : ''}
276
+ onChange={onToggleActive}
277
+ displayEmpty
278
+ IconComponent={ExpandMore}
279
+ inputProps={{ 'aria-label': 'Without label' }}>
280
+ <MenuItem value="">All</MenuItem>
281
+ <MenuItem value="active">Active</MenuItem>
282
+ </Select>
283
+ </FormControl>
284
+ )}
285
+ </Box>
286
+ <Box className="section-body">
287
+ {subscriptionLoading ? (
288
+ <Box>{t('common.loading')}</Box>
289
+ ) : (
290
+ <CurrentSubscriptions
291
+ id={data?.id}
292
+ onlyActive={state.onlyActive}
293
+ changeActive={(v) => setState({ onlyActive: v })}
294
+ status={state.onlyActive ? 'active,trialing,past_due' : 'active,trialing,paused,past_due,canceled'}
295
+ style={{
296
+ cursor: 'pointer',
297
+ }}
298
+ onClickSubscription={(subscription) => {
299
+ startTransition(() => {
300
+ navigate(`/customer/subscription/${subscription.id}`);
301
+ });
302
+ }}
303
+ setStatusState={setSubscriptionStatus}
304
+ setHasSubscriptions={setHasSubscriptions}
305
+ />
306
+ )}
307
+ </Box>
299
308
  </Box>
300
- </Box>
301
- );
309
+ );
302
310
 
303
311
  const SummaryCard = loadingCard ? (
304
312
  <SummaryCardSkeleton />
@@ -345,44 +353,66 @@ export default function CustomerHome() {
345
353
  </Box>
346
354
  <Stack
347
355
  className="section-body"
356
+ flexDirection="row"
348
357
  sx={{
349
- display: 'grid',
350
- gridTemplateColumns: '1fr 1fr',
351
- gap: 2,
358
+ gap: 3,
352
359
  mt: 1.5,
360
+ flexWrap: 'wrap',
361
+ position: 'relative',
362
+ '>div': {
363
+ flex: '1',
364
+ '@media (max-width: 1100px)': {
365
+ flex: '0 0 calc(50% - 12px)',
366
+ maxWidth: 'calc(50% - 12px)',
367
+ },
368
+ },
369
+ '&::after': {
370
+ content: '""',
371
+ position: 'absolute',
372
+ right: 0,
373
+ top: 0,
374
+ bottom: 0,
375
+ width: '1px',
376
+ backgroundColor: 'background.paper',
377
+ zIndex: 1,
378
+ },
353
379
  }}>
354
380
  <CurrencyCard
355
381
  label={t('admin.customer.summary.balance')}
356
382
  data={data?.summary?.token || emptyObject}
357
383
  currency={currency}
358
- sx={{
359
- background: 'var(--tags-tag-orange-bg, #B7FEE3)',
360
- color: 'var(--tags-tag-orange-text, #007C52)',
361
- }}
384
+ type="balance"
385
+ icon={<AccountBalanceWalletOutlined color="success" fontSize="small" />}
362
386
  />
363
387
  <CurrencyCard
364
388
  label={t('admin.customer.summary.spent')}
365
389
  data={data?.summary?.paid || emptyObject}
366
390
  currency={currency}
367
- sx={{ background: 'var(--tags-tag-green-bg, #B7FEE3)', color: 'var(--tags-tag-green-text, #007C52)' }}
391
+ type="spent"
392
+ icon={<CreditCardOutlined color="warning" fontSize="small" />}
368
393
  />
369
394
  <CurrencyCard
370
395
  label={t('admin.customer.summary.stake')}
371
396
  data={data?.summary?.stake || emptyObject}
372
397
  currency={currency}
373
- sx={{ background: 'var(--tags-tag-blue-bg, #D2ECFF)', color: 'var(--tags-tag-blue-text, #0051E9)' }}
398
+ type="stake"
399
+ icon={<AccountBalanceOutlined fontSize="small" color="info" />}
374
400
  />
375
401
  <CurrencyCard
376
402
  label={t('admin.customer.summary.refund')}
377
403
  data={data?.summary?.refund || emptyObject}
378
404
  currency={currency}
379
- sx={{ background: 'var(--tags-tag-purple-bg, #EFE9FF)', color: 'var(--tags-tag-purple-text, #007C52)' }}
405
+ type="refund"
406
+ icon={
407
+ <AssignmentReturnOutlined fontSize="small" sx={{ color: 'var(--tags-tag-purple-icon, #7c3aed)' }} />
408
+ }
380
409
  />
381
410
  <CurrencyCard
382
411
  label={t('admin.customer.summary.due')}
383
412
  data={data?.summary?.due || emptyObject}
384
413
  currency={currency}
385
- sx={{ background: 'var(--tags-tag-red-bg, #FFE2E6)', color: 'var(--tags-tag-red-text, #E40031)' }}
414
+ type="due"
415
+ icon={<InfoOutlined fontSize="small" color="error" />}
386
416
  />
387
417
  </Stack>
388
418
  </>
@@ -413,126 +443,11 @@ export default function CustomerHome() {
413
443
  )}
414
444
  </Box>
415
445
  <Box className="section-body">
416
- <CustomerInvoiceList customer_id={data?.id} type="table" include_staking />
446
+ <CustomerInvoiceList customer_id={data?.id} type="table" include_staking relatedSubscription />
417
447
  </Box>
418
448
  </Box>
419
449
  );
420
450
 
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
451
  const RevenueCard = loadingCard ? (
537
452
  <CardSkeleton height={200} />
538
453
  ) : (
@@ -551,31 +466,57 @@ export default function CustomerHome() {
551
466
  {data?.error}
552
467
  </Alert>
553
468
  )}
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>
469
+ {isEmpty(data?.summary?.due) === false && (
470
+ <Alert
471
+ severity="error"
472
+ sx={{
473
+ mb: 2,
474
+ '.MuiAlert-action': {
475
+ alignItems: 'center',
476
+ },
477
+ }}
478
+ action={
479
+ <Button
480
+ color="inherit"
481
+ size="small"
482
+ variant="outlined"
483
+ onClick={() => setShowOverduePayment(true)}
484
+ sx={{ whiteSpace: 'nowrap' }}>
485
+ {t('customer.pastDue.payNow')}
486
+ </Button>
487
+ }>
488
+ {t('customer.pastDue.alert')}
489
+ </Alert>
490
+ )}
491
+ {showOverduePayment && data && (
492
+ <OverdueInvoicePayment
493
+ customerId={data.id}
494
+ onPaid={() => {
495
+ setShowOverduePayment(false);
496
+ runAsync();
497
+ }}
498
+ successToast={false}
499
+ detailLinkOptions={{
500
+ enabled: true,
501
+ onClick: () => {
502
+ setShowOverduePayment(false);
503
+ navigate('/customer/invoice/past-due');
504
+ },
505
+ }}
506
+ dialogProps={{
507
+ open: showOverduePayment,
508
+ onClose: () => setShowOverduePayment(false),
509
+ title: t('customer.pastDue.title'),
510
+ }}
511
+ />
578
512
  )}
513
+ <Root>
514
+ {SummaryCard}
515
+ {SubscriptionCard}
516
+ {InvoiceCard}
517
+ {RevenueCard}
518
+ {/* {DetailCard} */}
519
+ </Root>
579
520
  </Content>
580
521
  );
581
522
  }
@@ -603,6 +544,7 @@ const Content = styled(Stack)`
603
544
  `;
604
545
 
605
546
  const Root = styled(Stack)`
547
+ gap: 24px;
606
548
  @media (max-width: ${({ theme }) => theme.breakpoints.values.xl}px) {
607
549
  padding: 0px;
608
550
  gap: 0;