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.
- package/api/src/crons/index.ts +1 -1
- package/api/src/hooks/pre-start.ts +2 -0
- package/api/src/index.ts +2 -0
- package/api/src/integrations/arcblock/stake.ts +7 -1
- package/api/src/integrations/stripe/resource.ts +1 -1
- package/api/src/libs/env.ts +12 -0
- package/api/src/libs/event.ts +8 -0
- package/api/src/libs/invoice.ts +585 -3
- package/api/src/libs/notification/template/subscription-succeeded.ts +1 -2
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +2 -2
- package/api/src/libs/notification/template/subscription-will-renew.ts +6 -2
- package/api/src/libs/notification/template/subscription.overdraft-protection.exhausted.ts +139 -0
- package/api/src/libs/overdraft-protection.ts +85 -0
- package/api/src/libs/payment.ts +1 -65
- package/api/src/libs/queue/index.ts +0 -1
- package/api/src/libs/subscription.ts +532 -2
- package/api/src/libs/util.ts +4 -0
- package/api/src/locales/en.ts +5 -0
- package/api/src/locales/zh.ts +5 -0
- package/api/src/queues/event.ts +3 -2
- package/api/src/queues/invoice.ts +28 -3
- package/api/src/queues/notification.ts +25 -3
- package/api/src/queues/payment.ts +154 -3
- package/api/src/queues/refund.ts +2 -2
- package/api/src/queues/subscription.ts +215 -4
- package/api/src/queues/webhook.ts +1 -0
- package/api/src/routes/connect/change-payment.ts +1 -1
- package/api/src/routes/connect/change-plan.ts +1 -1
- package/api/src/routes/connect/overdraft-protection.ts +120 -0
- package/api/src/routes/connect/recharge.ts +2 -1
- package/api/src/routes/connect/setup.ts +1 -1
- package/api/src/routes/connect/shared.ts +117 -350
- package/api/src/routes/connect/subscribe.ts +1 -1
- package/api/src/routes/customers.ts +2 -2
- package/api/src/routes/invoices.ts +9 -4
- package/api/src/routes/subscriptions.ts +172 -2
- package/api/src/store/migrate.ts +9 -10
- package/api/src/store/migrations/20240905-index.ts +95 -60
- package/api/src/store/migrations/20241203-overdraft-protection.ts +25 -0
- package/api/src/store/migrations/20241216-update-overdraft-protection.ts +30 -0
- package/api/src/store/models/customer.ts +2 -2
- package/api/src/store/models/invoice.ts +7 -0
- package/api/src/store/models/lock.ts +7 -0
- package/api/src/store/models/subscription.ts +15 -0
- package/api/src/store/sequelize.ts +6 -1
- package/blocklet.yml +1 -1
- package/package.json +23 -23
- package/src/components/customer/overdraft-protection.tsx +367 -0
- package/src/components/event/list.tsx +3 -4
- package/src/components/subscription/actions/cancel.tsx +3 -0
- package/src/components/subscription/portal/actions.tsx +324 -77
- package/src/components/uploader.tsx +31 -26
- package/src/env.d.ts +1 -0
- package/src/hooks/subscription.ts +30 -0
- package/src/libs/env.ts +4 -0
- package/src/locales/en.tsx +41 -0
- package/src/locales/zh.tsx +37 -0
- package/src/pages/admin/billing/invoices/detail.tsx +16 -15
- package/src/pages/customer/index.tsx +7 -2
- package/src/pages/customer/invoice/detail.tsx +29 -5
- package/src/pages/customer/invoice/past-due.tsx +18 -4
- package/src/pages/customer/recharge.tsx +2 -4
- package/src/pages/customer/subscription/change-payment.tsx +7 -1
- package/src/pages/customer/subscription/detail.tsx +69 -51
- package/tsconfig.json +0 -5
- 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 {
|
|
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({
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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}¤cy=${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
|
-
|
|
196
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
406
|
+
sx={action?.sx as any}
|
|
407
|
+
onClick={(e) => {
|
|
408
|
+
e.stopPropagation();
|
|
409
|
+
navigate(`/customer/invoice/past-due?subscription=${subscription.id}¤cy=${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
|
-
|
|
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
|
@@ -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
|
+
}
|
package/src/libs/env.ts
ADDED
package/src/locales/en.tsx
CHANGED
|
@@ -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
|
});
|