payment-kit 1.19.1 → 1.19.3

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 (36) hide show
  1. package/api/src/libs/notification/template/customer-credit-grant-low-balance.ts +1 -1
  2. package/api/src/libs/notification/template/customer-credit-insufficient.ts +1 -1
  3. package/api/src/libs/security.ts +6 -3
  4. package/api/src/libs/util.ts +3 -1
  5. package/api/src/queues/credit-consume.ts +15 -2
  6. package/api/src/routes/checkout-sessions.ts +9 -4
  7. package/api/src/routes/credit-grants.ts +24 -2
  8. package/api/src/routes/customers.ts +36 -12
  9. package/api/src/routes/payment-currencies.ts +8 -0
  10. package/api/src/routes/payment-methods.ts +1 -0
  11. package/api/src/routes/webhook-endpoints.ts +0 -3
  12. package/api/src/store/migrations/20250610-billing-credit.ts +0 -3
  13. package/api/src/store/migrations/20250708-currency-precision.ts +14 -0
  14. package/api/src/store/models/webhook-attempt.ts +1 -1
  15. package/blocklet.yml +1 -1
  16. package/package.json +25 -25
  17. package/src/components/conditional-section.tsx +87 -0
  18. package/src/components/customer/credit-overview.tsx +30 -17
  19. package/src/components/customer/form.tsx +2 -1
  20. package/src/components/edit-in-line.tsx +197 -0
  21. package/src/components/metadata/form.tsx +2 -2
  22. package/src/components/meter/add-usage-dialog.tsx +2 -2
  23. package/src/components/meter/form.tsx +2 -2
  24. package/src/components/meter/products.tsx +2 -2
  25. package/src/components/payment-link/item.tsx +2 -2
  26. package/src/components/payouts/portal/list.tsx +6 -11
  27. package/src/components/price/currency-select.tsx +13 -9
  28. package/src/components/price/form.tsx +47 -16
  29. package/src/components/product/form.tsx +3 -8
  30. package/src/components/subscription/portal/list.tsx +0 -1
  31. package/src/locales/en.tsx +6 -3
  32. package/src/locales/zh.tsx +6 -3
  33. package/src/pages/admin/customers/customers/detail.tsx +5 -13
  34. package/src/pages/admin/settings/payment-methods/index.tsx +56 -85
  35. package/src/pages/customer/index.tsx +17 -15
  36. package/src/pages/customer/recharge/account.tsx +1 -1
