payment-kit 1.18.56 → 1.19.1

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 (214) hide show
  1. package/.eslintrc.js +6 -0
  2. package/api/src/crons/index.ts +8 -0
  3. package/api/src/index.ts +4 -0
  4. package/api/src/libs/credit-grant.ts +146 -0
  5. package/api/src/libs/env.ts +1 -0
  6. package/api/src/libs/invoice.ts +4 -3
  7. package/api/src/libs/notification/template/base.ts +388 -2
  8. package/api/src/libs/notification/template/customer-credit-grant-granted.ts +149 -0
  9. package/api/src/libs/notification/template/customer-credit-grant-low-balance.ts +151 -0
  10. package/api/src/libs/notification/template/customer-credit-insufficient.ts +254 -0
  11. package/api/src/libs/notification/template/subscription-canceled.ts +193 -202
  12. package/api/src/libs/notification/template/subscription-refund-succeeded.ts +215 -237
  13. package/api/src/libs/notification/template/subscription-renewed.ts +130 -200
  14. package/api/src/libs/notification/template/subscription-succeeded.ts +100 -202
  15. package/api/src/libs/notification/template/subscription-trial-start.ts +142 -188
  16. package/api/src/libs/notification/template/subscription-trial-will-end.ts +146 -174
  17. package/api/src/libs/notification/template/subscription-upgraded.ts +96 -192
  18. package/api/src/libs/notification/template/subscription-will-canceled.ts +94 -135
  19. package/api/src/libs/notification/template/subscription-will-renew.ts +220 -245
  20. package/api/src/libs/payment.ts +69 -0
  21. package/api/src/libs/queue/index.ts +3 -2
  22. package/api/src/libs/session.ts +8 -0
  23. package/api/src/libs/subscription.ts +74 -3
  24. package/api/src/libs/ws.ts +23 -1
  25. package/api/src/locales/en.ts +33 -0
  26. package/api/src/locales/zh.ts +31 -0
  27. package/api/src/queues/credit-consume.ts +715 -0
  28. package/api/src/queues/credit-grant.ts +572 -0
  29. package/api/src/queues/notification.ts +173 -128
  30. package/api/src/queues/payment.ts +210 -122
  31. package/api/src/queues/subscription.ts +179 -0
  32. package/api/src/routes/checkout-sessions.ts +157 -9
  33. package/api/src/routes/connect/shared.ts +3 -2
  34. package/api/src/routes/credit-grants.ts +241 -0
  35. package/api/src/routes/credit-transactions.ts +208 -0
  36. package/api/src/routes/index.ts +8 -0
  37. package/api/src/routes/meter-events.ts +347 -0
  38. package/api/src/routes/meters.ts +219 -0
  39. package/api/src/routes/payment-currencies.ts +14 -2
  40. package/api/src/routes/payment-links.ts +1 -1
  41. package/api/src/routes/payment-methods.ts +14 -2
  42. package/api/src/routes/prices.ts +43 -0
  43. package/api/src/routes/pricing-table.ts +13 -7
  44. package/api/src/routes/products.ts +63 -4
  45. package/api/src/routes/settings.ts +1 -1
  46. package/api/src/routes/subscriptions.ts +4 -0
  47. package/api/src/store/migrations/20250610-billing-credit.ts +43 -0
  48. package/api/src/store/models/credit-grant.ts +486 -0
  49. package/api/src/store/models/credit-transaction.ts +268 -0
  50. package/api/src/store/models/customer.ts +8 -0
  51. package/api/src/store/models/index.ts +52 -1
  52. package/api/src/store/models/meter-event.ts +423 -0
  53. package/api/src/store/models/meter.ts +176 -0
  54. package/api/src/store/models/payment-currency.ts +66 -14
  55. package/api/src/store/models/price.ts +6 -0
  56. package/api/src/store/models/product.ts +2 -2
  57. package/api/src/store/models/subscription.ts +24 -0
  58. package/api/src/store/models/types.ts +28 -2
  59. package/api/tests/libs/subscription.spec.ts +53 -0
  60. package/blocklet.yml +9 -1
  61. package/package.json +57 -58
  62. package/scripts/sdk.js +233 -1
  63. package/src/app.tsx +10 -0
  64. package/src/components/actions.tsx +22 -9
  65. package/src/components/balance-list.tsx +40 -12
  66. package/src/components/collapse.tsx +33 -15
  67. package/src/components/copyable.tsx +8 -7
  68. package/src/components/currency.tsx +15 -7
  69. package/src/components/customer/actions.tsx +1 -5
  70. package/src/components/customer/credit-grant-item-list.tsx +99 -0
  71. package/src/components/customer/credit-overview.tsx +233 -0
  72. package/src/components/customer/form.tsx +7 -2
  73. package/src/components/customer/link.tsx +4 -12
  74. package/src/components/customer/notification-preference.tsx +18 -9
  75. package/src/components/customer/overdraft-protection.tsx +112 -41
  76. package/src/components/drawer-form.tsx +42 -18
  77. package/src/components/error.tsx +1 -5
  78. package/src/components/event/list.tsx +9 -10
  79. package/src/components/filter-toolbar.tsx +20 -19
  80. package/src/components/info-card.tsx +32 -18
  81. package/src/components/info-metric.tsx +16 -6
  82. package/src/components/info-row-group.tsx +1 -7
  83. package/src/components/info-row.tsx +30 -24
  84. package/src/components/invoice/action.tsx +1 -7
  85. package/src/components/invoice/list.tsx +34 -26
  86. package/src/components/invoice/recharge.tsx +5 -7
  87. package/src/components/invoice/table.tsx +17 -12
  88. package/src/components/layout/user.tsx +1 -1
  89. package/src/components/metadata/form.tsx +290 -94
  90. package/src/components/metadata/list.tsx +11 -3
  91. package/src/components/meter/actions.tsx +101 -0
  92. package/src/components/meter/add-usage-dialog.tsx +239 -0
  93. package/src/components/meter/events-list.tsx +657 -0
  94. package/src/components/meter/form.tsx +245 -0
  95. package/src/components/meter/products.tsx +264 -0
  96. package/src/components/meter/usage-guide.tsx +174 -0
  97. package/src/components/passport/actions.tsx +9 -4
  98. package/src/components/payment-currency/add.tsx +16 -3
  99. package/src/components/payment-currency/form.tsx +14 -6
  100. package/src/components/payment-intent/actions.tsx +24 -16
  101. package/src/components/payment-intent/list.tsx +30 -9
  102. package/src/components/payment-link/actions.tsx +1 -5
  103. package/src/components/payment-link/after-pay.tsx +4 -2
  104. package/src/components/payment-link/before-pay.tsx +14 -4
  105. package/src/components/payment-link/item.tsx +27 -6
  106. package/src/components/payment-link/preview.tsx +9 -9
  107. package/src/components/payment-link/product-select.tsx +69 -15
  108. package/src/components/payment-method/arcblock.tsx +8 -1
  109. package/src/components/payment-method/base.tsx +8 -1
  110. package/src/components/payment-method/bitcoin.tsx +8 -1
  111. package/src/components/payment-method/ethereum.tsx +8 -1
  112. package/src/components/payment-method/evm-rpc-input.tsx +11 -7
  113. package/src/components/payment-method/form.tsx +2 -7
  114. package/src/components/payment-method/stripe.tsx +2 -0
  115. package/src/components/payouts/actions.tsx +1 -5
  116. package/src/components/payouts/list.tsx +30 -10
  117. package/src/components/payouts/portal/list.tsx +11 -9
  118. package/src/components/price/currency-select.tsx +63 -32
  119. package/src/components/price/form.tsx +895 -370
  120. package/src/components/price/upsell-select.tsx +10 -2
  121. package/src/components/price/upsell.tsx +7 -2
  122. package/src/components/pricing-table/actions.tsx +1 -5
  123. package/src/components/pricing-table/customer-settings.tsx +5 -1
  124. package/src/components/pricing-table/payment-settings.tsx +14 -4
  125. package/src/components/pricing-table/preview.tsx +9 -9
  126. package/src/components/pricing-table/price-item.tsx +6 -1
  127. package/src/components/pricing-table/product-item.tsx +6 -1
  128. package/src/components/pricing-table/product-settings.tsx +17 -4
  129. package/src/components/product/actions.tsx +1 -5
  130. package/src/components/product/add-price.tsx +9 -7
  131. package/src/components/product/create.tsx +8 -9
  132. package/src/components/product/cross-sell-select.tsx +5 -1
  133. package/src/components/product/cross-sell.tsx +7 -2
  134. package/src/components/product/edit-price.tsx +21 -12
  135. package/src/components/product/features.tsx +26 -6
  136. package/src/components/product/form.tsx +115 -72
  137. package/src/components/progress-bar.tsx +1 -1
  138. package/src/components/refund/actions.tsx +1 -7
  139. package/src/components/refund/list.tsx +31 -18
  140. package/src/components/section/header.tsx +12 -14
  141. package/src/components/subscription/actions/cancel.tsx +22 -5
  142. package/src/components/subscription/actions/index.tsx +9 -10
  143. package/src/components/subscription/actions/pause.tsx +32 -6
  144. package/src/components/subscription/actions/slash-stake.tsx +5 -3
  145. package/src/components/subscription/description.tsx +12 -8
  146. package/src/components/subscription/items/index.tsx +31 -16
  147. package/src/components/subscription/items/usage-records.tsx +19 -5
  148. package/src/components/subscription/list.tsx +5 -7
  149. package/src/components/subscription/metrics.tsx +62 -15
  150. package/src/components/subscription/portal/actions.tsx +78 -71
  151. package/src/components/subscription/portal/cancel.tsx +10 -3
  152. package/src/components/subscription/portal/list.tsx +48 -26
  153. package/src/components/uploader.tsx +5 -13
  154. package/src/components/webhook/attempts.tsx +51 -16
  155. package/src/components/webhook/request-info.tsx +8 -6
  156. package/src/contexts/products.tsx +27 -10
  157. package/src/hooks/subscription.ts +34 -0
  158. package/src/libs/meter-utils.ts +196 -0
  159. package/src/libs/util.ts +4 -0
  160. package/src/locales/en.tsx +385 -4
  161. package/src/locales/zh.tsx +364 -0
  162. package/src/pages/admin/billing/index.tsx +61 -33
  163. package/src/pages/admin/billing/invoices/detail.tsx +49 -13
  164. package/src/pages/admin/billing/meters/create.tsx +60 -0
  165. package/src/pages/admin/billing/meters/detail.tsx +435 -0
  166. package/src/pages/admin/billing/meters/index.tsx +210 -0
  167. package/src/pages/admin/billing/meters/meter-event.tsx +346 -0
  168. package/src/pages/admin/billing/subscriptions/detail.tsx +90 -25
  169. package/src/pages/admin/customers/customers/credit-grant/detail.tsx +391 -0
  170. package/src/pages/admin/customers/customers/detail.tsx +67 -14
  171. package/src/pages/admin/customers/customers/index.tsx +6 -1
  172. package/src/pages/admin/customers/index.tsx +5 -0
  173. package/src/pages/admin/developers/events/detail.tsx +37 -11
  174. package/src/pages/admin/developers/index.tsx +1 -1
  175. package/src/pages/admin/developers/webhooks/detail.tsx +41 -11
  176. package/src/pages/admin/index.tsx +15 -2
  177. package/src/pages/admin/overview.tsx +107 -19
  178. package/src/pages/admin/payments/intents/detail.tsx +58 -14
  179. package/src/pages/admin/payments/payouts/detail.tsx +63 -15
  180. package/src/pages/admin/payments/refunds/detail.tsx +58 -14
  181. package/src/pages/admin/products/index.tsx +11 -4
  182. package/src/pages/admin/products/links/create.tsx +22 -4
  183. package/src/pages/admin/products/links/detail.tsx +43 -14
  184. package/src/pages/admin/products/passports/index.tsx +23 -4
  185. package/src/pages/admin/products/prices/actions.tsx +16 -9
  186. package/src/pages/admin/products/prices/detail.tsx +73 -14
  187. package/src/pages/admin/products/prices/list.tsx +15 -3
  188. package/src/pages/admin/products/pricing-tables/create.tsx +45 -12
  189. package/src/pages/admin/products/pricing-tables/detail.tsx +45 -14
  190. package/src/pages/admin/products/products/create.tsx +233 -54
  191. package/src/pages/admin/products/products/detail.tsx +74 -18
  192. package/src/pages/admin/settings/index.tsx +8 -1
  193. package/src/pages/admin/settings/payment-methods/index.tsx +87 -19
  194. package/src/pages/admin/settings/vault-config/edit-form.tsx +42 -28
  195. package/src/pages/admin/settings/vault-config/index.tsx +57 -10
  196. package/src/pages/customer/credit-grant/detail.tsx +308 -0
  197. package/src/pages/customer/index.tsx +76 -17
  198. package/src/pages/customer/invoice/detail.tsx +63 -14
  199. package/src/pages/customer/invoice/past-due.tsx +11 -3
  200. package/src/pages/customer/payout/detail.tsx +56 -13
  201. package/src/pages/customer/recharge/account.tsx +78 -18
  202. package/src/pages/customer/recharge/subscription.tsx +86 -25
  203. package/src/pages/customer/refund/list.tsx +60 -24
  204. package/src/pages/customer/subscription/change-payment.tsx +17 -6
  205. package/src/pages/customer/subscription/change-plan.tsx +34 -7
  206. package/src/pages/customer/subscription/detail.tsx +134 -34
  207. package/src/pages/customer/subscription/embed.tsx +25 -5
  208. package/src/pages/home.tsx +26 -4
  209. package/src/pages/integrations/donations/edit-form.tsx +25 -9
  210. package/src/pages/integrations/donations/index.tsx +26 -9
  211. package/src/pages/integrations/donations/preview.tsx +59 -15
  212. package/src/pages/integrations/index.tsx +10 -1
  213. package/src/pages/integrations/overview.tsx +78 -17
  214. package/vite.config.ts +60 -30
