payment-kit 1.13.73 → 1.13.74

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 (65) hide show
  1. package/api/src/{schedule → crons}/base.ts +1 -1
  2. package/api/src/index.ts +7 -7
  3. package/api/src/integrations/stripe/handlers/customer.ts +24 -0
  4. package/api/src/integrations/stripe/handlers/index.ts +4 -0
  5. package/api/src/integrations/stripe/handlers/payment-intent.ts +1 -1
  6. package/api/src/integrations/stripe/resource.ts +1 -1
  7. package/api/src/libs/audit.ts +34 -28
  8. package/api/src/libs/payment.ts +26 -0
  9. package/api/src/libs/queue/index.ts +18 -1
  10. package/api/src/libs/queue/store.ts +6 -5
  11. package/api/src/libs/session.ts +13 -12
  12. package/api/src/libs/subscription.ts +26 -0
  13. package/api/src/libs/util.ts +5 -1
  14. package/api/src/{jobs → queues}/checkout-session.ts +11 -0
  15. package/api/src/{jobs → queues}/invoice.ts +15 -6
  16. package/api/src/{jobs → queues}/payment.ts +182 -30
  17. package/api/src/{jobs → queues}/subscription.ts +36 -104
  18. package/api/src/{jobs → queues}/webhook.ts +2 -0
  19. package/api/src/routes/checkout-sessions.ts +68 -19
  20. package/api/src/routes/connect/collect.ts +2 -2
  21. package/api/src/routes/connect/pay.ts +1 -1
  22. package/api/src/routes/connect/setup.ts +2 -2
  23. package/api/src/routes/connect/shared.ts +94 -45
  24. package/api/src/routes/connect/subscribe.ts +3 -3
  25. package/api/src/routes/pricing-table.ts +2 -0
  26. package/api/src/routes/subscription-items.ts +1 -1
  27. package/api/src/routes/subscriptions.ts +434 -13
  28. package/api/src/store/migrate.ts +0 -1
  29. package/api/src/store/migrations/20231204-subupdate.ts +50 -0
  30. package/api/src/store/models/checkout-session.ts +4 -0
  31. package/api/src/store/models/customer.ts +52 -15
  32. package/api/src/store/models/invoice-item.ts +6 -1
  33. package/api/src/store/models/invoice.ts +41 -22
  34. package/api/src/store/models/payment-intent.ts +4 -0
  35. package/api/src/store/models/setup-intent.ts +4 -0
  36. package/api/src/store/models/subscription-item.ts +0 -4
  37. package/api/src/store/models/subscription.ts +77 -44
  38. package/api/src/store/models/types.ts +1 -0
  39. package/api/src/store/sequelize.ts +6 -0
  40. package/api/third.d.ts +2 -0
  41. package/blocklet.yml +1 -1
  42. package/jest.config.js +14 -0
  43. package/package.json +24 -19
  44. package/src/components/blockchain/tx.tsx +20 -11
  45. package/src/components/checkout/form/index.tsx +1 -1
  46. package/src/components/invoice/table.tsx +58 -19
  47. package/src/components/layout/admin.tsx +17 -5
  48. package/src/components/portal/invoice/list.tsx +12 -8
  49. package/src/components/portal/subscription/list.tsx +114 -77
  50. package/src/components/subscription/status.tsx +21 -19
  51. package/src/global.css +4 -0
  52. package/src/locales/en.tsx +14 -1
  53. package/src/locales/zh.tsx +14 -0
  54. package/src/pages/admin/customers/customers/detail.tsx +47 -3
  55. package/src/pages/admin/overview.tsx +21 -1
  56. package/src/pages/admin/payments/intents/detail.tsx +12 -3
  57. package/src/pages/customer/invoice.tsx +15 -1
  58. package/src/pages/customer/subscription/index.tsx +9 -2
  59. package/tests/api/libs/subscription.spec.ts +45 -0
  60. /package/api/src/{schedule → crons}/index.ts +0 -0
  61. /package/api/src/{schedule → crons}/interface/diff.ts +0 -0
  62. /package/api/src/{schedule → crons}/subscription-trail-will-end.ts +0 -0
  63. /package/api/src/{schedule → crons}/subscription-will-renew.ts +0 -0
  64. /package/api/src/{jobs → queues}/event.ts +0 -0
  65. /package/api/src/{jobs → queues}/notification.ts +0 -0
