payment-kit 1.16.17 → 1.16.18

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 (66) hide show
  1. package/api/src/crons/index.ts +1 -1
  2. package/api/src/hooks/pre-start.ts +2 -0
  3. package/api/src/index.ts +2 -0
  4. package/api/src/integrations/arcblock/stake.ts +7 -1
  5. package/api/src/integrations/stripe/resource.ts +1 -1
  6. package/api/src/libs/env.ts +12 -0
  7. package/api/src/libs/event.ts +8 -0
  8. package/api/src/libs/invoice.ts +585 -3
  9. package/api/src/libs/notification/template/subscription-succeeded.ts +1 -2
  10. package/api/src/libs/notification/template/subscription-trial-will-end.ts +2 -2
  11. package/api/src/libs/notification/template/subscription-will-renew.ts +6 -2
  12. package/api/src/libs/notification/template/subscription.overdraft-protection.exhausted.ts +139 -0
  13. package/api/src/libs/overdraft-protection.ts +85 -0
  14. package/api/src/libs/payment.ts +1 -65
  15. package/api/src/libs/queue/index.ts +0 -1
  16. package/api/src/libs/subscription.ts +532 -2
  17. package/api/src/libs/util.ts +4 -0
  18. package/api/src/locales/en.ts +5 -0
  19. package/api/src/locales/zh.ts +5 -0
  20. package/api/src/queues/event.ts +3 -2
  21. package/api/src/queues/invoice.ts +28 -3
  22. package/api/src/queues/notification.ts +25 -3
  23. package/api/src/queues/payment.ts +154 -3
  24. package/api/src/queues/refund.ts +2 -2
  25. package/api/src/queues/subscription.ts +215 -4
  26. package/api/src/queues/webhook.ts +1 -0
  27. package/api/src/routes/connect/change-payment.ts +1 -1
  28. package/api/src/routes/connect/change-plan.ts +1 -1
  29. package/api/src/routes/connect/overdraft-protection.ts +120 -0
  30. package/api/src/routes/connect/recharge.ts +2 -1
  31. package/api/src/routes/connect/setup.ts +1 -1
  32. package/api/src/routes/connect/shared.ts +117 -350
  33. package/api/src/routes/connect/subscribe.ts +1 -1
  34. package/api/src/routes/customers.ts +2 -2
  35. package/api/src/routes/invoices.ts +9 -4
  36. package/api/src/routes/subscriptions.ts +172 -2
  37. package/api/src/store/migrate.ts +9 -10
  38. package/api/src/store/migrations/20240905-index.ts +95 -60
  39. package/api/src/store/migrations/20241203-overdraft-protection.ts +25 -0
  40. package/api/src/store/migrations/20241216-update-overdraft-protection.ts +30 -0
  41. package/api/src/store/models/customer.ts +2 -2
  42. package/api/src/store/models/invoice.ts +7 -0
  43. package/api/src/store/models/lock.ts +7 -0
  44. package/api/src/store/models/subscription.ts +15 -0
  45. package/api/src/store/sequelize.ts +6 -1
  46. package/blocklet.yml +1 -1
  47. package/package.json +23 -23
  48. package/src/components/customer/overdraft-protection.tsx +367 -0
  49. package/src/components/event/list.tsx +3 -4
  50. package/src/components/subscription/actions/cancel.tsx +3 -0
  51. package/src/components/subscription/portal/actions.tsx +324 -77
  52. package/src/components/uploader.tsx +31 -26
  53. package/src/env.d.ts +1 -0
  54. package/src/hooks/subscription.ts +30 -0
  55. package/src/libs/env.ts +4 -0
  56. package/src/locales/en.tsx +41 -0
  57. package/src/locales/zh.tsx +37 -0
  58. package/src/pages/admin/billing/invoices/detail.tsx +16 -15
  59. package/src/pages/customer/index.tsx +7 -2
  60. package/src/pages/customer/invoice/detail.tsx +29 -5
  61. package/src/pages/customer/invoice/past-due.tsx +18 -4
  62. package/src/pages/customer/recharge.tsx +2 -4
  63. package/src/pages/customer/subscription/change-payment.tsx +7 -1
  64. package/src/pages/customer/subscription/detail.tsx +69 -51
  65. package/tsconfig.json +0 -5
  66. package/api/tests/libs/payment.spec.ts +0 -168
