payment-kit 1.19.0 → 1.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/api/src/crons/index.ts +8 -0
  2. package/api/src/index.ts +4 -0
  3. package/api/src/libs/credit-grant.ts +146 -0
  4. package/api/src/libs/env.ts +1 -0
  5. package/api/src/libs/invoice.ts +4 -3
  6. package/api/src/libs/notification/template/base.ts +388 -2
  7. package/api/src/libs/notification/template/customer-credit-grant-granted.ts +149 -0
  8. package/api/src/libs/notification/template/customer-credit-grant-low-balance.ts +151 -0
  9. package/api/src/libs/notification/template/customer-credit-insufficient.ts +254 -0
  10. package/api/src/libs/notification/template/subscription-canceled.ts +193 -202
  11. package/api/src/libs/notification/template/subscription-refund-succeeded.ts +215 -237
  12. package/api/src/libs/notification/template/subscription-renewed.ts +130 -200
  13. package/api/src/libs/notification/template/subscription-succeeded.ts +100 -202
  14. package/api/src/libs/notification/template/subscription-trial-start.ts +142 -188
  15. package/api/src/libs/notification/template/subscription-trial-will-end.ts +146 -174
  16. package/api/src/libs/notification/template/subscription-upgraded.ts +96 -192
  17. package/api/src/libs/notification/template/subscription-will-canceled.ts +94 -135
  18. package/api/src/libs/notification/template/subscription-will-renew.ts +220 -245
  19. package/api/src/libs/payment.ts +69 -0
  20. package/api/src/libs/queue/index.ts +3 -2
  21. package/api/src/libs/session.ts +8 -0
  22. package/api/src/libs/subscription.ts +74 -3
  23. package/api/src/libs/ws.ts +23 -1
  24. package/api/src/locales/en.ts +33 -0
  25. package/api/src/locales/zh.ts +31 -0
  26. package/api/src/queues/credit-consume.ts +715 -0
  27. package/api/src/queues/credit-grant.ts +572 -0
  28. package/api/src/queues/notification.ts +173 -128
  29. package/api/src/queues/payment.ts +210 -122
  30. package/api/src/queues/subscription.ts +179 -0
  31. package/api/src/routes/checkout-sessions.ts +157 -9
  32. package/api/src/routes/connect/shared.ts +3 -2
  33. package/api/src/routes/credit-grants.ts +241 -0
  34. package/api/src/routes/credit-transactions.ts +208 -0
  35. package/api/src/routes/index.ts +8 -0
  36. package/api/src/routes/meter-events.ts +347 -0
  37. package/api/src/routes/meters.ts +219 -0
  38. package/api/src/routes/payment-currencies.ts +14 -2
  39. package/api/src/routes/payment-links.ts +1 -1
  40. package/api/src/routes/payment-methods.ts +14 -2
  41. package/api/src/routes/prices.ts +43 -0
  42. package/api/src/routes/pricing-table.ts +13 -7
  43. package/api/src/routes/products.ts +63 -4
  44. package/api/src/routes/settings.ts +1 -1
  45. package/api/src/routes/subscriptions.ts +4 -0
  46. package/api/src/store/migrations/20250610-billing-credit.ts +43 -0
  47. package/api/src/store/models/credit-grant.ts +486 -0
  48. package/api/src/store/models/credit-transaction.ts +268 -0
  49. package/api/src/store/models/customer.ts +8 -0
  50. package/api/src/store/models/index.ts +52 -1
  51. package/api/src/store/models/meter-event.ts +423 -0
  52. package/api/src/store/models/meter.ts +176 -0
  53. package/api/src/store/models/payment-currency.ts +66 -14
  54. package/api/src/store/models/price.ts +6 -0
  55. package/api/src/store/models/product.ts +2 -2
  56. package/api/src/store/models/subscription.ts +24 -0
  57. package/api/src/store/models/types.ts +28 -2
  58. package/api/tests/libs/subscription.spec.ts +53 -0
  59. package/blocklet.yml +9 -1
  60. package/package.json +4 -4
  61. package/scripts/sdk.js +233 -1
  62. package/src/app.tsx +10 -0
  63. package/src/components/collapse.tsx +11 -1
  64. package/src/components/customer/credit-grant-item-list.tsx +99 -0
  65. package/src/components/customer/credit-overview.tsx +233 -0
  66. package/src/components/customer/form.tsx +5 -2
  67. package/src/components/invoice/list.tsx +19 -1
  68. package/src/components/metadata/form.tsx +286 -90
  69. package/src/components/meter/actions.tsx +101 -0
  70. package/src/components/meter/add-usage-dialog.tsx +239 -0
  71. package/src/components/meter/events-list.tsx +657 -0
  72. package/src/components/meter/form.tsx +245 -0
  73. package/src/components/meter/products.tsx +264 -0
  74. package/src/components/meter/usage-guide.tsx +174 -0
  75. package/src/components/payment-currency/form.tsx +2 -0
  76. package/src/components/payment-intent/list.tsx +19 -1
  77. package/src/components/payment-link/preview.tsx +1 -1
  78. package/src/components/payment-link/product-select.tsx +52 -12
  79. package/src/components/payment-method/arcblock.tsx +2 -0
  80. package/src/components/payment-method/base.tsx +2 -0
  81. package/src/components/payment-method/bitcoin.tsx +2 -0
  82. package/src/components/payment-method/ethereum.tsx +2 -0
  83. package/src/components/payment-method/stripe.tsx +2 -0
  84. package/src/components/payouts/list.tsx +19 -1
  85. package/src/components/price/currency-select.tsx +51 -31
  86. package/src/components/price/form.tsx +881 -407
  87. package/src/components/pricing-table/preview.tsx +1 -1
  88. package/src/components/product/add-price.tsx +9 -7
  89. package/src/components/product/create.tsx +7 -4
  90. package/src/components/product/edit-price.tsx +21 -12
  91. package/src/components/product/features.tsx +17 -7
  92. package/src/components/product/form.tsx +104 -89
  93. package/src/components/refund/list.tsx +19 -1
  94. package/src/components/section/header.tsx +5 -18
  95. package/src/components/subscription/items/index.tsx +1 -1
  96. package/src/components/subscription/metrics.tsx +37 -5
  97. package/src/components/subscription/portal/actions.tsx +2 -1
  98. package/src/contexts/products.tsx +26 -9
  99. package/src/hooks/subscription.ts +34 -0
  100. package/src/libs/meter-utils.ts +196 -0
  101. package/src/libs/util.ts +4 -0
  102. package/src/locales/en.tsx +385 -4
  103. package/src/locales/zh.tsx +364 -0
  104. package/src/pages/admin/billing/index.tsx +61 -33
  105. package/src/pages/admin/billing/invoices/detail.tsx +1 -1
  106. package/src/pages/admin/billing/meters/create.tsx +60 -0
  107. package/src/pages/admin/billing/meters/detail.tsx +435 -0
  108. package/src/pages/admin/billing/meters/index.tsx +210 -0
  109. package/src/pages/admin/billing/meters/meter-event.tsx +346 -0
  110. package/src/pages/admin/billing/subscriptions/detail.tsx +47 -14
  111. package/src/pages/admin/customers/customers/credit-grant/detail.tsx +391 -0
  112. package/src/pages/admin/customers/customers/detail.tsx +22 -10
  113. package/src/pages/admin/customers/index.tsx +5 -0
  114. package/src/pages/admin/developers/events/detail.tsx +1 -1
  115. package/src/pages/admin/developers/index.tsx +1 -1
  116. package/src/pages/admin/payments/intents/detail.tsx +1 -1
  117. package/src/pages/admin/payments/payouts/detail.tsx +1 -1
  118. package/src/pages/admin/payments/refunds/detail.tsx +1 -1
  119. package/src/pages/admin/products/index.tsx +3 -2
  120. package/src/pages/admin/products/links/detail.tsx +1 -1
  121. package/src/pages/admin/products/prices/actions.tsx +16 -4
  122. package/src/pages/admin/products/prices/detail.tsx +30 -3
  123. package/src/pages/admin/products/prices/list.tsx +8 -1
  124. package/src/pages/admin/products/pricing-tables/detail.tsx +1 -1
  125. package/src/pages/admin/products/products/create.tsx +233 -57
  126. package/src/pages/admin/products/products/detail.tsx +2 -1
  127. package/src/pages/admin/settings/payment-methods/index.tsx +3 -0
  128. package/src/pages/customer/credit-grant/detail.tsx +308 -0
  129. package/src/pages/customer/index.tsx +35 -2
  130. package/src/pages/customer/recharge/account.tsx +5 -5
  131. package/src/pages/customer/subscription/change-payment.tsx +4 -2
  132. package/src/pages/customer/subscription/detail.tsx +48 -14
  133. package/src/pages/customer/subscription/embed.tsx +1 -1