package/src/app.tsx CHANGED
@@ -27,6 +27,7 @@ const CustomerSubscriptionDetail = React.lazy(() => import('./pages/customer/sub
27
27
  const CustomerSubscriptionEmbed = React.lazy(() => import('./pages/customer/subscription/embed'));
28
28
  const CustomerSubscriptionChangePlan = React.lazy(() => import('./pages/customer/subscription/change-plan'));
29
29
  const CustomerSubscriptionChangePayment = React.lazy(() => import('./pages/customer/subscription/change-payment'));
30
+ const CustomerCreditGrantDetail = React.lazy(() => import('./pages/customer/credit-grant/detail'));
30
31
  const CustomerRecharge = React.lazy(() => import('./pages/customer/recharge/subscription'));
31
32
  const CustomerPayoutDetail = React.lazy(() => import('./pages/customer/payout/detail'));
32
33
  const IntegrationsPage = React.lazy(() => import('./pages/integrations'));
@@ -149,6 +150,15 @@ function App() {
149
150
  </UserLayout>
150
151
  }
151
152
  />
153
+ <Route
154
+ key="customer-credit-grant"
155
+ path="/customer/credit-grant/:id"
156
+ element={
157
+ <UserLayout>
158
+ <CustomerCreditGrantDetail />
159
+ </UserLayout>
160
+ }
161
+ />
152
162
  <Route key="customer-fallback" path="/customer/*" element={<Navigate to="/customer" />} />,
153
163
  <Route path="*" element={<Navigate to="/" />} />
154
164
  </Routes>
@@ -1,3 +1,4 @@
1
+ /* eslint-disable react/prop-types */
1
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
3
  import { stopEvent } from '@blocklet/payment-react';
3
4
  import { ExpandMoreOutlined, MoreHorizOutlined } from '@mui/icons-material';
@@ -21,13 +22,15 @@ export type ActionsProps = {
21
22
  onOpenCallback?: Function;
22
23
  };
23
24
 
24
- Actions.defaultProps = {
25
- variant: 'compact',
26
- sx: {},
27
- onOpenCallback: null,
28
- };
29
-
30
- export default function Actions(props: ActionsProps) {
25
+ export default function Actions(rawProps: ActionsProps) {
26
+ const props: ActionsProps = Object.assign(
27
+ {
28
+ variant: 'compact',
29
+ sx: {},
30
+ onOpenCallback: null,
31
+ },
32
+ rawProps
33
+ );
31
34
  const { t } = useLocaleContext();
32
35
  const [anchorEl, setAnchorEl] = useState(null);
33
36
  const open = Boolean(anchorEl);
@@ -102,7 +105,12 @@ export default function Actions(props: ActionsProps) {
102
105
  {props.actions.map((action) =>
103
106
  openLoading ? (
104
107
  <MenuItem key={action.label} dense disabled>
105
- <ListItemText primary={<Skeleton />} primaryTypographyProps={{ width: '56px' }} />
108
+ <ListItemText
109
+ primary={<Skeleton />}
110
+ slotProps={{
111
+ primary: { width: '56px' },
112
+ }}
113
+ />
106
114
  </MenuItem>
107
115
  ) : (
108
116
  <MenuItem
@@ -111,7 +119,12 @@ export default function Actions(props: ActionsProps) {
111
119
  dense={!!action.dense}
112
120
  disabled={!!action.disabled}
113
121
  onClick={(e) => onClose(e, action.handler)}>
114
- <ListItemText primary={action.label} primaryTypographyProps={{ color: action.color }} />
122
+ <ListItemText
123
+ primary={action.label}
124
+ slotProps={{
125
+ primary: { color: action.color },
126
+ }}
127
+ />
115
128
  </MenuItem>
116
129
  )
117
130
  )}
@@ -1,3 +1,4 @@
1
+ /* eslint-disable react/prop-types */
1
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
3
  import { formatBNStr, usePaymentContext } from '@blocklet/payment-react';
3
4
  import type { GroupedBN } from '@blocklet/payment-types';
@@ -10,17 +11,36 @@ type Props = {
10
11
  showLogo?: boolean;
11
12
  };
12
13
 
13
- export default function BalanceList(props: Props) {
14
+ export default function BalanceList(rawProps: Props) {
15
+ const props = Object.assign(
16
+ {
17
+ data: {},
18
+ showLogo: false,
19
+ },
20
+ rawProps
21
+ );
14
22
  const { t } = useLocaleContext();
15
23
  const { settings } = usePaymentContext();
16
24
  const currencies = flatten(settings.paymentMethods.map((method) => method.payment_currencies));
17
25
 
18
26
  if (isEmpty(props.data)) {
19
- return <Typography color="text.secondary">{t('common.none')}</Typography>;
27
+ return (
28
+ <Typography
29
+ sx={{
30
+ color: 'text.secondary',
31
+ }}>
32
+ {t('common.none')}
33
+ </Typography>
34
+ );
20
35
  }
21
36
 
22
37
  return (
23
- <Stack direction="column" alignItems="flex-start" sx={{ width: '100%' }}>
38
+ <Stack
39
+ direction="column"
40
+ sx={{
41
+ alignItems: 'flex-start',
42
+ width: '100%',
43
+ }}>
24
44
  {Object.entries(props.data).map(([currencyId, amount]) => {
25
45
  const currency = currencies.find((c) => c.id === currencyId);
26
46
  if (!currency) {
@@ -29,17 +49,30 @@ export default function BalanceList(props: Props) {
29
49
  return (
30
50
  <Stack
31
51
  key={currencyId}
32
- sx={{ width: '100%', maxWidth: '280px' }}
33
52
  direction="row"
34
53
  spacing={1}
35
- alignItems="center">
54
+ sx={{
55
+ alignItems: 'center',
56
+ width: '100%',
57
+ maxWidth: '280px',
58
+ }}>
36
59
  {props?.showLogo && (
37
60
  <Avatar src={currency.logo} alt={currency.symbol} style={{ width: '18px', height: '18px' }} />
38
61
  )}
39
- <Typography sx={{ width: '32px', minWidth: 100 }} color="text.secondary">
62
+ <Typography
63
+ sx={{
64
+ color: 'text.secondary',
65
+ width: '32px',
66
+ minWidth: 100,
67
+ }}>
40
68
  {currency.symbol}
41
69
  </Typography>
42
- <Typography sx={{ flex: 1, textAlign: 'right' }} color="text.primary">
70
+ <Typography
71
+ sx={{
72
+ color: 'text.primary',
73
+ flex: 1,
74
+ textAlign: 'right',
75
+ }}>
43
76
  {formatBNStr(amount, currency.decimal, 6, false)}
44
77
  </Typography>
45
78
  </Stack>
@@ -48,8 +81,3 @@ export default function BalanceList(props: Props) {
48
81
  </Stack>
49
82
  );
50
83
  }
51
-
52
- BalanceList.defaultProps = {
53
- data: {},
54
- showLogo: false,
55
- };
@@ -1,3 +1,4 @@
1
+ /* eslint-disable react/prop-types */
1
2
  import { ExpandLessOutlined, ExpandMoreOutlined } from '@mui/icons-material';
2
3
  import { Box, Collapse, Stack } from '@mui/material';
3
4
  import { useEffect, useState } from 'react';
@@ -11,19 +12,23 @@ type Props = {
11
12
  value?: string;
12
13
  onChange?: (value: string, expanded: boolean) => void;
13
14
  lazy?: boolean;
15
+ card?: boolean;
14
16
  };
15
17
 
16
- IconCollapse.defaultProps = {
17
- value: '',
18
- onChange: () => {},
19
- children: null,
20
- expanded: false,
21
- addons: null,
22
- style: {},
23
- lazy: true,
24
- };
25
-
26
- export default function IconCollapse(props: Props) {
18
+ export default function IconCollapse(rawProps: Props) {
19
+ const props = Object.assign(
20
+ {
21
+ value: '',
22
+ onChange: () => {},
23
+ children: null,
24
+ expanded: false,
25
+ addons: null,
26
+ style: {},
27
+ lazy: true,
28
+ card: false,
29
+ },
30
+ rawProps
31
+ );
27
32
  const [expanded, setExpanded] = useState(props.expanded || false);
28
33
  const toggleExpanded = () => {
29
34
  const newExpanded = !expanded;
@@ -38,27 +43,40 @@ export default function IconCollapse(props: Props) {
38
43
  <>
39
44
  <Stack
40
45
  direction="row"
41
- alignItems="center"
42
- justifyContent="space-between"
43
46
  onClick={(e) => {
44
47
  e.stopPropagation();
45
48
  props.onChange?.(props.value || '', !expanded);
46
49
  toggleExpanded();
47
50
  }}
48
51
  sx={{
52
+ alignItems: 'center',
53
+ justifyContent: 'space-between',
49
54
  width: 1,
50
55
  cursor: 'pointer',
51
56
  fontWeight: 500,
52
57
  color: 'text.primary',
53
58
  '& :hover': { color: 'primary.main' },
59
+ ...(props.card && {
60
+ borderRadius: 1,
61
+ padding: 1,
62
+ pl: 2,
63
+ backgroundColor: 'grey.100',
64
+ }),
54
65
  ...props.style,
55
66
  }}>
56
67
  <Box>{typeof props.trigger === 'function' ? props.trigger(expanded) : props.trigger}</Box>
57
- <Stack direction="row" alignItems="center" spacing={2}>
68
+ <Stack
69
+ direction="row"
70
+ spacing={2}
71
+ sx={{
72
+ alignItems: 'center',
73
+ }}>
58
74
  {props.addons} {expanded ? <ExpandLessOutlined /> : <ExpandMoreOutlined />}
59
75
  </Stack>
60
76
  </Stack>
61
- <Collapse in={expanded}>{expanded || props.lazy ? props.children : null}</Collapse>
77
+ <Collapse in={expanded} sx={{ width: '100%' }}>
78
+ {expanded || props.lazy ? props.children : null}
79
+ </Collapse>
62
80
  </>
63
81
  );
64
82
  }
@@ -9,7 +9,7 @@ interface CopyableProps {
9
9
  children?: ReactNode;
10
10
  style?: React.CSSProperties;
11
11
  }
12
- export default function Copyable({ text, children, style }: CopyableProps) {
12
+ export default function Copyable({ text, children = null, style = {} }: CopyableProps) {
13
13
  const { locale } = useLocaleContext();
14
14
  return (
15
15
  /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
@@ -20,7 +20,13 @@ export default function Copyable({ text, children, style }: CopyableProps) {
20
20
  locale={locale}
21
21
  style={style}
22
22
  render={({ copyButton, containerRef }: any) => (
23
- <Stack ref={containerRef} direction="row" alignItems="center" color="text.secondary">
23
+ <Stack
24
+ ref={containerRef}
25
+ direction="row"
26
+ sx={{
27
+ alignItems: 'center',
28
+ color: 'text.secondary',
29
+ }}>
24
30
  {children || (
25
31
  <Typography
26
32
  className="copyable-text"
@@ -41,8 +47,3 @@ export default function Copyable({ text, children, style }: CopyableProps) {
41
47
  />
42
48
  );
43
49
  }
44
-
45
- Copyable.defaultProps = {
46
- style: {},
47
- children: null,
48
- };
@@ -6,17 +6,25 @@ type Props = {
6
6
  sx?: SxProps;
7
7
  };
8
8
 
9
- export default function Currency({ logo, name, sx }: Props) {
9
+ export default function Currency({ logo, name, sx = {} }: Props) {
10
10
  return (
11
- <Stack direction="row" alignItems="center" spacing={0.5} sx={sx}>
11
+ <Stack
12
+ direction="row"
13
+ spacing={0.5}
14
+ sx={[
15
+ {
16
+ alignItems: 'center',
17
+ },
18
+ ...(Array.isArray(sx) ? sx : [sx]),
19
+ ]}>
12
20
  <Avatar src={logo} alt={name} sx={{ width: 20, height: 20 }} />
13
- <Typography color="text.primary" className="currency-name">
21
+ <Typography
22
+ className="currency-name"
23
+ sx={{
24
+ color: 'text.primary',
25
+ }}>
14
26
  {name}
15
27
  </Typography>
16
28
  </Stack>
17
29
  );
18
30
  }
19
-
20
- Currency.defaultProps = {
21
- sx: {},
22
- };
@@ -15,11 +15,7 @@ type Props = {
15
15
  variant?: LiteralUnion<'compact' | 'normal', string>;
16
16
  };
17
17
 
18
- CustomerActions.defaultProps = {
19
- variant: 'compact',
20
- };
21
-
22
- export default function CustomerActions({ data, onChange, variant }: Props) {
18
+ export default function CustomerActions({ data, onChange, variant = 'compact' }: Props) {
23
19
  const { t } = useLocaleContext();
24
20
  const [state, setState] = useSetState({
25
21
  action: '',
@@ -0,0 +1,99 @@
1
+ /* eslint-disable react/no-unstable-nested-components */
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import { formatPrice, Table, usePaymentContext } from '@blocklet/payment-react';
4
+ import type { TLineItemExpanded, TPaymentCurrency, TPriceExpanded } from '@blocklet/payment-types';
5
+ import { Avatar, Stack, Typography, Box } from '@mui/material';
6
+ import { useNavigate } from 'react-router-dom';
7
+ import Copyable from '../copyable';
8
+
9
+ type CreditGrantItemListProps = {
10
+ data: TLineItemExpanded[];
11
+ currency: TPaymentCurrency;
12
+ mode?: 'dashboard' | 'portal';
13
+ };
14
+
15
+ export default function CreditGrantItemList({ data, currency, mode = 'portal' }: CreditGrantItemListProps) {
16
+ const { t } = useLocaleContext();
17
+ const navigate = useNavigate();
18
+ const { session } = usePaymentContext();
19
+ const isAdmin = mode === 'dashboard' && ['owner', 'admin'].includes(session?.user?.role || '');
20
+
21
+ const viewProduct = (price: TPriceExpanded) => {
22
+ if (!isAdmin) {
23
+ return;
24
+ }
25
+ navigate(`/admin/products/${price.product_id}?price_id=${price.id}&currency_id=${price.currency_id}`);
26
+ };
27
+
28
+ const columns = [
29
+ {
30
+ label: t('admin.subscription.product'),
31
+ name: 'product_id',
32
+ options: {
33
+ customBodyRenderLite: (_: string, index: number) => {
34
+ const item = data[index] as TLineItemExpanded;
35
+ return (
36
+ <Stack direction="row" spacing={1} onClick={() => viewProduct(item.price)}>
37
+ <Avatar
38
+ src={item.price?.product?.images?.[0]}
39
+ alt={item.price?.product?.name}
40
+ variant="square"
41
+ sx={{ borderRadius: 1 }}
42
+ />
43
+ <Stack direction="column" spacing={0.5}>
44
+ <Typography
45
+ variant="body2"
46
+ sx={{
47
+ fontWeight: 500,
48
+ }}>
49
+ {item.price?.product?.name}
50
+ </Typography>
51
+ <Typography
52
+ variant="body2"
53
+ sx={{
54
+ color: 'text.secondary',
55
+ }}>
56
+ {formatPrice(item.price, currency, item.price?.product?.unit_label || '')}
57
+ </Typography>
58
+ </Stack>
59
+ </Stack>
60
+ );
61
+ },
62
+ },
63
+ },
64
+ {
65
+ label: t('common.id'),
66
+ name: 'id',
67
+ options: {
68
+ customBodyRenderLite: (_: string, index: number) => {
69
+ const item = data[index] as TLineItemExpanded;
70
+ return (
71
+ <Box onClick={() => viewProduct(item.price)}>
72
+ <Copyable text={item.id} />
73
+ </Box>
74
+ );
75
+ },
76
+ },
77
+ },
78
+ ].filter(Boolean);
79
+
80
+ return (
81
+ <Table
82
+ data={data}
83
+ columns={columns}
84
+ loading={false}
85
+ footer={false}
86
+ toolbar={false}
87
+ components={{
88
+ TableToolbar: () => null,
89
+ TableFooter: () => null,
90
+ }}
91
+ options={{
92
+ count: data.length,
93
+ page: 0,
94
+ rowsPerPage: 100,
95
+ }}
96
+ emptyNodeText={t('admin.customer.creditGrants.noApplicableProducts')}
97
+ />
98
+ );
99
+ }
@@ -0,0 +1,233 @@
1
+ import { formatBNStr, CreditGrantsList, CreditTransactionsList, api } from '@blocklet/payment-react';
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import { Avatar, Box, Card, CardContent, Stack, Typography, Tabs, Tab } from '@mui/material';
4
+ import { useState } from 'react';
5
+ import type { TPaymentCurrency } from '@blocklet/payment-types';
6
+ import { useRequest } from 'ahooks';
7
+
8
+ enum CreditTab {
9
+ OVERVIEW = 'overview',
10
+ GRANTS = 'grants',
11
+ TRANSACTIONS = 'transactions',
12
+ }
13
+
14
+ interface CreditOverviewProps {
15
+ customerId: string;
16
+ settings: any;
17
+ mode?: 'portal' | 'dashboard';
18
+ }
19
+
20
+ const fetchCreditSummary = async (customerId: string) => {
21
+ try {
22
+ const [grantsResponse, pendingAmountResponse] = await Promise.all([
23
+ api.get(`/api/credit-grants/summary?customer_id=${customerId}`),
24
+ api.get(`/api/meter-events/pending-amount?customer_id=${customerId}`),
25
+ // api.get(`/api/credit-transactions/summary?customer_id=${customerId}`),
26
+ ]);
27
+
28
+ return {
29
+ grants: grantsResponse.data,
30
+ // transactions: transactionsResponse.data,
31
+ pendingAmount: pendingAmountResponse.data,
32
+ };
33
+ } catch (error) {
34
+ console.error('Failed to fetch credit summary:', error);
35
+ return {
36
+ grants: null,
37
+ transactions: null,
38
+ pendingAmount: null,
39
+ };
40
+ }
41
+ };
42
+
43
+ export default function CreditOverview({ customerId, settings, mode = 'portal' }: CreditOverviewProps) {
44
+ const { t } = useLocaleContext();
45
+ const [creditTab, setCreditTab] = useState<CreditTab>(CreditTab.OVERVIEW);
46
+ const { data: creditSummary } = useRequest(fetchCreditSummary, {
47
+ defaultParams: [customerId],
48
+ refreshDeps: [creditTab === CreditTab.OVERVIEW],
49
+ });
50
+
51
+ // 渲染信用概览卡片
52
+ const renderCreditOverviewCard = (currency: any) => {
53
+ const method = settings?.paymentMethods?.find((m: any) => m.id === currency.payment_method_id);
54
+ if (method?.type !== 'arcblock') {
55
+ return null;
56
+ }
57
+
58
+ const currencyId = currency.id as string;
59
+ const grantData = creditSummary?.grants?.[currencyId];
60
+ const pendingAmount = creditSummary?.pendingAmount?.[currencyId] || '0';
61
+
62
+ if (!grantData) {
63
+ return null;
64
+ }
65
+
66
+ const totalAmount = grantData.totalAmount || '0';
67
+ const remainingAmount = grantData.remainingAmount || '0';
68
+
69
+ return (
70
+ <Card
71
+ key={currency.id}
72
+ sx={{
73
+ height: '100%',
74
+ display: 'flex',
75
+ flexDirection: 'column',
76
+ border: '1px solid',
77
+ borderColor: 'divider',
78
+ boxShadow: 1,
79
+ borderRadius: 1,
80
+ }}>
81
+ <CardContent sx={{ flexGrow: 1 }}>
82
+ <Stack spacing={2}>
83
+ {/* 货币信息 */}
84
+ <Stack
85
+ direction="row"
86
+ spacing={1}
87
+ sx={{
88
+ alignItems: 'center',
89
+ }}>
90
+ <Avatar src={currency.logo} alt={currency.symbol} sx={{ width: 24, height: 24 }} />
91
+ <Typography variant="h6" component="div">
92
+ {currency.name}
93
+ </Typography>
94
+ </Stack>
95
+
96
+ {/* 可用额度 / 总额度 */}
97
+ <Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
98
+ <Typography
99
+ variant="body2"
100
+ gutterBottom
101
+ sx={{
102
+ color: 'text.secondary',
103
+ }}>
104
+ {t('admin.customer.creditGrants.creditBalance')}
105
+ </Typography>
106
+ <Typography variant="h5" component="div" sx={{ fontWeight: 'normal' }}>
107
+ {totalAmount === '0' && remainingAmount === '0' ? (
108
+ <>0 {currency.symbol}</>
109
+ ) : (
110
+ <>
111
+ {formatBNStr(remainingAmount, currency.decimal, 6, true)} /{' '}
112
+ {formatBNStr(totalAmount, currency.decimal, 6, true)} {currency.symbol}
113
+ </>
114
+ )}
115
+ </Typography>
116
+ </Box>
117
+
118
+ {/* 欠费额度 */}
119
+ {pendingAmount !== '0' && (
120
+ <Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
121
+ <Typography
122
+ variant="body2"
123
+ gutterBottom
124
+ sx={{
125
+ color: 'text.secondary',
126
+ }}>
127
+ {t('admin.customer.creditGrants.pendingAmount')}
128
+ </Typography>
129
+ <Typography
130
+ variant="body1"
131
+ sx={{
132
+ color: 'error.main',
133
+ }}>
134
+ {formatBNStr(pendingAmount, currency.decimal, 6, true)} {currency.symbol}
135
+ </Typography>
136
+ </Box>
137
+ )}
138
+ </Stack>
139
+ </CardContent>
140
+ </Card>
141
+ );
142
+ };
143
+
144
+ // 获取有信用额度的货币
145
+ const creditCurrencies =
146
+ settings?.paymentMethods
147
+ ?.filter((method: any) => method.type === 'arcblock')
148
+ ?.flatMap((method: any) => method.payment_currencies)
149
+ ?.filter((currency: TPaymentCurrency) => {
150
+ const currencyId = currency.id as string;
151
+ const grantData = creditSummary?.grants?.[currencyId];
152
+ return grantData;
153
+ }) || [];
154
+
155
+ if (creditCurrencies.length === 0) {
156
+ return null;
157
+ }
158
+
159
+ return (
160
+ <Stack sx={{ width: '100%' }}>
161
+ <Tabs
162
+ value={creditTab}
163
+ onChange={(_, newValue) => setCreditTab(newValue as CreditTab)}
164
+ // sx={{ borderBottom: 1, borderColor: 'divider', }}
165
+ sx={{
166
+ flex: '1 0 auto',
167
+ maxWidth: '100%',
168
+ fontSize: 14,
169
+ borderBottom: 1,
170
+ borderColor: 'divider',
171
+ '.Mui-selected': {
172
+ fontSize: '14px !important',
173
+ color: 'primary.main',
174
+ },
175
+ }}>
176
+ <Tab label={t('admin.creditGrants.overview')} value={CreditTab.OVERVIEW} />
177
+ <Tab label={t('admin.creditGrants.title')} value={CreditTab.GRANTS} />
178
+ <Tab label={t('admin.creditTransactions.title')} value={CreditTab.TRANSACTIONS} />
179
+ </Tabs>
180
+ {/* 概览标签页 */}
181
+ {creditTab === CreditTab.OVERVIEW && (
182
+ <Box sx={{ width: '100%', mt: 1 }} key={creditTab}>
183
+ <Box
184
+ sx={{
185
+ display: 'grid',
186
+ gridTemplateColumns: {
187
+ xs: 'repeat(1, 1fr)',
188
+ md: 'repeat(2, 1fr)',
189
+ },
190
+ gap: 2,
191
+ '@container (max-width: 600px)': {
192
+ gridTemplateColumns: 'repeat(1, 1fr)',
193
+ },
194
+ }}>
195
+ {creditCurrencies.map(renderCreditOverviewCard)}
196
+ </Box>
197
+
198
+ {creditCurrencies.length === 0 && (
199
+ <Box
200
+ sx={{
201
+ display: 'flex',
202
+ flexDirection: 'column',
203
+ alignItems: 'center',
204
+ justifyContent: 'center',
205
+ py: 8,
206
+ textAlign: 'center',
207
+ }}>
208
+ <Typography
209
+ variant="h6"
210
+ gutterBottom
211
+ sx={{
212
+ color: 'text.secondary',
213
+ }}>
214
+ {t('admin.customer.creditGrants.noGrants')}
215
+ </Typography>
216
+ <Typography
217
+ variant="body2"
218
+ sx={{
219
+ color: 'text.secondary',
220
+ }}>
221
+ {t('admin.customer.creditGrants.noGrantsDescription')}
222
+ </Typography>
223
+ </Box>
224
+ )}
225
+ </Box>
226
+ )}
227
+ {creditTab === CreditTab.GRANTS && <CreditGrantsList customer_id={customerId} mode={mode} key={creditTab} />}
228
+ {creditTab === CreditTab.TRANSACTIONS && (
229
+ <CreditTransactionsList customer_id={customerId} mode={mode} key={creditTab} />
230
+ )}
231
+ </Stack>
232
+ );
233
+ }
@@ -81,12 +81,17 @@ export default function CustomerForm() {
81
81
  },
82
82
  }}
83
83
  />
84
-
85
84
  <FormLabel className="base-label">{t('payment.checkout.billing.required')}</FormLabel>
86
85
  <Controller
87
86
  name="address.country"
88
87
  control={control}
89
- render={({ field }) => <CountrySelect {...field} sx={{ pl: '6px' }} />}
88
+ render={({ field }) => (
89
+ <CountrySelect
90
+ {...field}
91
+ ref={field.ref as unknown as React.RefObject<HTMLDivElement | null>}
92
+ sx={{ pl: '6px' }}
93
+ />
94
+ )}
90
95
  />
91
96
  <FormInput
92
97
  name="address.state"