payment-kit 1.13.17 → 1.13.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 (109) hide show
  1. package/README.md +14 -0
  2. package/api/src/index.ts +17 -6
  3. package/api/src/integrations/stripe/handlers/index.ts +53 -0
  4. package/api/src/integrations/stripe/handlers/invoice.ts +252 -0
  5. package/api/src/integrations/stripe/handlers/payment-intent.ts +172 -0
  6. package/api/src/integrations/stripe/handlers/setup-intent.ts +42 -0
  7. package/api/src/integrations/stripe/handlers/subscription.ts +61 -0
  8. package/api/src/integrations/stripe/resource.ts +317 -0
  9. package/api/src/integrations/stripe/setup.ts +50 -0
  10. package/api/src/jobs/invoice.ts +11 -0
  11. package/api/src/jobs/payment.ts +15 -7
  12. package/api/src/jobs/subscription.ts +18 -2
  13. package/api/src/libs/session.ts +104 -8
  14. package/api/src/libs/util.ts +47 -1
  15. package/api/src/routes/checkout-sessions.ts +134 -27
  16. package/api/src/routes/connect/collect.ts +12 -4
  17. package/api/src/routes/connect/pay.ts +30 -20
  18. package/api/src/routes/connect/setup.ts +12 -4
  19. package/api/src/routes/connect/shared.ts +28 -4
  20. package/api/src/routes/connect/subscribe.ts +12 -5
  21. package/api/src/routes/customers.ts +5 -5
  22. package/api/src/routes/events.ts +9 -6
  23. package/api/src/routes/index.ts +2 -0
  24. package/api/src/routes/integrations/stripe.ts +64 -0
  25. package/api/src/routes/invoices.ts +19 -9
  26. package/api/src/routes/payment-intents.ts +19 -9
  27. package/api/src/routes/payment-links.ts +57 -15
  28. package/api/src/routes/payment-methods.ts +98 -1
  29. package/api/src/routes/prices.ts +71 -14
  30. package/api/src/routes/products.ts +79 -22
  31. package/api/src/routes/settings.ts +10 -11
  32. package/api/src/routes/subscription-items.ts +5 -5
  33. package/api/src/routes/subscriptions.ts +61 -10
  34. package/api/src/routes/usage-records.ts +52 -18
  35. package/api/src/routes/webhook-attempts.ts +5 -5
  36. package/api/src/routes/webhook-endpoints.ts +5 -5
  37. package/api/src/store/migrations/20230905-genesis.ts +2 -2
  38. package/api/src/store/migrations/20230911-seeding.ts +4 -3
  39. package/api/src/store/models/checkout-session.ts +15 -7
  40. package/api/src/store/models/index.ts +31 -7
  41. package/api/src/store/models/invoice.ts +1 -1
  42. package/api/src/store/models/payment-intent.ts +2 -5
  43. package/api/src/store/models/payment-link.ts +1 -1
  44. package/api/src/store/models/payment-method.ts +54 -33
  45. package/api/src/store/models/price.ts +52 -17
  46. package/api/src/store/models/product.ts +0 -3
  47. package/api/src/store/models/subscription.ts +3 -5
  48. package/api/src/store/models/types.ts +56 -2
  49. package/api/third.d.ts +2 -0
  50. package/blocklet.yml +1 -1
  51. package/package.json +36 -29
  52. package/public/currencies/dai.png +0 -0
  53. package/public/currencies/dollar.png +0 -0
  54. package/public/currencies/usdc.png +0 -0
  55. package/public/currencies/usdt.png +0 -0
  56. package/public/methods/arcblock.png +0 -0
  57. package/public/methods/binance.png +0 -0
  58. package/public/methods/coinbase.png +0 -0
  59. package/public/methods/ethereum.jpg +0 -0
  60. package/public/methods/stripe.png +0 -0
  61. package/src/components/checkout/form/address.tsx +86 -10
  62. package/src/components/checkout/form/index.tsx +169 -83
  63. package/src/components/checkout/form/phone.tsx +96 -0
  64. package/src/components/checkout/form/stripe.tsx +195 -0
  65. package/src/components/checkout/pay.tsx +115 -34
  66. package/src/components/checkout/product-item.tsx +4 -3
  67. package/src/components/checkout/summary.tsx +5 -4
  68. package/src/components/drawer-form.tsx +4 -4
  69. package/src/components/input.tsx +22 -4
  70. package/src/components/invoice/table.tsx +8 -3
  71. package/src/components/payment-link/before-pay.tsx +11 -6
  72. package/src/components/payment-link/chrome.tsx +13 -0
  73. package/src/components/payment-link/preview.tsx +31 -0
  74. package/src/components/payment-link/product-select.tsx +8 -3
  75. package/src/components/payment-method/arcblock.tsx +53 -0
  76. package/src/components/payment-method/bitcoin.tsx +53 -0
  77. package/src/components/payment-method/ethereum.tsx +53 -0
  78. package/src/components/payment-method/form.tsx +54 -0
  79. package/src/components/payment-method/stripe.tsx +45 -0
  80. package/src/components/portal/invoice/list.tsx +1 -1
  81. package/src/components/portal/subscription/list.tsx +1 -1
  82. package/src/components/price/currency-select.tsx +53 -0
  83. package/src/components/price/form.tsx +118 -24
  84. package/src/components/product/add-price.tsx +1 -1
  85. package/src/components/product/edit-price.tsx +6 -2
  86. package/src/components/subscription/items/index.tsx +7 -6
  87. package/src/components/subscription/items/usage-records.tsx +98 -0
  88. package/src/components/subscription/list.tsx +3 -2
  89. package/src/components/subscription/status.tsx +68 -0
  90. package/src/contexts/settings.tsx +2 -2
  91. package/src/env.d.ts +2 -0
  92. package/src/libs/util.ts +116 -21
  93. package/src/locales/en.tsx +71 -3
  94. package/src/pages/admin/billing/invoices/detail.tsx +5 -2
  95. package/src/pages/admin/billing/subscriptions/detail.tsx +6 -6
  96. package/src/pages/admin/customers/customers/detail.tsx +13 -1
  97. package/src/pages/admin/payments/intents/detail.tsx +8 -3
  98. package/src/pages/admin/payments/links/create.tsx +23 -3
  99. package/src/pages/admin/payments/links/detail.tsx +13 -26
  100. package/src/pages/admin/products/prices/detail.tsx +55 -11
  101. package/src/pages/admin/products/prices/list.tsx +7 -1
  102. package/src/pages/admin/products/products/create.tsx +1 -1
  103. package/src/pages/admin/products/products/detail.tsx +14 -7
  104. package/src/pages/admin/settings/index.tsx +16 -6
  105. package/src/pages/admin/settings/payment-methods/create.tsx +81 -0
  106. package/src/pages/admin/settings/{payment-methods.tsx → payment-methods/index.tsx} +9 -6
  107. package/src/pages/checkout/pay.tsx +3 -1
  108. package/src/pages/customer/index.tsx +12 -1
  109. package/public/.gitkeep +0 -0
