payment-kit 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.
@@ -796,6 +796,8 @@ export default flat({
796
796
  estimatedDuration: '{duration} {unit} est.',
797
797
  intervals: 'intervals',
798
798
  history: 'Fund History',
799
+ relatedSubscriptions: 'Related Subscriptions',
800
+ rechargeTooltip: 'Click to recharge balance',
799
801
  },
800
802
  delegation: {
801
803
  title:
@@ -855,6 +857,13 @@ export default flat({
855
857
  viewReference: 'View Reference',
856
858
  title: 'Revenue',
857
859
  },
860
+ pastDue: {
861
+ warning: 'You have due invoices, please pay them to keep your services active',
862
+ invoices: 'Due Invoices',
863
+ payNow: 'Pay Now',
864
+ alert: 'You have due invoices. Please pay them promptly to avoid service interruption.',
865
+ title: 'Settle Due Invoices',
866
+ },
858
867
  },
859
868
  integrations: {
860
869
  basicFeatures: 'Basic Features',
@@ -774,6 +774,8 @@ export default flat({
774
774
  custom: '自定义',
775
775
  intervals: '个周期',
776
776
  history: '充值记录',
777
+ relatedSubscriptions: '关联订阅',
778
+ rechargeTooltip: '点击充值余额',
777
779
  },
778
780
  delegation: {
779
781
  title: '检测到你未完成账户的授权,为了不影响订阅的扣费,请尽快完成授权',
@@ -828,6 +830,13 @@ export default flat({
828
830
  viewReference: '查看打赏原文',
829
831
  title: '收款记录',
830
832
  },
833
+ pastDue: {
834
+ warning: '您有欠费账单,请及时付款以保持服务正常运行',
835
+ invoices: '欠费账单',
836
+ payNow: '立即付款',
837
+ alert: '您有欠费账单需要处理,请及时付款以避免服务中断',
838
+ title: '结清欠费账单',
839
+ },
831
840
  },
832
841
  integrations: {
833
842
  basicFeatures: '基础功能',
@@ -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;