@@ -1,16 +1,27 @@
1
1
  /* eslint-disable react/no-unstable-nested-components */
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import Toast from '@arcblock/ux/lib/Toast';
4
- import { ConfirmDialog, api, formatError, formatToDate, getSubscriptionAction } from '@blocklet/payment-react';
4
+ import {
5
+ ConfirmDialog,
6
+ api,
7
+ formatError,
8
+ formatToDate,
9
+ getPrefix,
10
+ getSubscriptionAction,
11
+ usePaymentContext,
12
+ } from '@blocklet/payment-react';
5
13
  import type { TSubscriptionExpanded } from '@blocklet/payment-types';
6
- import { Button, Link, Stack } from '@mui/material';
14
+ import { Button, Link, Stack, Tooltip } from '@mui/material';
7
15
  import { useRequest, useSetState } from 'ahooks';
8
16
  import isEmpty from 'lodash/isEmpty';
9
17
  import { useState } from 'react';
10
18
  import { FormProvider, useForm, useFormContext } from 'react-hook-form';
11
19
  import { useNavigate } from 'react-router-dom';
12
-
20
+ import { joinURL } from 'ufo';
13
21
  import CustomerCancelForm from './cancel';
22
+ import OverdraftProtectionDialog from '../../customer/overdraft-protection';
23
+ import Actions from '../../actions';
24
+ import { useUnpaidInvoicesCheckForSubscription } from '../../../hooks/subscription';
14
25
 