@@ -0,0 +1,54 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import { Stack, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material';
3
+ import { styled } from '@mui/system';
4
+ import { Controller, useFormContext, useWatch } from 'react-hook-form';
5
+
6
+ import ArcBlockMethodForm from './arcblock';
7
+ import BitcoinMethodForm from './bitcoin';
8
+ import EthereumMethodForm from './ethereum';
9
+ import StripeMethodForm from './stripe';
10
+
11
+ export default function PaymentMethodForm() {
12
+ const { t } = useLocaleContext();
13
+ const { control, setValue } = useFormContext();
14
+
15
+ const type = useWatch({ control, name: 'type' });
16
+
17
+ return (
18
+ <Root direction="column" alignItems="flex-start" spacing={2}>
19
+ <Controller
20
+ name="type"
21
+ control={control}
22
+ render={({ field }) => (
23
+ <ToggleButtonGroup {...field} onChange={(_, value: string) => setValue(field.name, value)} exclusive>
24
+ <ToggleButton value="arcblock">ArcBlock</ToggleButton>
25
+ <ToggleButton value="stripe">Stripe</ToggleButton>
26
+ <ToggleButton value="ethereum" disabled>
27
+ Ethereum
28
+ </ToggleButton>
29
+ <ToggleButton value="bitcoin" disabled>
30
+ Bitcoin
31
+ </ToggleButton>
32
+ </ToggleButtonGroup>
33
+ )}
34
+ />
35
+ <Typography variant="h6" sx={{ mb: 3, fontWeight: 600 }}>
36
+ {t('admin.paymentMethod.settings')}
37
+ </Typography>
38
+ {type === 'stripe' && <StripeMethodForm />}
39
+ {type === 'arcblock' && <ArcBlockMethodForm />}
40
+ {type === 'ethereum' && <EthereumMethodForm />}
41
+ {type === 'bitcoin' && <BitcoinMethodForm />}
42
+ </Root>
43
+ );
44
+ }
45
+
46
+ const Root = styled(Stack)`
47
+ select {
48
+ border: none;
49
+ &:active,
50
+ &:focus {
51
+ border: none;
52
+ }
53
+ }
54
+ `;
@@ -0,0 +1,45 @@
1
+ /* eslint-disable no-nested-ternary */
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+
4
+ import FormInput from '../input';
5
+
6
+ export default function StripeMethodForm() {
7
+ const { t } = useLocaleContext();
8
+
9
+ return (
10
+ <>
11
+ <FormInput
12
+ key="name"
13
+ name="name"
14
+ type="text"
15
+ rules={{ required: true }}
16
+ label={t('admin.paymentMethod.name.label')}
17
+ placeholder={t('admin.paymentMethod.name.tip')}
18
+ />
19
+ <FormInput
20
+ key="description"
21
+ name="description"
22
+ type="text"
23
+ rules={{ required: true }}
24
+ label={t('admin.paymentMethod.description.label')}
25
+ placeholder={t('admin.paymentMethod.description.tip')}
26
+ />
27
+ <FormInput
28
+ key="publishable_key"
29
+ name="settings.stripe.publishable_key"
30
+ type="text"
31
+ rules={{ required: true }}
32
+ label={t('admin.paymentMethod.stripe.publishable_key.label')}
33
+ placeholder={t('admin.paymentMethod.stripe.publishable_key.tip')}
34
+ />
35
+ <FormInput
36
+ key="secret_key"
37
+ name="settings.stripe.secret_key"
38
+ type="password"
39
+ rules={{ required: true }}
40
+ label={t('admin.paymentMethod.stripe.secret_key.label')}
41
+ placeholder={t('admin.paymentMethod.stripe.secret_key.tip')}
42
+ />
43
+ </>
44
+ );
45
+ }
@@ -43,7 +43,7 @@ export default function CustomerInvoiceList({ customer_id }: Props) {
43
43
  const { data, loadMore, loadingMore, loading } = useInfiniteScroll<Paginated<TInvoiceExpanded>>(
44
44
  (d) => {
45
45
  const page = d ? Math.ceil(d.list.length / pageSize) + 1 : 1;
46
- return fetchData({ page, size: pageSize, status: 'open,paid', customer_id });
46
+ return fetchData({ page, pageSize, status: 'open,paid', customer_id });
47
47
  },
48
48
  {
49
49
  reloadDeps: [customer_id],
@@ -41,7 +41,7 @@ export function CurrentSubscriptionsInner({ id, onChange }: Props) {
41
41
  const { data, loadMore, loadingMore, loading } = useInfiniteScroll<Paginated<TSubscriptionExpanded>>(
42
42
  (d) => {
43
43
  const page = d ? Math.ceil(d.list.length / pageSize) + 1 : 1;
44
- return fetchData({ page, size: pageSize, status: 'active,trialing,paused', customer_id: id });
44
+ return fetchData({ page, pageSize, status: 'active,trialing,paused', customer_id: id });
45
45
  },
46
46
  {
47
47
  reloadDeps: [id],
@@ -0,0 +1,53 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import { AddOutlined } from '@mui/icons-material';
3
+ import { ListSubheader, MenuItem, Select, Stack, Typography } from '@mui/material';
4
+ import { useState } from 'react';
5
+ import type { LiteralUnion } from 'type-fest';
6
+
7
+ import { useSettingsContext } from '../../contexts/settings';
8
+ import { getSupportedPaymentMethods } from '../../libs/util';
9
+ import Currency from '../currency';
10
+
11
+ type Props = {
12
+ mode: LiteralUnion<'waiting' | 'selecting', string>;
13
+ hasSelected: (currency: any) => boolean;
14
+ onSelect: (currencyId: string) => void;
15
+ };
16
+
17
+ export default function CurrencySelect({ mode: initialMode, hasSelected, onSelect }: Props) {
18
+ const { t } = useLocaleContext();
19
+ const { settings } = useSettingsContext();
20
+ const [mode, setMode] = useState(initialMode);
21
+
22
+ const handleSelect = (e: any) => {
23
+ setMode('waiting');
24
+ onSelect(e.target.value);
25
+ };
26
+
27
+ if (mode === 'selecting') {
28
+ return (
29
+ <Select value="" sx={{ width: 260 }} size="small" onChange={handleSelect}>
30
+ {getSupportedPaymentMethods(settings.paymentMethods, hasSelected).map((method) => [
31
+ <ListSubheader key={method.id} sx={{ fontSize: '1rem', color: 'text.secondary', lineHeight: '2.5rem' }}>
32
+ {method.name}
33
+ </ListSubheader>,
34
+ ...method.payment_currencies.map((currency) => (
35
+ <MenuItem key={currency.id} sx={{ pl: 3 }} value={currency.id}>
36
+ <Stack direction="row" justifyContent="space-between" sx={{ width: '100%' }}>
37
+ <Currency logo={currency.logo} name={currency.name} />
38
+ <Typography fontWeight="bold">{currency.symbol}</Typography>
39
+ </Stack>
40
+ </MenuItem>
41
+ )),
42
+ ])}
43
+ </Select>
44
+ );
45
+ }
46
+
47
+ return (
48
+ <Stack sx={{ cursor: 'pointer' }} direction="row" alignItems="center" onClick={() => setMode('selecting')}>
49
+ <AddOutlined color="primary" />
50
+ <Typography color="primary">{t('admin.price.currency.add')}</Typography>
51
+ </Stack>
52
+ );
53
+ }
@@ -1,11 +1,14 @@
1
1
  /* eslint-disable no-nested-ternary */
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
- import type { InferFormType, PriceRecurring, TPriceExpanded } from '@did-pay/types';
3
+ import type { InferFormType, PriceRecurring, TPaymentMethodExpanded, TPriceExpanded } from '@did-pay/types';
4
+ import { DeleteOutlineOutlined, InfoOutlined } from '@mui/icons-material';
4
5
  import {
6
+ Alert,
5
7
  Box,
6
8
  Checkbox,
7
9
  FormControlLabel,
8
10
  FormLabel,
11
+ IconButton,
9
12
  InputAdornment,
10
13
  MenuItem,
11
14
  Select,
@@ -13,36 +16,39 @@ import {
13
16
  TextField,
14
17
  ToggleButton,
15
18
  ToggleButtonGroup,
19
+ Tooltip,
20
+ Typography,
16
21
  } from '@mui/material';
17
22
  import { styled } from '@mui/system';
18
- import { Controller, useFormContext, useWatch } from 'react-hook-form';
23
+ import { Controller, useFieldArray, useFormContext, useWatch } from 'react-hook-form';
19
24
  import type { LiteralUnion } from 'type-fest';
20
25
 
21
26
  import { useSettingsContext } from '../../contexts/settings';
27
+ import { findCurrency } from '../../libs/util';
22
28
  import Collapse from '../collapse';
29
+ import CurrencySelect from './currency-select';
23
30
 
24
- export type Price = Omit<InferFormType<TPriceExpanded>, 'product_id' | 'locked'> & {
31
+ export type Price = Omit<InferFormType<TPriceExpanded>, 'product_id' | 'object'> & {
25
32
  model: LiteralUnion<'standard' | 'package' | 'graduated' | 'volume' | 'custom', string>;
26
33
  recurring: Omit<PriceRecurring, 'usage_type'> & {
27
34
  interval_config: string;
28
- metered: boolean;
29
35
  };
30
36
  };
31
37
 
32
38
  export const DEFAULT_PRICE: Omit<Price, 'product' | 'currency'> = {
39
+ locked: false,
33
40
  model: 'standard',
34
41
  billing_scheme: '',
35
42
  currency_id: '',
36
43
  nickname: '',
37
- type: 'one_time',
44
+ type: 'recurring',
38
45
  unit_amount: '0',
39
46
  lookup_key: '',
40
47
  recurring: {
41
48
  interval_config: 'month_1',
42
49
  interval: 'month',
43
50
  interval_count: 1,
44
- metered: false,
45
- usage_type: '',
51
+ usage_type: 'licensed',
46
52
  aggregate_usage: 'sum',
47
53
  },
48
54
  transform_quantity: {
@@ -51,6 +57,9 @@ export const DEFAULT_PRICE: Omit<Price, 'product' | 'currency'> = {
51
57
  },
52
58
  tiers: [],
53
59
  metadata: [],
60
+ custom_unit_amount: null,
61
+ currency_options: [],
62
+ tiers_mode: null,
54
63
  };
55
64
 
56
65
  type PriceFormProps = {
@@ -63,6 +72,12 @@ PriceForm.defaultProps = {
63
72
  simple: false,
64
73
  };
65
74
 
75
+ const INPUT_WIDTH = 260;
76
+
77
+ const hasMoreCurrency = (methods: TPaymentMethodExpanded[]) => {
78
+ return methods.every((method) => method.payment_currencies.length > 1) || methods.length > 1;
79
+ };
80
+
66
81
  // FIXME: @wangshijun i18n
67
82
  export default function PriceForm({ prefix, simple }: PriceFormProps) {
68
83
  const getFieldName = (name: string) => (prefix ? `${prefix}.${name}` : name);
@@ -71,19 +86,24 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
71
86
  const { control, setValue, getFieldState } = useFormContext();
72
87
  const { settings, livemode } = useSettingsContext();
73
88
 
89
+ const currencies = useFieldArray({ control, name: getFieldName('currency_options') });
90
+
91
+ const isLocked = useWatch({ control, name: getFieldName('locked') });
74
92
  const isRecurring = useWatch({ control, name: getFieldName('type') }) === 'recurring';
75
- const isMetered = useWatch({ control, name: getFieldName('recurring.metered') });
93
+ const isMetered = useWatch({ control, name: getFieldName('recurring.usage_type') }) === 'metered';
76
94
  const isCustomInterval = useWatch({ control, name: getFieldName('recurring.interval_config') }) === 'month_2';
77
95
  const model = useWatch({ control, name: getFieldName('model') });
78
96
 
79
97
  return (
80
98
  <Root direction="column" alignItems="flex-start" spacing={2}>
99
+ {isLocked && <Alert severity="info">{t('admin.price.locked')}</Alert>}
81
100
  <Controller
82
- rules={{ required: true }}
83
101
  name={getFieldName('model')}
84
102
  control={control}
103
+ rules={{ required: true }}
104
+ disabled={isLocked}
85
105
  render={({ field }) => (
86
- <Box sx={{ width: 174 }}>
106
+ <Box sx={{ width: INPUT_WIDTH }}>
87
107
  <FormLabel>{t('admin.price.model')}</FormLabel>
88
108
  <Select {...field} fullWidth size="small">
89
109
  <MenuItem value="standard">Standard Pricing</MenuItem>
@@ -106,14 +126,24 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
106
126
  name={getFieldName('unit_amount')}
107
127
  control={control}
108
128
  rules={{ required: t('admin.price.unit_amount.required'), min: t('admin.price.unit_amount.positive') }}
129
+ disabled={isLocked}
109
130
  render={({ field }) => (
110
131
  <Box>
111
- <FormLabel>{t('admin.price.amount')}</FormLabel>
132
+ <FormLabel>
133
+ <Stack direction="row" alignItems="center" spacing={0.5}>
134
+ <Typography component="span" color="text.primary">
135
+ {t('admin.price.amount')}
136
+ </Typography>
137
+ <Tooltip title={t('admin.price.amountTip')} placement="top" arrow>
138
+ <InfoOutlined fontSize="small" sx={{ color: 'text.secondary' }} />
139
+ </Tooltip>
140
+ </Stack>
141
+ </FormLabel>
112
142
  <TextField
113
143
  {...field}
114
144
  type="number"
115
145
  size="small"
116
- sx={{ width: 174 }}
146
+ sx={{ width: INPUT_WIDTH }}
117
147
  error={!!getFieldState(getFieldName('unit_amount')).error}
118
148
  helperText={getFieldState(getFieldName('unit_amount')).error?.message}
119
149
  InputProps={{
@@ -127,6 +157,7 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
127
157
  <Controller
128
158
  name={getFieldName('transform_quantity.divide_by')}
129
159
  control={control}
160
+ disabled={isLocked}
130
161
  render={({ field }) => (
131
162
  <Box ml={2}>
132
163
  <FormLabel>&nbsp;</FormLabel>
@@ -134,7 +165,7 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
134
165
  {...field}
135
166
  type="number"
136
167
  size="small"
137
- sx={{ width: 174 }}
168
+ sx={{ width: INPUT_WIDTH }}
138
169
  InputProps={{
139
170
  startAdornment: <InputAdornment position="start">{t('common.per')}</InputAdornment>,
140
171
  endAdornment: <InputAdornment position="end">{t('common.unit')}</InputAdornment>,
@@ -145,13 +176,64 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
145
176
  />
146
177
  )}
147
178
  </Stack>
179
+ {hasMoreCurrency(settings.paymentMethods) && (
180
+ <Stack direction="column" spacing={2}>
181
+ {currencies.fields.map((item: any, index: number) => {
182
+ if (item.currency_id === settings.baseCurrency.id) {
183
+ return null;
184
+ }
185
+ const fieldName = getFieldName(`currency_options.${index}.unit_amount`);
186
+ return (
187
+ <Stack key={item.currency_id} direction="row" alignItems="center" spacing={1}>
188
+ <Controller
189
+ name={fieldName}
190
+ control={control}
191
+ rules={{ required: t('admin.price.unit_amount.required') }}
192
+ disabled={isLocked}
193
+ render={({ field }) => (
194
+ <TextField
195
+ {...field}
196
+ type="number"
197
+ size="small"
198
+ sx={{ width: INPUT_WIDTH }}
199
+ error={!!getFieldState(fieldName).error}
200
+ helperText={getFieldState(fieldName).error?.message}
201
+ InputProps={{
202
+ endAdornment: (
203
+ <InputAdornment position="end">
204
+ {findCurrency(settings.paymentMethods, item.currency_id)?.symbol}
205
+ </InputAdornment>
206
+ ),
207
+ }}
208
+ />
209
+ )}
210
+ />
211
+ <IconButton size="small" disabled={isLocked} onClick={() => currencies.remove(index)}>
212
+ <DeleteOutlineOutlined color="error" sx={{ opacity: 0.75 }} />
213
+ </IconButton>
214
+ </Stack>
215
+ );
216
+ })}
217
+ {isLocked === false && (
218
+ <CurrencySelect
219
+ mode="waiting"
220
+ hasSelected={(currency) =>
221
+ currencies.fields.some((x: any) => x.currency_id === currency.id) ||
222
+ currency.id === settings.baseCurrency.id
223
+ }
224
+ onSelect={(currencyId) => currencies.append({ currency_id: currencyId, unit_amount: 0 })}
225
+ />
226
+ )}
227
+ </Stack>
228
+ )}
148
229
  <Controller
149
230
  name={getFieldName('type')}
150
231
  control={control}
232
+ disabled={isLocked}
151
233
  render={({ field }) => (
152
234
  <ToggleButtonGroup {...field} onChange={(_, value: string) => setValue(field.name, value)} exclusive>
153
- <ToggleButton value="one_time">One Time</ToggleButton>
154
235
  <ToggleButton value="recurring">Recurring</ToggleButton>
236
+ <ToggleButton value="one_time">One Time</ToggleButton>
155
237
  </ToggleButtonGroup>
156
238
  )}
157
239
  />
@@ -160,6 +242,7 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
160
242
  <Controller
161
243
  name={getFieldName('recurring.interval_config')}
162
244
  control={control}
245
+ disabled={isLocked}
163
246
  render={({ field }) => (
164
247
  <Box>
165
248
  <FormLabel>{t('admin.price.recurring.interval')}</FormLabel>
@@ -168,10 +251,10 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
168
251
  onChange={(e) => {
169
252
  const [interval, count] = e.target.value.split('_');
170
253
  setValue(getFieldName('recurring.interval'), interval);
171
- setValue(getFieldName('recurring.interval_count'), count);
254
+ setValue(getFieldName('recurring.interval_count'), +count);
172
255
  setValue(getFieldName('recurring.interval_config'), e.target.value);
173
256
  }}
174
- sx={{ width: 174 }}
257
+ sx={{ width: INPUT_WIDTH }}
175
258
  size="small">
176
259
  {!livemode && <MenuItem value="hour_1">Hourly</MenuItem>}
177
260
  <MenuItem value="day_1">Daily</MenuItem>
@@ -189,6 +272,7 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
189
272
  <Controller
190
273
  name={getFieldName('recurring.interval_count')}
191
274
  control={control}
275
+ disabled={isLocked}
192
276
  render={({ field }) => (
193
277
  <Box ml={2}>
194
278
  <FormLabel>&nbsp;</FormLabel>
@@ -196,7 +280,7 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
196
280
  {...field}
197
281
  type="number"
198
282
  size="small"
199
- sx={{ width: 174 }}
283
+ sx={{ width: INPUT_WIDTH }}
200
284
  InputProps={{
201
285
  startAdornment: <InputAdornment position="start">{t('common.every')}</InputAdornment>,
202
286
  endAdornment: (
@@ -218,18 +302,27 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
218
302
  )}
219
303
  {isRecurring && (
220
304
  <Controller
221
- name={getFieldName('recurring.metered')}
305
+ name={getFieldName('recurring.usage_type')}
222
306
  control={control}
307
+ disabled={isLocked}
223
308
  render={({ field }) => (
224
309
  <FormControlLabel
310
+ sx={{ alignItems: 'flex-start' }}
225
311
  control={
226
312
  <Checkbox
227
313
  checked={isMetered}
228
314
  {...field}
229
- onChange={(_, checked: boolean) => setValue(field.name, checked)}
315
+ onChange={(_, checked: boolean) => setValue(field.name, checked ? 'metered' : 'licensed')}
230
316
  />
231
317
  }
232
- label={t('admin.price.recurring.metered')}
318
+ label={
319
+ <Stack>
320
+ <Typography color="text.primary">{t('admin.price.recurring.metered')}</Typography>
321
+ <Typography color="text.secondary" sx={{ maxWidth: '80%' }}>
322
+ {t('admin.price.recurring.meteredTip')}
323
+ </Typography>
324
+ </Stack>
325
+ }
233
326
  />
234
327
  )}
235
328
  />
@@ -238,10 +331,11 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
238
331
  <Controller
239
332
  name={getFieldName('recurring.aggregate_usage')}
240
333
  control={control}
334
+ disabled={isLocked}
241
335
  render={({ field }) => (
242
336
  <Box>
243
337
  <FormLabel>{t('admin.price.recurring.aggregate')}</FormLabel>
244
- <Select {...field} sx={{ width: 362 }} size="small">
338
+ <Select {...field} sx={{ width: INPUT_WIDTH }} size="small">
245
339
  <MenuItem value="sum">Sum of usage values during period</MenuItem>
246
340
  <MenuItem value="max">Maximum usage value during period</MenuItem>
247
341
  <MenuItem value="last_ever">Most recent usage value</MenuItem>
@@ -252,7 +346,7 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
252
346
  />
253
347
  )}
254
348
  {!simple && (
255
- <Collapse trigger={t('admin.price.additional')}>
349
+ <Collapse trigger={t('admin.price.additional')} expanded={isLocked}>
256
350
  <Stack spacing={2} alignItems="flex-start">
257
351
  <Controller
258
352
  name={getFieldName('nickname')}
@@ -260,7 +354,7 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
260
354
  render={({ field }) => (
261
355
  <Box>
262
356
  <FormLabel>{t('admin.price.nickname.label')}</FormLabel>
263
- <TextField {...field} size="small" />
357
+ <TextField {...field} size="small" sx={{ width: INPUT_WIDTH }} />
264
358
  </Box>
265
359
  )}
266
360
  />
@@ -270,7 +364,7 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
270
364
  render={({ field }) => (
271
365
  <Box>
272
366
  <FormLabel>{t('admin.price.lookup_key.label')}</FormLabel>
273
- <TextField {...field} size="small" />
367
+ <TextField {...field} size="small" sx={{ width: INPUT_WIDTH }} />
274
368
  </Box>
275
369
  )}
276
370
  />
@@ -37,7 +37,7 @@ export default function AddPrice({
37
37
  open
38
38
  disableEscapeKeyDown
39
39
  fullWidth
40
- maxWidth="md"
40
+ maxWidth="sm"
41
41
  onClose={() => onCancel(null)}
42
42
  showCloseButton={false}
43
43
  title={t('admin.price.add')}
@@ -3,7 +3,7 @@ import Dialog from '@arcblock/ux/lib/Dialog';
3
3
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
4
  import { Button, CircularProgress, Stack } from '@mui/material';
5
5
  import { fromUnitToToken } from '@ocap/util';
6
- import { isEmpty } from 'lodash';
6
+ import { cloneDeep, isEmpty } from 'lodash';
7
7
  import type { EventHandler } from 'react';
8
8
  import { FormProvider, useForm } from 'react-hook-form';
9
9
 
@@ -26,6 +26,7 @@ export default function EditPrice({
26
26
  defaultValues: {
27
27
  ...price,
28
28
  unit_amount: fromUnitToToken(price.unit_amount, price.currency.decimal),
29
+ // @ts-ignore
29
30
  model: getPricingModel(price as any),
30
31
  metadata: isEmpty(price.metadata)
31
32
  ? []
@@ -34,10 +35,13 @@ export default function EditPrice({
34
35
  recurring: price.recurring
35
36
  ? {
36
37
  ...price.recurring,
37
- metered: price.recurring.usage_type === 'metered',
38
38
  interval_config: [price.recurring.interval, price.recurring.interval_count].join('_'),
39
39
  }
40
40
  : DEFAULT_PRICE.recurring,
41
+ currency_options: cloneDeep(price.currency_options).map((x: any) => {
42
+ x.unit_amount = fromUnitToToken(x.unit_amount, x.currency.decimal);
43
+ return x;
44
+ }),
41
45
  },
42
46
  });
43
47
 
@@ -7,6 +7,7 @@ import { formatPrice } from '../../../libs/util';
7
7
  import Copyable from '../../copyable';
8
8
  import Table from '../../table';
9
9
  import LineItemActions from './actions';
10
+ import UsageRecords from './usage-records';
10
11
 
11
12
  type ListProps = {
12
13
  data: TSubscriptionItemExpanded[];
@@ -44,7 +45,7 @@ export default function SubscriptionItemList({ data, currency }: ListProps) {
44
45
  options: {
45
46
  customBodyRenderLite: (_: string, index: number) => {
46
47
  const item = data[index] as TSubscriptionItemExpanded;
47
- return <Copyable text={item?.id} />;
48
+ return <Copyable text={item.id} />;
48
49
  },
49
50
  },
50
51
  },
@@ -53,8 +54,8 @@ export default function SubscriptionItemList({ data, currency }: ListProps) {
53
54
  name: 'quantity',
54
55
  options: {
55
56
  customBodyRenderLite: (_: string, index: number) => {
56
- const item = data[index];
57
- return item?.quantity;
57
+ const item = data[index] as TSubscriptionItemExpanded;
58
+ return item.price.recurring?.usage_type === 'metered' ? <UsageRecords id={item.id} /> : item.quantity;
58
59
  },
59
60
  },
60
61
  },
@@ -66,9 +67,9 @@ export default function SubscriptionItemList({ data, currency }: ListProps) {
66
67
  sort: false,
67
68
  customBodyRenderLite: (_: string, index: number) => {
68
69
  const item = data[index] as TSubscriptionItemExpanded;
69
- return (
70
- <Typography>{formatPrice(item.price, currency, item?.price.product.unit_label, item?.quantity)}</Typography>
71
- );
70
+ return item.price.recurring?.usage_type === 'metered'
71
+ ? t('admin.subscription.usage.vary')
72
+ : formatPrice(item.price, currency, item?.price.product.unit_label, item?.quantity);
72
73
  },
73
74
  },
74
75
  },