payment-kit 1.19.0 → 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.
- package/api/src/crons/index.ts +8 -0
- package/api/src/index.ts +4 -0
- package/api/src/libs/credit-grant.ts +146 -0
- package/api/src/libs/env.ts +1 -0
- package/api/src/libs/invoice.ts +4 -3
- package/api/src/libs/notification/template/base.ts +388 -2
- package/api/src/libs/notification/template/customer-credit-grant-granted.ts +149 -0
- package/api/src/libs/notification/template/customer-credit-grant-low-balance.ts +151 -0
- package/api/src/libs/notification/template/customer-credit-insufficient.ts +254 -0
- package/api/src/libs/notification/template/subscription-canceled.ts +193 -202
- package/api/src/libs/notification/template/subscription-refund-succeeded.ts +215 -237
- package/api/src/libs/notification/template/subscription-renewed.ts +130 -200
- package/api/src/libs/notification/template/subscription-succeeded.ts +100 -202
- package/api/src/libs/notification/template/subscription-trial-start.ts +142 -188
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +146 -174
- package/api/src/libs/notification/template/subscription-upgraded.ts +96 -192
- package/api/src/libs/notification/template/subscription-will-canceled.ts +94 -135
- package/api/src/libs/notification/template/subscription-will-renew.ts +220 -245
- package/api/src/libs/payment.ts +69 -0
- package/api/src/libs/queue/index.ts +3 -2
- package/api/src/libs/session.ts +8 -0
- package/api/src/libs/subscription.ts +74 -3
- package/api/src/libs/ws.ts +23 -1
- package/api/src/locales/en.ts +33 -0
- package/api/src/locales/zh.ts +31 -0
- package/api/src/queues/credit-consume.ts +715 -0
- package/api/src/queues/credit-grant.ts +572 -0
- package/api/src/queues/notification.ts +173 -128
- package/api/src/queues/payment.ts +210 -122
- package/api/src/queues/subscription.ts +179 -0
- package/api/src/routes/checkout-sessions.ts +157 -9
- package/api/src/routes/connect/shared.ts +3 -2
- package/api/src/routes/credit-grants.ts +241 -0
- package/api/src/routes/credit-transactions.ts +208 -0
- package/api/src/routes/index.ts +8 -0
- package/api/src/routes/meter-events.ts +347 -0
- package/api/src/routes/meters.ts +219 -0
- package/api/src/routes/payment-currencies.ts +14 -2
- package/api/src/routes/payment-links.ts +1 -1
- package/api/src/routes/payment-methods.ts +14 -2
- package/api/src/routes/prices.ts +43 -0
- package/api/src/routes/pricing-table.ts +13 -7
- package/api/src/routes/products.ts +63 -4
- package/api/src/routes/settings.ts +1 -1
- package/api/src/routes/subscriptions.ts +4 -0
- package/api/src/store/migrations/20250610-billing-credit.ts +43 -0
- package/api/src/store/models/credit-grant.ts +486 -0
- package/api/src/store/models/credit-transaction.ts +268 -0
- package/api/src/store/models/customer.ts +8 -0
- package/api/src/store/models/index.ts +52 -1
- package/api/src/store/models/meter-event.ts +423 -0
- package/api/src/store/models/meter.ts +176 -0
- package/api/src/store/models/payment-currency.ts +66 -14
- package/api/src/store/models/price.ts +6 -0
- package/api/src/store/models/product.ts +2 -2
- package/api/src/store/models/subscription.ts +24 -0
- package/api/src/store/models/types.ts +28 -2
- package/api/tests/libs/subscription.spec.ts +53 -0
- package/blocklet.yml +9 -1
- package/package.json +4 -4
- package/scripts/sdk.js +233 -1
- package/src/app.tsx +10 -0
- package/src/components/collapse.tsx +11 -1
- package/src/components/customer/credit-grant-item-list.tsx +99 -0
- package/src/components/customer/credit-overview.tsx +233 -0
- package/src/components/customer/form.tsx +5 -2
- package/src/components/invoice/list.tsx +19 -1
- package/src/components/metadata/form.tsx +286 -90
- package/src/components/meter/actions.tsx +101 -0
- package/src/components/meter/add-usage-dialog.tsx +239 -0
- package/src/components/meter/events-list.tsx +657 -0
- package/src/components/meter/form.tsx +245 -0
- package/src/components/meter/products.tsx +264 -0
- package/src/components/meter/usage-guide.tsx +174 -0
- package/src/components/payment-currency/form.tsx +2 -0
- package/src/components/payment-intent/list.tsx +19 -1
- package/src/components/payment-link/preview.tsx +1 -1
- package/src/components/payment-link/product-select.tsx +52 -12
- package/src/components/payment-method/arcblock.tsx +2 -0
- package/src/components/payment-method/base.tsx +2 -0
- package/src/components/payment-method/bitcoin.tsx +2 -0
- package/src/components/payment-method/ethereum.tsx +2 -0
- package/src/components/payment-method/stripe.tsx +2 -0
- package/src/components/payouts/list.tsx +19 -1
- package/src/components/price/currency-select.tsx +51 -31
- package/src/components/price/form.tsx +881 -407
- package/src/components/pricing-table/preview.tsx +1 -1
- package/src/components/product/add-price.tsx +9 -7
- package/src/components/product/create.tsx +7 -4
- package/src/components/product/edit-price.tsx +21 -12
- package/src/components/product/features.tsx +17 -7
- package/src/components/product/form.tsx +104 -89
- package/src/components/refund/list.tsx +19 -1
- package/src/components/section/header.tsx +5 -18
- package/src/components/subscription/items/index.tsx +1 -1
- package/src/components/subscription/metrics.tsx +37 -5
- package/src/components/subscription/portal/actions.tsx +2 -1
- package/src/contexts/products.tsx +26 -9
- package/src/hooks/subscription.ts +34 -0
- package/src/libs/meter-utils.ts +196 -0
- package/src/libs/util.ts +4 -0
- package/src/locales/en.tsx +385 -4
- package/src/locales/zh.tsx +364 -0
- package/src/pages/admin/billing/index.tsx +61 -33
- package/src/pages/admin/billing/invoices/detail.tsx +1 -1
- package/src/pages/admin/billing/meters/create.tsx +60 -0
- package/src/pages/admin/billing/meters/detail.tsx +435 -0
- package/src/pages/admin/billing/meters/index.tsx +210 -0
- package/src/pages/admin/billing/meters/meter-event.tsx +346 -0
- package/src/pages/admin/billing/subscriptions/detail.tsx +47 -14
- package/src/pages/admin/customers/customers/credit-grant/detail.tsx +391 -0
- package/src/pages/admin/customers/customers/detail.tsx +22 -10
- package/src/pages/admin/customers/index.tsx +5 -0
- package/src/pages/admin/developers/events/detail.tsx +1 -1
- package/src/pages/admin/developers/index.tsx +1 -1
- package/src/pages/admin/payments/intents/detail.tsx +1 -1
- package/src/pages/admin/payments/payouts/detail.tsx +1 -1
- package/src/pages/admin/payments/refunds/detail.tsx +1 -1
- package/src/pages/admin/products/index.tsx +3 -2
- package/src/pages/admin/products/links/detail.tsx +1 -1
- package/src/pages/admin/products/prices/actions.tsx +16 -4
- package/src/pages/admin/products/prices/detail.tsx +30 -3
- package/src/pages/admin/products/prices/list.tsx +8 -1
- package/src/pages/admin/products/pricing-tables/detail.tsx +1 -1
- package/src/pages/admin/products/products/create.tsx +233 -57
- package/src/pages/admin/products/products/detail.tsx +2 -1
- package/src/pages/admin/settings/payment-methods/index.tsx +3 -0
- package/src/pages/customer/credit-grant/detail.tsx +308 -0
- package/src/pages/customer/index.tsx +35 -2
- package/src/pages/customer/recharge/account.tsx +5 -5
- package/src/pages/customer/subscription/change-payment.tsx +4 -2
- package/src/pages/customer/subscription/detail.tsx +48 -14
- package/src/pages/customer/subscription/embed.tsx +1 -1
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/* eslint-disable react/no-unstable-nested-components */
|
|
2
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
|
+
import Toast from '@arcblock/ux/lib/Toast';
|
|
4
|
+
import {
|
|
5
|
+
api,
|
|
6
|
+
formatBNStr,
|
|
7
|
+
formatError,
|
|
8
|
+
formatTime,
|
|
9
|
+
CreditTransactionsList,
|
|
10
|
+
CreditStatusChip,
|
|
11
|
+
getCustomerAvatar,
|
|
12
|
+
} from '@blocklet/payment-react';
|
|
13
|
+
import type { TCreditGrantExpanded } from '@blocklet/payment-types';
|
|
14
|
+
import { ArrowBackOutlined } from '@mui/icons-material';
|
|
15
|
+
import { Alert, Avatar, Box, Button, CircularProgress, Divider, Stack, Typography } from '@mui/material';
|
|
16
|
+
import { useRequest, useSetState } from 'ahooks';
|
|
17
|
+
import { styled } from '@mui/system';
|
|
18
|
+
import { useCallback } from 'react';
|
|
19
|
+
|
|
20
|
+
import InfoMetric from '../../../../../components/info-metric';
|
|
21
|
+
import InfoRow from '../../../../../components/info-row';
|
|
22
|
+
import InfoRowGroup from '../../../../../components/info-row-group';
|
|
23
|
+
import Copyable from '../../../../../components/copyable';
|
|
24
|
+
import SectionHeader from '../../../../../components/section/header';
|
|
25
|
+
import MetadataList from '../../../../../components/metadata/list';
|
|
26
|
+
import MetadataEditor from '../../../../../components/metadata/editor';
|
|
27
|
+
import CreditGrantItemList from '../../../../../components/customer/credit-grant-item-list';
|
|
28
|
+
import { goBackOrFallback } from '../../../../../libs/util';
|
|
29
|
+
import EventList from '../../../../../components/event/list';
|
|
30
|
+
|
|
31
|
+
const fetchData = (id: string | undefined): Promise<TCreditGrantExpanded> => {
|
|
32
|
+
return api.get(`/api/credit-grants/${id}`).then((res) => res.data);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default function AdminCreditGrantDetail({ id }: { id: string }) {
|
|
36
|
+
const { t } = useLocaleContext();
|
|
37
|
+
const [state, setState] = useSetState({
|
|
38
|
+
editing: {
|
|
39
|
+
metadata: false,
|
|
40
|
+
creditGrant: false,
|
|
41
|
+
},
|
|
42
|
+
loading: {
|
|
43
|
+
metadata: false,
|
|
44
|
+
creditGrant: false,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const { loading, error, data, runAsync } = useRequest(() => fetchData(id));
|
|
49
|
+
|
|
50
|
+
const handleBack = useCallback(() => {
|
|
51
|
+
goBackOrFallback('/admin/customers');
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
if (error) {
|
|
55
|
+
return <Alert severity="error">{formatError(error)}</Alert>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (loading || !data) {
|
|
59
|
+
return <CircularProgress />;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const isDepleted = data?.status === 'depleted';
|
|
63
|
+
|
|
64
|
+
const getStatusText = (status: string) => {
|
|
65
|
+
switch (status) {
|
|
66
|
+
case 'granted':
|
|
67
|
+
return t('admin.customer.creditGrants.status.granted');
|
|
68
|
+
case 'pending':
|
|
69
|
+
return t('admin.customer.creditGrants.status.pending');
|
|
70
|
+
case 'depleted':
|
|
71
|
+
return t('admin.customer.creditGrants.status.depleted');
|
|
72
|
+
case 'expired':
|
|
73
|
+
return t('admin.customer.creditGrants.status.expired');
|
|
74
|
+
case 'voided':
|
|
75
|
+
return t('admin.customer.creditGrants.status.voided');
|
|
76
|
+
default:
|
|
77
|
+
return status;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const getUsagePercentage = () => {
|
|
82
|
+
if (!data.amount || !data.remaining_amount) return 0;
|
|
83
|
+
const total = parseFloat(data.amount);
|
|
84
|
+
const remaining = parseFloat(data.remaining_amount);
|
|
85
|
+
return ((total - remaining) / total) * 100;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const onUpdateMetadata = async (updates: any) => {
|
|
89
|
+
try {
|
|
90
|
+
setState((prev) => ({ loading: { ...prev.loading, metadata: true } }));
|
|
91
|
+
await api.put(`/api/credit-grants/${id}`, updates);
|
|
92
|
+
Toast.success(t('common.saved'));
|
|
93
|
+
runAsync();
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error(err);
|
|
96
|
+
Toast.error(formatError(err));
|
|
97
|
+
} finally {
|
|
98
|
+
setState((prev) => ({ loading: { ...prev.loading, metadata: false } }));
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const handleEditMetadata = () => {
|
|
103
|
+
setState((prev) => ({ editing: { ...prev.editing, metadata: true } }));
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<Root direction="column" spacing={2.5} sx={{ mb: 4 }}>
|
|
108
|
+
<Box>
|
|
109
|
+
<Stack className="page-header" direction="row" justifyContent="space-between" alignItems="center">
|
|
110
|
+
<Stack
|
|
111
|
+
direction="row"
|
|
112
|
+
alignItems="center"
|
|
113
|
+
sx={{ fontWeight: 'normal', cursor: 'pointer' }}
|
|
114
|
+
onClick={handleBack}>
|
|
115
|
+
<ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
|
|
116
|
+
<Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
|
|
117
|
+
{t('admin.customer.creditGrants.title')}
|
|
118
|
+
</Typography>
|
|
119
|
+
</Stack>
|
|
120
|
+
</Stack>
|
|
121
|
+
|
|
122
|
+
<Box
|
|
123
|
+
mt={4}
|
|
124
|
+
mb={3}
|
|
125
|
+
sx={{
|
|
126
|
+
display: 'flex',
|
|
127
|
+
gap: {
|
|
128
|
+
xs: 2,
|
|
129
|
+
sm: 2,
|
|
130
|
+
md: 5,
|
|
131
|
+
},
|
|
132
|
+
flexWrap: 'wrap',
|
|
133
|
+
flexDirection: {
|
|
134
|
+
xs: 'column',
|
|
135
|
+
sm: 'column',
|
|
136
|
+
md: 'row',
|
|
137
|
+
},
|
|
138
|
+
alignItems: {
|
|
139
|
+
xs: 'flex-start',
|
|
140
|
+
sm: 'flex-start',
|
|
141
|
+
md: 'center',
|
|
142
|
+
},
|
|
143
|
+
}}>
|
|
144
|
+
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
|
145
|
+
<Stack direction="column" spacing={0.5}>
|
|
146
|
+
<Typography variant="h2" sx={{ fontWeight: 600 }}>
|
|
147
|
+
{data.name || `${t('admin.customer.creditGrants.grant')} ${data.id.slice(-8)}`}
|
|
148
|
+
</Typography>
|
|
149
|
+
<Copyable text={data.id} />
|
|
150
|
+
</Stack>
|
|
151
|
+
</Stack>
|
|
152
|
+
|
|
153
|
+
<Stack
|
|
154
|
+
className="section-body"
|
|
155
|
+
justifyContent="flex-start"
|
|
156
|
+
flexWrap="wrap"
|
|
157
|
+
sx={{
|
|
158
|
+
'hr.MuiDivider-root:last-child': {
|
|
159
|
+
display: 'none',
|
|
160
|
+
},
|
|
161
|
+
flexDirection: {
|
|
162
|
+
xs: 'column',
|
|
163
|
+
sm: 'column',
|
|
164
|
+
md: 'row',
|
|
165
|
+
},
|
|
166
|
+
alignItems: 'flex-start',
|
|
167
|
+
gap: {
|
|
168
|
+
xs: 1,
|
|
169
|
+
sm: 1,
|
|
170
|
+
md: 3,
|
|
171
|
+
},
|
|
172
|
+
}}>
|
|
173
|
+
<InfoMetric
|
|
174
|
+
label={t('common.status')}
|
|
175
|
+
value={<CreditStatusChip status={data.status} label={getStatusText(data.status)} />}
|
|
176
|
+
divider
|
|
177
|
+
/>
|
|
178
|
+
<InfoMetric
|
|
179
|
+
label={t('admin.customer.creditGrants.originalAmount')}
|
|
180
|
+
value={
|
|
181
|
+
<Stack direction="row" alignItems="center" spacing={0.5}>
|
|
182
|
+
<Avatar
|
|
183
|
+
src={data.paymentCurrency?.logo}
|
|
184
|
+
sx={{ width: 16, height: 16 }}
|
|
185
|
+
alt={data.paymentCurrency?.symbol}
|
|
186
|
+
/>
|
|
187
|
+
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
|
188
|
+
{formatBNStr(data.amount, data.paymentCurrency.decimal)} {data.paymentCurrency.symbol}
|
|
189
|
+
</Typography>
|
|
190
|
+
</Stack>
|
|
191
|
+
}
|
|
192
|
+
divider
|
|
193
|
+
/>
|
|
194
|
+
<InfoMetric
|
|
195
|
+
label={t('common.remainingCredit')}
|
|
196
|
+
value={
|
|
197
|
+
<Stack direction="row" alignItems="center" spacing={0.5}>
|
|
198
|
+
<Avatar
|
|
199
|
+
src={data.paymentCurrency?.logo}
|
|
200
|
+
sx={{ width: 16, height: 16 }}
|
|
201
|
+
alt={data.paymentCurrency?.symbol}
|
|
202
|
+
/>
|
|
203
|
+
<Typography variant="body2" color={isDepleted ? 'error.main' : 'text.secondary'}>
|
|
204
|
+
{formatBNStr(data.remaining_amount, data.paymentCurrency.decimal)} {data.paymentCurrency.symbol}
|
|
205
|
+
</Typography>
|
|
206
|
+
</Stack>
|
|
207
|
+
}
|
|
208
|
+
divider
|
|
209
|
+
/>
|
|
210
|
+
<InfoMetric
|
|
211
|
+
label={t('admin.customer.creditGrants.usage')}
|
|
212
|
+
value={
|
|
213
|
+
<Typography variant="body2" color={isDepleted ? 'error.main' : 'text.secondary'}>
|
|
214
|
+
{getUsagePercentage().toFixed(1)}%
|
|
215
|
+
</Typography>
|
|
216
|
+
}
|
|
217
|
+
/>
|
|
218
|
+
</Stack>
|
|
219
|
+
</Box>
|
|
220
|
+
<Divider />
|
|
221
|
+
</Box>
|
|
222
|
+
|
|
223
|
+
<Stack
|
|
224
|
+
sx={{
|
|
225
|
+
flexDirection: {
|
|
226
|
+
xs: 'column',
|
|
227
|
+
lg: 'row',
|
|
228
|
+
},
|
|
229
|
+
gap: {
|
|
230
|
+
xs: 2.5,
|
|
231
|
+
md: 4,
|
|
232
|
+
},
|
|
233
|
+
'.credit-grant-column-1': {
|
|
234
|
+
minWidth: {
|
|
235
|
+
xs: '100%',
|
|
236
|
+
lg: '600px',
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
'.credit-grant-column-2': {
|
|
240
|
+
width: {
|
|
241
|
+
xs: '100%',
|
|
242
|
+
md: '100%',
|
|
243
|
+
lg: '320px',
|
|
244
|
+
},
|
|
245
|
+
maxWidth: {
|
|
246
|
+
xs: '100%',
|
|
247
|
+
md: '33%',
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
}}>
|
|
251
|
+
<Box flex={1} className="credit-grant-column-1" sx={{ gap: 2.5, display: 'flex', flexDirection: 'column' }}>
|
|
252
|
+
{/* 基本信息 */}
|
|
253
|
+
<Box className="section" sx={{ containerType: 'inline-size' }}>
|
|
254
|
+
<SectionHeader title={t('admin.details')} />
|
|
255
|
+
<InfoRowGroup
|
|
256
|
+
sx={{
|
|
257
|
+
display: 'grid',
|
|
258
|
+
gridTemplateColumns: {
|
|
259
|
+
xs: 'repeat(1, 1fr)',
|
|
260
|
+
xl: 'repeat(2, 1fr)',
|
|
261
|
+
},
|
|
262
|
+
'@container (min-width: 1000px)': {
|
|
263
|
+
gridTemplateColumns: 'repeat(2, 1fr)',
|
|
264
|
+
},
|
|
265
|
+
'.info-row-wrapper': {
|
|
266
|
+
gap: 1,
|
|
267
|
+
flexDirection: {
|
|
268
|
+
xs: 'column',
|
|
269
|
+
xl: 'row',
|
|
270
|
+
},
|
|
271
|
+
alignItems: {
|
|
272
|
+
xs: 'flex-start',
|
|
273
|
+
xl: 'center',
|
|
274
|
+
},
|
|
275
|
+
'@container (min-width: 1000px)': {
|
|
276
|
+
flexDirection: 'row',
|
|
277
|
+
alignItems: 'center',
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
}}>
|
|
281
|
+
<InfoRow
|
|
282
|
+
label={t('common.customer')}
|
|
283
|
+
value={
|
|
284
|
+
<Stack direction="row" alignItems="center" spacing={1}>
|
|
285
|
+
<Avatar
|
|
286
|
+
src={getCustomerAvatar(
|
|
287
|
+
data.customer?.did,
|
|
288
|
+
data.customer?.updated_at ? new Date(data.customer.updated_at).toISOString() : '',
|
|
289
|
+
24
|
|
290
|
+
)}
|
|
291
|
+
alt={data.customer?.name}
|
|
292
|
+
sx={{ width: 24, height: 24 }}
|
|
293
|
+
/>
|
|
294
|
+
<Typography>{data.customer?.name}</Typography>
|
|
295
|
+
</Stack>
|
|
296
|
+
}
|
|
297
|
+
/>
|
|
298
|
+
<InfoRow
|
|
299
|
+
label={t('common.scope')}
|
|
300
|
+
value={
|
|
301
|
+
<Typography>
|
|
302
|
+
{data.applicability_config?.scope?.prices ? t('common.specific') : t('common.general')}
|
|
303
|
+
</Typography>
|
|
304
|
+
}
|
|
305
|
+
/>
|
|
306
|
+
<InfoRow
|
|
307
|
+
label={t('admin.creditProduct.priority.label')}
|
|
308
|
+
value={<Typography>{data.priority}</Typography>}
|
|
309
|
+
/>
|
|
310
|
+
<InfoRow label={t('common.createdAt')} value={formatTime(data.created_at)} />
|
|
311
|
+
<InfoRow
|
|
312
|
+
label={t('common.effectiveDate')}
|
|
313
|
+
value={formatTime(data.effective_at ? data.effective_at * 1000 : data.created_at)}
|
|
314
|
+
/>
|
|
315
|
+
{data.expires_at && (
|
|
316
|
+
<InfoRow label={t('common.expirationDate')} value={formatTime(data.expires_at * 1000)} />
|
|
317
|
+
)}
|
|
318
|
+
</InfoRowGroup>
|
|
319
|
+
</Box>
|
|
320
|
+
|
|
321
|
+
<Divider />
|
|
322
|
+
|
|
323
|
+
{/* 适用范围 - 使用新的CreditGrantItemList组件 */}
|
|
324
|
+
{data.items && data.items.length > 0 && (
|
|
325
|
+
<>
|
|
326
|
+
<Box className="section">
|
|
327
|
+
<SectionHeader title={t('admin.creditProduct.associatedPrices.label')} />
|
|
328
|
+
<Box className="section-body">
|
|
329
|
+
<CreditGrantItemList data={data.items} currency={data.paymentCurrency} mode="dashboard" />
|
|
330
|
+
</Box>
|
|
331
|
+
</Box>
|
|
332
|
+
<Divider />
|
|
333
|
+
</>
|
|
334
|
+
)}
|
|
335
|
+
|
|
336
|
+
{/* 交易记录 */}
|
|
337
|
+
<Box className="section">
|
|
338
|
+
<SectionHeader title={t('admin.creditTransactions.title')} />
|
|
339
|
+
<Box className="section-body">
|
|
340
|
+
<CreditTransactionsList
|
|
341
|
+
customer_id={data.customer_id}
|
|
342
|
+
credit_grant_id={data.id}
|
|
343
|
+
showAdminColumns
|
|
344
|
+
showTimeFilter
|
|
345
|
+
mode="dashboard"
|
|
346
|
+
/>
|
|
347
|
+
</Box>
|
|
348
|
+
</Box>
|
|
349
|
+
|
|
350
|
+
<Divider />
|
|
351
|
+
<Box className="section">
|
|
352
|
+
<SectionHeader title={t('admin.events.title')} />
|
|
353
|
+
<Box className="section-body">
|
|
354
|
+
<EventList features={{ toolbar: false }} object_id={data.id} />
|
|
355
|
+
</Box>
|
|
356
|
+
</Box>
|
|
357
|
+
</Box>
|
|
358
|
+
|
|
359
|
+
<Box className="credit-grant-column-2" sx={{ gap: 2.5, display: 'flex', flexDirection: 'column' }}>
|
|
360
|
+
{/* 元数据 */}
|
|
361
|
+
<Box className="section">
|
|
362
|
+
<SectionHeader title={t('common.metadata.label')}>
|
|
363
|
+
<Button
|
|
364
|
+
variant="text"
|
|
365
|
+
color="inherit"
|
|
366
|
+
size="small"
|
|
367
|
+
sx={{ color: 'text.link' }}
|
|
368
|
+
disabled={state.editing.metadata}
|
|
369
|
+
onClick={handleEditMetadata}>
|
|
370
|
+
{t('common.edit')}
|
|
371
|
+
</Button>
|
|
372
|
+
</SectionHeader>
|
|
373
|
+
<Box className="section-body">
|
|
374
|
+
<MetadataList data={data.metadata} handleEditMetadata={handleEditMetadata} />
|
|
375
|
+
{state.editing.metadata && (
|
|
376
|
+
<MetadataEditor
|
|
377
|
+
data={data}
|
|
378
|
+
loading={state.loading.metadata}
|
|
379
|
+
onSave={onUpdateMetadata}
|
|
380
|
+
onCancel={() => setState((prev) => ({ editing: { ...prev.editing, metadata: false } }))}
|
|
381
|
+
/>
|
|
382
|
+
)}
|
|
383
|
+
</Box>
|
|
384
|
+
</Box>
|
|
385
|
+
</Box>
|
|
386
|
+
</Stack>
|
|
387
|
+
</Root>
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const Root = styled(Stack)``;
|
|
@@ -22,6 +22,7 @@ import { useMemo } from 'react';
|
|
|
22
22
|
import BalanceList from '../../../../components/balance-list';
|
|
23
23
|
import Copyable from '../../../../components/copyable';
|
|
24
24
|
import EditCustomer from '../../../../components/customer/edit';
|
|
25
|
+
import CreditOverview from '../../../../components/customer/credit-overview';
|
|
25
26
|
import EventList from '../../../../components/event/list';
|
|
26
27
|
import InfoMetric from '../../../../components/info-metric';
|
|
27
28
|
import InfoRow from '../../../../components/info-row';
|
|
@@ -38,7 +39,14 @@ import InfoRowGroup from '../../../../components/info-row-group';
|
|
|
38
39
|
|
|
39
40
|
const fetchData = async (
|
|
40
41
|
id: string
|
|
41
|
-
): Promise<{
|
|
42
|
+
): Promise<{
|
|
43
|
+
customer: TCustomerExpanded;
|
|
44
|
+
summary: { [key: string]: GroupedBN };
|
|
45
|
+
creditSummary?: {
|
|
46
|
+
grants?: { [currencyId: string]: { totalAmount: string; remainingAmount: string } };
|
|
47
|
+
pendingAmount?: { [currencyId: string]: string };
|
|
48
|
+
} | null;
|
|
49
|
+
}> => {
|
|
42
50
|
const [customer, summary] = await Promise.all([
|
|
43
51
|
api.get(`/api/customers/${id}`).then((res) => res.data),
|
|
44
52
|
api.get(`/api/customers/${id}/summary`).then((res) => res.data),
|
|
@@ -301,14 +309,18 @@ export default function CustomerDetail(props: { id: string }) {
|
|
|
301
309
|
},
|
|
302
310
|
},
|
|
303
311
|
}}>
|
|
304
|
-
<Box
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
+
<Box className="payment-link-column-1" sx={{ flex: 1, gap: 2.5, display: 'flex', flexDirection: 'column' }}>
|
|
313
|
+
{/* 信用管理区域 */}
|
|
314
|
+
{data.creditSummary && (
|
|
315
|
+
<>
|
|
316
|
+
<Box className="section">
|
|
317
|
+
<SectionHeader title={t('admin.creditGrants.title')} mb={0} />
|
|
318
|
+
<CreditOverview customerId={props.id} settings={settings} mode="dashboard" />
|
|
319
|
+
</Box>
|
|
320
|
+
<Divider />
|
|
321
|
+
</>
|
|
322
|
+
)}
|
|
323
|
+
|
|
312
324
|
<Box className="section" sx={{ containerType: 'inline-size' }}>
|
|
313
325
|
<SectionHeader title={t('admin.details')} />
|
|
314
326
|
<InfoRowGroup
|
|
@@ -410,7 +422,7 @@ export default function CustomerDetail(props: { id: string }) {
|
|
|
410
422
|
</Box>
|
|
411
423
|
<Divider />
|
|
412
424
|
<Box className="section">
|
|
413
|
-
<SectionHeader title={t('admin.events')} />
|
|
425
|
+
<SectionHeader title={t('admin.events.title')} />
|
|
414
426
|
<Box className="section-body">
|
|
415
427
|
<EventList features={{ toolbar: false }} object_id={data.customer.id} />
|
|
416
428
|
</Box>
|
|
@@ -6,6 +6,7 @@ import { useNavigate, useParams } from 'react-router-dom';
|
|
|
6
6
|
import { useTransitionContext } from '../../../components/progress-bar';
|
|
7
7
|
|
|
8
8
|
const CustomerDetail = React.lazy(() => import('./customers/detail'));
|
|
9
|
+
const CreditGrantDetail = React.lazy(() => import('./customers/credit-grant/detail'));
|
|
9
10
|
|
|
10
11
|
const pages = {
|
|
11
12
|
overview: React.lazy(() => import('./customers')),
|
|
@@ -21,6 +22,10 @@ export default function CustomerIndex() {
|
|
|
21
22
|
return <CustomerDetail id={page} />;
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
if (page.startsWith('credgr_')) {
|
|
26
|
+
return <CreditGrantDetail id={page} />;
|
|
27
|
+
}
|
|
28
|
+
|
|
24
29
|
const onTabChange = (newTab: string) => {
|
|
25
30
|
startTransition(() => {
|
|
26
31
|
navigate(`/admin/customers/${newTab}`);
|
|
@@ -50,7 +50,7 @@ export default function EventDetail(props: { id: string }) {
|
|
|
50
50
|
}}>
|
|
51
51
|
<ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
|
|
52
52
|
<Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
|
|
53
|
-
{t('admin.events')}
|
|
53
|
+
{t('admin.events.title')}
|
|
54
54
|
</Typography>
|
|
55
55
|
</Stack>
|
|
56
56
|
</Stack>
|
|
@@ -41,7 +41,7 @@ export default function DevelopersIndex() {
|
|
|
41
41
|
const tabs = [
|
|
42
42
|
{ label: t('admin.overview'), value: 'overview' },
|
|
43
43
|
{ label: t('admin.webhooks'), value: 'webhooks' },
|
|
44
|
-
{ label: t('admin.events'), value: 'events' },
|
|
44
|
+
{ label: t('admin.events.title'), value: 'events' },
|
|
45
45
|
{ label: t('admin.logs'), value: 'logs' },
|
|
46
46
|
];
|
|
47
47
|
|
|
@@ -330,7 +330,7 @@ export default function PaymentIntentDetail(props: { id: string }) {
|
|
|
330
330
|
</Box>
|
|
331
331
|
<Divider />
|
|
332
332
|
<Box className="section">
|
|
333
|
-
<SectionHeader title={t('admin.events')} />
|
|
333
|
+
<SectionHeader title={t('admin.events.title')} />
|
|
334
334
|
<Box className="section-body">
|
|
335
335
|
<EventList features={{ toolbar: false }} object_id={data.id} />
|
|
336
336
|
</Box>
|
|
@@ -382,7 +382,7 @@ export default function PayoutDetail(props: { id: string }) {
|
|
|
382
382
|
</Box>
|
|
383
383
|
<Divider />
|
|
384
384
|
<Box className="section">
|
|
385
|
-
<SectionHeader title={t('admin.events')} />
|
|
385
|
+
<SectionHeader title={t('admin.events.title')} />
|
|
386
386
|
<Box className="section-body">
|
|
387
387
|
<EventList features={{ toolbar: false }} object_id={data.id} />
|
|
388
388
|
</Box>
|
|
@@ -312,7 +312,7 @@ export default function RefundDetail(props: { id: string }) {
|
|
|
312
312
|
</Box>
|
|
313
313
|
<Divider />
|
|
314
314
|
<Box className="section">
|
|
315
|
-
<SectionHeader title={t('admin.events')} />
|
|
315
|
+
<SectionHeader title={t('admin.events.title')} />
|
|
316
316
|
<Box className="section-body">
|
|
317
317
|
<EventList features={{ toolbar: false }} object_id={data.id} />
|
|
318
318
|
</Box>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
2
|
import Tabs from '@arcblock/ux/lib/Tabs';
|
|
3
3
|
import { Box, Stack } from '@mui/material';
|
|
4
|
-
import React, { isValidElement } from 'react';
|
|
4
|
+
import React, { isValidElement, useState } from 'react';
|
|
5
5
|
import { useNavigate, useParams } from 'react-router-dom';
|
|
6
6
|
|
|
7
7
|
import { useTransitionContext } from '../../../components/progress-bar';
|
|
@@ -26,6 +26,7 @@ export default function Products() {
|
|
|
26
26
|
const navigate = useNavigate();
|
|
27
27
|
const { t } = useLocaleContext();
|
|
28
28
|
const { page = 'products' } = useParams();
|
|
29
|
+
const [createProduct, setCreateProduct] = useState(false);
|
|
29
30
|
const { startTransition } = useTransitionContext();
|
|
30
31
|
|
|
31
32
|
if (page.startsWith('prod_')) {
|
|
@@ -56,7 +57,7 @@ export default function Products() {
|
|
|
56
57
|
|
|
57
58
|
let extra = null;
|
|
58
59
|
if (page === 'products') {
|
|
59
|
-
extra = <ProductCreate />;
|
|
60
|
+
extra = <ProductCreate open={createProduct} onClose={() => setCreateProduct(false)} />;
|
|
60
61
|
} else if (page === 'links') {
|
|
61
62
|
extra = <PaymentLinkCreate />;
|
|
62
63
|
} else if (page === 'pricing-tables') {
|
|
@@ -377,7 +377,7 @@ export default function PaymentLinkDetail(props: { id: string }) {
|
|
|
377
377
|
</Box>
|
|
378
378
|
<Divider />
|
|
379
379
|
<Box className="section">
|
|
380
|
-
<SectionHeader title={t('admin.events')} />
|
|
380
|
+
<SectionHeader title={t('admin.events.title')} />
|
|
381
381
|
<Box className="section-body">
|
|
382
382
|
<EventList features={{ toolbar: false }} object_id={data.id} />
|
|
383
383
|
</Box>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
2
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
3
3
|
import { ConfirmDialog, api, formatError } from '@blocklet/payment-react';
|
|
4
|
-
import type { TPrice } from '@blocklet/payment-types';
|
|
4
|
+
import type { TPrice, TProduct } from '@blocklet/payment-types';
|
|
5
5
|
import { useSetState } from 'ahooks';
|
|
6
6
|
import noop from 'lodash/noop';
|
|
7
7
|
import { useNavigate } from 'react-router-dom';
|
|
@@ -14,9 +14,16 @@ type Props = {
|
|
|
14
14
|
onChange: Function;
|
|
15
15
|
variant?: string;
|
|
16
16
|
setAsDefault?: boolean;
|
|
17
|
+
product?: TProduct | null;
|
|
17
18
|
};
|
|
18
19
|
|
|
19
|
-
export default function PriceActions({
|
|
20
|
+
export default function PriceActions({
|
|
21
|
+
data,
|
|
22
|
+
onChange,
|
|
23
|
+
variant = 'compact',
|
|
24
|
+
setAsDefault = false,
|
|
25
|
+
product = null,
|
|
26
|
+
}: Props) {
|
|
20
27
|
const { t } = useLocaleContext();
|
|
21
28
|
const navigate = useNavigate();
|
|
22
29
|
|
|
@@ -145,8 +152,13 @@ export default function PriceActions({ data, onChange, variant = 'compact', setA
|
|
|
145
152
|
<>
|
|
146
153
|
<Actions variant={variant} actions={actions} />
|
|
147
154
|
{state.action === 'edit' && (
|
|
148
|
-
|
|
149
|
-
|
|
155
|
+
<EditPrice
|
|
156
|
+
// @ts-ignore
|
|
157
|
+
price={data}
|
|
158
|
+
onSave={onEditPrice}
|
|
159
|
+
onCancel={() => setState({ action: '' })}
|
|
160
|
+
productType={product?.type}
|
|
161
|
+
/>
|
|
150
162
|
)}
|
|
151
163
|
{state.action === 'archive' && (
|
|
152
164
|
<ConfirmDialog
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
2
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
api,
|
|
5
|
+
formatError,
|
|
6
|
+
formatPrice,
|
|
7
|
+
formatRecurring,
|
|
8
|
+
formatTime,
|
|
9
|
+
Table,
|
|
10
|
+
useMobile,
|
|
11
|
+
isCreditMetered,
|
|
12
|
+
} from '@blocklet/payment-react';
|
|
4
13
|
import type { PriceRecurring, TPrice, TPriceExpanded } from '@blocklet/payment-types';
|
|
5
14
|
import { ArrowBackOutlined } from '@mui/icons-material';
|
|
6
15
|
import { Alert, AlertTitle, Box, Button, CircularProgress, Divider, Stack, Typography } from '@mui/material';
|
|
7
16
|
import { styled } from '@mui/system';
|
|
8
17
|
import { useRequest, useSetState } from 'ahooks';
|
|
9
|
-
import { useNavigate } from 'react-router-dom';
|
|
18
|
+
import { Link, useNavigate } from 'react-router-dom';
|
|
10
19
|
|
|
11
20
|
import Copyable from '../../../../components/copyable';
|
|
12
21
|
import Currency from '../../../../components/currency';
|
|
@@ -299,6 +308,24 @@ export default function PriceDetail(props: { id: string }) {
|
|
|
299
308
|
</Box>
|
|
300
309
|
|
|
301
310
|
<Divider />
|
|
311
|
+
{isCreditMetered(data) && (
|
|
312
|
+
<>
|
|
313
|
+
<Box className="section">
|
|
314
|
+
<SectionHeader title={t('admin.meters')} />
|
|
315
|
+
<Box className="section-body">
|
|
316
|
+
<InfoRow
|
|
317
|
+
label={t('admin.meter.eventName.label')}
|
|
318
|
+
value={
|
|
319
|
+
<Link to={`/admin/billing/${data.meter?.id}`} target="_blank">
|
|
320
|
+
{data.meter?.event_name}
|
|
321
|
+
</Link>
|
|
322
|
+
}
|
|
323
|
+
/>
|
|
324
|
+
</Box>
|
|
325
|
+
</Box>
|
|
326
|
+
<Divider />
|
|
327
|
+
</>
|
|
328
|
+
)}
|
|
302
329
|
<Box className="section">
|
|
303
330
|
<SectionHeader title={t('admin.price.currency.list')} />
|
|
304
331
|
<Box className="section-body">
|
|
@@ -357,7 +384,7 @@ export default function PriceDetail(props: { id: string }) {
|
|
|
357
384
|
</Box>
|
|
358
385
|
<Divider />
|
|
359
386
|
<Box className="section">
|
|
360
|
-
<SectionHeader title={t('admin.events')} />
|
|
387
|
+
<SectionHeader title={t('admin.events.title')} />
|
|
361
388
|
<Box className="section-body">
|
|
362
389
|
<EventList features={{ toolbar: false }} object_id={data.id} />
|
|
363
390
|
</Box>
|
|
@@ -128,7 +128,14 @@ export default function PricesList({ product, onChange }: { product: Product; on
|
|
|
128
128
|
sort: false,
|
|
129
129
|
customBodyRenderLite: (_: any, index: number) => {
|
|
130
130
|
const price = product.prices[index] as any;
|
|
131
|
-
return
|
|
131
|
+
return (
|
|
132
|
+
<PriceActions
|
|
133
|
+
data={price}
|
|
134
|
+
onChange={onChange}
|
|
135
|
+
setAsDefault={price.id === product.default_price_id}
|
|
136
|
+
product={product as any}
|
|
137
|
+
/>
|
|
138
|
+
);
|
|
132
139
|
},
|
|
133
140
|
},
|
|
134
141
|
},
|