15
26
  type ActionProps = {
16
27
  [key: string]: {
@@ -27,17 +38,27 @@ type Props = {
27
38
  subscription: TSubscriptionExpanded;
28
39
  showExtra?: boolean;
29
40
  showRecharge?: boolean;
41
+ showOverdraftProtection?:
42
+ | boolean
43
+ | {
44
+ show: boolean;
45
+ onChange: () => any | Promise<any>;
46
+ };
47
+ showDelegation?: boolean;
30
48
  onChange?: (action?: string) => any | Promise<any>;
31
49
  actionProps?: ActionProps;
50
+ mode?: 'menu' | 'btn';
32
51
  };
33
52
 
34
53
  SubscriptionActions.defaultProps = {
35
54
  showExtra: false,
36
55
  showRecharge: false,
56
+ showOverdraftProtection: false,
57
+ showDelegation: false,
37
58
  onChange: null,
38
59
  actionProps: {},
60
+ mode: 'btn',
39
61
  };
40
-
41
62
  const fetchExtraActions = async ({
42
63
  id,
43
64
  showExtra,
@@ -60,7 +81,6 @@ const fetchExtraActions = async ({
60
81
  if (!isEmpty(res.data) && Object.keys(res.data).length === 1) {
61
82
  return Object.keys(res.data)[0] as string;
62
83
  }
63
-
64
84
  return '';
65
85
  })
66
86
  .catch(() => ''),
@@ -76,11 +96,22 @@ const supportRecharge = (subscription: TSubscriptionExpanded) => {
76
96
  );
77
97
  };
78
98
 
79
- export function SubscriptionActionsInner({ subscription, showExtra, showRecharge, onChange, actionProps }: Props) {
99
+ export function SubscriptionActionsInner({
100
+ subscription,
101
+ showExtra,
102
+ showRecharge,
103
+ showOverdraftProtection,
104
+ showDelegation,
105
+ onChange,
106
+ actionProps,
107
+ mode,
108
+ }: Props) {
80
109
  const { t, locale } = useLocaleContext();
81
110
  const { reset, getValues } = useFormContext();
82
111
  const navigate = useNavigate();
112
+ const { connect } = usePaymentContext();
83
113
  const [subs, setSubscription] = useState(subscription);
114
+ const { checkUnpaidInvoices } = useUnpaidInvoicesCheckForSubscription(subscription.id, true);
84
115
  const action = getSubscriptionAction(subs, actionProps ?? {});
85
116
 
86
117
  const { data: extraActions } = useRequest(() => fetchExtraActions({ id: subscription.id, showExtra: !!showExtra }));
@@ -89,10 +120,39 @@ export function SubscriptionActionsInner({ subscription, showExtra, showRecharge
89
120
  action: '',
90
121
  subscription: '',
91
122
  loading: false,
123
+ openProtection: false,
124
+ protectionLoading: false,
92
125
  });
93
126
 
127
+ const shouldFetchDelegation = showDelegation && ['active', 'trialing', 'past_due'].includes(subscription?.status);
128
+ const shouldFetchOverdraftProtection =
129
+ ((typeof showOverdraftProtection === 'boolean' && showOverdraftProtection) ||
130
+ (typeof showOverdraftProtection === 'object' && showOverdraftProtection.show)) &&
131
+ subscription?.paymentMethod?.type === 'arcblock' &&
132
+ ['active', 'trialing', 'past_due'].includes(subscription?.status);
133
+
134
+ const { data: delegation = { sufficient: true }, refresh: refreshDelegation } = useRequest(
135
+ () => api.get(`/api/subscriptions/${subscription.id}/delegation`).then((res) => res.data),
136
+ {
137
+ ready: shouldFetchDelegation,
138
+ refreshDeps: [subscription.id, shouldFetchDelegation],
139
+ }
140
+ );
141
+ const noDelegation = delegation && typeof delegation === 'object' && !delegation.sufficient;
142
+
143
+ const { data: overdraftProtection = { enabled: false, remaining: '0' }, refresh: refreshOverdraftProtection } =
144
+ useRequest(() => api.get(`/api/subscriptions/${subscription.id}/overdraft-protection`).then((res) => res.data), {
145
+ ready: shouldFetchOverdraftProtection,
146
+ refreshDeps: [subscription.id, shouldFetchOverdraftProtection],
147
+ });
148
+
94
149
  const handleCancel = async () => {
95
150
  try {
151
+ const result = await checkUnpaidInvoices();
152
+ if (result) {
153
+ setState({ action: '' });
154
+ return;
155
+ }
96
156
  setState({ loading: true });
97
157
  const sub = await api
98
158
  .put(`/api/subscriptions/${state.subscription}/cancel`, {
@@ -128,81 +188,252 @@ export function SubscriptionActionsInner({ subscription, showExtra, showRecharge
128
188
  }
129
189
  };
130
190
 
131
- return (
132
- <Stack direction="row" alignItems="center" gap={1} flexWrap="wrap">
133
- {showRecharge && supportRecharge(subscription) && (
134
- <Button
135
- variant="outlined"
136
- color="primary"
137
- onClick={(e) => {
138
- e.stopPropagation();
139
- navigate(`/customer/subscription/${subscription.id}/recharge`);
140
- }}>
141
- {t('customer.recharge.title')}
142
- </Button>
143
- )}
144
- {!extraActions?.batchPay && action && (
145
- <Button
146
- variant={action.variant as any}
147
- color={action.color as any}
148
- size="small"
149
- sx={action?.sx as any}
150
- onClick={(e) => {
151
- e.stopPropagation();
152
- if (action.action === 'pastDue') {
153
- navigate(`/customer/invoice/past-due?subscription=${subscription.id}`);
154
- } else {
155
- setState({
156
- action: action.action,
157
- subscription: subscription.id,
158
- });
159
- }
160
- }}>
161
- {action?.text || t(`payment.customer.${action.action}.button`)}
162
- </Button>
163
- )}
164
- {extraActions?.changePlan && (
165
- <Button
166
- variant="contained"
167
- color="primary"
168
- size="small"
169
- sx={action?.sx as any}
170
- onClick={(e) => {
171
- e.stopPropagation();
172
- navigate(`/customer/subscription/${subscription.id}/change-plan`);
173
- }}>
174
- {action?.text || t('payment.customer.changePlan.button')}
175
- </Button>
176
- )}
177
- {!!extraActions?.batchPay && (
178
- <Button
179
- variant="contained"
180
- color="info"
181
- size="small"
182
- sx={action?.sx as any}
183
- onClick={(e) => {
184
- e.stopPropagation();
185
- navigate(`/customer/invoice/past-due?subscription=${subscription.id}&currency=${extraActions.batchPay}`);
186
- }}>
187
- {action?.text || t('admin.subscription.batchPay.button')}
188
- </Button>
189
- )}
190
- {subscription.service_actions
191
- ?.filter((x: any) => x?.type !== 'notification')
192
- .map((x) => (
191
+ const handleDelegate = () => {
192
+ connect.open({
193
+ containerEl: undefined as unknown as Element,
194
+ saveConnect: false,
195
+ action: 'delegation',
196
+ prefix: joinURL(getPrefix(), '/api/did'),
197
+ extraParams: { subscriptionId: subscription.id },
198
+ onSuccess: () => {
199
+ connect.close();
200
+ Toast.success(t('customer.delegation.success'));
201
+ refreshDelegation();
202
+ },
203
+ onClose: () => {
204
+ connect.close();
205
+ },
206
+ onError: (err: any) => {
207
+ Toast.error(formatError(err));
208
+ },
209
+ });
210
+ };
211
+
212
+ const handleOverdraftProtection = async (formData: any) => {
213
+ try {
214
+ if (!formData.enabled) {
215
+ const result = await checkUnpaidInvoices();
216
+ if (result) {
217
+ setState({ openProtection: false });
218
+ return;
219
+ }
220
+ }
221
+ setState({ protectionLoading: true });
222
+ const result = await api
223
+ .post(`/api/subscriptions/${subscription.id}/overdraft-protection`, formData)
224
+ .then((res) => res.data);
225
+
226
+ const { open, amount } = result;
227
+
228
+ if (formData.return_stake && overdraftProtection.remaining !== '0') {
229
+ Toast.success(t('customer.overdraftProtection.applyRemainingSuccess'));
230
+ setState({ openProtection: false });
231
+ refreshOverdraftProtection();
232
+ // @ts-ignore
233
+ showOverdraftProtection?.onChange?.();
234
+ onChange?.();
235
+ return;
236
+ }
237
+
238
+ if (open) {
239
+ connect.open({
240
+ containerEl: undefined as unknown as Element,
241
+ saveConnect: false,
242
+ action: 'overdraft-protection',
243
+ prefix: joinURL(getPrefix(), '/api/did'),
244
+ extraParams: { subscriptionId: subscription.id, amount },
245
+ onSuccess: () => {
246
+ connect.close();
247
+ Toast.success(t('customer.overdraftProtection.settingSuccess'));
248
+ setState({ openProtection: false });
249
+ refreshOverdraftProtection();
250
+ // @ts-ignore
251
+ showOverdraftProtection?.onChange?.();
252
+ onChange?.();
253
+ },
254
+ onClose: () => {
255
+ connect.close();
256
+ setState({ protectionLoading: false });
257
+ },
258
+ });
259
+ } else {
260
+ Toast.success(t('customer.overdraftProtection.settingSuccess'));
261
+ setState({ openProtection: false });
262
+ refreshOverdraftProtection();
263
+ // @ts-ignore
264
+ showOverdraftProtection?.onChange?.();
265
+ onChange?.();
266
+ }
267
+ } catch (err) {
268
+ Toast.error(formatError(err) || t('customer.overdraftProtection.settingError'));
269
+ throw err;
270
+ } finally {
271
+ setState({ protectionLoading: false });
272
+ }
273
+ };
274
+
275
+ const renderActions = () => {
276
+ if (mode === 'menu') {
277
+ const actions = [
278
+ showDelegation &&
279
+ noDelegation && {
280
+ label: t('customer.delegation.btn'),
281
+ handler: handleDelegate,
282
+ color: 'primary',
283
+ divider: true,
284
+ },
285
+ shouldFetchOverdraftProtection && {
286
+ label: t('customer.overdraftProtection.title'),
287
+ handler: () => setState({ openProtection: true }),
288
+ color: 'primary',
289
+ },
290
+ showRecharge &&
291
+ supportRecharge(subscription) && {
292
+ label: t('customer.recharge.title'),
293
+ handler: () => navigate(`/customer/subscription/${subscription.id}/recharge`),
294
+ color: 'primary',
295
+ divider: true,
296
+ },
297
+ !extraActions?.batchPay &&
298
+ action && {
299
+ label: action?.text || t(`payment.customer.${action.action}.button`),
300
+ handler: () => {
301
+ if (action.action === 'pastDue') {
302
+ navigate(`/customer/invoice/past-due?subscription=${subscription.id}`);
303
+ } else {
304
+ setState({
305
+ action: action.action,
306
+ subscription: subscription.id,
307
+ });
308
+ }
309
+ },
310
+ color: action.color as any,
311
+ },
312
+ extraActions?.changePlan && {
313
+ label: action?.text || t('payment.customer.changePlan.button'),
314
+ handler: () => navigate(`/customer/subscription/${subscription.id}/change-plan`),
315
+ color: 'primary',
316
+ },
317
+ !!extraActions?.batchPay && {
318
+ label: action?.text || t('admin.subscription.batchPay.button'),
319
+ handler: () =>
320
+ navigate(`/customer/invoice/past-due?subscription=${subscription.id}&currency=${extraActions.batchPay}`),
321
+ color: 'error',
322
+ },
323
+ ...(subscription.service_actions
324
+ ?.filter((x: any) => x?.type !== 'notification')
325
+ .map((x) => ({
326
+ label: x.text[locale] || x.text.en || x.name,
327
+ handler: () => {
328
+ window.open(x.link, '_blank');
329
+ },
330
+ color: x?.color || 'primary',
331
+ })) || []),
332
+ ].filter(Boolean);
333
+
334
+ return (
335
+ <Actions
193
336
  // @ts-ignore
337
+ actions={actions}
338
+ />
339
+ );
340
+ }
341
+ return (
342
+ <>
343
+ {showDelegation && noDelegation && (
344
+ <Tooltip title={t('customer.delegation.title')}>
345
+ <Button variant="outlined" color="primary" onClick={handleDelegate}>
346
+ {t('customer.delegation.btn')}
347
+ </Button>
348
+ </Tooltip>
349
+ )}
350
+
351
+ {shouldFetchOverdraftProtection && (
352
+ <Button variant="outlined" color="primary" onClick={() => setState({ openProtection: true })}>
353
+ {t('customer.overdraftProtection.title')}
354
+ </Button>
355
+ )}
356
+
357
+ {showRecharge && supportRecharge(subscription) && (
358
+ <Button
359
+ variant="outlined"
360
+ color="primary"
361
+ onClick={(e) => {
362
+ e.stopPropagation();
363
+ navigate(`/customer/subscription/${subscription.id}/recharge`);
364
+ }}>
365
+ {t('customer.recharge.title')}
366
+ </Button>
367
+ )}
368
+ {!extraActions?.batchPay && action && (
369
+ <Button
370
+ variant={action.variant as any}
371
+ color={action.color as any}
372
+ size="small"
373
+ sx={action?.sx as any}
374
+ onClick={(e) => {
375
+ e.stopPropagation();
376
+ if (action.action === 'pastDue') {
377
+ navigate(`/customer/invoice/past-due?subscription=${subscription.id}`);
378
+ } else {
379
+ setState({
380
+ action: action.action,
381
+ subscription: subscription.id,
382
+ });
383
+ }
384
+ }}>
385
+ {action?.text || t(`payment.customer.${action.action}.button`)}
386
+ </Button>
387
+ )}
388
+ {extraActions?.changePlan && (
389
+ <Button
390
+ variant="contained"
391
+ color="primary"
392
+ size="small"
393
+ sx={action?.sx as any}
394
+ onClick={(e) => {
395
+ e.stopPropagation();
396
+ navigate(`/customer/subscription/${subscription.id}/change-plan`);
397
+ }}>
398
+ {action?.text || t('payment.customer.changePlan.button')}
399
+ </Button>
400
+ )}
401
+ {!!extraActions?.batchPay && (
194
402
  <Button
195
- component={Link}
196
- key={x.name}
197
- variant={x?.variant || 'contained'}
198
- color={x?.color || 'primary'}
199
- href={x.link}
403
+ variant="outlined"
404
+ color="error"
200
405
  size="small"
201
- target="_blank"
202
- sx={{ textDecoration: 'none !important' }}>
203
- {x.text[locale] || x.text.en || x.name}
406
+ sx={action?.sx as any}
407
+ onClick={(e) => {
408
+ e.stopPropagation();
409
+ navigate(`/customer/invoice/past-due?subscription=${subscription.id}&currency=${extraActions.batchPay}`);
410
+ }}>
411
+ {action?.text || t('admin.subscription.batchPay.button')}
204
412
  </Button>
205
- ))}
413
+ )}
414
+ {subscription.service_actions
415
+ ?.filter((x: any) => x?.type !== 'notification')
416
+ .map((x) => (
417
+ // @ts-ignore
418
+ <Button
419
+ component={Link}
420
+ key={x.name}
421
+ variant={x?.variant || 'contained'}
422
+ color={x?.color || 'primary'}
423
+ href={x.link}
424
+ size="small"
425
+ target="_blank"
426
+ sx={{ textDecoration: 'none !important' }}>
427
+ {x.text[locale] || x.text.en || x.name}
428
+ </Button>
429
+ ))}
430
+ </>
431
+ );
432
+ };
433
+
434
+ return (
435
+ <Stack direction="row" alignItems="center" gap={1} flexWrap="wrap">
436
+ {renderActions()}
206
437
  {state.action === 'cancel' && state.subscription && (
207
438
  <ConfirmDialog
208
439
  onConfirm={handleCancel}
@@ -229,6 +460,19 @@ export function SubscriptionActionsInner({ subscription, showExtra, showRecharge
229
460
  loading={state.loading}
230
461
  />
231
462
  )}
463
+
464
+ {state.openProtection && (
465
+ <OverdraftProtectionDialog
466
+ value={overdraftProtection}
467
+ onSave={handleOverdraftProtection}
468
+ open={state.openProtection}
469
+ paymentAddress={subscription.overdraft_protection?.payment_details?.arcblock?.payer}
470
+ currency={subscription.paymentCurrency}
471
+ subscription={subscription}
472
+ loading={state.protectionLoading}
473
+ onCancel={() => setState({ openProtection: false })}
474
+ />
475
+ )}
232
476
  </Stack>
233
477
  );
234
478
  }
@@ -254,6 +498,9 @@ export default function SubscriptionActions(props: Props) {
254
498
  SubscriptionActionsInner.defaultProps = {
255
499
  showExtra: false,
256
500
  showRecharge: false,
501
+ showOverdraftProtection: false,
502
+ showDelegation: false,
257
503
  onChange: null,
258
504
  actionProps: {},
505
+ mode: 'btn',
259
506
  };
@@ -2,6 +2,7 @@ import { Avatar, Box, Button } from '@mui/material';
2
2
  import { Stack, styled } from '@mui/system';
3
3
  import { lazy, Suspense, useCallback, useEffect, useRef } from 'react';
4
4
  import { CloudUpload, Delete, Edit } from '@mui/icons-material';
5
+ import { createPortal } from 'react-dom';
5
6
 
6
7
  const UploaderComponent = lazy(() =>
7
8
  import(/* webpackChunkName: "blocklet-uploader" */ '@blocklet/uploader').then((res) => ({
@@ -37,6 +38,35 @@ export default function Uploader({ onUploaded, preview, maxFileSize, maxNumberOf
37
38
  }
38
39
  }, [onUploaded]);
39
40
 
41
+ const uploaderPortal = (
42
+ <Suspense fallback={null}>
43
+ <UploaderComponent
44
+ // @ts-ignore
45
+ ref={uploaderRef}
46
+ popup
47
+ onUploadFinish={(result: any) => onUploaded({ url: result.data.url })}
48
+ uploadedProps={{
49
+ onSelectedFiles: (files: any[]) => {
50
+ if (files.length) {
51
+ onUploaded({ url: files[0].fileUrl });
52
+ }
53
+ },
54
+ }}
55
+ coreProps={{
56
+ restrictions: {
57
+ allowedFileExts,
58
+ maxFileSize,
59
+ maxNumberOfFiles,
60
+ },
61
+ }}
62
+ apiPathProps={{
63
+ uploader: '/api/uploads',
64
+ companion: '/api/companion',
65
+ }}
66
+ />
67
+ </Suspense>
68
+ );
69
+
40
70
  return (
41
71
  <>
42
72
  {!preview && (
@@ -99,32 +129,7 @@ export default function Uploader({ onUploaded, preview, maxFileSize, maxNumberOf
99
129
  </Div>
100
130
  )}
101
131
 
102
- <Suspense fallback={null}>
103
- <UploaderComponent
104
- // @ts-ignore
105
- ref={uploaderRef}
106
- popup
107
- onUploadFinish={(result: any) => onUploaded({ url: result.data.url })}
108
- uploadedProps={{
109
- onSelectedFiles: (files: any[]) => {
110
- if (files.length) {
111
- onUploaded({ url: files[0].fileUrl });
112
- }
113
- },
114
- }}
115
- coreProps={{
116
- restrictions: {
117
- allowedFileExts,
118
- maxFileSize,
119
- maxNumberOfFiles,
120
- },
121
- }}
122
- apiPathProps={{
123
- uploader: '/api/uploads',
124
- companion: '/api/companion',
125
- }}
126
- />
127
- </Suspense>
132
+ {createPortal(uploaderPortal, document.body)}
128
133
  </>
129
134
  );
130
135
  }
package/src/env.d.ts CHANGED
@@ -4,6 +4,7 @@ declare var blocklet: {
4
4
  appUrl: string;
5
5
  prefix: string;
6
6
  languages: { code: string; name: string }[];
7
+ preferences: { overdraftProtectionMaxCount: number };
7
8
  };
8
9
 
9
10
  declare module '*.svg';
@@ -0,0 +1,30 @@
1
+ import { useCallback } from 'react';
2
+ import Toast from '@arcblock/ux/lib/Toast';
3
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
+ import { useRequest } from 'ahooks';
5
+ import { api } from '@blocklet/payment-react';
6
+
7
+ export function useUnpaidInvoicesCheckForSubscription(subscriptionId: string, manualCheck: boolean = false) {
8
+ const { t } = useLocaleContext();
9
+
10
+ const { data = { count: 0 }, runAsync } = useRequest(
11
+ () => api.get(`/api/subscriptions/${subscriptionId}/unpaid-invoices`).then((res) => res.data),
12
+ {
13
+ manual: manualCheck,
14
+ }
15
+ );
16
+
17
+ const checkUnpaidInvoices = useCallback(async () => {
18
+ const { count } = await runAsync();
19
+ if (count > 0) {
20
+ Toast.warning(t('customer.unpaidInvoicesWarning'));
21
+ return true;
22
+ }
23
+ return false;
24
+ }, []);
25
+
26
+ return {
27
+ checkUnpaidInvoices,
28
+ hasUnpaid: data?.count > 0,
29
+ };
30
+ }
@@ -0,0 +1,4 @@
1
+ export const preferences = {
2
+ ...(window.blocklet?.preferences || {}),
3
+ overdraftProtectionMaxCount: Number(window.blocklet?.preferences?.overdraftProtectionMaxCount || 10),
4
+ };
@@ -23,6 +23,8 @@ export default flat({
23
23
  'At least one letter and cannot include Chinese characters and special characters such as <, >、"、’ or \\',
24
24
  loading: 'Loading...',
25
25
  rechargeTime: 'Recharge Time',
26
+ submit: 'Submit',
27
+ custom: 'Custom',
26
28
  },
27
29
  admin: {
28
30
  balances: 'Balances',
@@ -645,5 +647,44 @@ export default flat({
645
647
  success: 'Delegate successful',
646
648
  error: 'Delegate failed',
647
649
  },
650
+ overdraftProtection: {
651
+ title: 'Overdraft Protection',
652
+ setting: 'Overdraft Protection Setting',
653
+ tip: 'To avoid service interruption due to unpaid invoices, you can enable overdraft protection by staking. Timely payment will not incur additional fees. Please settle your invoices promptly. If your available stake is insufficient or payment is overdue, we will deduct the amount from your stake and charge a service fee.',
654
+ enabled: 'Enabled',
655
+ disabled: 'Disabled',
656
+ returnRemaining: 'Return Remaining Stake',
657
+ returnRemainingTip:
658
+ 'Once the remaining stake is returned, the overdraft protection will be automatically disabled. Please confirm the action.',
659
+ applyRemainingSuccess: 'Stake return application successful',
660
+ remaining:
661
+ 'Your current remaining stake: {amount} {symbol}, estimated required stake per cycle: {estimateAmount} {symbol}.',
662
+ noRemaining:
663
+ 'No remaining stake available. Please stake at least {estimateAmount} {symbol} as soon as possible to ensure overdraft protection is enabled.',
664
+ remainingNotEnough:
665
+ 'You have unpaid invoices totaling {due} {symbol}. If not paid, your remaining stake will be insufficient to cover the next invoice. Available stake: {unused} {symbol}. Please stake at least {min} {symbol}.',
666
+ due: 'Please pay the outstanding amount first',
667
+ insufficient: 'Insufficient Stake to cover the next invoice',
668
+ insufficientTip: 'Insufficient Stake, please stake to ensure overdraft protection is enabled.',
669
+ intervals: 'cycles',
670
+ estimatedDuration: '{duration} {unit} est.',
671
+ rule: 'Rule: N * ( P + Fee )',
672
+ ruleTip:
673
+ 'N is the number of cycles, P is the subscription bill amount, Fee is the overdraft protection service fee, the single fee is {gas} {symbol}',
674
+ min: 'The amount must be greater or equal to {min} {symbol}',
675
+ settingSuccess: 'Overdraft protection setting successful',
676
+ settingError: 'Overdraft protection setting failed',
677
+ keepStake: 'Not Return',
678
+ returnStake: 'Return Remaining Stake',
679
+ stake: 'Stake',
680
+ address: 'Staking Address',
681
+ total: 'Total Stake: {total} {symbol}, ',
682
+ disableConfirm: 'You currently have unpaid invoices, please settle your invoices first.',
683
+ },
684
+ unpaidInvoicesWarning: 'You currently have unpaid invoices, please settle your invoices first.',
685
+ unpaidInvoicesWarningTip: 'You currently have unpaid invoices, please settle your invoices promptly.',
686
+ invoice: {
687
+ relatedInvoice: 'Related Invoice',
688
+ },
648
689
  },
649
690
  });