@@ -4,6 +4,7 @@ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
4
  import Dashboard from '@blocklet/ui-react/lib/Dashboard';
5
5
  import { Avatar, Button, Stack, Typography } from '@mui/material';
6
6
  import { styled } from '@mui/system';
7
+ import { useEffect, useState } from 'react';
7
8
 
8
9
  import { useSessionContext } from '../../contexts/session';
9
10
 
@@ -54,12 +55,27 @@ const Root = styled(Dashboard)`
54
55
  export default function Layout(props: any) {
55
56
  const { t } = useLocaleContext();
56
57
  const { session } = useSessionContext();
58
+ const [showLogin, setShowLogin] = useState(false);
59
+
60
+ useEffect(() => {
61
+ const timer = setTimeout(() => {
62
+ if (!session.user) {
63
+ setShowLogin(true);
64
+ }
65
+ }, 500);
66
+
67
+ return () => clearTimeout(timer);
68
+ });
57
69
 
58
70
  const handleLogin = () => {
59
71
  session.login();
60
72
  };
61
73
 
62
- if (!session.user) {
74
+ if (session.user) {
75
+ return <Root {...props} footerProps={{ className: 'dashboard-footer' }} />;
76
+ }
77
+
78
+ if (showLogin) {
63
79
  return (
64
80
  <Center>
65
81
  <Stack maxWidth="sm" direction="column" alignItems="center" spacing={3}>
@@ -75,9 +91,5 @@ export default function Layout(props: any) {
75
91
  );
76
92
  }
77
93
 
78
- if (session.user) {
79
- return <Root {...props} footerProps={{ className: 'dashboard-footer' }} />;
80
- }
81
-
82
94
  return null;
83
95
  }
@@ -42,7 +42,7 @@ export default function CustomerInvoiceList({ customer_id }: Props) {
42
42
  const { data, loadMore, loadingMore, loading } = useInfiniteScroll<Paginated<TInvoiceExpanded>>(
43
43
  (d) => {
44
44
  const page = d ? Math.ceil(d.list.length / pageSize) + 1 : 1;
45
- return fetchData({ page, pageSize, status: 'open,paid', customer_id });
45
+ return fetchData({ page, pageSize, status: 'open,paid,uncollectible', customer_id });
46
46
  },
47
47
  {
48
48
  reloadDeps: [customer_id],
@@ -54,7 +54,11 @@ export default function CustomerInvoiceList({ customer_id }: Props) {
54
54
  }
55
55
 
56
56
  if (data && data.list.length === 0) {
57
- return <Typography color="text.secondary">{t('admin.invoice.empty')}</Typography>;
57
+ return (
58
+ <Typography color="text.secondary">
59
+ {customer_id ? t('customer.invoice.empty') : t('admin.invoice.empty')}
60
+ </Typography>
61
+ );
58
62
  }
59
63
 
60
64
  const hasMore = data && data.list.length < data.count;
@@ -77,27 +81,27 @@ export default function CustomerInvoiceList({ customer_id }: Props) {
77
81
  gap={{
78
82
  xs: 0.5,
79
83
  sm: 1.5,
80
- md: 4,
84
+ md: 3,
81
85
  }}
82
86
  flexWrap="nowrap">
83
- <Box flex={2}>
87
+ <Box flex={3}>
84
88
  <Link to={`/customer/invoice/${invoice.id}`}>
85
89
  <Typography component="span">{invoice.number}</Typography>
86
90
  </Link>
87
91
  </Box>
88
- <Box flex={2}>
92
+ <Box flex={3}>
89
93
  <Typography>{formatToDate(invoice.created_at)}</Typography>
90
94
  </Box>
91
95
  <Box flex={1}>
92
- <Typography>
96
+ <Typography textAlign="right">
93
97
  {fromUnitToToken(invoice.total, invoice.paymentCurrency.decimal)}&nbsp;
94
98
  {invoice.paymentCurrency.symbol}
95
99
  </Typography>
96
100
  </Box>
97
- <Box flex={1}>
101
+ <Box flex={2}>
98
102
  <Status label={invoice.status} color={getInvoiceStatusColor(invoice.status)} />
99
103
  </Box>
100
- <Box flex={3}>
104
+ <Box flex={4}>
101
105
  <Typography>{invoice.description || invoice.id}</Typography>
102
106
  </Box>
103
107
  </Stack>
@@ -2,10 +2,10 @@
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import Toast from '@arcblock/ux/lib/Toast';
4
4
  import type { Paginated, TSubscriptionExpanded } from '@did-pay/types';
5
- import { ScheduleOutlined } from '@mui/icons-material';
6
5
  import { Avatar, AvatarGroup, Box, Button, CircularProgress, Stack, StackProps, Typography } from '@mui/material';
7
6
  import { useInfiniteScroll, useSetState } from 'ahooks';
8
7
  import { FormProvider, useForm, useFormContext } from 'react-hook-form';
8
+ import { useNavigate } from 'react-router-dom';
9
9
 
10
10
  import api from '../../../libs/api';
11
11
  import {
@@ -35,14 +35,52 @@ type Props = {
35
35
 
36
36
  const pageSize = 4;
37
37
 
38
+ export const getSubscriptionTimeSummary = (subscription: TSubscriptionExpanded) => {
39
+ const lines = [`Started on ${formatToDate(subscription.start_date * 1000)}`];
40
+ if (subscription.status === 'active' || subscription.status === 'trialing') {
41
+ if (subscription.cancel_at) {
42
+ lines.push(`will cancel on ${formatToDate(subscription.cancel_at * 1000)}`);
43
+ } else if (subscription.cancel_at_period_end) {
44
+ lines.push(`will cancel on ${formatToDate(subscription.current_period_end * 1000)}`);
45
+ } else {
46
+ lines.push(`will renew on ${formatToDate(subscription.current_period_end * 1000)}`);
47
+ }
48
+ } else if (subscription.status === 'past_due') {
49
+ lines.push(`will cancel on ${formatToDate(subscription.current_period_end * 1000)}`);
50
+ } else if (subscription.status === 'canceled') {
51
+ lines.push(`canceled on ${formatToDate(subscription.canceled_at * 1000)}`);
52
+ }
53
+
54
+ return lines.join(', ');
55
+ };
56
+
57
+ export const getSubscriptionAction = (subscription: TSubscriptionExpanded) => {
58
+ if (subscription.status === 'active' || subscription.status === 'trialing') {
59
+ if (subscription.cancel_at) {
60
+ return null;
61
+ }
62
+ if (subscription.cancel_at_period_end) {
63
+ return { action: 'recover', color: 'secondary' };
64
+ }
65
+
66
+ return { action: 'cancel', color: 'error' };
67
+ }
68
+ if (subscription.status === 'past_due') {
69
+ return { action: 'pastDue', color: 'primary' };
70
+ }
71
+
72
+ return null;
73
+ };
74
+
38
75
  export function CurrentSubscriptionsInner({ id, onChange, onClickSubscription, ...rest }: Props) {
39
76
  const { t } = useLocaleContext();
40
77
  const { reset, getValues } = useFormContext();
78
+ const navigate = useNavigate();
41
79
 
42
80
  const { data, loadMore, loadingMore, loading } = useInfiniteScroll<Paginated<TSubscriptionExpanded>>(
43
81
  (d) => {
44
82
  const page = d ? Math.ceil(d.list.length / pageSize) + 1 : 1;
45
- return fetchData({ page, pageSize, status: 'active,trialing,paused', customer_id: id });
83
+ return fetchData({ page, pageSize, status: 'active,trialing,paused,past_due,canceled', customer_id: id });
46
84
  },
47
85
  {
48
86
  reloadDeps: [id],
@@ -99,89 +137,88 @@ export function CurrentSubscriptionsInner({ id, onChange, onClickSubscription, .
99
137
 
100
138
  return (
101
139
  <Stack direction="column" spacing={4} sx={{ mt: 2 }}>
102
- {data.list.map((subscription) => (
103
- <Stack
104
- key={subscription.id}
105
- direction="row"
106
- justifyContent="space-between"
107
- gap={{
108
- xs: 1,
109
- sm: 3,
110
- }}
111
- flexWrap="wrap">
112
- <Stack direction="column" spacing={0.5} onClick={() => onClickSubscription(subscription)} {...rest}>
113
- <Stack direction="row" spacing={1} alignItems="center">
114
- <AvatarGroup max={3}>
115
- {subscription.items.map((item) =>
116
- item.price.product.images.length > 0 ? (
117
- // @ts-ignore
118
- <Avatar
119
- key={item.price.product_id}
120
- src={item.price.product.images[0]}
121
- alt={item.price.product.name}
122
- variant="rounded"
123
- sx={size}
140
+ {data.list.map((subscription) => {
141
+ const action = getSubscriptionAction(subscription);
142
+ return (
143
+ <Stack
144
+ key={subscription.id}
145
+ direction="row"
146
+ justifyContent="space-between"
147
+ gap={{
148
+ xs: 1,
149
+ sm: 3,
150
+ }}
151
+ flexWrap="wrap">
152
+ <Stack direction="column" spacing={0.5} onClick={() => onClickSubscription(subscription)} {...rest}>
153
+ <Stack direction="row" spacing={1} alignItems="center">
154
+ <AvatarGroup max={3}>
155
+ {subscription.items.map((item) =>
156
+ item.price.product.images.length > 0 ? (
157
+ // @ts-ignore
158
+ <Avatar
159
+ key={item.price.product_id}
160
+ src={item.price.product.images[0]}
161
+ alt={item.price.product.name}
162
+ variant="rounded"
163
+ sx={size}
164
+ />
165
+ ) : (
166
+ <Avatar key={item.price.product_id} variant="rounded" sx={size}>
167
+ {item.price.product.name.slice(0, 1)}
168
+ </Avatar>
169
+ )
170
+ )}
171
+ </AvatarGroup>
172
+ <Stack direction="column" spacing={0.5}>
173
+ <Stack direction="row" spacing={2} alignItems="center">
174
+ <Typography variant="body1" fontWeight={600}>
175
+ {formatSubscriptionProduct(subscription.items)}
176
+ </Typography>
177
+ <Status
178
+ size="small"
179
+ sx={{ height: 18 }}
180
+ label={subscription.status}
181
+ color={getSubscriptionStatusColor(subscription.status)}
124
182
  />
125
- ) : (
126
- <Avatar key={item.price.product_id} variant="rounded" sx={size}>
127
- {item.price.product.name.slice(0, 1)}
128
- </Avatar>
129
- )
130
- )}
131
- </AvatarGroup>
132
- <Stack direction="column" spacing={0.5}>
133
- <Stack direction="row" spacing={2} alignItems="center">
134
- <Typography variant="body1" fontWeight={600}>
135
- {formatSubscriptionProduct(subscription.items)}
183
+ </Stack>
184
+ <Typography variant="subtitle1" fontWeight={500}>
185
+ {
186
+ // @ts-ignore
187
+ formatPrice(subscription.items[0].price, subscription.paymentCurrency)
188
+ }
136
189
  </Typography>
137
- <Status
138
- size="small"
139
- sx={{ height: 18 }}
140
- label={subscription.status}
141
- color={getSubscriptionStatusColor(subscription.status)}
142
- />
143
190
  </Stack>
144
- <Typography variant="subtitle1" fontWeight={500}>
145
- {
146
- // @ts-ignore
147
- formatPrice(subscription.items[0].price, subscription.paymentCurrency)
148
- }
191
+ </Stack>
192
+ <Stack direction="row" spacing={2} alignItems="center">
193
+ <Typography variant="body1" color="text.secondary">
194
+ {getSubscriptionTimeSummary(subscription)}
149
195
  </Typography>
150
196
  </Stack>
151
197
  </Stack>
152
- <Stack direction="row" spacing={2} alignItems="center">
153
- <Typography variant="body1" color="text.secondary">
154
- Started on {formatToDate(subscription.start_date * 1000)}, will{' '}
155
- {subscription.cancel_at_period_end ? 'end' : 'renew'} on{' '}
156
- {formatToDate(subscription.current_period_end * 1000)}
157
- </Typography>
198
+ <Stack direction="column" alignItems="flex-end" spacing={1}>
199
+ {action && (
200
+ <Button
201
+ variant="outlined"
202
+ // @ts-ignore
203
+ color={action.color}
204
+ size="small"
205
+ onClick={() => {
206
+ if (action.action === 'pastDue') {
207
+ navigate(`/customer/invoice/${subscription.latest_invoice_id}?action=pay`);
208
+ } else {
209
+ setState({
210
+ action: action.action,
211
+ subscription: subscription.id,
212
+ });
213
+ }
214
+ }}>
215
+ {t(`customer.${action.action}.button`)}
216
+ </Button>
217
+ )}
158
218
  </Stack>
159
219
  </Stack>
160
- <Stack direction="column" alignItems="flex-end" spacing={1}>
161
- <Button
162
- variant="outlined"
163
- color={subscription.cancel_at_period_end ? 'inherit' : 'error'}
164
- size="small"
165
- onClick={() =>
166
- setState({
167
- action: subscription.cancel_at_period_end ? 'recover' : 'cancel',
168
- subscription: subscription.id,
169
- })
170
- }>
171
- {t(`customer.${subscription.cancel_at_period_end ? 'recover' : 'cancel'}.button`)}
172
- </Button>
173
- {subscription.cancel_at_period_end && (
174
- <Stack direction="row" alignItems="center" spacing={0.5}>
175
- <ScheduleOutlined fontSize="small" sx={{ color: 'text.secondary' }} />
176
- <Typography component="span" variant="body1" color="text.secondary">
177
- will {subscription.cancel_at_period_end ? 'cancel' : 'renew'} on{' '}
178
- {formatToDate(subscription.current_period_end * 1000)}
179
- </Typography>
180
- </Stack>
181
- )}
182
- </Stack>
183
- </Stack>
184
- ))}
220
+ );
221
+ })}
185
222
  <Box>
186
223
  {hasMore && (
187
224
  <Button variant="text" type="button" color="inherit" onClick={loadMore} disabled={loadingMore}>
@@ -13,26 +13,28 @@ export default function SubscriptionStatus({
13
13
  [key: string]: any;
14
14
  }) {
15
15
  const { t } = useLocaleContext();
16
- if (subscription.cancel_at_period_end && subscription.current_period_end > Date.now() / 1000) {
17
- return (
18
- <Status
19
- icon={<AccessTimeOutlined />}
20
- label={t('admin.subscription.cancel.will', { date: formatToDate(subscription.current_period_end * 1000) })}
21
- color="default"
22
- {...rest}
23
- />
24
- );
25
- }
16
+ if (subscription.status === 'active' || subscription.status === 'trialing') {
17
+ if (subscription.cancel_at_period_end && subscription.current_period_end > Date.now() / 1000) {
18
+ return (
19
+ <Status
20
+ icon={<AccessTimeOutlined />}
21
+ label={t('admin.subscription.cancel.will', { date: formatToDate(subscription.current_period_end * 1000) })}
22
+ color="default"
23
+ {...rest}
24
+ />
25
+ );
26
+ }
26
27
 
27
- if (subscription.cancel_at && subscription.cancel_at >= Date.now() / 1000) {
28
- return (
29
- <Status
30
- icon={<AccessTimeOutlined />}
31
- label={t('admin.subscription.cancel.will', { date: formatToDate(subscription.cancel_at * 1000) })}
32
- color="default"
33
- {...rest}
34
- />
35
- );
28
+ if (subscription.cancel_at && subscription.cancel_at >= Date.now() / 1000) {
29
+ return (
30
+ <Status
31
+ icon={<AccessTimeOutlined />}
32
+ label={t('admin.subscription.cancel.will', { date: formatToDate(subscription.cancel_at * 1000) })}
33
+ color="default"
34
+ {...rest}
35
+ />
36
+ );
37
+ }
36
38
  }
37
39
 
38
40
  if (subscription.pause_collection) {
package/src/global.css CHANGED
@@ -97,3 +97,7 @@ th.MuiTableCell-head {
97
97
  max-height: 100% !important;
98
98
  overflow: auto !important;
99
99
  }
100
+
101
+ .MuiTooltip-tooltip {
102
+ max-width: 500px;
103
+ }
@@ -18,6 +18,7 @@ export default flat({
18
18
  name: 'Name',
19
19
  amount: 'Amount',
20
20
  total: 'Total',
21
+ subtotal: 'Subtotal',
21
22
  status: 'Status',
22
23
  livemode: 'Test mode',
23
24
  afterTime: 'After {time}',
@@ -408,6 +409,7 @@ export default flat({
408
409
  title: 'Cancel subscription',
409
410
  required: 'Custom cancel time is required',
410
411
  will: 'Cancels on {date}',
412
+ done: 'Canceled',
411
413
  at: {
412
414
  title: 'Cancel',
413
415
  now: 'Immediately ({date})',
@@ -455,6 +457,7 @@ export default flat({
455
457
  email: 'Email',
456
458
  phone: 'Phone',
457
459
  invoicePrefix: 'Invoice Prefix',
460
+ balance: 'Balance ({currency})',
458
461
  address: {
459
462
  label: 'Address',
460
463
  country: 'Country',
@@ -592,6 +595,9 @@ export default flat({
592
595
  other: 'Other reasons',
593
596
  },
594
597
  },
598
+ pastDue: {
599
+ button: 'Pay',
600
+ },
595
601
  recover: {
596
602
  button: 'Renew',
597
603
  title: 'Renew your subscription',
@@ -601,12 +607,19 @@ export default flat({
601
607
  summary: 'Summary',
602
608
  details: 'Details',
603
609
  download: 'Download PDF',
610
+ unitPrice: 'Unit Price',
611
+ amountPaid: 'Amount Paid',
612
+ amountDue: 'Amount Due',
613
+ amountApplied: 'Applied Balance',
604
614
  pay: 'Pay this invoice',
615
+ paySuccess: 'You have successfully paid the invoice',
616
+ payError: 'Failed to paid the invoice',
617
+ empty: 'Seems you do not have any payments here',
605
618
  },
606
-
607
619
  subscriptions: {
608
620
  title: 'Manage subscriptions',
609
621
  current: 'Current subscriptions',
622
+ empty: 'Seems you do not have any subscriptions here',
610
623
  },
611
624
  },
612
625
  });
@@ -18,6 +18,7 @@ export default flat({
18
18
  login: '登录以访问此页面',
19
19
  amount: '金额',
20
20
  total: '总计',
21
+ subtotal: '小计',
21
22
  status: '状态',
22
23
  livemode: '测试模式',
23
24
  afterTime: '在{time}后',
@@ -399,6 +400,7 @@ export default flat({
399
400
  title: '取消订阅',
400
401
  required: '必须指定自定义取消时间',
401
402
  will: '于{date}取消',
403
+ done: '已取消',
402
404
  at: {
403
405
  title: '取消',
404
406
  now: '立即取消({date})',
@@ -446,6 +448,7 @@ export default flat({
446
448
  email: '电子邮件',
447
449
  phone: '电话',
448
450
  invoicePrefix: '发票前缀',
451
+ balance: '余额 ({currency})',
449
452
  address: {
450
453
  label: '地址',
451
454
  country: '国家',
@@ -583,16 +586,27 @@ export default flat({
583
586
  title: '续订您的订阅',
584
587
  description: '您的订阅将不再被取消,将在{date}续订',
585
588
  },
589
+ pastDue: {
590
+ button: '续费',
591
+ },
586
592
  invoice: {
587
593
  summary: '摘要',
588
594
  details: '详情',
589
595
  download: '下载PDF',
596
+ unitPrice: '单价',
597
+ amountPaid: '已支付',
598
+ amountDue: '待支付',
599
+ amountApplied: '余额变更',
590
600
  pay: '支付此发票',
601
+ paySuccess: '支付成功',
602
+ payError: '支付失败',
603
+ empty: '你没有任何支付',
591
604
  },
592
605
 
593
606
  subscriptions: {
594
607
  title: '订阅管理',
595
608
  current: '当前订阅',
609
+ empty: '你还没有任何订阅',
596
610
  },
597
611
  },
598
612
  });
@@ -2,10 +2,11 @@
2
2
  import DidAddress from '@arcblock/ux/lib/DID';
3
3
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
4
  import Toast from '@arcblock/ux/lib/Toast';
5
- import type { TCustomerExpanded } from '@did-pay/types';
5
+ import type { TCustomerExpanded, TPaymentMethodExpanded } from '@did-pay/types';
6
6
  import { ArrowBackOutlined, Edit } from '@mui/icons-material';
7
7
  import { Alert, Box, Button, CircularProgress, Stack, Typography } from '@mui/material';
8
8
  import { styled } from '@mui/system';
9
+ import { fromUnitToToken } from '@ocap/util';
9
10
  import { useRequest, useSetState } from 'ahooks';
10
11
  import { isEmpty } from 'lodash';
11
12
  import { FlagEmoji } from 'react-international-phone';
@@ -22,6 +23,7 @@ import MetadataEditor from '../../../../components/metadata/editor';
22
23
  import PaymentList from '../../../../components/payment-intent/list';
23
24
  import SectionHeader from '../../../../components/section/header';
24
25
  import SubscriptionList from '../../../../components/subscription/list';
26
+ import { useSettingsContext } from '../../../../contexts/settings';
25
27
  import api from '../../../../libs/api';
26
28
  import { formatError, formatTime } from '../../../../libs/util';
27
29
 
@@ -29,9 +31,43 @@ const fetchData = (id: string): Promise<TCustomerExpanded> => {
29
31
  return api.get(`/api/customers/${id}`).then((res) => res.data);
30
32
  };
31
33
 
34
+ function getTokenBalances(customer: TCustomerExpanded, paymentMethods: TPaymentMethodExpanded[]) {
35
+ const tokens: { currency: string; balance: string }[] = [];
36
+ // @ts-ignore
37
+ const currencies = paymentMethods.reduce((acc, x) => acc.concat(x.payment_currencies), []);
38
+
39
+ if (customer.balance) {
40
+ const currency = currencies.find((x: any) => x.symbol === 'USD');
41
+ tokens.push({
42
+ // @ts-ignore
43
+ currency: currency.symbol,
44
+ // @ts-ignore
45
+ balance: fromUnitToToken(customer.balance, currency.decimal),
46
+ });
47
+ }
48
+
49
+ if (customer.token_balance) {
50
+ Object.keys(customer.token_balance).forEach((currencyId) => {
51
+ const currency = currencies.find((x: any) => x.id === currencyId);
52
+ // @ts-ignore
53
+ if (currency && customer.token_balance[currencyId] !== '0') {
54
+ tokens.push({
55
+ // @ts-ignore
56
+ currency: currency.symbol,
57
+ // @ts-ignore
58
+ balance: fromUnitToToken(customer.token_balance[currencyId], currency.decimal),
59
+ });
60
+ }
61
+ });
62
+ }
63
+
64
+ return tokens;
65
+ }
66
+
32
67
  export default function CustomerDetail(props: { id: string }) {
33
68
  const { t } = useLocaleContext();
34
69
  const navigate = useNavigate();
70
+ const { settings } = useSettingsContext();
35
71
  const [state, setState] = useSetState({
36
72
  adding: {
37
73
  price: false,
@@ -92,6 +128,8 @@ export default function CustomerDetail(props: { id: string }) {
92
128
  }
93
129
  };
94
130
 
131
+ const tokenBalances = getTokenBalances(data, settings.paymentMethods);
132
+
95
133
  return (
96
134
  <Root direction="column" spacing={4} sx={{ mb: 4 }}>
97
135
  <Box>
@@ -123,8 +161,14 @@ export default function CustomerDetail(props: { id: string }) {
123
161
  <InfoMetric label={t('common.createdAt')} value={formatTime(data.created_at)} divider />
124
162
  <InfoMetric label={t('common.updatedAt')} value={formatTime(data.updated_at)} divider />
125
163
  <InfoMetric label={t('admin.customer.spent')} value={0} divider />
126
- <InfoMetric label={t('admin.customer.refund')} value={0} divider />
127
- <InfoMetric label={t('admin.customer.dispute')} value={0} divider />
164
+ {tokenBalances.map((x) => (
165
+ <InfoMetric
166
+ key={x.currency}
167
+ label={t('admin.customer.balance', { currency: x.currency })}
168
+ value={x.balance}
169
+ divider
170
+ />
171
+ ))}
128
172
  </Stack>
129
173
  </Box>
130
174
  </Box>
@@ -1,3 +1,23 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import { Button, Stack } from '@mui/material';
3
+ import { Link } from 'react-router-dom';
4
+
1
5
  export default function Overview() {
2
- return <div>Overview</div>;
6
+ const { t } = useLocaleContext();
7
+ return (
8
+ <Stack direction="row" spacing={2}>
9
+ <Button component={Link} variant="outlined" to="/admin/products">
10
+ {t('admin.products')}
11
+ </Button>
12
+ <Button component={Link} variant="outlined" to="/admin/payments">
13
+ {t('admin.payments')}
14
+ </Button>
15
+ <Button component={Link} variant="outlined" to="/admin/customers">
16
+ {t('admin.customers')}
17
+ </Button>
18
+ <Button component={Link} variant="outlined" to="/admin/billing/subscriptions">
19
+ {t('admin.subscriptions')}
20
+ </Button>
21
+ </Stack>
22
+ );
3
23
  }
@@ -2,8 +2,8 @@
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import Toast from '@arcblock/ux/lib/Toast';
4
4
  import type { TPaymentIntentExpanded } from '@did-pay/types';
5
- import { ArrowBackOutlined, Edit } from '@mui/icons-material';
6
- import { Alert, Box, Button, CircularProgress, Stack, Typography } from '@mui/material';
5
+ import { ArrowBackOutlined, Edit, InfoOutlined } from '@mui/icons-material';
6
+ import { Alert, Box, Button, CircularProgress, Stack, Tooltip, Typography } from '@mui/material';
7
7
  import { styled } from '@mui/system';
8
8
  import { fromUnitToToken } from '@ocap/util';
9
9
  import { useRequest, useSetState } from 'ahooks';
@@ -118,7 +118,16 @@ export default function PaymentIntentDetail(props: { id: string }) {
118
118
  <InfoRow label={t('common.amount')} value={amount} />
119
119
  <InfoRow
120
120
  label={t('common.status')}
121
- value={<Status label={data.status} color={getPaymentIntentStatusColor(data.status)} />}
121
+ value={
122
+ <Stack direction="row" alignItems="center" spacing={1}>
123
+ <Status label={data.status} color={getPaymentIntentStatusColor(data.status)} />
124
+ {data.last_payment_error && (
125
+ <Tooltip title={<pre>{JSON.stringify(data.last_payment_error, null, 2)}</pre>}>
126
+ <InfoOutlined fontSize="small" color="error" />
127
+ </Tooltip>
128
+ )}
129
+ </Stack>
130
+ }
122
131
  />
123
132
  <InfoRow label={t('common.description')} value={data.description} />
124
133
  <InfoRow label={t('common.statementDescriptor')} value={data.statement_descriptor} />