@@ -1,30 +1,96 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
2
  import { FormInput } from '@blocklet/payment-react';
3
- import { AddOutlined, DeleteOutlineOutlined } from '@mui/icons-material';
4
- import { Box, Button, Divider, IconButton, Stack, Typography } from '@mui/material';
5
- import { useEffect, useRef } from 'react';
3
+ import { AddOutlined, Autorenew, DeleteOutlineOutlined, FormatAlignLeft } from '@mui/icons-material';
4
+ import { Box, Button, Divider, IconButton, Stack, TextField, InputAdornment, Tooltip, FormLabel } from '@mui/material';
5
+ import { useEffect, useRef, useState, useCallback } from 'react';
6
6
  import { useFieldArray, useFormContext } from 'react-hook-form';
7
+ import { isObject, debounce } from 'lodash';
8
+
9
+ import { isObjectContent } from '../../libs/util';
7
10
 
8
11
  export default function MetadataForm({
9
12
  title = undefined,
10
13
  actions = null,
11
14
  minHeight = 'auto',
15
+ showModeToggle = true,
16
+ color = 'primary',
17
+ name = 'metadata',
12
18
  }: {
13
19
  title?: string;
14
20
  actions?: React.ReactNode;
15
21
  minHeight?: string;
22
+ showModeToggle?: boolean;
23
+ color?: 'primary' | 'inherit';
24
+ name?: string;
16
25
  }) {
17
26
  const { t } = useLocaleContext();
18
27
  const {
19
28
  control,
20
29
  formState: { errors },
30
+ setValue,
31
+ getValues,
21
32
  } = useFormContext();
22
33
 
23
- const metadata = useFieldArray({ control, name: 'metadata' });
34
+ const metadata = useFieldArray({ control, name });
24
35
  const lastItemRef = useRef<HTMLDivElement | null>(null);
25
36
  const errorRef = useRef<HTMLDivElement | null>(null);
26
37
  const initRef = useRef(false);
27
38
 
39
+ const [mode, setMode] = useState<'form' | 'json'>('form');
40
+ const [jsonValue, setJsonValue] = useState('');
41
+ const [formatError, setFormatError] = useState<string | null>(null);
42
+
43
+ // Initialize JSON value from form data
44
+ useEffect(() => {
45
+ const formData = getValues();
46
+ if (Array.isArray(formData.metadata)) {
47
+ const metadataObj = formData.metadata.reduce((acc: any, x: any) => {
48
+ if (x.key) {
49
+ acc[x.key] = isObjectContent(x.value) ? JSON.parse(x.value) : x.value;
50
+ }
51
+ return acc;
52
+ }, {});
53
+ setJsonValue(JSON.stringify(metadataObj, null, 2));
54
+ }
55
+ }, [getValues]);
56
+
57
+ // Debounced function to sync JSON changes to form
58
+ const debouncedSyncToForm = useCallback(
59
+ debounce((jsonString: string) => {
60
+ try {
61
+ const parsedData = JSON.parse(jsonString);
62
+ const formMetadata: Array<{ key: string; value: string }> = [];
63
+ const orderedKeys = Object.keys(JSON.parse(JSON.stringify(parsedData)));
64
+ orderedKeys.forEach((key) => {
65
+ formMetadata.push({
66
+ key,
67
+ value: isObject(parsedData[key]) ? JSON.stringify(parsedData[key]) : parsedData[key],
68
+ });
69
+ });
70
+
71
+ setValue('metadata', formMetadata.length > 0 ? formMetadata : [{ key: '', value: '' }]);
72
+ } catch (err) {
73
+ // eslint-disable-next-line no-console
74
+ console.log(err);
75
+ }
76
+ }, 500),
77
+ [setValue]
78
+ );
79
+
80
+ // Sync JSON changes to form when in JSON mode
81
+ useEffect(() => {
82
+ if (mode === 'json') {
83
+ debouncedSyncToForm(jsonValue);
84
+ }
85
+ }, [jsonValue, mode, debouncedSyncToForm]);
86
+
87
+ // Cleanup debounce on unmount
88
+ useEffect(() => {
89
+ return () => {
90
+ debouncedSyncToForm.cancel();
91
+ };
92
+ }, [debouncedSyncToForm]);
93
+
28
94
  useEffect(() => {
29
95
  if (!initRef.current) {
30
96
  initRef.current = true;
@@ -41,99 +107,229 @@ export default function MetadataForm({
41
107
  }
42
108
  }, [errors.metadata, errorRef]);
43
109
 
44
- return (
45
- <Box sx={{ width: 1, height: '100%', display: 'flex', flexDirection: 'column' }}>
46
- {!!title && <Typography>{title}</Typography>}
47
- <Stack
110
+ const handleModeChange = (newMode: 'form' | 'json') => {
111
+ if (newMode === 'json' && mode === 'form') {
112
+ // 从表单模式切换到JSON模式,同步数据
113
+ const formData = getValues();
114
+ if (Array.isArray(formData.metadata)) {
115
+ const metadataObj = formData.metadata.reduce((acc: any, x: any) => {
116
+ if (x.key) {
117
+ acc[x.key] = isObjectContent(x.value) ? JSON.parse(x.value) : x.value;
118
+ }
119
+ return acc;
120
+ }, {});
121
+ setJsonValue(JSON.stringify(metadataObj, null, 2));
122
+ }
123
+ }
124
+ setMode(newMode);
125
+ };
126
+
127
+ const handleJsonChange = (value: string) => {
128
+ setJsonValue(value);
129
+ // 清除格式化错误状态
130
+ if (formatError) {
131
+ setFormatError(null);
132
+ }
133
+ };
134
+
135
+ const handleFormatJson = () => {
136
+ try {
137
+ const parsed = JSON.parse(jsonValue);
138
+ const formatted = JSON.stringify(parsed, null, 2);
139
+ setJsonValue(formatted);
140
+ setFormatError(null);
141
+ } catch (err) {
142
+ setFormatError(err instanceof Error ? err.message : 'Invalid JSON format');
143
+ }
144
+ };
145
+
146
+ const toggleBtn = showModeToggle && (
147
+ <Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
148
+ <Button
149
+ size="small"
150
+ variant="text"
151
+ color="primary"
48
152
  sx={{
49
- maxHeight: {
50
- xs: 'calc(100vh - 130px)',
51
- md: 400,
52
- },
53
- minHeight,
54
- overflow: 'auto',
55
- pb: 1.5,
56
- mr: -1.5,
57
- pr: 1.5,
58
- flex: 1,
59
- }}>
60
- {metadata.fields.map((meta, index) => (
153
+ color: color === 'inherit' ? 'text.secondary' : 'primary.main',
154
+ }}
155
+ onClick={() => handleModeChange(mode === 'form' ? 'json' : 'form')}
156
+ startIcon={<Autorenew />}>
157
+ {mode === 'form' ? t('common.metadata.jsonMode') : t('common.metadata.formMode')}
158
+ </Button>
159
+ </Box>
160
+ );
161
+ return (
162
+ <Box sx={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column' }}>
163
+ {title ? (
164
+ <Stack sx={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
165
+ <FormLabel sx={{ color: 'text.primary', mt: 0 }}>{title}</FormLabel>
166
+ {toggleBtn}
167
+ </Stack>
168
+ ) : (
169
+ toggleBtn
170
+ )}
171
+ {mode === 'form' ? (
172
+ <>
61
173
  <Stack
62
- key={meta.id}
63
- spacing={2}
64
- direction="row"
65
- ref={index === metadata.fields.length - 1 ? lastItemRef : null}
66
174
  sx={{
67
- mt: 2,
68
- alignItems: 'flex-end',
175
+ maxHeight: {
176
+ xs: showModeToggle ? 'calc(100vh - 180px)' : 'calc(100vh - 130px)',
177
+ md: 400,
178
+ },
179
+ minHeight,
180
+ overflow: 'auto',
181
+ pb: 1.5,
182
+ mr: -1.5,
183
+ pr: 1.5,
184
+ flex: 1,
69
185
  }}>
70
- <Stack direction="row" spacing={2} sx={{ flex: 1 }}>
71
- <FormInput
72
- sx={{ flex: 1 }}
73
- errorPosition="right"
74
- size="small"
75
- name={`metadata.${index}.key`}
76
- rules={{
77
- required: t('payment.checkout.required'),
78
- maxLength: {
79
- value: 64,
80
- message: t('common.maxLength', { len: 40 }),
81
- },
82
- }}
83
- placeholder="Key"
84
- label="Key"
85
- // @ts-ignore
86
- ref={errors?.metadata?.[index]?.key ? errorRef : null}
87
- inputProps={{
88
- maxLength: 40,
89
- }}
90
- />
91
- <FormInput
92
- sx={{ flex: 2 }}
93
- size="small"
94
- errorPosition="right"
95
- name={`metadata.${index}.value`}
96
- placeholder="Value"
97
- rules={{
98
- required: t('payment.checkout.required'),
99
- maxLength: {
100
- value: 256,
101
- message: t('common.maxLength', { len: 256 }),
102
- },
103
- }}
104
- label="Value"
105
- // @ts-ignore
106
- ref={errors?.metadata?.[index]?.value ? errorRef : null}
107
- inputProps={{
108
- maxLength: 256,
186
+ {metadata.fields.map((meta, index) => (
187
+ <Stack
188
+ key={meta.id}
189
+ ref={index === metadata.fields.length - 1 ? lastItemRef : null}
190
+ sx={{
191
+ mt: 2,
192
+ spacing: 2,
193
+
194
+ alignItems: 'flex-end',
109
195
  }}
110
- />
111
- </Stack>
112
- <IconButton
113
- onClick={() => metadata.remove(index)}
196
+ spacing={2}
197
+ direction="row">
198
+ <Stack direction="row" spacing={2} sx={{ flex: 1 }}>
199
+ <FormInput
200
+ sx={{ flex: 1 }}
201
+ errorPosition="right"
202
+ size="small"
203
+ name={`metadata.${index}.key`}
204
+ rules={{
205
+ required: t('payment.checkout.required'),
206
+ maxLength: {
207
+ value: 64,
208
+ message: t('common.maxLength', { len: 40 }),
209
+ },
210
+ }}
211
+ placeholder="Key"
212
+ label="Key"
213
+ // @ts-ignore
214
+ ref={errors?.metadata?.[index]?.key ? errorRef : null}
215
+ inputProps={{
216
+ maxLength: 40,
217
+ }}
218
+ />
219
+ <FormInput
220
+ sx={{ flex: 2 }}
221
+ size="small"
222
+ errorPosition="right"
223
+ name={`metadata.${index}.value`}
224
+ placeholder="Value"
225
+ rules={{
226
+ required: t('payment.checkout.required'),
227
+ maxLength: {
228
+ value: 256,
229
+ message: t('common.maxLength', { len: 256 }),
230
+ },
231
+ }}
232
+ label="Value"
233
+ // @ts-ignore
234
+ ref={errors?.metadata?.[index]?.value ? errorRef : null}
235
+ inputProps={{
236
+ maxLength: 256,
237
+ }}
238
+ />
239
+ </Stack>
240
+ <IconButton
241
+ onClick={() => metadata.remove(index)}
242
+ sx={{
243
+ border: '1px solid',
244
+ borderColor: 'grey.100',
245
+ borderRadius: 1,
246
+ padding: '8px',
247
+ }}>
248
+ <DeleteOutlineOutlined color="error" sx={{ opacity: 0.75 }} />
249
+ </IconButton>
250
+ </Stack>
251
+ ))}
252
+ </Stack>
253
+
254
+ <Divider />
255
+ <Stack
256
+ direction="row"
257
+ sx={{
258
+ mt: metadata.fields.length ? 2 : 1,
259
+ justifyContent: 'space-between',
260
+ }}>
261
+ <Button
262
+ size="small"
263
+ variant="outlined"
264
+ color="primary"
114
265
  sx={{
115
- border: '1px solid',
116
- borderColor: 'grey.100',
117
- borderRadius: 1,
118
- padding: '8px',
119
- }}>
120
- <DeleteOutlineOutlined color="error" sx={{ opacity: 0.75 }} />
121
- </IconButton>
266
+ color: color === 'inherit' ? 'text.primary' : 'primary.main',
267
+ }}
268
+ onClick={() => metadata.append({ key: '', value: '' })}>
269
+ <AddOutlined fontSize="small" /> {t('common.metadata.add')}
270
+ </Button>
271
+ {actions}
122
272
  </Stack>
123
- ))}
124
- </Stack>
125
- <Divider />
126
- <Stack
127
- direction="row"
128
- sx={{
129
- mt: metadata.fields.length ? 2 : 1,
130
- justifyContent: 'space-between',
131
- }}>
132
- <Button size="small" variant="outlined" color="primary" onClick={() => metadata.append({ key: '', value: '' })}>
133
- <AddOutlined fontSize="small" /> {t('common.metadata.add')}
134
- </Button>
135
- {actions}
136
- </Stack>
273
+ </>
274
+ ) : (
275
+ <>
276
+ <TextField
277
+ multiline
278
+ fullWidth
279
+ rows={15}
280
+ value={jsonValue}
281
+ onChange={(e) => handleJsonChange(e.target.value)}
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',
305
+ }}>
306
+ <FormatAlignLeft fontSize="small" />
307
+ </IconButton>
308
+ </Tooltip>
309
+ </InputAdornment>
310
+ ),
311
+ }}
312
+ sx={{
313
+ mb: 2,
314
+ mt: 2,
315
+ flex: 1,
316
+ '& .MuiInputBase-input': {
317
+ fontFamily: 'monospace',
318
+ fontSize: '14px',
319
+ },
320
+ }}
321
+ />
322
+ <Divider />
323
+ <Stack
324
+ direction="row"
325
+ sx={{
326
+ mt: 2,
327
+ justifyContent: 'flex-end',
328
+ }}>
329
+ {actions}
330
+ </Stack>
331
+ </>
332
+ )}
137
333
  </Box>
