payment-kit 1.19.18 → 1.19.20

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 (52) hide show
  1. package/api/src/index.ts +3 -1
  2. package/api/src/integrations/ethereum/tx.ts +11 -0
  3. package/api/src/integrations/stripe/handlers/invoice.ts +26 -6
  4. package/api/src/integrations/stripe/handlers/setup-intent.ts +34 -2
  5. package/api/src/integrations/stripe/resource.ts +185 -1
  6. package/api/src/libs/invoice.ts +2 -1
  7. package/api/src/libs/session.ts +6 -1
  8. package/api/src/queues/auto-recharge.ts +343 -0
  9. package/api/src/queues/credit-consume.ts +15 -1
  10. package/api/src/queues/credit-grant.ts +15 -0
  11. package/api/src/queues/payment.ts +14 -1
  12. package/api/src/queues/space.ts +1 -0
  13. package/api/src/routes/auto-recharge-configs.ts +454 -0
  14. package/api/src/routes/connect/auto-recharge-auth.ts +182 -0
  15. package/api/src/routes/connect/recharge-account.ts +72 -10
  16. package/api/src/routes/connect/setup.ts +5 -3
  17. package/api/src/routes/connect/shared.ts +45 -4
  18. package/api/src/routes/customers.ts +10 -6
  19. package/api/src/routes/index.ts +2 -0
  20. package/api/src/routes/invoices.ts +10 -1
  21. package/api/src/routes/meters.ts +1 -1
  22. package/api/src/routes/payment-currencies.ts +129 -0
  23. package/api/src/store/migrate.ts +20 -0
  24. package/api/src/store/migrations/20250821-auto-recharge-config.ts +38 -0
  25. package/api/src/store/models/auto-recharge-config.ts +225 -0
  26. package/api/src/store/models/credit-grant.ts +1 -1
  27. package/api/src/store/models/customer.ts +1 -0
  28. package/api/src/store/models/index.ts +3 -0
  29. package/api/src/store/models/invoice.ts +2 -1
  30. package/api/src/store/models/payment-currency.ts +10 -2
  31. package/api/src/store/models/types.ts +11 -0
  32. package/blocklet.yml +1 -1
  33. package/package.json +18 -17
  34. package/src/components/customer/credit-overview.tsx +103 -18
  35. package/src/components/customer/overdraft-protection.tsx +5 -5
  36. package/src/components/info-metric.tsx +11 -2
  37. package/src/components/invoice/recharge.tsx +8 -2
  38. package/src/components/metadata/form.tsx +29 -27
  39. package/src/components/meter/form.tsx +1 -2
  40. package/src/components/price/form.tsx +39 -26
  41. package/src/components/product/form.tsx +1 -2
  42. package/src/locales/en.tsx +15 -0
  43. package/src/locales/zh.tsx +14 -0
  44. package/src/pages/admin/billing/meters/detail.tsx +18 -0
  45. package/src/pages/admin/customers/customers/credit-grant/detail.tsx +10 -0
  46. package/src/pages/admin/products/prices/actions.tsx +42 -2
  47. package/src/pages/admin/products/products/create.tsx +1 -2
  48. package/src/pages/admin/settings/vault-config/edit-form.tsx +8 -8
  49. package/src/pages/customer/credit-grant/detail.tsx +9 -1
  50. package/src/pages/customer/recharge/account.tsx +14 -7
  51. package/src/pages/customer/recharge/subscription.tsx +4 -4
  52. package/vite.config.ts +26 -1
@@ -65,6 +65,8 @@ type ListProps = {
65
65
  };
66
66
  currency_id?: string;
67
67
  subscription_id?: string;
68
+ recharge_address?: string;
69
+ customer_id?: string;
68
70
  };
69
71
 
