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
@@ -0,0 +1,308 @@
1
+ /* eslint-disable react/no-unstable-nested-components */
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import {
4
+ api,
5
+ formatBNStr,
6
+ formatTime,
7
+ CreditTransactionsList,
8
+ CreditStatusChip,
9
+ getCustomerAvatar,
10
+ } from '@blocklet/payment-react';
11
+ import type { TCreditGrantExpanded } from '@blocklet/payment-types';
12
+ import { ArrowBackOutlined } from '@mui/icons-material';
13
+ import { Alert, Avatar, Box, CircularProgress, Divider, Stack, Typography } from '@mui/material';
14
+ import { useRequest } from 'ahooks';
15
+ import { useNavigate, useParams } from 'react-router-dom';
16
+ import { styled } from '@mui/system';
17
+ import { useCallback } from 'react';
18
+ import InfoMetric from '../../../components/info-metric';
19
+ import { useSessionContext } from '../../../contexts/session';
20
+ import SectionHeader from '../../../components/section/header';
21
+ import CreditGrantItemList from '../../../components/customer/credit-grant-item-list';
22
+ import InfoRow from '../../../components/info-row';
23
+ import InfoRowGroup from '../../../components/info-row-group';
24
+
25
+ const fetchData = (id: string | undefined): Promise<TCreditGrantExpanded> => {
26
+ return api.get(`/api/credit-grants/${id}`).then((res: any) => res.data);
27
+ };
28
+
29
+ export default function CustomerCreditGrantDetail() {
30
+ const { id } = useParams() as { id: string };
31
+ const navigate = useNavigate();
32
+ const { t } = useLocaleContext();
33
+ const { session } = useSessionContext();
34
+ const { loading, error, data } = useRequest(() => fetchData(id));
35
+
36
+ const handleBack = useCallback(() => {
37
+ navigate('/customer', { replace: true });
38
+ }, [navigate]);
39
+
40
+ if (data?.customer?.did && session?.user?.did && data.customer.did !== session.user.did) {
41
+ return <Alert severity="error">You do not have permission to access other customer data</Alert>;
42
+ }
43
+ if (error) {
44
+ return <Alert severity="error">{error.message}</Alert>;
45
+ }
46
+
47
+ if (loading || !data) {
48
+ return <CircularProgress />;
49
+ }
50
+
51
+ const isDepleted = data?.status === 'depleted';
52
+
53
+ const getStatusText = (status: string) => {
54
+ switch (status) {
55
+ case 'granted':
56
+ return t('admin.customer.creditGrants.status.granted');
57
+ case 'pending':
58
+ return t('admin.customer.creditGrants.status.pending');
59
+ case 'depleted':
60
+ return t('admin.customer.creditGrants.status.depleted');
61
+ case 'expired':
62
+ return t('admin.customer.creditGrants.status.expired');
63
+ case 'voided':
64
+ return t('admin.customer.creditGrants.status.voided');
65
+ default:
66
+ return status;
67
+ }
68
+ };
69
+
70
+ const getUsagePercentage = () => {
71
+ if (!data.amount || !data.remaining_amount) return 0;
72
+ const total = parseFloat(data.amount);
73
+ const remaining = parseFloat(data.remaining_amount);
74
+ return ((total - remaining) / total) * 100;
75
+ };
76
+
77
+ return (
78
+ <Root>
79
+ <Box>
80
+ <Stack
81
+ className="page-header"
82
+ direction="row"
83
+ justifyContent="space-between"
84
+ alignItems="center"
85
+ sx={{ position: 'relative' }}>
86
+ <Stack
87
+ direction="row"
88
+ onClick={handleBack}
89
+ alignItems="center"
90
+ sx={{ fontWeight: 'normal', cursor: 'pointer' }}>
91
+ <ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
92
+ <Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
93
+ {t('admin.customer.creditGrants.title')}
94
+ </Typography>
95
+ </Stack>
96
+ </Stack>
97
+ <Box
98
+ mt={4}
99
+ sx={{
100
+ display: 'flex',
101
+ gap: {
102
+ xs: 2,
103
+ sm: 2,
104
+ md: 5,
105
+ },
106
+ flexWrap: 'wrap',
107
+ flexDirection: {
108
+ xs: 'column',
109
+ sm: 'column',
110
+ md: 'row',
111
+ },
112
+ alignItems: {
113
+ xs: 'flex-start',
114
+ sm: 'flex-start',
115
+ md: 'center',
116
+ },
117
+ }}>
118
+ <Stack direction="row" justifyContent="space-between" alignItems="center">
119
+ <Stack direction="row" alignItems="center" flexWrap="wrap" gap={1}>
120
+ <Stack direction="column" alignItems="flex-start" justifyContent="space-around">
121
+ <Typography variant="h2" color="text.primary">
122
+ {data.name || data.id}
123
+ </Typography>
124
+ </Stack>
125
+ </Stack>
126
+ </Stack>
127
+ <Stack
128
+ className="section-body"
129
+ justifyContent="flex-start"
130
+ flexWrap="wrap"
131
+ sx={{
132
+ 'hr.MuiDivider-root:last-child': {
133
+ display: 'none',
134
+ },
135
+ flexDirection: {
136
+ xs: 'column',
137
+ sm: 'column',
138
+ md: 'row',
139
+ },
140
+ alignItems: 'flex-start',
141
+ gap: {
142
+ xs: 1,
143
+ sm: 1,
144
+ md: 3,
145
+ },
146
+ }}>
147
+ <InfoMetric
148
+ label={t('common.status')}
149
+ value={<CreditStatusChip status={data.status} label={getStatusText(data.status)} />}
150
+ />
151
+ <InfoMetric
152
+ label={t('admin.customer.creditGrants.originalAmount')}
153
+ value={
154
+ <Stack direction="row" alignItems="center" spacing={0.5}>
155
+ <Avatar
156
+ src={data.paymentCurrency?.logo}
157
+ sx={{ width: 16, height: 16 }}
158
+ alt={data.paymentCurrency?.symbol}
159
+ />
160
+ <Typography variant="body2">
161
+ {formatBNStr(data.amount, data.paymentCurrency.decimal)} {data.paymentCurrency.symbol}
162
+ </Typography>
163
+ </Stack>
164
+ }
165
+ divider
166
+ />
167
+ <InfoMetric
168
+ label={t('common.remainingCredit')}
169
+ value={
170
+ <Stack direction="row" alignItems="center" spacing={0.5}>
171
+ <Avatar
172
+ src={data.paymentCurrency?.logo}
173
+ sx={{ width: 16, height: 16 }}
174
+ alt={data.paymentCurrency?.symbol}
175
+ />
176
+ <Typography variant="body2">
177
+ {formatBNStr(data.remaining_amount, data.paymentCurrency.decimal)} {data.paymentCurrency.symbol}
178
+ </Typography>
179
+ </Stack>
180
+ }
181
+ divider
182
+ />
183
+ <InfoMetric
184
+ label={t('admin.customer.creditGrants.usage')}
185
+ value={
186
+ <Typography variant="body2" color={isDepleted ? 'error.main' : 'text.primary'}>
187
+ {getUsagePercentage().toFixed(1)}%
188
+ </Typography>
189
+ }
190
+ divider
191
+ />
192
+ <InfoMetric
193
+ label={t('common.effectiveDate')}
194
+ value={formatTime(data.effective_at ? data.effective_at * 1000 : data.created_at, 'YYYY-MM-DD HH:mm:ss')}
195
+ />
196
+ {data.expires_at && (
197
+ <InfoMetric
198
+ label={t('common.expirationDate')}
199
+ value={formatTime(data.expires_at * 1000, 'YYYY-MM-DD HH:mm:ss')}
200
+ />
201
+ )}
202
+ </Stack>
203
+ </Box>
204
+ </Box>
205
+ <Box className="section" sx={{ containerType: 'inline-size' }}>
206
+ <SectionHeader title={t('admin.details')} />
207
+ <InfoRowGroup
208
+ sx={{
209
+ display: 'grid',
210
+ gridTemplateColumns: {
211
+ xs: 'repeat(1, 1fr)',
212
+ xl: 'repeat(2, 1fr)',
213
+ },
214
+ '@container (min-width: 1000px)': {
215
+ gridTemplateColumns: 'repeat(2, 1fr)',
216
+ },
217
+ '.info-row-wrapper': {
218
+ gap: 1,
219
+ flexDirection: {
220
+ xs: 'column',
221
+ xl: 'row',
222
+ },
223
+ alignItems: {
224
+ xs: 'flex-start',
225
+ xl: 'center',
226
+ },
227
+ '@container (min-width: 1000px)': {
228
+ flexDirection: 'row',
229
+ alignItems: 'center',
230
+ },
231
+ },
232
+ }}>
233
+ <InfoRow
234
+ label={t('common.customer')}
235
+ value={
236
+ <Stack direction="row" alignItems="center" spacing={1}>
237
+ <Avatar
238
+ src={getCustomerAvatar(
239
+ data.customer?.did,
240
+ data.customer?.updated_at ? new Date(data.customer.updated_at).toISOString() : '',
241
+ 24
242
+ )}
243
+ alt={data.customer?.name}
244
+ sx={{ width: 24, height: 24 }}
245
+ />
246
+ <Typography>{data.customer?.name}</Typography>
247
+ </Stack>
248
+ }
249
+ />
250
+ <InfoRow
251
+ label={t('common.scope')}
252
+ value={
253
+ <Typography>
254
+ {data.applicability_config?.scope?.prices ? t('common.specific') : t('common.general')}
255
+ </Typography>
256
+ }
257
+ />
258
+ <InfoRow label={t('admin.creditProduct.priority.label')} value={<Typography>{data.priority}</Typography>} />
259
+ <InfoRow label={t('common.createdAt')} value={formatTime(data.created_at)} />
260
+ </InfoRowGroup>
261
+ </Box>
262
+
263
+ <Divider />
264
+ {data.items && data.items.length > 0 && (
265
+ <>
266
+ <Box className="section">
267
+ <SectionHeader title={t('admin.creditProduct.associatedPrices.label')} />
268
+ <Box className="section-body">
269
+ <CreditGrantItemList data={data.items} currency={data.paymentCurrency} />
270
+ </Box>
271
+ </Box>
272
+ <Divider />
273
+ </>
274
+ )}
275
+
276
+ <Divider />
277
+ <Box className="section">
278
+ <Typography variant="h3" className="section-header">
279
+ {t('admin.creditTransactions.title')}
280
+ </Typography>
281
+ <Box className="section-body">
282
+ <CreditTransactionsList
283
+ customer_id={data.customer_id}
284
+ credit_grant_id={data.id}
285
+ showAdminColumns={false}
286
+ showTimeFilter
287
+ />
288
+ </Box>
289
+ </Box>
290
+ </Root>
291
+ );
292
+ }
293
+
294
+ const Root = styled(Stack)`
295
+ margin-bottom: 24px;
296
+ gap: 24px;
297
+ flex-direction: column;
298
+ @media (max-width: ${({ theme }) => theme.breakpoints.values.md}px) {
299
+ . {
300
+ border: none;
301
+ box-shadow: none;
302
+ padding: 0;
303
+ }
304
+ .section-header {
305
+ font-size: 18px;
306
+ }
307
+ }
308
+ `;
@@ -7,6 +7,7 @@ import {
7
7
  usePaymentContext,
8
8
  OverdueInvoicePayment,
9
9
  } from '@blocklet/payment-react';