138
334
  );
139
335
  }
@@ -0,0 +1,101 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import Toast from '@arcblock/ux/lib/Toast';
3
+ import { ConfirmDialog, api, formatError } from '@blocklet/payment-react';
4
+ import { useSetState } from 'ahooks';
5
+ import { useNavigate } from 'react-router-dom';
6
+ import type { LiteralUnion } from 'type-fest';
7
+ import type { TMeter } from '@blocklet/payment-types';
8
+ import Actions from '../actions';
9
+ import ClickBoundary from '../click-boundary';
10
+
11
+ interface MeterActionsProps {
12
+ data: TMeter;
13
+ variant?: LiteralUnion<'compact' | 'normal', string>;
14
+ onChange: (action?: string) => void;
15
+ }
16
+
17
+ export default function MeterActions({ data, variant = 'compact', onChange }: MeterActionsProps) {
18
+ const { t } = useLocaleContext();
19
+ const navigate = useNavigate();
20
+ const [state, setState] = useSetState({
21
+ action: '',
22
+ loading: false,
23
+ });
24
+
25
+ const createHandler = (action: string) => {
26
+ return async () => {
27
+ try {
28
+ setState({ loading: true });
29
+
30
+ if (action === 'delete') {
31
+ await api.delete(`/api/meters/${data.id}`);
32
+ } else {
33
+ await api.put(`/api/meters/${data.id}/${action}`);
34
+ }
35
+
36
+ if (action === 'activate') {
37
+ Toast.success(t('admin.meter.activated'));
38
+ } else if (action === 'deactivate') {
39
+ Toast.success(t('admin.meter.deactivated'));
40
+ } else if (action === 'delete') {
41
+ Toast.success(t('admin.meter.deleted'));
42
+ }
43
+
44
+ onChange(action);
45
+ } catch (err) {
46
+ console.error(err);
47
+ Toast.error(formatError(err));
48
+ } finally {
49
+ setState({ loading: false, action: '' });
50
+ }
51
+ };
52
+ };
53
+
54
+ const handleCancel = () => {
55
+ setState({ action: '' });
56
+ };
57
+
58
+ const actions = [
59
+ {
60
+ label: data.status === 'inactive' ? t('admin.meter.activate') : t('admin.meter.deactivate'),
61
+ handler: () => setState({ action: data.status === 'inactive' ? 'activate' : 'deactivate' }),
62
+ color: data.status === 'inactive' ? 'primary' : 'error',
63
+ },
64
+ ];
65
+
66
+ if (variant === 'compact') {
67
+ actions.push({
68
+ label: t('admin.meter.view'),
69
+ handler: () => navigate(`/admin/billing/meters/${data.id}`),
70
+ color: 'primary',
71
+ });
72
+ }
73
+
74
+ return (
75
+ <ClickBoundary>
76
+ <Actions variant={variant} actions={actions} />
77
+
78
+ {state.action === 'activate' && (
79
+ <ConfirmDialog
80
+ onConfirm={createHandler('activate')}
81
+ onCancel={handleCancel}
82
+ title={t('admin.meter.activate')}
83
+ message={t('admin.meter.activateConfirm')}
84
+ loading={state.loading}
85
+ color="primary"
86
+ />
87
+ )}
88
+
89
+ {state.action === 'deactivate' && (
90
+ <ConfirmDialog
91
+ onConfirm={createHandler('deactivate')}
92
+ onCancel={handleCancel}
93
+ title={t('admin.meter.deactivate')}
94
+ message={t('admin.meter.deactivateConfirm')}
95
+ loading={state.loading}
96
+ color="warning"
97
+ />
98
+ )}
99
+ </ClickBoundary>
100
+ );
101
+ }