payment-kit 1.18.3 → 1.18.5

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 (31) hide show
  1. package/api/src/integrations/stripe/setup.ts +27 -0
  2. package/api/src/routes/payment-methods.ts +28 -2
  3. package/api/src/routes/settings.ts +251 -2
  4. package/api/src/store/migrations/20250211-setting.ts +10 -0
  5. package/api/src/store/migrations/20250214-setting-component.ts +22 -0
  6. package/api/src/store/models/index.ts +6 -1
  7. package/api/src/store/models/payment-method.ts +12 -6
  8. package/api/src/store/models/setting.ts +84 -0
  9. package/api/src/store/models/types.ts +2 -0
  10. package/blocklet.yml +13 -1
  11. package/package.json +8 -8
  12. package/src/app.tsx +6 -1
  13. package/src/components/invoice/list.tsx +1 -3
  14. package/src/components/payment-intent/list.tsx +5 -6
  15. package/src/components/payouts/list.tsx +3 -3
  16. package/src/components/refund/list.tsx +3 -6
  17. package/src/components/subscription/list.tsx +1 -2
  18. package/src/locales/en.tsx +100 -1
  19. package/src/locales/zh.tsx +96 -0
  20. package/src/pages/admin/customers/customers/index.tsx +27 -16
  21. package/src/pages/admin/developers/webhooks/index.tsx +14 -12
  22. package/src/pages/admin/products/links/index.tsx +22 -15
  23. package/src/pages/admin/products/pricing-tables/index.tsx +16 -14
  24. package/src/pages/admin/products/products/index.tsx +19 -15
  25. package/src/pages/admin/settings/payment-methods/edit.tsx +3 -0
  26. package/src/pages/home.tsx +1 -1
  27. package/src/pages/integrations/donations/edit-form.tsx +349 -0
  28. package/src/pages/integrations/donations/index.tsx +360 -0
  29. package/src/pages/integrations/donations/preview.tsx +229 -0
  30. package/src/pages/integrations/index.tsx +80 -0
  31. package/src/pages/integrations/overview.tsx +121 -0
@@ -8,6 +8,7 @@ import { useEffect, useState } from 'react';
8
8
  import { Link } from 'react-router-dom';
9
9
  import useBus from 'use-bus';
10
10
 
11
+ import { useLocalStorageState } from 'ahooks';
11
12
  import FilterToolbar from '../../../../components/filter-toolbar';
12
13
  import InfoCard from '../../../../components/info-card';
13
14
  import ProductActions from '../../../../components/product/actions';
