payment-kit 1.19.17 → 1.19.19

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 (62) 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/notification/template/customer-credit-low-balance.ts +155 -0
  8. package/api/src/libs/session.ts +6 -1
  9. package/api/src/libs/ws.ts +3 -2
  10. package/api/src/locales/en.ts +6 -6
  11. package/api/src/locales/zh.ts +4 -4
  12. package/api/src/queues/auto-recharge.ts +343 -0
  13. package/api/src/queues/credit-consume.ts +51 -1
  14. package/api/src/queues/credit-grant.ts +15 -0
  15. package/api/src/queues/notification.ts +16 -13
  16. package/api/src/queues/payment.ts +14 -1
  17. package/api/src/queues/space.ts +1 -0
  18. package/api/src/routes/auto-recharge-configs.ts +454 -0
  19. package/api/src/routes/connect/auto-recharge-auth.ts +182 -0
  20. package/api/src/routes/connect/recharge-account.ts +72 -10
  21. package/api/src/routes/connect/setup.ts +5 -3
  22. package/api/src/routes/connect/shared.ts +45 -4
  23. package/api/src/routes/customers.ts +10 -6
  24. package/api/src/routes/index.ts +2 -0
  25. package/api/src/routes/invoices.ts +10 -1
  26. package/api/src/routes/meter-events.ts +1 -1
  27. package/api/src/routes/meters.ts +1 -1
  28. package/api/src/routes/payment-currencies.ts +129 -0
  29. package/api/src/store/migrate.ts +20 -0
  30. package/api/src/store/migrations/20250821-auto-recharge-config.ts +38 -0
  31. package/api/src/store/models/auto-recharge-config.ts +225 -0
  32. package/api/src/store/models/credit-grant.ts +2 -11
  33. package/api/src/store/models/customer.ts +1 -0
  34. package/api/src/store/models/index.ts +3 -0
  35. package/api/src/store/models/invoice.ts +2 -1
  36. package/api/src/store/models/payment-currency.ts +10 -2
  37. package/api/src/store/models/types.ts +12 -1
  38. package/blocklet.yml +3 -3
  39. package/package.json +18 -18
  40. package/src/components/currency.tsx +3 -1
  41. package/src/components/customer/credit-overview.tsx +103 -18
  42. package/src/components/customer/overdraft-protection.tsx +5 -5
  43. package/src/components/info-metric.tsx +11 -2
  44. package/src/components/invoice/recharge.tsx +8 -2
  45. package/src/components/metadata/form.tsx +29 -27
  46. package/src/components/meter/form.tsx +1 -2
  47. package/src/components/price/form.tsx +39 -26
  48. package/src/components/product/form.tsx +1 -2
  49. package/src/components/subscription/items/index.tsx +8 -2
  50. package/src/components/subscription/metrics.tsx +5 -1
  51. package/src/locales/en.tsx +15 -0
  52. package/src/locales/zh.tsx +14 -0
  53. package/src/pages/admin/billing/meters/detail.tsx +18 -0
  54. package/src/pages/admin/customers/customers/credit-grant/detail.tsx +10 -0
  55. package/src/pages/admin/products/prices/actions.tsx +42 -2
  56. package/src/pages/admin/products/products/create.tsx +1 -2
  57. package/src/pages/admin/settings/vault-config/edit-form.tsx +8 -8
  58. package/src/pages/customer/credit-grant/detail.tsx +9 -1
  59. package/src/pages/customer/recharge/account.tsx +14 -7
  60. package/src/pages/customer/recharge/subscription.tsx +4 -4
  61. package/src/pages/customer/subscription/detail.tsx +6 -1
  62. package/api/src/libs/notification/template/customer-credit-grant-low-balance.ts +0 -151
@@ -598,11 +598,11 @@ export default function OverdraftProtectionDialog({
598
598
  <Currency logo={currency.logo} name={currency.symbol} />
599
599
  </Box>
600
600
  ),
601
- inputProps: {
602
- min: 0,
603
- max: MAX_SAFE_AMOUNT,
604
- step: Number(estimateAmount),
605
- },
601
+ },
602
+ htmlInput: {
603
+ min: 0,
604
+ max: MAX_SAFE_AMOUNT,
605
+ step: Number(estimateAmount),
606
606
  },
607
607
  }}
608
608
  />
@@ -31,7 +31,16 @@ export default function InfoMetric(rawProps: Props) {
31
31
  return (
32
32
  <>
33
33
  <Stack sx={stackSx}>
34
- <Typography className="info-metric-label" component="div" variant="subtitle2" mb={1} color="text.primary">
34
+ <Typography
35
+ className="info-metric-label"
36
+ component="div"
37
+ variant="subtitle2"
38
+ mb={1}
39
+ color="text.primary"
40
+ sx={{
41
+ display: 'flex',
42
+ alignItems: 'center',
43
+ }}>
35
44
  {/* eslint-disable-next-line react/prop-types */}
36
45
  {props.label}
37
46
  {/* eslint-disable-next-line react/prop-types */}
@@ -39,7 +48,7 @@ export default function InfoMetric(rawProps: Props) {
39
48
  {!!props.tip && (
40
49
  // eslint-disable-next-line react/prop-types
41
50
  <Tooltip title={props.tip}>
42
- <InfoOutlined fontSize="small" />
51
+ <InfoOutlined sx={{ color: 'text.secondary', cursor: 'pointer', fontSize: 'medium', ml: 0.5 }} />
43
52
  </Tooltip>
44
53
  )}
45
54
  </Typography>
@@ -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';
@@ -42,7 +42,10 @@ export default function SubscriptionItemList({ data, currency, mode = 'customer'
42
42
  sm: 2,
43
43
  },
44
44
  }}>
45
- <Box order={isMobile ? 2 : 1}>
45
+ <Box
46
+ sx={{
47
+ order: isMobile ? 2 : 1,
48
+ }}>
46
49
  {item.price.product.images.length > 0 ? (
47
50
  // @ts-ignore
48
51
  <Avatar
@@ -58,7 +61,10 @@ export default function SubscriptionItemList({ data, currency, mode = 'customer'
58
61
  </Avatar>
59
62
  )}
60
63
  </Box>
61
- <Box order={isMobile ? 1 : 2}>
64
+ <Box
65
+ sx={{
66
+ order: isMobile ? 1 : 2,
67
+ }}>
62
68
  {isAdmin ? (
63
69
  <>
64
70
  <Typography
@@ -169,7 +169,11 @@ export default function SubscriptionMetrics({ subscription, showBalance = true }
169
169
  alignItems: 'center',
170
170
  fontWeight: 500,
171
171
  }}>
172
- <Typography fontWeight={500} sx={{ fontSize: 14 }}>
172
+ <Typography
173
+ sx={{
174
+ fontWeight: 500,
175
+ fontSize: 14,
176
+ }}>
173
177
  {t('admin.subscription.currentBalance')}
174
178
  </Typography>
175
179
  <Tooltip
@@ -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}