@@ -0,0 +1,197 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import Toast from '@arcblock/ux/lib/Toast';
3
+ import { formatError } from '@blocklet/payment-react';
4
+ import { Check, Close, EditOutlined } from '@mui/icons-material';
5
+ import { IconButton, Stack, TextField, Typography } from '@mui/material';
6
+ import { useState, useEffect } from 'react';
7
+
8
+ interface EditInLineProps {
9
+ value: string;
10
+ onSave: (newValue: string) => Promise<void>;
11
+ placeholder?: string;
12
+ required?: boolean;
13
+ disabled?: boolean;
14
+ multiline?: boolean;
15
+ maxLength?: number;
16
+ validate?: (value: string) => string | null;
17
+ successMessage?: string;
18
+ autoFocus?: boolean;
19
+ onEdit?: () => void;
20
+ onCancel?: () => void;
21
+ hideSuccessToast?: boolean;
22
+ }
23
+
24
+ export default function EditInLine({
25
+ value,
26
+ onSave,
27
+ placeholder = '',
28
+ required = true,
29
+ disabled = false,
30
+ multiline = false,
31
+ maxLength = 255,
32
+ validate = () => null,
33
+ successMessage = '',
34
+ autoFocus = false,
35
+ onEdit = () => {},
36
+ onCancel = () => {},
37
+ hideSuccessToast = false,
38
+ }: EditInLineProps) {
39
+ const [edit, setEdit] = useState(false);
40
+ const [tempValue, setTempValue] = useState(value);
41
+ const [error, setError] = useState('');
42
+ const [loading, setLoading] = useState(false);
43
+ const { t } = useLocaleContext();
44
+
45
+ // 当外部 value 变化时同步更新
46
+ useEffect(() => {
47
+ if (!edit) {
48
+ setTempValue(value);
49
+ }
50
+ }, [value, edit]);
51
+
52
+ const validateValue = (val: string): string => {
53
+ if (required && !val.trim()) {
54
+ return t('common.required');
55
+ }
56
+ if (maxLength && val.length > maxLength) {
57
+ return t('common.maxLength', { len: maxLength });
58
+ }
59
+ if (validate) {
60
+ const customError = validate(val);
61
+ if (customError) {
62
+ return customError;
63
+ }
64
+ }
65
+ return '';
66
+ };
67
+
68
+ const handleSave = async () => {
69
+ const validationError = validateValue(tempValue);
70
+ if (validationError) {
71
+ setError(validationError);
72
+ return;
73
+ }
74
+
75
+ setLoading(true);
76
+ try {
77
+ await onSave(tempValue);
78
+ if (!hideSuccessToast) {
79
+ Toast.success(successMessage || t('common.saved'));
80
+ }
81
+ setEdit(false);
82
+ setError('');
83
+ } catch (err) {
84
+ Toast.error(formatError(err));
85
+ } finally {
86
+ setLoading(false);
87
+ }
88
+ };
89
+
90
+ const handleCancel = () => {
91
+ setTempValue(value);
92
+ setEdit(false);
93
+ setError('');
94
+ onCancel?.();
95
+ };
96
+
97
+ const handleChange = (newValue: string) => {
98
+ setTempValue(newValue);
99
+ const validationError = validateValue(newValue);
100
+ setError(validationError);
101
+ };
102
+
103
+ const handleEdit = () => {
104
+ setEdit(true);
105
+ setTempValue(value);
106
+ setError('');
107
+ onEdit?.();
108
+ };
109
+
110
+ if (disabled) {
111
+ return (
112
+ <Typography variant="body2" sx={{ color: 'text.secondary' }}>
113
+ {value || <span style={{ fontStyle: 'italic' }}>{placeholder}</span>}
114
+ </Typography>
115
+ );
116
+ }
117
+
118
+ return (
119
+ <Stack
120
+ direction="row"
121
+ spacing={1}
122
+ sx={{
123
+ alignItems: multiline ? 'flex-start' : 'center',
124
+ flexWrap: 'wrap',
125
+ width: '100%',
126
+ }}>
127
+ {edit ? (
128
+ <>
129
+ <TextField
130
+ value={tempValue}
131
+ onChange={(e) => handleChange(e.target.value)}
132
+ variant="outlined"
133
+ size="small"
134
+ multiline={multiline}
135
+ rows={multiline ? 3 : 1}
136
+ sx={{ flex: 1, minWidth: '200px' }}
137
+ placeholder={placeholder}
138
+ error={!!error}
139
+ disabled={loading}
140
+ autoFocus={autoFocus}
141
+ slotProps={{
142
+ input: {
143
+ endAdornment: error ? (
144
+ <Typography color="error" sx={{ whiteSpace: 'nowrap' }}>
145
+ {error}
146
+ </Typography>
147
+ ) : undefined,
148
+ },
149
+ htmlInput: {
150
+ maxLength,
151
+ },
152
+ }}
153
+ onKeyDown={(e) => {
154
+ if (e.key === 'Enter' && !multiline && !e.shiftKey) {
155
+ e.preventDefault();
156
+ handleSave();
157
+ }
158
+ if (e.key === 'Escape') {
159
+ e.preventDefault();
160
+ handleCancel();
161
+ }
162
+ }}
163
+ />
164
+ <Stack direction="row" spacing={0.5}>
165
+ <IconButton
166
+ onClick={handleSave}
167
+ size="small"
168
+ disabled={!!error || loading}
169
+ color="primary"
170
+ title={t('common.save')}>
171
+ <Check />
172
+ </IconButton>
173
+ <IconButton onClick={handleCancel} size="small" disabled={loading} title={t('common.cancel')}>
174
+ <Close />
175
+ </IconButton>
176
+ </Stack>
177
+ </>
178
+ ) : (
179
+ <>
180
+ <Typography
181
+ variant="body2"
182
+ sx={{
183
+ wordBreak: 'break-word',
184
+ whiteSpace: multiline ? 'pre-wrap' : 'nowrap',
185
+ overflow: multiline ? 'visible' : 'hidden',
186
+ textOverflow: multiline ? 'clip' : 'ellipsis',
187
+ }}>
188
+ {value || <span style={{ color: '#999', fontStyle: 'italic' }}>{placeholder}</span>}
189
+ </Typography>
190
+ <IconButton onClick={handleEdit} size="small" title={t('common.edit')}>
191
+ <EditOutlined fontSize="small" />
192
+ </IconButton>
193
+ </>
194
+ )}
195
+ </Stack>
196
+ );
197
+ }
@@ -1,7 +1,7 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
- import { FormInput } from '@blocklet/payment-react';
2
+ import { FormInput, FormLabel } from '@blocklet/payment-react';
3
3
  import { AddOutlined, Autorenew, DeleteOutlineOutlined, FormatAlignLeft } from '@mui/icons-material';