10
+
10
11
  import type { GroupedBN, TCustomerExpanded } from '@blocklet/payment-types';
11
12
  import {
12
13
  ExpandMore,
@@ -38,6 +39,7 @@ import { flatten, isEmpty } from 'lodash';
38
39
  import { memo, useEffect, useState } from 'react';
39
40
  import { useNavigate, useSearchParams } from 'react-router-dom';
40
41
  import { joinURL } from 'ufo';
42
+ import CreditOverview from '../../components/customer/credit-overview';
41
43
 
42
44
  import { useTransitionContext } from '../../components/progress-bar';
43
45
  import CurrentSubscriptions from '../../components/subscription/portal/list';
@@ -46,7 +48,15 @@ import { useSessionContext } from '../../contexts/session';
46
48
  import api from '../../libs/api';
47
49
  import CustomerRevenueList from '../../components/payouts/portal/list';
48
50
 
49
- type Result = TCustomerExpanded & { summary: { [key: string]: GroupedBN }; error?: string };
51
+ type Result = TCustomerExpanded & {
52
+ summary: { [key: string]: GroupedBN };
53
+ creditSummary?: {
54
+ grants?: { [key: string]: { totalAmount: string; remainingAmount: string; grantCount: number } };
55
+ transactions?: any;
56
+ pendingAmount?: { [key: string]: string };
57
+ };
58
+ error?: string;
59
+ };
50
60
 
51
61
  const fetchData = (): Promise<Result> => {
52
62
  return api.get('/api/customers/me?skipError=true&create=true').then((res) => res.data);
@@ -105,7 +115,14 @@ const CurrencyCard = memo(
105
115
  const CardSkeleton = memo(({ height = 100 }: { height: number }) => {
106
116
  return (
107
117
  <Box className="base-card section">
108
- <Box className="section-header" display="flex" justifyContent="space-between" alignItems="center" mb={2}>
118
+ <Box
119
+ className="section-header"
120
+ sx={{
121
+ display: 'flex',
122
+ justifyContent: 'space-between',
123
+ alignItems: 'center',
124
+ mb: 2,
125
+ }}>
109
126
  <Skeleton variant="text" width={150} height={32} />
110
127
  </Box>
111
128
  <Skeleton variant="rectangular" height={height} />
@@ -266,6 +283,12 @@ export default function CustomerHome() {
266
283
  }
267
284
  };
268
285
 
286
+ // const handleAddCredit = (currency: any) => {
287
+ // if (currency?.id) {
288
+ // navigate(`/customer/credit-packages?currency=${currency.id}`);
289
+ // }
290
+ // };
291
+
269
292
  const onToggleActive = (e: SelectChangeEvent) => {
270
293
  setSubscriptionLoading(true);
271
294
  setState({ onlyActive: e.target.value === 'active' });
@@ -274,12 +297,21 @@ export default function CustomerHome() {
274
297
  }, 300);
275
298
  };
276
299
 
300
+ // const handleViewCreditDetails = (currencyId: string) => {
301
+ // navigate(`/customer/credit-grants?currency=${currencyId}`);
302
+ // };
303
+
277
304
  const SubscriptionCard =
278
305
  loadingCard || !hasSubscriptions ? null : (
279
306
  <Box className="base-card section section-subscription">
280
307
  <Box className="section-header">
281
308
  <Typography variant="h3">{t('customer.subscription.title')}</Typography>
282
- <Stack direction="row" spacing={1} alignItems="center">
309
+ <Stack
310
+ direction="row"
311
+ spacing={1}
312
+ sx={{
313
+ alignItems: 'center',
314
+ }}>
283
315
  <NotificationPreference />
284
316
  {subscriptionStatus && (
285
317
  <FormControl
@@ -340,14 +372,16 @@ export default function CustomerHome() {
340
372
  <Typography variant="h3">{t('admin.customer.summary.stats')}</Typography>
341
373
  </Box>
342
374
  <Stack
343
- gap={2}
344
- mt={2}
345
375
  sx={{
376
+ gap: 2,
377
+ mt: 2,
346
378
  display: 'grid',
379
+
347
380
  gridTemplateColumns: {
348
381
  xs: 'repeat(1, 1fr)',
349
382
  md: 'repeat(2, 1fr)',
350
383
  },
384
+
351
385
  '@container (max-width: 600px)': {
352
386
  gridTemplateColumns: 'repeat(1, 1fr)',
353
387
  },
@@ -368,8 +402,17 @@ export default function CustomerHome() {
368
402
  borderColor: 'grey.100',
369
403
  boxShadow: 1,
370
404
  }}>
371
- <Stack flexDirection="row" justifyContent="space-between">
372
- <Box alignItems="center" display="flex" gap={1}>
405
+ <Stack
406
+ sx={{
407
+ flexDirection: 'row',
408
+ justifyContent: 'space-between',
409
+ }}>
410
+ <Box
411
+ sx={{
412
+ alignItems: 'center',
413
+ display: 'flex',
414
+ gap: 1,
415
+ }}>
373
416
  <Avatar src={c?.logo} alt={c?.symbol} sx={{ width: 18, height: 18 }} />
374
417
  <Typography
375
418
  variant="h5"
@@ -377,7 +420,11 @@ export default function CustomerHome() {
377
420
  sx={{ fontSize: '16px', color: 'text.primary', fontWeight: '500' }}>
378
421
  {c?.symbol}
379
422
  </Typography>
380
- <Typography sx={{ fontSize: 12 }} color="text.lighter">
423
+ <Typography
424
+ sx={{
425
+ color: 'text.lighter',
426
+ fontSize: 12,
427
+ }}>
381
428
  {c?.methodName}
382
429
  </Typography>
383
430
  </Box>
@@ -401,7 +448,6 @@ export default function CustomerHome() {
401
448
  gap: 1,
402
449
  mt: 1.5,
403
450
  }}>
404
- {/* 使用配置渲染卡片 */}
405
451
  {Object.entries(CARD_CONFIG).map(([type, config]) => {
406
452
  if (!isCardVisible(type, config, data, c, method)) {
407
453
  return null;
@@ -433,6 +479,18 @@ export default function CustomerHome() {
433
479
  </Box>
434
480
  );
435
481
 
482
+ // 独立的Credit Card组件
483
+ const CreditCard = loadingCard ? (
484
+ <CardSkeleton height={400} />
485
+ ) : (
486
+ <Box className="base-card section section-credit">
487
+ <Box className="section-header" sx={{ mb: 2 }}>
488
+ <Typography variant="h3">{t('admin.creditGrants.title')}</Typography>
489
+ </Box>
490
+ <CreditOverview customerId={data?.id} settings={settings} />
491
+ </Box>
492
+ );
493
+
436
494
  const InvoiceCard = loadingCard ? (
437
495
  <CardSkeleton height={200} />
438
496
  ) : (
@@ -461,15 +519,14 @@ export default function CustomerHome() {
461
519
  </Box>
462
520
  );
463
521
 
464
- const RevenueCard =
465
- loadingCard || !hasRevenues ? null : (
466
- <Box className="base-card section section-revenue">
467
- <Box className="section-header">
468
- <Typography variant="h3">{t('customer.payout.title')}</Typography>
469
- </Box>
470
- <CustomerRevenueList setHasRevenues={setHasRevenues} />
522
+ const RevenueCard = loadingCard ? null : (
523
+ <Box className="base-card section section-revenue" sx={{ visibility: hasRevenues ? 'visible' : 'hidden' }}>
524
+ <Box className="section-header">
525
+ <Typography variant="h3">{t('customer.payout.title')}</Typography>
471
526
  </Box>
472
- );
527
+ <CustomerRevenueList setHasRevenues={setHasRevenues} />
528
+ </Box>
529
+ );
473
530
 
474
531
  return (
475
532
  <Content>
@@ -525,6 +582,8 @@ export default function CustomerHome() {
525
582
  <Root>
526
583
  {SummaryCard}
527
584
  {SummaryCard && <Divider />}
585
+ {CreditCard}
586
+ {CreditCard && <Divider />}
528
587
  {SubscriptionCard}
529
588
  {SubscriptionCard && <Divider />}
530
589
  {InvoiceCard}
@@ -128,18 +128,32 @@ export default function CustomerInvoiceDetail() {
128
128
  const paymentDetails = data.paymentIntent?.payment_details || data.metadata?.payment_details;
129
129
  return (
130
130
  <InvoiceDetailRoot direction="column" spacing={3}>
131
- <Stack direction="row" justifyContent="space-between">
131
+ <Stack
132
+ direction="row"
133
+ sx={{
134
+ justifyContent: 'space-between',
135
+ }}>
132
136
  <Stack
133
137
  direction="row"
134
138
  onClick={() => goBackOrFallback('/customer')}
135
- alignItems="center"
136
- sx={{ fontWeight: 'normal', padding: '10px 0', cursor: 'pointer' }}>
139
+ sx={{
140
+ alignItems: 'center',
141
+ fontWeight: 'normal',
142
+ padding: '10px 0',
143
+ cursor: 'pointer',
144
+ }}>
137
145
  <ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
138
146
  <Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
139
147
  {t('common.previous')}
140
148
  </Typography>
141
149
  </Stack>
142
- <Stack direction="row" spacing={1} justifyContent="flex-end" alignItems="center">
150
+ <Stack
151
+ direction="row"
152
+ spacing={1}
153
+ sx={{
154
+ justifyContent: 'flex-end',
155
+ alignItems: 'center',
156
+ }}>
143
157
  {['open', 'paid', 'uncollectible'].includes(data.status) && !isDonation && <Download data={data} />}
144
158
  {data?.paymentLink?.donation_settings?.reference && (
145
159
  <Button
@@ -164,49 +178,68 @@ export default function CustomerInvoiceDetail() {
164
178
  </Stack>
165
179
  </Stack>
166
180
  <Box
167
- mt={4}
168
181
  sx={{
182
+ mt: 4,
169
183
  display: 'flex',
184
+
170
185
  gap: {
171
186
  xs: 2,
172
187
  sm: 2,
173
188
  md: 5,
174
189
  },
190
+
175
191
  flexWrap: 'wrap',
192
+
176
193
  flexDirection: {
177
194
  xs: 'column',
178
195
  sm: 'column',
179
196
  md: 'row',
180
197
  },
198
+
181
199
  alignItems: {
182
200
  xs: 'flex-start',
183
201
  sm: 'flex-start',
184
202
  md: 'center',
185
203
  },
186
204
  }}>
187
- <Stack direction="row" justifyContent="space-between" alignItems="center">
188
- <Stack direction="row" alignItems="center" flexWrap="wrap" gap={1}>
205
+ <Stack
206
+ direction="row"
207
+ sx={{
208
+ justifyContent: 'space-between',
209
+ alignItems: 'center',
210
+ }}>
211
+ <Stack
212
+ direction="row"
213
+ sx={{
214
+ alignItems: 'center',
215
+ flexWrap: 'wrap',
216
+ gap: 1,
217
+ }}>
189
218
  <Typography variant="h1">{data.number}</Typography>
190
219
  </Stack>
191
220
  </Stack>
192
221
  <Stack
193
222
  className="section-body"
194
- justifyContent="flex-start"
195
- flexWrap="wrap"
196
223
  sx={{
224
+ justifyContent: 'flex-start',
225
+ flexWrap: 'wrap',
226
+
197
227
  'hr.MuiDivider-root:last-child': {
198
228
  display: 'none',
199
229
  },
230
+
200
231
  flexDirection: {
201
232
  xs: 'column',
202
233
  sm: 'column',
203
234
  md: 'row',
204
235
  },
236
+
205
237
  alignItems: {
206
238
  xs: 'flex-start',
207
239
  sm: 'flex-start',
208
240
  md: 'center',
209
241
  },
242
+
210
243
  gap: {
211
244
  xs: 1,
212
245
  sm: 1,
@@ -232,7 +265,11 @@ export default function CustomerInvoiceDetail() {
232
265
  label={t('admin.subscription.name')}
233
266
  value={
234
267
  <Link to={`/customer/subscription/${data.subscription.id}`}>
235
- <Typography variant="body1" color="text.link">
268
+ <Typography
269
+ variant="body1"
270
+ sx={{
271
+ color: 'text.link',
272
+ }}>
236
273
  {data.subscription.description || data.subscription.id}
237
274
  </Typography>
238
275
  </Link>
@@ -252,7 +289,12 @@ export default function CustomerInvoiceDetail() {
252
289
  </Box>
253
290
  <Divider />
254
291
  <Box className="section" sx={{ containerType: 'inline-size' }}>
255
- <Typography variant="h3" mb={3} className="section-header">
292
+ <Typography
293
+ variant="h3"
294
+ className="section-header"
295
+ sx={{
296
+ mb: 3,
297
+ }}>
256
298
  {t('payment.customer.invoice.details')}
257
299
  </Typography>
258
300
  <InfoRowGroup
@@ -331,11 +373,13 @@ export default function CustomerInvoiceDetail() {
331
373
  value={
332
374
  <Typography
333
375
  variant="body1"
334
- color="text.link"
335
- sx={{ cursor: 'pointer' }}
336
376
  component="a"
337
377
  onClick={() => {
338
378
  window.open(joinURL(getPrefix(), `/customer/invoice/${data.relatedInvoice?.id}`), '_self');
379
+ }}
380
+ sx={{
381
+ color: 'text.link',
382
+ cursor: 'pointer',
339
383
  }}>
340
384
  {data.relatedInvoice?.number}
341
385
  </Typography>
@@ -367,7 +411,12 @@ export default function CustomerInvoiceDetail() {
367
411
  <>
368
412
  <Divider />
369
413
  <Box className="section">
370
- <Typography variant="h3" mb={1.5} className="section-header">
414
+ <Typography
415
+ variant="h3"
416
+ className="section-header"
417
+ sx={{
418
+ mb: 1.5,
419
+ }}>
371
420
  {t('payment.customer.products')}
372
421
  </Typography>
373
422
  <InvoiceTable invoice={data} simple />
@@ -109,12 +109,20 @@ export default function CustomerInvoicePastDue() {
109
109
 
110
110
  return (
111
111
  <Stack direction="column" spacing={3} sx={{ my: 2 }}>
112
- <Stack direction="row" alignItems="center" justifyContent="space-between">
112
+ <Stack
113
+ direction="row"
114
+ sx={{
115
+ alignItems: 'center',
116
+ justifyContent: 'space-between',
117
+ }}>
113
118
  <Stack
114
119
  direction="row"
115
120
  onClick={() => goBackOrFallback('/customer')}
116
- alignItems="center"
117
- sx={{ fontWeight: 'normal', cursor: 'pointer' }}>
121
+ sx={{
122
+ alignItems: 'center',
123
+ fontWeight: 'normal',
124
+ cursor: 'pointer',
125
+ }}>
118
126
  <ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
119
127
  <Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
120
128
  {t('common.previous')}