@@ -43,11 +44,13 @@ export default function ProductsList() {
43
44
 
44
45
  const { t, locale } = useLocaleContext();
45
46
  const { settings } = usePaymentContext();
46
- const [search, setSearch] = useState<SearchProps>({
47
- active: '',
48
- pageSize: persisted.rowsPerPage || 20,
49
- page: persisted.page ? persisted.page + 1 : 1,
50
- donation: 'hide',
47
+ const [search, setSearch] = useLocalStorageState<SearchProps>(listKey, {
48
+ defaultValue: {
49
+ active: '',
50
+ pageSize: persisted.rowsPerPage || 20,
51
+ page: persisted.page ? persisted.page + 1 : 1,
52
+ donation: 'hide',
53
+ },
51
54
  });
52
55
 
53
56
  const [data, setData] = useState({}) as any;
@@ -142,10 +145,10 @@ export default function ProductsList() {
142
145
  ];
143
146
 
144
147
  const onTableChange = ({ page, rowsPerPage }: any) => {
145
- if (search.pageSize !== rowsPerPage) {
146
- setSearch((x) => ({ ...x, pageSize: rowsPerPage, page: 1 }));
147
- } else if (search.page !== page + 1) {
148
- setSearch((x) => ({ ...x, page: page + 1 }));
148
+ if (search!.pageSize !== rowsPerPage) {
149
+ setSearch((x: any) => ({ ...x, pageSize: rowsPerPage, page: 1 }));
150
+ } else if (search!.page !== page + 1) {
151
+ setSearch((x: any) => ({ ...x, page: page + 1 }));
149
152
  }
150
153
  };
151
154
  return (
@@ -165,31 +168,32 @@ export default function ProductsList() {
165
168
  columns={columns}
166
169
  options={{
167
170
  count: data.count,
168
- page: search.page - 1,
169
- rowsPerPage: search.pageSize,
171
+ page: search!.page - 1,
172
+ rowsPerPage: search!.pageSize,
170
173
  onColumnSortChange(_: any, order: any) {
171
174
  setSearch({
172
- ...search,
173
- q: search.q || {},
175
+ ...search!,
176
+ q: search!.q || {},
174
177
  o: order,
175
178
  });
176
179
  },
177
180
  onSearchChange: (text: string) => {
178
181
  if (text) {
179
182
  setSearch({
183
+ ...search!,
180
184
  q: {
181
185
  'like-description': text,
182
186
  'like-name': text,
183
187
  },
184
- active: '',
185
188
  pageSize: 100,
186
189
  page: 1,
187
190
  });
188
191
  } else {
189
192
  setSearch({
190
- active: '',
193
+ ...search!,
191
194
  pageSize: 100,
192
195
  page: 1,
196
+ q: {},
193
197
  });
194
198
  }
195
199
  },
@@ -73,6 +73,9 @@ export default function PaymentMethodEdit({ onClose, value }: { onClose: () => v
73
73
  'settings.base.explorer_host',
74
74
  'settings.ethereum.api_host',
75
75
  'settings.ethereum.explorer_host',
76
+ 'settings.stripe.publishable_key',
77
+ 'settings.stripe.secret_key',
78
+ 'settings.stripe.dashboard',
76
79
  ]}
77
80
  />
78
81
  </FormProvider>
@@ -36,7 +36,7 @@ function Home() {
36
36
  </Typography>
37
37
  </Stack>
38
38
  <Stack direction="row" spacing={3}>
39
- <Button variant="outlined" color="secondary" size="large" component={Link} to="/admin/overview">
39
+ <Button variant="outlined" color="secondary" size="large" component={Link} to="/integrations">
40
40
  Admin Dashboard
41
41
  </Button>
42
42
  <Button
@@ -0,0 +1,349 @@
1
+ import { useForm, Controller, FormProvider } from 'react-hook-form';
2
+ import {
3
+ Box,
4
+ Grid,
5
+ TextField,
6
+ Switch,
7
+ Button,
8
+ Typography,
9
+ Stack,
10
+ Paper,
11
+ Divider,
12
+ ToggleButtonGroup,
13
+ ToggleButton,
14
+ } from '@mui/material';
15
+ import { api, formatError, clearDonateCache } from '@blocklet/payment-react';
16
+ import { Toast } from '@arcblock/ux';
17
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
18
+ import type { TSetting } from '@blocklet/payment-types';
19
+ import { useState, useEffect, useRef } from 'react';
20
+ import DonationPreview from './preview';
21
+
22
+ type FormData = {
23
+ settings: {
24
+ btnText: string;
25
+ amount: {
26
+ presets: string[];
27
+ preset: string;
28
+ minimum: string;
29
+ maximum: string;
30
+ custom: boolean;
31
+ };
32
+ historyType: 'avatar' | 'table';
33
+ };
34
+ };
35
+
36
+ function NumberArrayInput({ value, onChange }: { value: string[]; onChange: (value: string[]) => void }) {
37
+ const { t } = useLocaleContext();
38
+ const [inputValue, setInputValue] = useState(value.join(', '));
39
+ const isSelfUpdate = useRef(false); // 新增标志位
40
+
41
+ useEffect(() => {
42
+ if (isSelfUpdate.current) {
43
+ isSelfUpdate.current = false;
44
+ return;
45
+ }
46
+ setInputValue(value.join(', '));
47
+ }, [value]);
48
+
49
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
50
+ const { value: val } = e.target;
51
+
52
+ if (!/^[\d,.\s]*$/.test(val)) return;
53
+
54
+ setInputValue(val);
55
+
56
+ // 添加处理空字符串的情况
57
+ const rawValues = val.split(/[,\s]+/).filter(Boolean);
58
+ const numbers = rawValues
59
+ .map((v) => v.trim())
60
+ .filter((v) => !Number.isNaN(Number(v)))
61
+ .map(Number)
62
+ .filter((v, i, arr) => arr.indexOf(v) === i);
63
+
64
+ isSelfUpdate.current = true; // 标记为内部更新
65
+ onChange(numbers.map(String));
66
+ };
67
+
68
+ return (
69
+ <TextField
70
+ fullWidth
71
+ label={t('admin.donate.presets')}
72
+ helperText={t('admin.donate.presetsHelper')}
73
+ value={inputValue}
74
+ onChange={handleChange}
75
+ placeholder="1, 5, 10"
76
+ sx={{ bgcolor: 'background.paper' }}
77
+ onBlur={() => {
78
+ // 失焦时强制同步显示格式
79
+ const formatted = (inputValue.match(/[\d.]+/g) || []).join(', ');
80
+ setInputValue(formatted);
81
+ }}
82
+ />
83
+ );
84
+ }
85
+
86
+ export default function EditDonationForm({
87
+ item,
88
+ onSuccess,
89
+ onCancel,
90
+ }: {
91
+ item: TSetting;
92
+ onSuccess: () => void;
93
+ onCancel: () => void;
94
+ }) {
95
+ const { t } = useLocaleContext();
96
+ const methods = useForm<FormData>({
97
+ defaultValues: {
98
+ settings: {
99
+ amount: {
100
+ presets: item?.settings?.amount?.presets,
101
+ preset: item?.settings?.amount?.preset,
102
+ minimum: item?.settings?.amount?.minimum,
103
+ maximum: item?.settings?.amount?.maximum,
104
+ custom: item?.settings?.amount?.custom,
105
+ },
106
+ btnText: item?.settings?.btnText,
107
+ historyType: item?.settings?.historyType,
108
+ },
109
+ },
110
+ });
111
+
112
+ const { handleSubmit, watch } = methods;
113
+ const formValues = watch();
114
+
115
+ const onSubmit = async (data: FormData) => {
116
+ if (data.settings.amount.presets?.length > 0 && data.settings.amount.preset) {
117
+ const inPresets = data.settings.amount.presets.includes(data.settings.amount.preset);
118
+ if (!inPresets) {
119
+ Toast.error(t('admin.donate.validation.invalidPreset'));
120
+ return;
121
+ }
122
+ }
123
+ try {
124
+ await api.put(`/api/settings/${item.id}`, data);
125
+ clearDonateCache(item.mount_location);
126
+ Toast.success(t('admin.donate.updateSuccess'));
127
+ onSuccess();
128
+ } catch (err) {
129
+ Toast.error(formatError(err));
130
+ }
131
+ };
132
+
133
+ return (
134
+ <Box
135
+ sx={{
136
+ display: 'flex',
137
+ flexDirection: 'column',
138
+ height: '100%',
139
+ minHeight: '100vh',
140
+ }}>
141
+ <Box
142
+ sx={{
143
+ flex: 1,
144
+ overflow: {
145
+ xs: 'visible',
146
+ md: 'auto',
147
+ },
148
+ pb: 2,
149
+ display: 'flex',
150
+ flexDirection: 'column',
151
+ }}>
152
+ <FormProvider {...methods}>
153
+ <Grid
154
+ container
155
+ spacing={3}
156
+ sx={{
157
+ flex: 1,
158
+ minHeight: 0,
159
+ }}>
160
+ <Grid
161
+ item
162
+ xs={12}
163
+ md={6}
164
+ sx={{
165
+ height: '100%',
166
+ }}>
167
+ <Paper
168
+ elevation={0}
169
+ sx={{
170
+ p: { xs: 2, md: 3 },
171
+ bgcolor: 'background.neutral',
172
+ height: '100%',
173
+ overflow: 'auto',
174
+ }}>
175
+ <Stack spacing={4}>
176
+ <Box>
177
+ <Typography variant="h6" gutterBottom color="text.primary" sx={{ mb: 3 }}>
178
+ {t('admin.donate.buttonSettings')}
179
+ </Typography>
180
+ <Stack spacing={3}>
181
+ <Controller
182
+ name="settings.btnText"
183
+ render={({ field }) => (
184
+ <TextField
185
+ {...field}
186
+ fullWidth
187
+ label={t('admin.donate.btn.text')}
188
+ helperText={t('admin.donate.btn.helper')}
189
+ sx={{ bgcolor: 'background.paper' }}
190
+ />
191
+ )}
192
+ />
193
+
194
+ <Stack gap={1}>
195
+ <Typography variant="subtitle2" gutterBottom>
196
+ {t('admin.donate.historyType.title')}
197
+ </Typography>
198
+ <Controller
199
+ name="settings.historyType"
200
+ render={({ field }) => (
201
+ <ToggleButtonGroup
202
+ value={field.value}
203
+ exclusive
204
+ onChange={(_, value) => field.onChange(value)}
205
+ size="small"
206
+ sx={{ bgcolor: 'background.paper' }}>
207
+ <ToggleButton value="table" size="small">
208
+ {t('admin.donate.historyType.list')}
209
+ </ToggleButton>
210
+ <ToggleButton value="avatar" size="small">
211
+ {t('admin.donate.historyType.avatar')}
212
+ </ToggleButton>
213
+ </ToggleButtonGroup>
214
+ )}
215
+ />
216
+ </Stack>
217
+ </Stack>
218
+ </Box>
219
+
220
+ <Divider />
221
+
222
+ <Box>
223
+ <Typography variant="h6" gutterBottom color="text.primary" sx={{ mb: 3 }}>
224
+ {t('admin.donate.amountSettings')}
225
+ </Typography>
226
+ <Stack spacing={3}>
227
+ <Controller
228
+ control={methods.control}
229
+ name="settings.amount.presets"
230
+ render={({ field }) => <NumberArrayInput value={field.value || []} onChange={field.onChange} />}
231
+ />
232
+ <Controller
233
+ name="settings.amount.preset"
234
+ render={({ field }) => (
235
+ <TextField
236
+ {...field}
237
+ fullWidth
238
+ label={t('admin.donate.preset')}
239
+ helperText={t('admin.donate.presetHelper')}
240
+ sx={{ bgcolor: 'background.paper' }}
241
+ />
242
+ )}
243
+ />
244
+
245
+ <Box
246
+ sx={{
247
+ display: 'flex',
248
+ alignItems: 'center',
249
+ gap: 2,
250
+ }}>
251
+ <Typography variant="subtitle2" gutterBottom>
252
+ {t('admin.donate.custom')}
253
+ </Typography>
254
+ <Controller
255
+ name="settings.amount.custom"
256
+ render={({ field }) => (
257
+ <Switch
258
+ checked={field.value}
259
+ onChange={(e) => field.onChange(e.target.checked)}
260
+ size="small"
261
+ />
262
+ )}
263
+ />
264
+ </Box>
265
+
266
+ {formValues.settings.amount.custom && (
267
+ <Stack spacing={2} sx={{ mt: 2 }}>
268
+ <Controller
269
+ name="settings.amount.minimum"
270
+ render={({ field }) => (
271
+ <TextField
272
+ {...field}
273
+ fullWidth
274
+ label={t('admin.donate.minimum')}
275
+ type="number"
276
+ sx={{ bgcolor: 'background.paper' }}
277
+ />
278
+ )}
279
+ />
280
+ <Controller
281
+ name="settings.amount.maximum"
282
+ render={({ field }) => (
283
+ <TextField
284
+ {...field}
285
+ fullWidth
286
+ label={t('admin.donate.maximum')}
287
+ type="number"
288
+ sx={{ bgcolor: 'background.paper' }}
289
+ />
290
+ )}
291
+ />
292
+ </Stack>
293
+ )}
294
+ </Stack>
295
+ </Box>
296
+ </Stack>
297
+ </Paper>
298
+ </Grid>
299
+
300
+ <Grid
301
+ item
302
+ md={6}
303
+ sx={{
304
+ display: { xs: 'none', md: 'block' },
305
+ height: '100%',
306
+ }}>
307
+ <Paper
308
+ elevation={0}
309
+ sx={{
310
+ p: 3,
311
+ bgcolor: 'background.neutral',
312
+ height: '100%',
313
+ overflow: 'auto',
314
+ }}>
315
+ <DonationPreview config={methods.watch()} />
316
+ </Paper>
317
+ </Grid>
318
+ </Grid>
319
+ </FormProvider>
320
+ </Box>
321
+
322
+ <Box
323
+ sx={{
324
+ mt: 2,
325
+ pt: 2,
326
+ pb: 2,
327
+ px: 2,
328
+ borderTop: 1,
329
+ borderColor: 'divider',
330
+ display: 'flex',
331
+ justifyContent: 'flex-end',
332
+ gap: 2,
333
+ bgcolor: 'background.paper',
334
+ position: 'sticky',
335
+ bottom: 0,
336
+ left: 0,
337
+ right: 0,
338
+ zIndex: 1,
339
+ }}>
340
+ <Button onClick={onCancel} variant="outlined">
341
+ {t('common.cancel')}
342
+ </Button>
343
+ <Button variant="contained" onClick={handleSubmit(onSubmit)}>
344
+ {t('common.save')}
345
+ </Button>
346
+ </Box>
347
+ </Box>
348
+ );
349
+ }