4
- import { Box, Button, Divider, IconButton, Stack, TextField, InputAdornment, Tooltip, FormLabel } from '@mui/material';
4
+ import { Box, Button, Divider, IconButton, Stack, TextField, InputAdornment, Tooltip } from '@mui/material';
5
5
  import { useEffect, useRef, useState, useCallback } from 'react';
6
6
  import { useFieldArray, useFormContext } from 'react-hook-form';
7
7
  import { isObject, debounce } from 'lodash';
@@ -1,7 +1,7 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
- import { api, formatError, useMobile, getCustomerAvatar } from '@blocklet/payment-react';
2
+ import { api, formatError, useMobile, getCustomerAvatar, FormLabel } from '@blocklet/payment-react';
3
3
  import type { TCustomer, TPaymentCurrency } from '@blocklet/payment-types';
4
- import { Box, Stack, Autocomplete, TextField, Button, Avatar, FormLabel, Typography } from '@mui/material';
4
+ import { Box, Stack, Autocomplete, TextField, Button, Avatar, Typography } from '@mui/material';
5
5
  import { useSetState } from 'ahooks';
6
6
  import Toast from '@arcblock/ux/lib/Toast';
7
7
  import Dialog from '@arcblock/ux/lib/Dialog';
@@ -1,6 +1,6 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
- import { FormInput } from '@blocklet/payment-react';
3
- import { FormControl, Select, MenuItem, Typography, Box, FormHelperText, Stack, FormLabel } from '@mui/material';
2
+ import { FormInput, FormLabel } from '@blocklet/payment-react';
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
 
@@ -38,7 +38,7 @@ export default function MeterProducts({ meterId, meter = undefined }: MeterProdu
38
38
  const [loading, setLoading] = useState(false);
39
39
  const [creating, setCreating] = useState(false);
40
40
  const [error, setError] = useState<string | null>(null);
41
- const [activeTab, setActiveTab] = useState<ProductType>('meter');
41
+ const [activeTab, setActiveTab] = useState<ProductType>('credit');
42
42
 
43
43
  const loadProducts = async (type: ProductType = activeTab) => {
44
44
  setLoading(true);
@@ -183,8 +183,8 @@ export default function MeterProducts({ meterId, meter = undefined }: MeterProdu
183
183
  color: 'primary.main',
184
184
  },
185
185
  }}>
186
- <Tab label={t('admin.meter.products.meterService')} value="meter" />
187
186
  <Tab label={t('admin.meter.products.creditCharge')} value="credit" />
187
+ <Tab label={t('admin.meter.products.meterService')} value="meter" />
188
188
  </Tabs>
189
189
  </Stack>
190
190
  <Stack
@@ -1,8 +1,8 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
2
  import Toast from '@arcblock/ux/lib/Toast';
3
- import { api, findCurrency, formatError, formatPrice, usePaymentContext } from '@blocklet/payment-react';
3
+ import { api, findCurrency, formatError, formatPrice, usePaymentContext, FormLabel } from '@blocklet/payment-react';
4
4
  import type { TPrice, TProduct, TProductExpanded } from '@blocklet/payment-types';
5
- import { Box, Checkbox, FormControlLabel, FormLabel, Stack, TextField, Typography } from '@mui/material';
5
+ import { Box, Checkbox, FormControlLabel, Stack, TextField, Typography } from '@mui/material';
6
6
  import { useSetState } from 'ahooks';
7
7
  import { Controller, useFormContext, useWatch } from 'react-hook-form';
8
8
 
