payment-kit 1.22.32 → 1.23.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/index.ts +4 -0
- package/api/src/integrations/arcblock/token.ts +599 -0
- package/api/src/libs/credit-grant.ts +7 -6
- package/api/src/libs/util.ts +34 -0
- package/api/src/queues/credit-consume.ts +29 -4
- package/api/src/queues/credit-grant.ts +245 -50
- package/api/src/queues/credit-reconciliation.ts +253 -0
- package/api/src/queues/refund.ts +263 -30
- package/api/src/queues/token-transfer.ts +331 -0
- package/api/src/routes/checkout-sessions.ts +94 -29
- package/api/src/routes/credit-grants.ts +35 -9
- package/api/src/routes/credit-tokens.ts +38 -0
- package/api/src/routes/credit-transactions.ts +20 -3
- package/api/src/routes/index.ts +2 -0
- package/api/src/routes/meter-events.ts +4 -0
- package/api/src/routes/meters.ts +32 -10
- package/api/src/routes/payment-currencies.ts +103 -0
- package/api/src/routes/payment-links.ts +3 -1
- package/api/src/routes/products.ts +2 -2
- package/api/src/routes/settings.ts +4 -3
- package/api/src/store/migrations/20251120-add-token-config-to-currencies.ts +20 -0
- package/api/src/store/migrations/20251204-add-chain-fields.ts +74 -0
- package/api/src/store/migrations/20251211-optimize-slow-queries.ts +33 -0
- package/api/src/store/models/credit-grant.ts +47 -9
- package/api/src/store/models/credit-transaction.ts +18 -1
- package/api/src/store/models/index.ts +2 -1
- package/api/src/store/models/payment-currency.ts +31 -4
- package/api/src/store/models/refund.ts +12 -2
- package/api/src/store/models/types.ts +48 -0
- package/api/src/store/sequelize.ts +1 -0
- package/api/third.d.ts +2 -0
- package/blocklet.yml +1 -1
- package/package.json +7 -6
- package/src/app.tsx +10 -0
- package/src/components/customer/credit-overview.tsx +19 -3
- package/src/components/meter/form.tsx +191 -18
- package/src/components/price/form.tsx +49 -37
- package/src/locales/en.tsx +25 -1
- package/src/locales/zh.tsx +27 -1
- package/src/pages/admin/billing/meters/create.tsx +42 -13
- package/src/pages/admin/billing/meters/detail.tsx +56 -5
- package/src/pages/admin/customers/customers/credit-grant/detail.tsx +13 -0
- package/src/pages/admin/customers/customers/credit-transaction/detail.tsx +324 -0
- package/src/pages/admin/customers/index.tsx +5 -0
- package/src/pages/customer/credit-grant/detail.tsx +14 -1
- package/src/pages/customer/credit-transaction/detail.tsx +289 -0
- package/src/pages/customer/invoice/detail.tsx +1 -1
- package/src/pages/customer/recharge/subscription.tsx +1 -1
- package/src/pages/customer/subscription/detail.tsx +1 -1
|
@@ -5,15 +5,24 @@ import { api, formatError, usePaymentContext } from '@blocklet/payment-react';
|
|
|
5
5
|
import { useForm, FormProvider } from 'react-hook-form';
|
|
6
6
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
7
7
|
import { dispatch } from 'use-bus';
|
|
8
|
+
import { useLocalStorageState } from 'ahooks';
|
|
8
9
|
|
|
9
10
|
import type { TMeter } from '@blocklet/payment-types';
|
|
10
|
-
import MeterForm from '../../../../components/meter/form';
|
|
11
|
+
import MeterForm, { type TokenConfig } from '../../../../components/meter/form';
|
|
11
12
|
import DrawerForm from '../../../../components/drawer-form';
|
|
12
13
|
|
|
14
|
+
type TMeterForm = TMeter & {
|
|
15
|
+
mode?: 'OffChain' | 'OnChain';
|
|
16
|
+
tokenFactoryAddress?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
13
19
|
export default function MeterCreate() {
|
|
14
20
|
const { t } = useLocaleContext();
|
|
15
21
|
const { refresh } = usePaymentContext();
|
|
16
|
-
const
|
|
22
|
+
const [tokenConfig, setTokenConfig] = useLocalStorageState<TokenConfig>('payment_kit_token_config', {
|
|
23
|
+
defaultValue: { name: '', symbol: '', tokenFactoryAddress: '' },
|
|
24
|
+
});
|
|
25
|
+
const methods = useForm<TMeterForm>({
|
|
17
26
|
defaultValues: {
|
|
18
27
|
name: '',
|
|
19
28
|
event_name: '',
|
|
@@ -21,22 +30,38 @@ export default function MeterCreate() {
|
|
|
21
30
|
unit: '',
|
|
22
31
|
description: '',
|
|
23
32
|
metadata: {},
|
|
33
|
+
mode: 'OffChain',
|
|
24
34
|
},
|
|
25
35
|
});
|
|
26
36
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
const onSubmit = async (data: TMeter) => {
|
|
37
|
+
const onSubmit = async (data: TMeterForm) => {
|
|
30
38
|
try {
|
|
31
|
-
|
|
39
|
+
const submitData: any = { ...data };
|
|
40
|
+
delete submitData.mode;
|
|
41
|
+
|
|
42
|
+
if (data.mode === 'OnChain') {
|
|
43
|
+
let tokenFactoryAddress = tokenConfig?.tokenFactoryAddress;
|
|
44
|
+
// Create new token if no existing token selected
|
|
45
|
+
if (!tokenFactoryAddress && tokenConfig?.symbol) {
|
|
46
|
+
const { data: factory } = await api.post('/api/credit-tokens', {
|
|
47
|
+
name: tokenConfig.name,
|
|
48
|
+
symbol: tokenConfig.symbol,
|
|
49
|
+
});
|
|
50
|
+
tokenFactoryAddress = factory.address;
|
|
51
|
+
}
|
|
52
|
+
if (tokenFactoryAddress) {
|
|
53
|
+
submitData.token = { tokenFactoryAddress };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await api.post('/api/meters', submitData);
|
|
32
58
|
Toast.success(t('admin.meter.saved'));
|
|
33
|
-
|
|
34
|
-
|
|
59
|
+
setTokenConfig({ name: '', symbol: '', tokenFactoryAddress: '' });
|
|
60
|
+
methods.reset();
|
|
35
61
|
dispatch('meter.created');
|
|
36
62
|
dispatch('drawer.submitted');
|
|
37
63
|
refresh(true);
|
|
38
|
-
} catch (err) {
|
|
39
|
-
console.error(err);
|
|
64
|
+
} catch (err: any) {
|
|
40
65
|
Toast.error(formatError(err));
|
|
41
66
|
}
|
|
42
67
|
};
|
|
@@ -45,15 +70,19 @@ export default function MeterCreate() {
|
|
|
45
70
|
<DrawerForm
|
|
46
71
|
icon={<AddOutlined />}
|
|
47
72
|
text={t('admin.meter.add')}
|
|
48
|
-
onClose={
|
|
73
|
+
onClose={methods.clearErrors}
|
|
49
74
|
width={640}
|
|
50
75
|
addons={
|
|
51
|
-
<Button
|
|
76
|
+
<Button
|
|
77
|
+
variant="contained"
|
|
78
|
+
size="small"
|
|
79
|
+
disabled={methods.formState.isSubmitting}
|
|
80
|
+
onClick={methods.handleSubmit(onSubmit as any)}>
|
|
52
81
|
{t('admin.meter.save')}
|
|
53
82
|
</Button>
|
|
54
83
|
}>
|
|
55
84
|
<FormProvider {...methods}>
|
|
56
|
-
<MeterForm />
|
|
85
|
+
<MeterForm tokenConfig={tokenConfig!} onTokenConfigChange={setTokenConfig} />
|
|
57
86
|
</FormProvider>
|
|
58
87
|
</DrawerForm>
|
|
59
88
|
);
|
|
@@ -20,7 +20,7 @@ import SectionHeader from '../../../../components/section/header';
|
|
|
20
20
|
import { goBackOrFallback } from '../../../../libs/util';
|
|
21
21
|
import MeterEventsList from '../../../../components/meter/events-list';
|
|
22
22
|
import MeterProducts from '../../../../components/meter/products';
|
|
23
|
-
import MeterForm from '../../../../components/meter/form';
|
|
23
|
+
import MeterForm, { type TokenConfig } from '../../../../components/meter/form';
|
|
24
24
|
import DrawerForm from '../../../../components/drawer-form';
|
|
25
25
|
|
|
26
26
|
const getMeter = (id: string): Promise<TMeter & { paymentCurrency: TPaymentCurrency }> => {
|
|
@@ -58,9 +58,14 @@ export default function MeterDetail(props: { id: string }) {
|
|
|
58
58
|
product: false,
|
|
59
59
|
creditProduct: false,
|
|
60
60
|
},
|
|
61
|
+
tokenConfig: { name: '', symbol: '', tokenFactoryAddress: '' } as TokenConfig,
|
|
61
62
|
});
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
type TMeterForm = TMeter & {
|
|
65
|
+
mode?: 'OffChain' | 'OnChain';
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const methods = useForm<TMeterForm>({
|
|
64
69
|
defaultValues: {
|
|
65
70
|
name: '',
|
|
66
71
|
event_name: '',
|
|
@@ -68,6 +73,7 @@ export default function MeterDetail(props: { id: string }) {
|
|
|
68
73
|
unit: '',
|
|
69
74
|
description: '',
|
|
70
75
|
metadata: {},
|
|
76
|
+
mode: 'OffChain',
|
|
71
77
|
},
|
|
72
78
|
});
|
|
73
79
|
|
|
@@ -108,6 +114,7 @@ export default function MeterDetail(props: { id: string }) {
|
|
|
108
114
|
};
|
|
109
115
|
|
|
110
116
|
const handleEditMeter = () => {
|
|
117
|
+
const hasTokenConfig = !!data.paymentCurrency?.token_config;
|
|
111
118
|
// 设置表单默认值
|
|
112
119
|
methods.reset({
|
|
113
120
|
name: data.name,
|
|
@@ -116,14 +123,53 @@ export default function MeterDetail(props: { id: string }) {
|
|
|
116
123
|
unit: data.unit,
|
|
117
124
|
description: data.description || '',
|
|
118
125
|
metadata: data.metadata || {},
|
|
126
|
+
mode: hasTokenConfig ? 'OnChain' : 'OffChain',
|
|
119
127
|
});
|
|
120
|
-
setState((prev) => ({
|
|
128
|
+
setState((prev) => ({
|
|
129
|
+
editing: { ...prev.editing, meter: true },
|
|
130
|
+
tokenConfig: { name: '', symbol: '', tokenFactoryAddress: '' },
|
|
131
|
+
}));
|
|
121
132
|
};
|
|
122
133
|
|
|
123
|
-
const onUpdateMeter = async (formData:
|
|
134
|
+
const onUpdateMeter = async (formData: TMeterForm) => {
|
|
124
135
|
try {
|
|
125
136
|
setState((prev) => ({ loading: { ...prev.loading, meter: true } }));
|
|
137
|
+
|
|
138
|
+
const currencyId = data.paymentCurrency?.id;
|
|
139
|
+
const hasTokenConfig = !!data.paymentCurrency?.token_config;
|
|
140
|
+
|
|
141
|
+
// If enabling on-chain and no token_config yet
|
|
142
|
+
if (currencyId && !hasTokenConfig && formData.mode === 'OnChain') {
|
|
143
|
+
let { tokenFactoryAddress } = state.tokenConfig;
|
|
144
|
+
|
|
145
|
+
// Create new token if not using existing
|
|
146
|
+
if (!tokenFactoryAddress && state.tokenConfig.symbol) {
|
|
147
|
+
const { data: factory } = await api.post('/api/credit-tokens', {
|
|
148
|
+
name: state.tokenConfig.name,
|
|
149
|
+
symbol: state.tokenConfig.symbol,
|
|
150
|
+
});
|
|
151
|
+
tokenFactoryAddress = factory.address;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Update currency with token_config
|
|
155
|
+
if (tokenFactoryAddress) {
|
|
156
|
+
await api.put(`/api/payment-currencies/${currencyId}/token-config`, {
|
|
157
|
+
token_factory_address: tokenFactoryAddress,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// If disabling on-chain and has token_config, remove it
|
|
163
|
+
if (currencyId && hasTokenConfig && formData.mode === 'OffChain') {
|
|
164
|
+
await api.delete(`/api/payment-currencies/${currencyId}/token-config`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Clean up frontend-only fields before sending
|
|
168
|
+
delete formData.mode;
|
|
169
|
+
|
|
170
|
+
// Update meter basic info
|
|
126
171
|
await api.put(`/api/meters/${props.id}`, formData);
|
|
172
|
+
|
|
127
173
|
Toast.success(t('admin.meter.saved'));
|
|
128
174
|
setState((prev) => ({ editing: { ...prev.editing, meter: false } }));
|
|
129
175
|
runAsync();
|
|
@@ -442,7 +488,12 @@ export default function MeterDetail(props: { id: string }) {
|
|
|
442
488
|
</Button>
|
|
443
489
|
}>
|
|
444
490
|
<FormProvider {...methods}>
|
|
445
|
-
<MeterForm
|
|
491
|
+
<MeterForm
|
|
492
|
+
mode="edit"
|
|
493
|
+
paymentCurrency={data.paymentCurrency}
|
|
494
|
+
tokenConfig={state.tokenConfig}
|
|
495
|
+
onTokenConfigChange={(next) => setState({ tokenConfig: next })}
|
|
496
|
+
/>
|
|
446
497
|
</FormProvider>
|
|
447
498
|
</DrawerForm>
|
|
448
499
|
)}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
CreditTransactionsList,
|
|
10
10
|
CreditStatusChip,
|
|
11
11
|
getCustomerAvatar,
|
|
12
|
+
TxLink,
|
|
12
13
|
} from '@blocklet/payment-react';
|
|
13
14
|
import type { TCreditGrantExpanded } from '@blocklet/payment-types';
|
|
14
15
|
import { ArrowBackOutlined } from '@mui/icons-material';
|
|
@@ -325,6 +326,18 @@ export default function AdminCreditGrantDetail({ id }: { id: string }) {
|
|
|
325
326
|
{data.expires_at && (
|
|
326
327
|
<InfoRow label={t('common.expirationDate')} value={formatTime(data.expires_at * 1000)} />
|
|
327
328
|
)}
|
|
329
|
+
{data.chain_detail?.mint?.hash && data.paymentMethod?.type === 'arcblock' && (
|
|
330
|
+
<InfoRow
|
|
331
|
+
label={t('common.mintTxHash')}
|
|
332
|
+
value={
|
|
333
|
+
<TxLink
|
|
334
|
+
details={{ arcblock: { tx_hash: data.chain_detail.mint.hash, payer: '' } }}
|
|
335
|
+
method={data.paymentMethod}
|
|
336
|
+
mode="dashboard"
|
|
337
|
+
/>
|
|
338
|
+
}
|
|
339
|
+
/>
|
|
340
|
+
)}
|
|
328
341
|
</InfoRowGroup>
|
|
329
342
|
</Box>
|
|
330
343
|
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
+
import { api, formatBNStr, formatError, getCustomerAvatar, TxLink, SourceDataViewer } from '@blocklet/payment-react';
|
|
3
|
+
import type { TCreditTransactionExpanded } from '@blocklet/payment-types';
|
|
4
|
+
import { ArrowBackOutlined } from '@mui/icons-material';
|
|
5
|
+
import { Alert, Avatar, Box, Button, Chip, CircularProgress, Divider, Stack, Typography } from '@mui/material';
|
|
6
|
+
import { useRequest } from 'ahooks';
|
|
7
|
+
import { styled } from '@mui/system';
|
|
8
|
+
import { useCallback } from 'react';
|
|
9
|
+
import { useNavigate } from 'react-router-dom';
|
|
10
|
+
|
|
11
|
+
import InfoMetric from '../../../../../components/info-metric';
|
|
12
|
+
import InfoRow from '../../../../../components/info-row';
|
|
13
|
+
import InfoRowGroup from '../../../../../components/info-row-group';
|
|
14
|
+
import Copyable from '../../../../../components/copyable';
|
|
15
|
+
import SectionHeader from '../../../../../components/section/header';
|
|
16
|
+
import MetadataList from '../../../../../components/metadata/list';
|
|
17
|
+
import { goBackOrFallback } from '../../../../../libs/util';
|
|
18
|
+
import EventList from '../../../../../components/event/list';
|
|
19
|
+
|
|
20
|
+
const fetchData = (id: string | undefined): Promise<TCreditTransactionExpanded> => {
|
|
21
|
+
return api.get(`/api/credit-transactions/${id}`).then((res) => res.data);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default function AdminCreditTransactionDetail({ id }: { id: string }) {
|
|
25
|
+
const { t } = useLocaleContext();
|
|
26
|
+
const navigate = useNavigate();
|
|
27
|
+
|
|
28
|
+
const { loading, error, data } = useRequest(() => fetchData(id));
|
|
29
|
+
|
|
30
|
+
const handleBack = useCallback(() => {
|
|
31
|
+
goBackOrFallback('/admin/customers');
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
if (error) {
|
|
35
|
+
return <Alert severity="error">{formatError(error)}</Alert>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (loading || !data) {
|
|
39
|
+
return <CircularProgress />;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const getTransferStatusChip = (status: string | null | undefined) => {
|
|
43
|
+
if (!status) return null;
|
|
44
|
+
|
|
45
|
+
const statusConfig = {
|
|
46
|
+
pending: { label: t('common.pending'), color: 'warning' as const },
|
|
47
|
+
completed: { label: t('common.completed'), color: 'success' as const },
|
|
48
|
+
failed: { label: t('common.failed'), color: 'error' as const },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const config = statusConfig[status as keyof typeof statusConfig] || {
|
|
52
|
+
label: status,
|
|
53
|
+
color: 'default' as const,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return <Chip label={config.label} size="small" color={config.color} />;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Root direction="column" spacing={2.5} sx={{ mb: 4 }}>
|
|
61
|
+
<Box>
|
|
62
|
+
<Stack className="page-header" direction="row" justifyContent="space-between" alignItems="center">
|
|
63
|
+
<Stack
|
|
64
|
+
direction="row"
|
|
65
|
+
alignItems="center"
|
|
66
|
+
sx={{ fontWeight: 'normal', cursor: 'pointer' }}
|
|
67
|
+
onClick={handleBack}>
|
|
68
|
+
<ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
|
|
69
|
+
<Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
|
|
70
|
+
{t('admin.creditTransactions.title')}
|
|
71
|
+
</Typography>
|
|
72
|
+
</Stack>
|
|
73
|
+
<Stack direction="row" spacing={1}>
|
|
74
|
+
{data.creditGrant && (
|
|
75
|
+
<Button
|
|
76
|
+
variant="outlined"
|
|
77
|
+
size="small"
|
|
78
|
+
onClick={() => navigate(`/admin/customers/${data.credit_grant_id}`)}>
|
|
79
|
+
{t('common.viewGrant')}
|
|
80
|
+
</Button>
|
|
81
|
+
)}
|
|
82
|
+
{data.subscription && (
|
|
83
|
+
<Button
|
|
84
|
+
variant="outlined"
|
|
85
|
+
size="small"
|
|
86
|
+
onClick={() => navigate(`/admin/billing/${data.subscription_id}`)}>
|
|
87
|
+
{t('common.viewSubscription')}
|
|
88
|
+
</Button>
|
|
89
|
+
)}
|
|
90
|
+
</Stack>
|
|
91
|
+
</Stack>
|
|
92
|
+
|
|
93
|
+
<Box
|
|
94
|
+
mt={4}
|
|
95
|
+
mb={3}
|
|
96
|
+
sx={{
|
|
97
|
+
display: 'flex',
|
|
98
|
+
gap: {
|
|
99
|
+
xs: 2,
|
|
100
|
+
sm: 2,
|
|
101
|
+
md: 5,
|
|
102
|
+
},
|
|
103
|
+
flexWrap: 'wrap',
|
|
104
|
+
flexDirection: {
|
|
105
|
+
xs: 'column',
|
|
106
|
+
sm: 'column',
|
|
107
|
+
md: 'row',
|
|
108
|
+
},
|
|
109
|
+
alignItems: {
|
|
110
|
+
xs: 'flex-start',
|
|
111
|
+
sm: 'flex-start',
|
|
112
|
+
md: 'center',
|
|
113
|
+
},
|
|
114
|
+
}}>
|
|
115
|
+
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
|
116
|
+
<Stack direction="column" spacing={0.5}>
|
|
117
|
+
<Typography variant="h2" sx={{ fontWeight: 600 }}>
|
|
118
|
+
{data.description || t('common.creditTransaction')}
|
|
119
|
+
</Typography>
|
|
120
|
+
<Copyable text={data.id} />
|
|
121
|
+
</Stack>
|
|
122
|
+
</Stack>
|
|
123
|
+
|
|
124
|
+
<Stack
|
|
125
|
+
className="section-body"
|
|
126
|
+
justifyContent="flex-start"
|
|
127
|
+
flexWrap="wrap"
|
|
128
|
+
sx={{
|
|
129
|
+
'hr.MuiDivider-root:last-child': {
|
|
130
|
+
display: 'none',
|
|
131
|
+
},
|
|
132
|
+
flexDirection: {
|
|
133
|
+
xs: 'column',
|
|
134
|
+
sm: 'column',
|
|
135
|
+
md: 'row',
|
|
136
|
+
},
|
|
137
|
+
alignItems: 'flex-start',
|
|
138
|
+
gap: {
|
|
139
|
+
xs: 1,
|
|
140
|
+
sm: 1,
|
|
141
|
+
md: 3,
|
|
142
|
+
},
|
|
143
|
+
}}>
|
|
144
|
+
<InfoMetric
|
|
145
|
+
label={t('common.creditAmount')}
|
|
146
|
+
value={
|
|
147
|
+
<Stack direction="row" alignItems="center" spacing={0.5}>
|
|
148
|
+
<Typography variant="body2" sx={{ color: 'error.main' }}>
|
|
149
|
+
-{formatBNStr(data.credit_amount, data.creditGrant?.paymentCurrency?.decimal || 0)}{' '}
|
|
150
|
+
{data.creditGrant?.paymentCurrency?.symbol}
|
|
151
|
+
</Typography>
|
|
152
|
+
</Stack>
|
|
153
|
+
}
|
|
154
|
+
divider={!!data.transfer_status}
|
|
155
|
+
/>
|
|
156
|
+
{data.transfer_status && (
|
|
157
|
+
<InfoMetric label={t('common.transferStatus')} value={getTransferStatusChip(data.transfer_status)} />
|
|
158
|
+
)}
|
|
159
|
+
</Stack>
|
|
160
|
+
</Box>
|
|
161
|
+
<Divider />
|
|
162
|
+
</Box>
|
|
163
|
+
|
|
164
|
+
<Stack
|
|
165
|
+
sx={{
|
|
166
|
+
flexDirection: {
|
|
167
|
+
xs: 'column',
|
|
168
|
+
lg: 'row',
|
|
169
|
+
},
|
|
170
|
+
gap: {
|
|
171
|
+
xs: 2.5,
|
|
172
|
+
md: 4,
|
|
173
|
+
},
|
|
174
|
+
'.transaction-column-1': {
|
|
175
|
+
minWidth: {
|
|
176
|
+
xs: '100%',
|
|
177
|
+
lg: '600px',
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
'.transaction-column-2': {
|
|
181
|
+
width: {
|
|
182
|
+
xs: '100%',
|
|
183
|
+
md: '100%',
|
|
184
|
+
lg: '320px',
|
|
185
|
+
},
|
|
186
|
+
maxWidth: {
|
|
187
|
+
xs: '100%',
|
|
188
|
+
md: '33%',
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
}}>
|
|
192
|
+
<Box flex={1} className="transaction-column-1" sx={{ gap: 2.5, display: 'flex', flexDirection: 'column' }}>
|
|
193
|
+
<Box className="section" sx={{ containerType: 'inline-size' }}>
|
|
194
|
+
<SectionHeader title={t('admin.details')} />
|
|
195
|
+
<InfoRowGroup
|
|
196
|
+
sx={{
|
|
197
|
+
display: 'grid',
|
|
198
|
+
gridTemplateColumns: {
|
|
199
|
+
xs: 'repeat(1, 1fr)',
|
|
200
|
+
xl: 'repeat(2, 1fr)',
|
|
201
|
+
},
|
|
202
|
+
'@container (min-width: 1000px)': {
|
|
203
|
+
gridTemplateColumns: 'repeat(2, 1fr)',
|
|
204
|
+
},
|
|
205
|
+
'.info-row-wrapper': {
|
|
206
|
+
gap: 1,
|
|
207
|
+
flexDirection: {
|
|
208
|
+
xs: 'column',
|
|
209
|
+
xl: 'row',
|
|
210
|
+
},
|
|
211
|
+
alignItems: {
|
|
212
|
+
xs: 'flex-start',
|
|
213
|
+
xl: 'center',
|
|
214
|
+
},
|
|
215
|
+
'@container (min-width: 1000px)': {
|
|
216
|
+
flexDirection: 'row',
|
|
217
|
+
alignItems: 'center',
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
}}>
|
|
221
|
+
<InfoRow
|
|
222
|
+
label={t('common.customer')}
|
|
223
|
+
value={
|
|
224
|
+
<Stack direction="row" alignItems="center" spacing={1}>
|
|
225
|
+
<Avatar
|
|
226
|
+
src={getCustomerAvatar(
|
|
227
|
+
data.customer?.did,
|
|
228
|
+
data.customer?.updated_at ? new Date(data.customer.updated_at).toISOString() : '',
|
|
229
|
+
24
|
|
230
|
+
)}
|
|
231
|
+
alt={data.customer?.name}
|
|
232
|
+
sx={{ width: 24, height: 24 }}
|
|
233
|
+
/>
|
|
234
|
+
<Typography>{data.customer?.name}</Typography>
|
|
235
|
+
</Stack>
|
|
236
|
+
}
|
|
237
|
+
/>
|
|
238
|
+
<InfoRow
|
|
239
|
+
label={t('common.creditGrant')}
|
|
240
|
+
value={
|
|
241
|
+
<Typography
|
|
242
|
+
component="span"
|
|
243
|
+
onClick={() => navigate(`/admin/customers/${data.credit_grant_id}`)}
|
|
244
|
+
sx={{ color: 'text.link', cursor: 'pointer' }}>
|
|
245
|
+
{data.creditGrant?.name || data.credit_grant_id}
|
|
246
|
+
</Typography>
|
|
247
|
+
}
|
|
248
|
+
/>
|
|
249
|
+
{data.meter && (
|
|
250
|
+
<InfoRow
|
|
251
|
+
label={t('common.meterEvent')}
|
|
252
|
+
value={
|
|
253
|
+
<Typography
|
|
254
|
+
component="span"
|
|
255
|
+
onClick={() => navigate(`/admin/billing/${data.meter_id}`)}
|
|
256
|
+
sx={{ color: 'text.link', cursor: 'pointer' }}>
|
|
257
|
+
{data.meter.name || data.meter.event_name}
|
|
258
|
+
</Typography>
|
|
259
|
+
}
|
|
260
|
+
/>
|
|
261
|
+
)}
|
|
262
|
+
{data.subscription && (
|
|
263
|
+
<InfoRow
|
|
264
|
+
label={t('admin.subscription.name')}
|
|
265
|
+
value={
|
|
266
|
+
<Typography
|
|
267
|
+
component="span"
|
|
268
|
+
onClick={() => navigate(`/admin/billing/${data.subscription_id}`)}
|
|
269
|
+
sx={{ color: 'text.link', cursor: 'pointer' }}>
|
|
270
|
+
{data.subscription.description || data.subscription_id}
|
|
271
|
+
</Typography>
|
|
272
|
+
}
|
|
273
|
+
/>
|
|
274
|
+
)}
|
|
275
|
+
{data.transfer_hash && data.paymentMethod && (
|
|
276
|
+
<InfoRow
|
|
277
|
+
label={t('common.transferTxHash')}
|
|
278
|
+
value={
|
|
279
|
+
<TxLink
|
|
280
|
+
details={{ arcblock: { tx_hash: data.transfer_hash, payer: '' } }}
|
|
281
|
+
method={data.paymentMethod}
|
|
282
|
+
mode="dashboard"
|
|
283
|
+
/>
|
|
284
|
+
}
|
|
285
|
+
/>
|
|
286
|
+
)}
|
|
287
|
+
</InfoRowGroup>
|
|
288
|
+
</Box>
|
|
289
|
+
|
|
290
|
+
{data.meterEvent?.source_data && (
|
|
291
|
+
<>
|
|
292
|
+
<Divider />
|
|
293
|
+
<Box className="section">
|
|
294
|
+
<SectionHeader title={t('common.sourceData')} />
|
|
295
|
+
<Box className="section-body">
|
|
296
|
+
<SourceDataViewer data={data.meterEvent.source_data} />
|
|
297
|
+
</Box>
|
|
298
|
+
</Box>
|
|
299
|
+
</>
|
|
300
|
+
)}
|
|
301
|
+
|
|
302
|
+
<Divider />
|
|
303
|
+
<Box className="section">
|
|
304
|
+
<SectionHeader title={t('admin.events.title')} />
|
|
305
|
+
<Box className="section-body">
|
|
306
|
+
<EventList features={{ toolbar: false }} object_id={data.id} />
|
|
307
|
+
</Box>
|
|
308
|
+
</Box>
|
|
309
|
+
</Box>
|
|
310
|
+
|
|
311
|
+
<Box className="transaction-column-2" sx={{ gap: 2.5, display: 'flex', flexDirection: 'column' }}>
|
|
312
|
+
<Box className="section">
|
|
313
|
+
<SectionHeader title={t('common.metadata.label')} />
|
|
314
|
+
<Box className="section-body">
|
|
315
|
+
<MetadataList data={data.metadata} />
|
|
316
|
+
</Box>
|
|
317
|
+
</Box>
|
|
318
|
+
</Box>
|
|
319
|
+
</Stack>
|
|
320
|
+
</Root>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const Root = styled(Stack)``;
|
|
@@ -7,6 +7,7 @@ import { useTransitionContext } from '../../../components/progress-bar';
|
|
|
7
7
|
|
|
8
8
|
const CustomerDetail = React.lazy(() => import('./customers/detail'));
|
|
9
9
|
const CreditGrantDetail = React.lazy(() => import('./customers/credit-grant/detail'));
|
|
10
|
+
const CreditTransactionDetail = React.lazy(() => import('./customers/credit-transaction/detail'));
|
|
10
11
|
|
|
11
12
|
const pages = {
|
|
12
13
|
overview: React.lazy(() => import('./customers')),
|
|
@@ -26,6 +27,10 @@ export default function CustomerIndex() {
|
|
|
26
27
|
return <CreditGrantDetail id={page} />;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
if (page.startsWith('cbtxn_')) {
|
|
31
|
+
return <CreditTransactionDetail id={page} />;
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
const onTabChange = (newTab: string) => {
|
|
30
35
|
startTransition(() => {
|
|
31
36
|
navigate(`/admin/customers/${newTab}`);
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
CreditTransactionsList,
|
|
8
8
|
CreditStatusChip,
|
|
9
9
|
getCustomerAvatar,
|
|
10
|
+
TxLink,
|
|
10
11
|
} from '@blocklet/payment-react';
|
|
11
12
|
import type { TCreditGrantExpanded } from '@blocklet/payment-types';
|
|
12
13
|
import { ArrowBackOutlined } from '@mui/icons-material';
|
|
@@ -40,7 +41,7 @@ export default function CustomerCreditGrantDetail() {
|
|
|
40
41
|
}, [navigate]);
|
|
41
42
|
|
|
42
43
|
if (data?.customer?.did && session?.user?.did && data.customer.did !== session.user.did) {
|
|
43
|
-
return <Alert severity="error">
|
|
44
|
+
return <Alert severity="error">{t('common.accessDenied')}</Alert>;
|
|
44
45
|
}
|
|
45
46
|
if (error) {
|
|
46
47
|
return <Alert severity="error">{error.message}</Alert>;
|
|
@@ -271,6 +272,18 @@ export default function CustomerCreditGrantDetail() {
|
|
|
271
272
|
/>
|
|
272
273
|
<InfoRow label={t('admin.creditProduct.priority.label')} value={<Typography>{data.priority}</Typography>} />
|
|
273
274
|
<InfoRow label={t('common.createdAt')} value={formatTime(data.created_at)} />
|
|
275
|
+
{data.chain_detail?.mint?.hash && data.paymentMethod?.type === 'arcblock' && (
|
|
276
|
+
<InfoRow
|
|
277
|
+
label={t('common.mintTxHash')}
|
|
278
|
+
value={
|
|
279
|
+
<TxLink
|
|
280
|
+
details={{ arcblock: { tx_hash: data.chain_detail.mint.hash, payer: '' } }}
|
|
281
|
+
method={data.paymentMethod}
|
|
282
|
+
mode="customer"
|
|
283
|
+
/>
|
|
284
|
+
}
|
|
285
|
+
/>
|
|
286
|
+
)}
|
|
274
287
|
</InfoRowGroup>
|
|
275
288
|
</Box>
|
|
276
289
|
|