70
72
  const getListKey = (props: ListProps) => {
@@ -80,12 +82,14 @@ const getListKey = (props: ListProps) => {
80
82
  export default function RechargeList({
81
83
  currency_id = '',
82
84
  subscription_id = '',
85
+ recharge_address = '',
86
+ customer_id = '',
83
87
  features = {
84
88
  customer: true,
85
89
  filter: true,
86
90
  },
87
91
  }: ListProps) {
88
- const listKey = getListKey({ currency_id, subscription_id });
92
+ const listKey = getListKey({ currency_id, subscription_id, recharge_address });
89
93
 
90
94
  const { t, locale } = useLocaleContext();
91
95
  const defaultPageSize = useDefaultPageSize(20);
@@ -106,6 +110,8 @@ export default function RechargeList({
106
110
  try {
107
111
  const res = await fetchData(subscription_id || '', currency_id || '', {
108
112
  ...search,
113
+ recharge_address,
114
+ customer_id,
109
115
  });
110
116
  setData(res);
111
117
  } catch (error) {
@@ -117,7 +123,7 @@ export default function RechargeList({
117
123
 
118
124
  useEffect(() => {
119
125
  refresh();
120
- }, [search, currency_id, subscription_id]);
126
+ }, [search, currency_id, subscription_id, recharge_address]);
121
127
 
122
128
  const getInvoiceLink = (invoice: TInvoiceExpanded) => {
123
129
  return {
@@ -280,34 +280,36 @@ export default function MetadataForm({
280
280
  value={jsonValue}
281
281
  onChange={(e) => handleJsonChange(e.target.value)}
282
282
  placeholder={t('common.metadata.jsonPlaceholder')}
283
- InputProps={{
284
- endAdornment: (
285
- <InputAdornment position="end" sx={{ alignSelf: 'flex-start', mt: 1 }}>
286
- <Tooltip
287
- title={formatError || t('common.metadata.formatJson')}
288
- placement="top"
289
- componentsProps={{
290
- tooltip: {
291
- sx: formatError
292
- ? {
293
- maxWidth: 300,
294
- backgroundColor: 'error.main',
295
- opacity: 0.8,
296
- }
297
- : {},
298
- },
299
- }}>
300
- <IconButton
301
- onClick={handleFormatJson}
302
- size="small"
303
- sx={{
304
- color: formatError ? 'error.main' : 'text.secondary',
283
+ slotProps={{
284
+ input: {
285
+ endAdornment: (
286
+ <InputAdornment position="end" sx={{ alignSelf: 'flex-start', mt: 1 }}>
287
+ <Tooltip
288
+ title={formatError || t('common.metadata.formatJson')}
289
+ placement="top"
290
+ componentsProps={{
291
+ tooltip: {
292
+ sx: formatError
293
+ ? {
294
+ maxWidth: 300,
295
+ backgroundColor: 'error.main',
296
+ opacity: 0.8,
297
+ }
298
+ : {},
299
+ },
305
300
  }}>
306
- <FormatAlignLeft fontSize="small" />
307
- </IconButton>
308
- </Tooltip>
309
- </InputAdornment>
310
- ),
301
+ <IconButton
302
+ onClick={handleFormatJson}
303
+ size="small"
304
+ sx={{
305
+ color: formatError ? 'error.main' : 'text.secondary',
306
+ }}>
307
+ <FormatAlignLeft fontSize="small" />
308
+ </IconButton>
309
+ </Tooltip>
310
+ </InputAdornment>
311
+ ),
312
+ },
311
313
  }}
312
314
  sx={{
313
315
  mb: 2,
@@ -1,11 +1,10 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
- import { FormInput, FormLabel } from '@blocklet/payment-react';
2
+ import { FormInput, FormLabel, Collapse } from '@blocklet/payment-react';
3
3
  import { FormControl, Select, MenuItem, Typography, Box, FormHelperText, Stack } from '@mui/material';
4
4
  import { useFormContext, useWatch } from 'react-hook-form';
5
5
  import { InfoOutlined } from '@mui/icons-material';
6
6
 
7
7
  import type { TMeter } from '@blocklet/payment-types';
8
- import Collapse from '../collapse';
9
8
  import MetadataForm from '../metadata/form';
10
9
 
11
10
  type Props = {
@@ -8,6 +8,7 @@ import {
8
8
  FormLabel,
9
9
  isCreditMetered,
10
10
  formatPrice,
11
+ Collapse,
11
12
  } from '@blocklet/payment-react';
12
13
  import type {
13
14
  InferFormType,
@@ -46,7 +47,6 @@ import { useMemo, useState } from 'react';
46
47
  import { get } from 'lodash';
47
48
  import { useRequest } from 'ahooks';
48
49
  import ProductSelect from '../payment-link/product-select';
49
- import Collapse from '../collapse';
50
50
  import { useProductsContext } from '../../contexts/products';
51
51
  import CurrencySelect from './currency-select';
52
52
  import { getProductByPriceId } from '../../libs/util';
@@ -782,29 +782,33 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
782
782
  metadata.credit_config = creditConfig;
783
783
  field.onChange(metadata);
784
784
  }}
785
- inputProps={{ min: 0, step: 'any' }}
786
- // eslint-disable-next-line react/jsx-no-duplicate-props
787
- InputProps={{
788
- endAdornment: (
789
- <InputAdornment position="end">
790
- <CurrencySelect
791
- mode="selected"
792
- hasSelected={(c) => currencies.fields.some((x: any) => x.currency_id === c.id)}
793
- currencyFilter={(c) => c.type === 'credit'}
794
- onSelect={(currencyId) => {
795
- const metadata = field.value || {};
796
- const creditConfig = metadata.credit_config || {};
797
- creditConfig.currency_id = currencyId;
798
- metadata.credit_config = creditConfig;
799
- field.onChange(metadata);
800
- }}
801
- value={field.value?.credit_config?.currency_id || creditCurrencies?.[0]?.id}
802
- disabled={isLocked}
803
- hideMethod
804
- selectSX={{ '.MuiOutlinedInput-notchedOutline': { border: 'none' } }}
805
- />
806
- </InputAdornment>
807
- ),
785
+ slotProps={{
786
+ input: {
787
+ endAdornment: (
788
+ <InputAdornment position="end">
789
+ <CurrencySelect
790
+ mode="selected"
791
+ hasSelected={(c) => currencies.fields.some((x: any) => x.currency_id === c.id)}
792
+ currencyFilter={(c) => c.type === 'credit'}
793
+ onSelect={(currencyId) => {
794
+ const metadata = field.value || {};
795
+ const creditConfig = metadata.credit_config || {};
796
+ creditConfig.currency_id = currencyId;
797
+ metadata.credit_config = creditConfig;
798
+ field.onChange(metadata);
799
+ }}
800
+ value={field.value?.credit_config?.currency_id || creditCurrencies?.[0]?.id}
801
+ disabled={isLocked}
802
+ hideMethod
803
+ selectSX={{ '.MuiOutlinedInput-notchedOutline': { border: 'none' } }}
804
+ />
805
+ </InputAdornment>
806
+ ),
807
+ },
808
+ htmlInput: {
809
+ min: 0,
810
+ step: 'any',
811
+ },
808
812
  }}
809
813
  />
810
814
  <Typography
@@ -843,7 +847,11 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
843
847
  value={field.value ?? '0'}
844
848
  placeholder="0"
845
849
  disabled={isLocked}
846
- inputProps={{ min: 0 }}
850
+ slotProps={{
851
+ htmlInput: {
852
+ min: 0,
853
+ },
854
+ }}
847
855
  />
848
856
  )}
849
857
  />
@@ -1000,7 +1008,12 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
1000
1008
  metadata.credit_config = creditConfig;
1001
1009
  field.onChange(metadata);
1002
1010
  }}
1003
- inputProps={{ min: 0, max: 100 }}
1011
+ slotProps={{
1012
+ htmlInput: {
1013
+ min: 0,
1014
+ max: 100,
1015
+ },
1016
+ }}
1004
1017
  />
1005
1018
  <Typography
1006
1019
  variant="caption"
@@ -1,11 +1,10 @@
1
1
  /* eslint-disable no-nested-ternary */
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
- import { FormInput, FormLabel } from '@blocklet/payment-react';
3
+ import { FormInput, FormLabel, Collapse } from '@blocklet/payment-react';
4
4
  import type { InferFormType, TProduct } from '@blocklet/payment-types';
5
5
  import { Box, Stack, Typography, Select, MenuItem } from '@mui/material';
6
6
  import { useFormContext, useWatch, Controller } from 'react-hook-form';
7
7
 
8
- import Collapse from '../collapse';
9
8
  import MetadataForm from '../metadata/form';
10
9
  import type { Price } from '../price/form';
11
10
  import Uploader from '../uploader';
@@ -222,6 +222,11 @@ export default flat({
222
222
  creditProducts: {
223
223
  emptyTip: 'No credit products are associated with this meter yet.',
224
224
  },
225
+ rechargePackage: {
226
+ priceId: 'Top-up Package',
227
+ tooltip:
228
+ 'The top-up package is used to recharge {name}, and users can get the corresponding {name} after purchase.',
229
+ },
225
230
  },
226
231
  creditProduct: {
227
232
  create: 'Create Credit Product',
@@ -489,6 +494,10 @@ export default flat({
489
494
  description:
490
495
  'Credit metered billing requires a corresponding Credit consumption. If the Credit is consumed, the corresponding service will stop.',
491
496
  },
497
+ credit: {
498
+ saveAsBasePrice: 'Set as Top-up Package',
499
+ saveSuccess: 'Successfully set as top-up package',
500
+ },
492
501
  types: {
493
502
  onetime: 'One time',
494
503
  onetimeDesc: 'Charge a one-time fee',
@@ -970,6 +979,7 @@ export default flat({
970
979
  },
971
980
  originalAmount: 'Original Amount',
972
981
  usage: 'Usage',
982
+ viewInvoice: 'View Invoice',
973
983
  grantId: 'Grant ID',
974
984
  currency: 'Currency',
975
985
  created: 'Created',
@@ -1221,6 +1231,11 @@ export default flat({
1221
1231
  product: {
1222
1232
  empty: 'No Products',
1223
1233
  },
1234
+ credit: {
1235
+ recharge: 'Buy Credits',
1236
+ autoRecharge: 'Auto Top-up',
1237
+ unsupported: 'Not supported yet',
1238
+ },
1224
1239
  recharge: {
1225
1240
  title: 'Add Funds',
1226
1241
  amount: 'Amount',
@@ -221,6 +221,10 @@ export default flat({
221
221
  creditProducts: {
222
222
  emptyTip: '此计量器尚未关联任何 Credit 产品。',
223
223
  },
224
+ rechargePackage: {
225
+ priceId: '充值套餐',
226
+ tooltip: '充值套餐是用于充值 {name} 的套餐,用户购买后可以获得对应的 {name}。',
227
+ },
224
228
  },
225
229
  creditProduct: {
226
230
  create: '创建 Credit 产品',
@@ -459,6 +463,10 @@ export default flat({
459
463
  pricingNote: '对于计量产品,定价会自动基于所选计量器跟踪的使用情况。',
460
464
  description: 'Credit 计量计费的订阅需要有对应的 Credit 消费,如果 Credit 消耗完,对应的服务会停止。',
461
465
  },
466
+ credit: {
467
+ saveAsBasePrice: '设为充值套餐',
468
+ saveSuccess: '设置成功',
469
+ },
462
470
  types: {
463
471
  onetime: '一次性',
464
472
  onetimeDesc: '收取一次性费用',
@@ -937,6 +945,7 @@ export default flat({
937
945
  overviewDescription: '监控所有货币的信用余额、使用情况和未偿债务。',
938
946
  availableBalance: '可用余额',
939
947
  usage: '使用情况',
948
+ viewInvoice: '查看账单',
940
949
  viewGrants: '查看额度',
941
950
  viewUsage: '查看使用情况',
942
951
  recentActivity: '最近活动',
@@ -1181,6 +1190,11 @@ export default flat({
1181
1190
  product: {
1182
1191
  empty: '没有订阅产品',
1183
1192
  },
1193
+ credit: {
1194
+ recharge: '购买额度',
1195
+ autoRecharge: '自动充值',
1196
+ unsupported: '暂不支持购买额度',
1197
+ },
1184
1198
  recharge: {
1185
1199
  title: '充值',
1186
1200
  amount: '金额',
@@ -253,6 +253,24 @@ export default function MeterDetail(props: { id: string }) {
253
253
  }}>
254
254
  <InfoMetric label={t('common.id')} value={<Copyable text={props.id} style={{ marginLeft: 4 }} />} divider />
255
255
  <InfoMetric label={t('common.createdAt')} value={formatTime(data.created_at)} divider />
256
+ {data.paymentCurrency?.recharge_config?.base_price_id && (
257
+ <InfoMetric
258
+ label={t('admin.meter.rechargePackage.priceId')}
259
+ tip={t('admin.meter.rechargePackage.tooltip', {
260
+ name: data.paymentCurrency.name,
261
+ })}
262
+ value={
263
+ <Button
264
+ variant="text"
265
+ size="small"
266
+ onClick={() => navigate(`/admin/products/${data.paymentCurrency.recharge_config!.base_price_id}`)}
267
+ sx={{ p: 0, minWidth: 'auto', textAlign: 'left', ml: 0.5, color: 'text.link' }}>
268
+ {data.paymentCurrency.recharge_config!.base_price_id}
269
+ </Button>
270
+ }
271
+ divider
272
+ />
273
+ )}
256
274
  </Stack>
257
275
  </Box>
258
276
  <Divider />
@@ -16,6 +16,7 @@ import { Alert, Avatar, Box, Button, CircularProgress, Divider, Stack, Typograph
16
16
  import { useRequest, useSetState } from 'ahooks';
17
17
  import { styled } from '@mui/system';
18
18
  import { useCallback } from 'react';
19
+ import { useNavigate } from 'react-router-dom';
19
20
 
20
21
  import InfoMetric from '../../../../../components/info-metric';
21
22
  import InfoRow from '../../../../../components/info-row';
@@ -34,6 +35,7 @@ const fetchData = (id: string | undefined): Promise<TCreditGrantExpanded> => {
34
35
 
35
36
  export default function AdminCreditGrantDetail({ id }: { id: string }) {
36
37
  const { t } = useLocaleContext();
38
+ const navigate = useNavigate();
37
39
  const [state, setState] = useSetState({
38
40
  editing: {
39
41
  metadata: false,
@@ -117,6 +119,14 @@ export default function AdminCreditGrantDetail({ id }: { id: string }) {
117
119
  {t('admin.customer.creditGrants.title')}
118
120
  </Typography>
119
121
  </Stack>
122
+ {data.metadata!.invoice_id && (
123
+ <Button
124
+ variant="outlined"
125
+ size="small"
126
+ onClick={() => navigate(`/admin/billing/${data.metadata!.invoice_id}`)}>
127
+ {t('admin.customer.creditGrants.viewInvoice')}
128
+ </Button>
129
+ )}
120
130
  </Stack>
121
131
 
122
132
  <Box
@@ -2,7 +2,7 @@ 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
4
  import type { TPrice, TProduct } from '@blocklet/payment-types';
5
- import { useSetState } from 'ahooks';
5
+ import { useRequest, useSetState } from 'ahooks';
6
6
  import noop from 'lodash/noop';
7
7
  import { useNavigate } from 'react-router-dom';
8
8
 
@@ -18,6 +18,11 @@ type Props = {
18
18
  product?: TProduct | null;
19
19
  };
20
20
 
21
+ const getCreditCurrencyConfig = async (currencyId: string) => {
22
+ const res = await api.get(`/api/payment-currencies/${currencyId}/recharge-config`);
23
+ return res.data;
24
+ };
25
+
21
26
  export default function PriceActions({
22
27
  data,
23
28
  onChange,
@@ -106,6 +111,30 @@ export default function PriceActions({
106
111
  navigate(`/admin/products/pricing-tables?price_id=${data.id}`);
107
112
  };
108
113
 
114
+ const creditCurrencyId = data.metadata?.credit_config?.currency_id;
115
+
116
+ const { data: creditCurrencyConfig } = useRequest(() => getCreditCurrencyConfig(creditCurrencyId), {
117
+ refreshDeps: [creditCurrencyId],
118
+ ready: !!creditCurrencyId,
119
+ });
120
+
121
+ const onSaveAsTopupPrice = async () => {
122
+ try {
123
+ setState({ loading: true });
124
+ await api.put(`/api/payment-currencies/${creditCurrencyId}/recharge-config`, {
125
+ base_price_id: data.id,
126
+ });
127
+
128
+ Toast.success(t('admin.price.credit.saveSuccess'));
129
+ onChange(state.action);
130
+ } catch (err) {
131
+ console.error(err);
132
+ Toast.error(formatError(err));
133
+ } finally {
134
+ setState({ loading: false, action: '' });
135
+ }
136
+ };
137
+
109
138
  const actions = [
110
139
  {
111
140
  label: t('admin.pricing'),
@@ -140,7 +169,7 @@ export default function PriceActions({
140
169
  },
141
170
  { label: t('admin.paymentLink.add'), handler: onCreatePaymentLink, color: 'primary' },
142
171
  { label: t('admin.pricingTable.add'), handler: onCreatePricingTable, color: 'primary', disabled: !data.recurring },
143
- ];
172
+ ].filter(Boolean);
144
173
 
145
174
  if (!setAsDefault) {
146
175
  actions.splice(4, 0, {
@@ -152,6 +181,17 @@ export default function PriceActions({
152
181
  });
153
182
  }
154
183
 
184
+ if (creditCurrencyId && creditCurrencyConfig?.recharge_config?.base_price_id !== data.id) {
185
+ const insertIndex = !setAsDefault ? 5 : 4;
186
+ actions.splice(insertIndex, 0, {
187
+ label: t('admin.price.credit.saveAsBasePrice'),
188
+ handler: onSaveAsTopupPrice,
189
+ color: 'text.primary',
190
+ disabled: false,
191
+ divider: false,
192
+ });
193
+ }
194
+
155
195
  return (
156
196
  <>
157
197
  <Actions variant={variant} actions={actions} />
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable no-nested-ternary */
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import Toast from '@arcblock/ux/lib/Toast';
4
- import { api, findCurrency, formatError, formatPrice, usePaymentContext } from '@blocklet/payment-react';
4
+ import { api, findCurrency, formatError, formatPrice, usePaymentContext, Collapse } from '@blocklet/payment-react';
5
5
  import { AddOutlined } from '@mui/icons-material';
6
6
  import { Box, Button, Divider } from '@mui/material';
7
7
  import { cloneDeep } from 'lodash';
@@ -9,7 +9,6 @@ import { Fragment, useState, useEffect } from 'react';
9
9
  import { FormProvider, useFieldArray, useForm } from 'react-hook-form';
10
10
  import { dispatch } from 'use-bus';
11
11
 
12
- import Collapse from '../../../../components/collapse';
13
12
  import DrawerForm from '../../../../components/drawer-form';
14
13
  import PriceActions from '../../../../components/price/actions';
15
14
  import PriceForm, { DEFAULT_PRICE } from '../../../../components/price/form';
@@ -216,10 +216,10 @@ function EditForm({ item, onClose, onSuccess, isOwner }: EditFormProps) {
216
216
  </Box>
217
217
  </InputAdornment>
218
218
  ),
219
- inputProps: {
220
- min: 0,
221
- max: MAX_SAFE_AMOUNT,
222
- },
219
+ },
220
+ htmlInput: {
221
+ min: 0,
222
+ max: MAX_SAFE_AMOUNT,
223
223
  },
224
224
  }}
225
225
  />
@@ -269,10 +269,10 @@ function EditForm({ item, onClose, onSuccess, isOwner }: EditFormProps) {
269
269
  </Box>
270
270
  </InputAdornment>
271
271
  ),
272
- inputProps: {
273
- min: 0,
274
- max: MAX_SAFE_AMOUNT,
275
- },
272
+ },
273
+ htmlInput: {
274
+ min: 0,
275
+ max: MAX_SAFE_AMOUNT,
276
276
  },
277
277
  }}
278
278
  />
@@ -10,7 +10,7 @@ import {
10
10
  } from '@blocklet/payment-react';
11
11
  import type { TCreditGrantExpanded } from '@blocklet/payment-types';
12
12
  import { ArrowBackOutlined } from '@mui/icons-material';
13
- import { Alert, Avatar, Box, CircularProgress, Divider, Stack, Typography } from '@mui/material';
13
+ import { Alert, Avatar, Box, Button, CircularProgress, Divider, Stack, Typography } from '@mui/material';
14
14
  import { useRequest } from 'ahooks';
15
15
  import { useNavigate, useParams } from 'react-router-dom';
16
16
  import { styled } from '@mui/system';
@@ -93,6 +93,14 @@ export default function CustomerCreditGrantDetail() {
93
93
  {t('admin.customer.creditGrants.title')}
94
94
  </Typography>
95
95
  </Stack>
96
+ {data.metadata?.invoice_id && (
97
+ <Button
98
+ variant="outlined"
99
+ size="small"
100
+ onClick={() => navigate(`/customer/invoice/${data.metadata!.invoice_id}`)}>
101
+ {t('admin.customer.creditGrants.viewInvoice')}
102
+ </Button>
103
+ )}
96
104
  </Stack>
97
105
  <Box
98
106
  mt={4}
@@ -1,5 +1,5 @@
1
1
  import React, { useState, useEffect, useRef } from 'react';
2
- import { useParams, useNavigate } from 'react-router-dom';
2
+ import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
3
3
  import Toast from '@arcblock/ux/lib/Toast';
4
4
  import {
5
5
  Box,
@@ -77,6 +77,8 @@ const BalanceCard = styled(Card)(({ theme }) => ({
77
77
  export default function BalanceRechargePage() {
78
78
  const { t, locale } = useLocaleContext();
79
79
  const { currencyId } = useParams<{ currencyId: string }>();
80
+ const [searchParams] = useSearchParams();
81
+ const rechargeAddress = searchParams.get('rechargeAddress');
80
82
  const navigate = useNavigate();
81
83
  const { connect } = usePaymentContext();
82
84
  const [amount, setAmount] = useState('50');
@@ -103,6 +105,8 @@ export default function BalanceRechargePage() {
103
105
  const [currency, setCurrency] = useState<ExtendedPaymentCurrency | null>(null);
104
106
  const [relatedSubscriptions, setRelatedSubscriptions] = useState<Subscription[]>([]);
105
107
 
108
+ const paymentAddress = rechargeAddress || payerValue?.paymentAddress || session?.user?.did;
109
+
106
110
  const fetchData = async () => {
107
111
  try {
108
112
  setLoading(true);
@@ -174,9 +178,11 @@ export default function BalanceRechargePage() {
174
178
  ]);
175
179
  }
176
180
 
177
- const supportRecharge = data.currency?.paymentMethod?.type === 'arcblock';
181
+ const supportRecharge = ['arcblock', 'ethereum', 'base'].includes(data.currency?.paymentMethod?.type);
178
182
  if (supportRecharge) {
179
- const payerTokenRes = await api.get(`/api/customers/payer-token?currencyId=${currencyId}`);
183
+ const payerTokenRes = await api.get(
184
+ `/api/customers/payer-token?currencyId=${currencyId}&payerAddress=${paymentAddress}`
185
+ );
180
186
  if (payerTokenRes?.data) {
181
187
  setPayerValue(payerTokenRes.data);
182
188
  }
@@ -231,6 +237,7 @@ export default function BalanceRechargePage() {
231
237
  customerDid: session?.user?.did,
232
238
  currencyId: currency.id,
233
239
  amount: Number(amount),
240
+ rechargeAddress: rechargeAddress || session?.user?.did,
234
241
  },
235
242
  messages: {
236
243
  scan: t('common.connect.defaultScan'),
@@ -291,7 +298,7 @@ export default function BalanceRechargePage() {
291
298
  );
292
299
  }
293
300
 
294
- if (currency?.paymentMethod?.type !== 'arcblock') {
301
+ if (!['arcblock', 'ethereum', 'base'].includes(currency?.paymentMethod?.type || '')) {
295
302
  return (
296
303
  <Box>
297
304
  <Button startIcon={<ArrowBackOutlined />} variant="outlined" onClick={() => goBackOrFallback('/customer')}>
@@ -317,7 +324,7 @@ export default function BalanceRechargePage() {
317
324
 
318
325
  const currentBalance = formatBNStr(payerValue?.token || '0', currency?.decimal || 0, 6, false);
319
326
  const balanceLink = currency?.paymentMethod
320
- ? getTokenBalanceLink(currency.paymentMethod, payerValue?.paymentAddress || '')
327
+ ? getTokenBalanceLink(currency.paymentMethod, paymentAddress || '')
321
328
  : undefined;
322
329
 
323
330
  return (
@@ -362,7 +369,7 @@ export default function BalanceRechargePage() {
362
369
  {t('customer.recharge.receiveAddress')}
363
370
  </Typography>
364
371
  <Typography variant="body1" sx={{ wordBreak: 'break-all', fontFamily: 'monospace' }}>
365
- {payerValue?.paymentAddress || t('customer.balance.addressNotFound')}
372
+ {paymentAddress || t('customer.balance.addressNotFound')}
366
373
  </Typography>
367
374
  </Stack>
368
375
  {currency.logo && (
@@ -669,7 +676,7 @@ export default function BalanceRechargePage() {
669
676
  {t('customer.recharge.history')}
670
677
  </Typography>
671
678
 
672
- <RechargeList currency_id={currencyId} />
679
+ <RechargeList currency_id={currencyId} customer_id={session?.user?.did} recharge_address={paymentAddress} />
673
680
  </Box>
674
681
  )}
675
682
  </Root>
@@ -537,11 +537,11 @@ export default function RechargePage() {
537
537
  slotProps={{
538
538
  input: {
539
539
  endAdornment: <Typography>{subscription.paymentCurrency.symbol}</Typography>,
540
+ },
541
+ htmlInput: {
542
+ min: 0,
543
+ max: MAX_SAFE_AMOUNT,
540
544
  autoComplete: 'off',
541
- inputProps: {
542
- min: 0,
543
- max: MAX_SAFE_AMOUNT,
544
- },
545
545
  },
546
546
  }}
547
547
  />