@@ -10,6 +10,7 @@ import { Link } from 'react-router-dom';
10
10
  import { styled } from '@mui/system';
11
11
  import { debounce } from '../../../libs/util';
12
12
  import CustomerLink from '../../customer/link';
13
+ import { useConditionalSection } from '../../conditional-section';
13
14
 
14
15
  const fetchData = (
15
16
  params: Record<string, any> = {}
@@ -47,7 +48,6 @@ type ListProps = {
47
48
  status?: string;
48
49
  customer_id?: string;
49
50
  currency_id?: string;
50
- setHasRevenues?: (hasRevenues: boolean) => void;
51
51
  };
52
52
 
53
53
  const getListKey = (props: ListProps) => {
@@ -57,14 +57,10 @@ const getListKey = (props: ListProps) => {
57
57
  return 'payouts-mine';
58
58
  };
59
59
 
60
- export default function CustomerRevenueList({
61
- currency_id = '',
62
- status = '',
63
- customer_id = '',
64
- setHasRevenues = () => {},
65
- }: ListProps) {
60
+ export default function CustomerRevenueList({ currency_id = '', status = '', customer_id = '' }: ListProps) {
66
61
  const { t } = useLocaleContext();
67
62
  const { isMobile } = useMobile('sm');
63
+ const conditionalSection = useConditionalSection();
68
64
 
69
65
  const listKey = getListKey({ customer_id });
70
66
  const defaultPageSize = useDefaultPageSize(10);
@@ -87,12 +83,11 @@ export default function CustomerRevenueList({
87
83
  debounce(() => {
88
84
  fetchData(search).then((res: any) => {
89
85
  setData(res);
90
- if (setHasRevenues) {
91
- setHasRevenues(res.count > 0);
92
- }
86
+ const hasData = res.count > 0;
87
+ conditionalSection?.hideRender(!hasData);
93
88
  });
94
89
  }, 300)();
95
- }, [search]);
90
+ }, [search, conditionalSection]);
96
91
 
97
92
  const columns = [
98
93
  {
@@ -18,6 +18,7 @@ type Props = {
18
18
  disabled?: boolean;
19
19
  selectSX?: SxProps;
20
20
  currencyFilter?: (currency: any) => boolean;
21
+ hideMethod?: boolean;
21
22
  };
22
23
 
23
24
  export default function CurrencySelect({
@@ -29,6 +30,7 @@ export default function CurrencySelect({
29
30
  disabled = false,
30
31
  selectSX = {},
31
32
  currencyFilter = () => true,
33
+ hideMethod = false,
32
34
  }: Props) {
33
35
  const { t } = useLocaleContext();
34
36
  const { settings } = usePaymentContext();
@@ -80,7 +82,7 @@ export default function CurrencySelect({
80
82
  justifyContent: 'flex-end',
81
83
  textAlign: 'right',
82
84
  }}>
83
- {selectedCurrency?.symbol} ({selectedPaymentMethod?.name})
85
+ {selectedCurrency?.symbol} {hideMethod ? '' : `(${selectedPaymentMethod?.name})`}
84
86
  {canSelect && <ArrowDropDown sx={{ color: 'text.secondary', fontSize: 21 }} />}
85
87
  </Typography>
86
88
  );
@@ -97,7 +99,7 @@ export default function CurrencySelect({
97
99
  value={value}
98
100
  renderValue={() => (
99
101
  <Typography variant="body1" sx={{ display: 'inline-flex', fontSize: '12px', color: 'text.secondary' }}>
100
- {selectedCurrency?.symbol} ({selectedPaymentMethod?.name})
102
+ {selectedCurrency?.symbol} {hideMethod ? '' : `(${selectedPaymentMethod?.name})`}
101
103
  </Typography>
102
104
  )}
103
105
  onChange={handleSelect}
@@ -113,18 +115,20 @@ export default function CurrencySelect({
113
115
  }
114
116
 
115
117
  return [
116
- <ListSubheader
117
- key={method.id}
118
- sx={{ fontSize: '0.875rem', color: 'text.secondary', lineHeight: '2.1875rem' }}>
119
- {method.name}
120
- </ListSubheader>,
118
+ hideMethod ? null : (
119
+ <ListSubheader
120
+ key={method.id}
121
+ sx={{ fontSize: '0.875rem', color: 'text.secondary', lineHeight: '2.1875rem' }}>
122
+ {method.name}
123
+ </ListSubheader>
124
+ ),
121
125
  ...filteredCurrencies.map((currency) => (
122
126
  <MenuItem key={currency.id} sx={{ pl: 3 }} value={currency.id}>
123
127
  <Stack direction="row" sx={{ width: '100%', justifyContent: 'space-between', gap: 2 }}>
124
- <Currency logo={currency.logo} name={currency.name} />
128
+ {hideMethod ? null : <Currency logo={currency.logo} name={currency.name} />}
125
129
  <Typography
126
130
  sx={{
127
- fontWeight: 'bold',
131
+ fontWeight: hideMethod ? 'normal' : 'bold',
128
132
  }}>
129
133
  {currency.symbol}
130
134
  </Typography>
@@ -50,7 +50,6 @@ import ProductSelect from '../payment-link/product-select';
50
50
  import Collapse from '../collapse';
51
51
  import { useProductsContext } from '../../contexts/products';
52
52
  import CurrencySelect from './currency-select';
53
- import MetadataForm from '../metadata/form';
54
53
  import { getProductByPriceId } from '../../libs/util';
55
54
  import InfoCard from '../info-card';
56
55
 
@@ -225,8 +224,11 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
225
224
  if (value !== null) {
226
225
  setValue(field.name, value);
227
226
  }
228
- if (value === 'one_time' && isCreditBilling) {
227
+ if (value === 'one_time') {
229
228
  setValue(getFieldName('model'), 'standard');
229
+ setValue(getFieldName('currency_id'), settings.baseCurrency?.id);
230
+ setValue(getFieldName('recurring.meter_id'), '');
231
+ setValue(getFieldName('recurring.usage_type'), 'licensed');
230
232
  }
231
233
  }}
232
234
  exclusive
@@ -299,7 +301,7 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
299
301
  fullWidth
300
302
  size="small"
301
303
  onChange={(e) => {
302
- if (e.target.value === 'standard' && isCreditBilling) {
304
+ if (e.target.value === 'standard') {
303
305
  setValue(getFieldName('currency_id'), settings.baseCurrency?.id);
304
306
  }
305
307
  field.onChange(e.target.value);
@@ -313,13 +315,15 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
313
315
  }}>
314
316
  <MenuItem value="standard">{t('admin.price.models.standard')}</MenuItem>
315
317
  <MenuItem value="package">{t('admin.price.models.package')}</MenuItem>
318
+ <MenuItem value="credit_metered" disabled={isCreditMode}>
319
+ {t('admin.price.models.creditMetered')}
320
+ </MenuItem>
316
321
  <MenuItem value="graduated" disabled>
317
322
  {t('admin.price.models.graduated')}
318
323
  </MenuItem>
319
324
  <MenuItem value="volume" disabled>
320
325
  {t('admin.price.models.volume')}
321
326
  </MenuItem>
322
- <MenuItem value="credit_metered">{t('admin.price.models.creditMetered')}</MenuItem>
323
327
  </Select>
324
328
  </Box>
325
329
  )}
@@ -771,7 +775,7 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
771
775
  }
772
776
  expanded={isLocked}
773
777
  style={{ width: INPUT_WIDTH }}>
774
- <Box sx={{ width: INPUT_WIDTH, mb: 2 }}>
778
+ <Box sx={{ width: INPUT_WIDTH, mb: 2, pl: 2, pr: 1 }}>
775
779
  {/* Credit 数量配置 */}
776
780
  <Controller
777
781
  name={getFieldName('metadata')}
@@ -815,6 +819,7 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
815
819
  }}
816
820
  value={field.value?.credit_config?.currency_id || creditCurrencies?.[0]?.id}
817
821
  disabled={isLocked}
822
+ hideMethod
818
823
  selectSX={{ '.MuiOutlinedInput-notchedOutline': { border: 'none' } }}
819
824
  />
820
825
  </InputAdornment>
@@ -1045,16 +1050,17 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
1045
1050
  },
1046
1051
  }}>
1047
1052
  <Stack
1048
- spacing={2}
1049
1053
  sx={{
1050
1054
  alignItems: 'flex-start',
1051
1055
  width: INPUT_WIDTH,
1056
+ pl: 2,
1057
+ pr: 1,
1052
1058
  }}>
1053
1059
  <Controller
1054
1060
  name={getFieldName('quantity_available')}
1055
1061
  control={control}
1056
1062
  render={({ field }) => (
1057
- <>
1063
+ <Box sx={{ width: '100%', mb: 2 }}>
1058
1064
  <FormLabel>{t('admin.price.quantityAvailable.label')}</FormLabel>
1059
1065
  <TextField
1060
1066
  {...field}
@@ -1065,14 +1071,23 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
1065
1071
  error={!quantityPositive(field.value)}
1066
1072
  helperText={!quantityPositive(field.value) && t('admin.price.quantity.tip')}
1067
1073
  />
1068
- </>
1074
+ <Typography
1075
+ variant="caption"
1076
+ sx={{
1077
+ color: 'text.secondary',
1078
+ display: 'block',
1079
+ mt: 0.5,
1080
+ }}>
1081
+ {t('admin.price.quantityAvailable.description')}
1082
+ </Typography>
1083
+ </Box>
1069
1084
  )}
1070
1085
  />
1071
1086
  <Controller
1072
1087
  name={getFieldName('quantity_limit_per_checkout')}
1073
1088
  control={control}
1074
1089
  render={({ field }) => (
1075
- <>
1090
+ <Box sx={{ width: '100%', mb: 2 }}>
1076
1091
  <FormLabel>{t('admin.price.quantityLimitPerCheckout.label')}</FormLabel>
1077
1092
  <TextField
1078
1093
  {...field}
@@ -1084,7 +1099,16 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
1084
1099
  error={!quantityPositive(field.value)}
1085
1100
  helperText={!quantityPositive(field.value) && t('admin.price.quantity.tip')}
1086
1101
  />
1087
- </>
1102
+ <Typography
1103
+ variant="caption"
1104
+ sx={{
1105
+ color: 'text.secondary',
1106
+ display: 'block',
1107
+ mt: 0.5,
1108
+ }}>
1109
+ {t('admin.price.quantityLimitPerCheckout.description')}
1110
+ </Typography>
1111
+ </Box>
1088
1112
  )}
1089
1113
  />
1090
1114
  <Controller
@@ -1097,10 +1121,10 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
1097
1121
  },
1098
1122
  }}
1099
1123
  render={({ field }) => (
1100
- <>
1124
+ <Box sx={{ width: '100%', mb: 2 }}>
1101
1125
  <FormLabel>{t('admin.price.nickname.label')}</FormLabel>
1102
1126
  <TextField {...field} size="small" sx={{ width: INPUT_WIDTH }} inputProps={{ maxLength: 64 }} />
1103
- </>
1127
+ </Box>
1104
1128
  )}
1105
1129
  />
1106
1130
  <Controller
@@ -1113,14 +1137,21 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
1113
1137
  },
1114
1138
  }}
1115
1139
  render={({ field }) => (
1116
- <>
1140
+ <Box sx={{ width: '100%', mb: 2 }}>
1117
1141
  <FormLabel>{t('admin.price.lookup_key.label')}</FormLabel>
1118
1142
  <TextField {...field} size="small" sx={{ width: INPUT_WIDTH }} inputProps={{ maxLength: 64 }} />
1119
- </>
1143
+ <Typography
1144
+ variant="caption"
1145
+ sx={{
1146
+ color: 'text.secondary',
1147
+ display: 'block',
1148
+ mt: 0.5,
1149
+ }}>
1150
+ {t('admin.price.lookup_key.description')}
1151
+ </Typography>
1152
+ </Box>
1120
1153
  )}
1121
1154
  />
1122
- {/* 元数据 */}
1123
- <MetadataForm title={t('common.metadata.label')} color="inherit" name={getFieldName('metadata')} />
1124
1155
  </Stack>
1125
1156
  </Collapse>
1126
1157
  </>
@@ -1,8 +1,8 @@
1
1
  /* eslint-disable no-nested-ternary */
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
- import { FormInput } from '@blocklet/payment-react';
3
+ import { FormInput, FormLabel } from '@blocklet/payment-react';
4
4
  import type { InferFormType, TProduct } from '@blocklet/payment-types';
5
- import { Box, Stack, Typography, FormLabel, Select, MenuItem } from '@mui/material';
5
+ import { Box, Stack, Typography, Select, MenuItem } from '@mui/material';
6
6
  import { useFormContext, useWatch, Controller } from 'react-hook-form';
7
7
 
8
8
  import Collapse from '../collapse';
@@ -46,7 +46,7 @@ export default function ProductForm({ simple = false }: Props) {
46
46
  rules={{ required: true }}
47
47
  render={({ field }) => (
48
48
  <Box sx={{ width: '100%' }}>
49
- <FormLabel sx={{ color: 'text.primary' }}>{t('admin.product.type.label')}</FormLabel>
49
+ <FormLabel sx={{ color: 'text.primary', fontSize: '0.875rem' }}>{t('admin.product.type.label')}</FormLabel>
50
50
  <Select {...field} fullWidth size="small">
51
51
  <MenuItem value="good">{t('admin.product.type.good')}</MenuItem>
52
52
  <MenuItem value="service">{t('admin.product.type.service')}</MenuItem>
@@ -135,11 +135,6 @@ export default function ProductForm({ simple = false }: Props) {
135
135
  <FormInput
136
136
  name="unit_label"
137
137
  label={t('admin.product.unit_label.label')}
138
- placeholder={
139
- productType === 'credit'
140
- ? t('admin.creditProduct.unitLabel.placeholder')
141
- : t('admin.product.unit_label.placeholder')
142
- }
143
138
  rules={{
144
139
  maxLength: { value: 12, message: t('common.maxLength', { len: 12 }) },
145
140
  }}
@@ -138,7 +138,6 @@ export default function CurrentSubscriptions({
138
138
  sx={{
139
139
  flexWrap: 'wrap',
140
140
  padding: 2,
141
- height: '100%',
142
141
  borderRadius: 1,
143
142
  border: '1px solid',
144
143
  borderColor: 'divider',
@@ -384,7 +384,7 @@ export default flat({
384
384
  },
385
385
  unit_label: {
386
386
  label: 'Unit label',
387
- placeholder: 'Seat',
387
+ placeholder: 'Unit',
388
388
  },
389
389
  billingType: {
390
390
  label: 'Billing type',
@@ -452,6 +452,7 @@ export default flat({
452
452
  lookup_key: {
453
453
  label: 'Lookup key',
454
454
  placeholder: '',
455
+ description: 'Lookup key is used to identify the price in the API',
455
456
  },
456
457
  recurring: {
457
458
  interval: 'Billing period',
@@ -526,6 +527,7 @@ export default flat({
526
527
  format: 'Available {num} pieces',
527
528
  noLimit: 'No limit on available quantity',
528
529
  valid: 'Available quantity must be greater than or equal to sold quantity',
530
+ description: 'Enter the number of units that can be sold, 0 means unlimited',
529
531
  },
530
532
  quantitySold: {
531
533
  label: 'Sold quantity',
@@ -536,6 +538,7 @@ export default flat({
536
538
  placeholder: '0 means unlimited',
537
539
  format: 'Limit {num} pieces per checkout',
538
540
  noLimit: 'No limit on quantity per checkout',
541
+ description: 'Enter the number of units that can be purchased in a single checkout, 0 means unlimited',
539
542
  },
540
543
  inventory: 'Inventory Settings',
541
544
  },
@@ -987,7 +990,7 @@ export default flat({
987
990
  viewAllActivity: 'View All Activity',
988
991
  pendingAmount: 'Outstanding Charges',
989
992
  grantCount: 'Grant Count',
990
- noGrantsDescription: "You don't have any credit grants yet. Please contact the administrator to add them.",
993
+ noGrantsDescription: "You don't have any credit grants yet.",
991
994
  addCredit: 'Add Credit',
992
995
  },
993
996
  },
@@ -1162,7 +1165,7 @@ export default flat({
1162
1165
  totalAmount: 'Total Amount',
1163
1166
  pendingAmount: 'Pending Amount',
1164
1167
  grantCount: 'Grant Count',
1165
- noGrantsDescription: "You don't have any credit grants yet. Please contact the administrator to add them.",
1168
+ noGrantsDescription: "You don't have any credit grants yet.",
1166
1169
  status: {
1167
1170
  granted: 'Active',
1168
1171
  pending: 'Pending',