payment-kit 1.17.2 → 1.17.4
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/integrations/arcblock/nft.ts +1 -1
- package/api/src/libs/env.ts +0 -2
- package/api/src/libs/middleware.ts +2 -2
- package/api/src/libs/overdraft-protection.ts +4 -1
- package/api/src/libs/security.ts +2 -2
- package/api/src/libs/subscription.ts +1 -1
- package/api/src/locales/en.ts +2 -2
- package/api/src/locales/zh.ts +2 -2
- package/api/src/queues/notification.ts +1 -1
- package/api/src/queues/subscription.ts +1 -1
- package/api/src/routes/connect/overdraft-protection.ts +4 -1
- package/api/src/routes/connect/shared.ts +12 -7
- package/api/src/routes/invoices.ts +21 -5
- package/api/src/routes/subscriptions.ts +1 -1
- package/blocklet.yml +1 -1
- package/package.json +23 -23
- package/src/components/customer/overdraft-protection.tsx +219 -74
- package/src/components/subscription/metrics.tsx +83 -7
- package/src/components/subscription/portal/actions.tsx +188 -153
- package/src/components/subscription/portal/list.tsx +1 -0
- package/src/libs/dayjs.ts +132 -0
- package/src/locales/en.tsx +18 -11
- package/src/locales/zh.tsx +17 -10
- package/src/pages/admin/billing/subscriptions/detail.tsx +1 -1
- package/src/pages/customer/recharge.tsx +63 -27
- package/src/pages/customer/subscription/detail.tsx +153 -10
- package/src/pages/customer/subscription/embed.tsx +1 -0
- /package/api/src/libs/notification/template/{subscription.overdraft-protection.exhausted.ts → subscription-overdraft-protection-exhausted.ts} +0 -0
|
@@ -14,7 +14,7 @@ import type { TSubscriptionExpanded } from '@blocklet/payment-types';
|
|
|
14
14
|
import { Button, Link, Stack, Tooltip } from '@mui/material';
|
|
15
15
|
import { useRequest, useSetState } from 'ahooks';
|
|
16
16
|
import isEmpty from 'lodash/isEmpty';
|
|
17
|
-
import { useState } from 'react';
|
|
17
|
+
import { useEffect, useState } from 'react';
|
|
18
18
|
import { FormProvider, useForm, useFormContext } from 'react-hook-form';
|
|
19
19
|
import { useNavigate } from 'react-router-dom';
|
|
20
20
|
import { joinURL } from 'ufo';
|
|
@@ -23,6 +23,22 @@ import OverdraftProtectionDialog from '../../customer/overdraft-protection';
|
|
|
23
23
|
import Actions from '../../actions';
|
|
24
24
|
import { useUnpaidInvoicesCheckForSubscription } from '../../../hooks/subscription';
|
|
25
25
|
|
|
26
|
+
interface ActionConfig {
|
|
27
|
+
key: string;
|
|
28
|
+
show: boolean;
|
|
29
|
+
label: string;
|
|
30
|
+
tooltip?: string;
|
|
31
|
+
onClick: (e?: React.MouseEvent) => void;
|
|
32
|
+
variant?: 'text' | 'outlined' | 'contained';
|
|
33
|
+
color?: 'inherit' | 'primary' | 'secondary' | 'error';
|
|
34
|
+
sx?: any;
|
|
35
|
+
primary?: boolean;
|
|
36
|
+
component?: any;
|
|
37
|
+
href?: string;
|
|
38
|
+
target?: string;
|
|
39
|
+
divider?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
26
42
|
type ActionProps = {
|
|
27
43
|
[key: string]: {
|
|
28
44
|
color?: string;
|
|
@@ -33,6 +49,15 @@ type ActionProps = {
|
|
|
33
49
|
text?: string;
|
|
34
50
|
};
|
|
35
51
|
};
|
|
52
|
+
export interface ProtectionInitValues {
|
|
53
|
+
enabled?: boolean;
|
|
54
|
+
return_stake?: boolean;
|
|
55
|
+
}
|
|
56
|
+
export interface ActionMethods {
|
|
57
|
+
openOverdraftProtection: (initValues?: ProtectionInitValues) => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type ActionDisplayMode = 'all-buttons' | 'primary-buttons' | 'menu-only';
|
|
36
61
|
|
|
37
62
|
type Props = {
|
|
38
63
|
subscription: TSubscriptionExpanded;
|
|
@@ -45,9 +70,11 @@ type Props = {
|
|
|
45
70
|
onChange: () => any | Promise<any>;
|
|
46
71
|
};
|
|
47
72
|
showDelegation?: boolean;
|
|
73
|
+
showUnsubscribe?: boolean;
|
|
48
74
|
onChange?: (action?: string) => any | Promise<any>;
|
|
49
75
|
actionProps?: ActionProps;
|
|
50
|
-
mode?:
|
|
76
|
+
mode?: ActionDisplayMode;
|
|
77
|
+
setUp?: (methods: ActionMethods) => void;
|
|
51
78
|
};
|
|
52
79
|
|
|
53
80
|
SubscriptionActions.defaultProps = {
|
|
@@ -57,7 +84,9 @@ SubscriptionActions.defaultProps = {
|
|
|
57
84
|
showDelegation: false,
|
|
58
85
|
onChange: null,
|
|
59
86
|
actionProps: {},
|
|
60
|
-
mode: '
|
|
87
|
+
mode: 'all-buttons',
|
|
88
|
+
setUp: null,
|
|
89
|
+
showUnsubscribe: true,
|
|
61
90
|
};
|
|
62
91
|
const fetchExtraActions = async ({
|
|
63
92
|
id,
|
|
@@ -102,9 +131,11 @@ export function SubscriptionActionsInner({
|
|
|
102
131
|
showRecharge,
|
|
103
132
|
showOverdraftProtection,
|
|
104
133
|
showDelegation,
|
|
134
|
+
showUnsubscribe,
|
|
105
135
|
onChange,
|
|
106
136
|
actionProps,
|
|
107
137
|
mode,
|
|
138
|
+
setUp,
|
|
108
139
|
}: Props) {
|
|
109
140
|
const { t, locale } = useLocaleContext();
|
|
110
141
|
const { reset, getValues } = useFormContext();
|
|
@@ -122,6 +153,7 @@ export function SubscriptionActionsInner({
|
|
|
122
153
|
loading: false,
|
|
123
154
|
openProtection: false,
|
|
124
155
|
protectionLoading: false,
|
|
156
|
+
protectionInitValues: null,
|
|
125
157
|
});
|
|
126
158
|
|
|
127
159
|
const shouldFetchDelegation = showDelegation && ['active', 'trialing', 'past_due'].includes(subscription?.status);
|
|
@@ -146,6 +178,19 @@ export function SubscriptionActionsInner({
|
|
|
146
178
|
refreshDeps: [subscription.id, shouldFetchOverdraftProtection],
|
|
147
179
|
});
|
|
148
180
|
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (setUp) {
|
|
183
|
+
setUp({
|
|
184
|
+
openOverdraftProtection: (initValues?: ProtectionInitValues) =>
|
|
185
|
+
setState({
|
|
186
|
+
openProtection: true,
|
|
187
|
+
// @ts-ignore
|
|
188
|
+
protectionInitValues: initValues,
|
|
189
|
+
}),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}, [setUp]);
|
|
193
|
+
|
|
149
194
|
const handleCancel = async () => {
|
|
150
195
|
try {
|
|
151
196
|
const result = await checkUnpaidInvoices();
|
|
@@ -273,162 +318,148 @@ export function SubscriptionActionsInner({
|
|
|
273
318
|
};
|
|
274
319
|
|
|
275
320
|
const renderActions = () => {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
321
|
+
const supportUnsubscribe = action?.action === 'cancel' && showUnsubscribe;
|
|
322
|
+
const supportAction = action && (action?.action !== 'cancel' || supportUnsubscribe);
|
|
323
|
+
|
|
324
|
+
const serviceActions = subscription.service_actions?.filter((x: any) => x?.type !== 'notification') || [];
|
|
325
|
+
const actionConfigs: ActionConfig[] = [
|
|
326
|
+
{
|
|
327
|
+
key: 'delegation',
|
|
328
|
+
show: showDelegation && noDelegation,
|
|
329
|
+
label: t('customer.delegation.btn'),
|
|
330
|
+
tooltip: t('customer.delegation.title'),
|
|
331
|
+
onClick: handleDelegate,
|
|
332
|
+
variant: 'outlined',
|
|
333
|
+
color: 'primary',
|
|
334
|
+
primary: true,
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
key: 'protection',
|
|
338
|
+
show: shouldFetchOverdraftProtection,
|
|
339
|
+
label: t('customer.overdraftProtection.setting'),
|
|
340
|
+
onClick: () => setState({ openProtection: true, protectionInitValues: null }),
|
|
341
|
+
variant: 'outlined',
|
|
342
|
+
color: 'primary',
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
key: 'recharge',
|
|
346
|
+
show: !!(showRecharge && supportRecharge(subscription)),
|
|
347
|
+
label: t('customer.recharge.title'),
|
|
348
|
+
onClick: (e) => {
|
|
349
|
+
e?.stopPropagation();
|
|
350
|
+
navigate(`/customer/subscription/${subscription.id}/recharge`);
|
|
289
351
|
},
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
label: action?.text || t('payment.customer.changePlan.button'),
|
|
314
|
-
handler: () => navigate(`/customer/subscription/${subscription.id}/change-plan`),
|
|
315
|
-
color: 'primary',
|
|
352
|
+
variant: 'outlined',
|
|
353
|
+
color: 'primary',
|
|
354
|
+
primary: true,
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
key: 'changePlan',
|
|
358
|
+
show: !!extraActions?.changePlan,
|
|
359
|
+
label: action?.text || t('payment.customer.changePlan.button'),
|
|
360
|
+
onClick: (e) => {
|
|
361
|
+
e?.stopPropagation();
|
|
362
|
+
navigate(`/customer/subscription/${subscription.id}/change-plan`);
|
|
363
|
+
},
|
|
364
|
+
variant: 'contained',
|
|
365
|
+
color: 'primary',
|
|
366
|
+
sx: action?.sx,
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
key: 'batchPay',
|
|
370
|
+
show: !!extraActions?.batchPay,
|
|
371
|
+
label: action?.text || t('admin.subscription.batchPay.button'),
|
|
372
|
+
onClick: (e) => {
|
|
373
|
+
e?.stopPropagation();
|
|
374
|
+
navigate(`/customer/invoice/past-due?subscription=${subscription.id}¤cy=${extraActions?.batchPay}`);
|
|
316
375
|
},
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
376
|
+
variant: 'outlined',
|
|
377
|
+
color: 'error',
|
|
378
|
+
sx: action?.sx,
|
|
379
|
+
primary: true,
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
key: 'mainAction',
|
|
383
|
+
show: !!(!extraActions?.batchPay && supportAction),
|
|
384
|
+
label: action?.text || t(`payment.customer.${action?.action}.button`),
|
|
385
|
+
onClick: (e) => {
|
|
386
|
+
e?.stopPropagation();
|
|
387
|
+
if (action?.action === 'pastDue') {
|
|
388
|
+
navigate(`/customer/invoice/past-due?subscription=${subscription.id}`);
|
|
389
|
+
} else {
|
|
390
|
+
// @ts-ignore
|
|
391
|
+
setState({ action: action?.action, subscription: subscription.id });
|
|
392
|
+
}
|
|
322
393
|
},
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
394
|
+
// @ts-ignore
|
|
395
|
+
variant: action?.variant || 'outlined',
|
|
396
|
+
// @ts-ignore
|
|
397
|
+
color: action?.color || 'primary',
|
|
398
|
+
sx: action?.sx,
|
|
399
|
+
divider: serviceActions.length > 0,
|
|
400
|
+
},
|
|
401
|
+
// @ts-ignore
|
|
402
|
+
...serviceActions.map((x) => ({
|
|
403
|
+
key: x.name,
|
|
404
|
+
show: true,
|
|
405
|
+
label: x.text[locale] || x.text.en || x.name,
|
|
406
|
+
onClick: () => window.open(x.link, '_blank'),
|
|
407
|
+
variant: x?.variant || 'contained',
|
|
408
|
+
color: x?.color || 'primary',
|
|
409
|
+
component: Link,
|
|
410
|
+
href: x.link,
|
|
411
|
+
target: '_blank',
|
|
412
|
+
sx: { textDecoration: 'none !important' },
|
|
413
|
+
})),
|
|
414
|
+
];
|
|
333
415
|
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
)}
|
|
416
|
+
// 过滤出要显示的操作
|
|
417
|
+
const visibleActions = actionConfigs.filter((a) => a.show);
|
|
350
418
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
419
|
+
// 转换为菜单项
|
|
420
|
+
const toMenuItem = (item: any) => ({
|
|
421
|
+
label: item.label,
|
|
422
|
+
handler: item.onClick,
|
|
423
|
+
color: item.color,
|
|
424
|
+
divider: item.divider,
|
|
425
|
+
});
|
|
356
426
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
{
|
|
369
|
-
<
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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 && (
|
|
402
|
-
<Button
|
|
403
|
-
variant="outlined"
|
|
404
|
-
color="error"
|
|
405
|
-
size="small"
|
|
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')}
|
|
412
|
-
</Button>
|
|
427
|
+
const toButton = (item: ActionConfig) => (
|
|
428
|
+
<Button
|
|
429
|
+
key={item.key}
|
|
430
|
+
variant={item.variant}
|
|
431
|
+
color={item.color}
|
|
432
|
+
onClick={item.onClick}
|
|
433
|
+
component={item.component}
|
|
434
|
+
href={item.href}
|
|
435
|
+
target={item.target}
|
|
436
|
+
sx={item.sx}
|
|
437
|
+
size="small">
|
|
438
|
+
{item.tooltip ? (
|
|
439
|
+
<Tooltip title={item.tooltip}>
|
|
440
|
+
<span>{item.label}</span>
|
|
441
|
+
</Tooltip>
|
|
442
|
+
) : (
|
|
443
|
+
item.label
|
|
413
444
|
)}
|
|
414
|
-
|
|
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
|
-
</>
|
|
445
|
+
</Button>
|
|
431
446
|
);
|
|
447
|
+
if (mode === 'menu-only') {
|
|
448
|
+
return <Actions actions={visibleActions.map(toMenuItem)} />;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (mode === 'primary-buttons') {
|
|
452
|
+
const primaryButtons = visibleActions.filter((a) => a.primary);
|
|
453
|
+
const menuItems = visibleActions.filter((a) => !a.primary);
|
|
454
|
+
return (
|
|
455
|
+
<>
|
|
456
|
+
{primaryButtons.map(toButton)}
|
|
457
|
+
{menuItems.length > 0 && <Actions actions={menuItems.map(toMenuItem)} />}
|
|
458
|
+
</>
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return <>{visibleActions.map(toButton)}</>;
|
|
432
463
|
};
|
|
433
464
|
|
|
434
465
|
return (
|
|
@@ -466,11 +497,13 @@ export function SubscriptionActionsInner({
|
|
|
466
497
|
value={overdraftProtection}
|
|
467
498
|
onSave={handleOverdraftProtection}
|
|
468
499
|
open={state.openProtection}
|
|
469
|
-
|
|
500
|
+
payerAddress={subscription.overdraft_protection?.payment_details?.arcblock?.payer}
|
|
501
|
+
stakingAddress={subscription.overdraft_protection?.payment_details?.arcblock?.staking?.address}
|
|
470
502
|
currency={subscription.paymentCurrency}
|
|
471
503
|
subscription={subscription}
|
|
472
504
|
loading={state.protectionLoading}
|
|
473
505
|
onCancel={() => setState({ openProtection: false })}
|
|
506
|
+
initValues={state.protectionInitValues}
|
|
474
507
|
/>
|
|
475
508
|
)}
|
|
476
509
|
</Stack>
|
|
@@ -500,7 +533,9 @@ SubscriptionActionsInner.defaultProps = {
|
|
|
500
533
|
showRecharge: false,
|
|
501
534
|
showOverdraftProtection: false,
|
|
502
535
|
showDelegation: false,
|
|
536
|
+
showUnsubscribe: true,
|
|
503
537
|
onChange: null,
|
|
504
538
|
actionProps: {},
|
|
505
|
-
mode: '
|
|
539
|
+
mode: 'all-buttons',
|
|
540
|
+
setUp: null,
|
|
506
541
|
};
|
package/src/libs/dayjs.ts
CHANGED
|
@@ -1,3 +1,135 @@
|
|
|
1
1
|
import { dayjs } from '@blocklet/payment-react';
|
|
2
2
|
|
|
3
3
|
export default dayjs;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Time unit type definition
|
|
7
|
+
*/
|
|
8
|
+
export type TimeUnit = 'hour' | 'day' | 'week' | 'month' | 'year';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Format options interface
|
|
12
|
+
*/
|
|
13
|
+
interface FormatDurationOptions {
|
|
14
|
+
t: (key: string, options?: Record<string, any>) => string;
|
|
15
|
+
pluralSuffix?: string;
|
|
16
|
+
separator?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Conversion rule type
|
|
21
|
+
*/
|
|
22
|
+
export type ConversionRule = {
|
|
23
|
+
threshold: number;
|
|
24
|
+
convert: (value: number) => { main: number; remainder: number };
|
|
25
|
+
format: (main: number, remainder: number, value: number) => Array<[TimeUnit, number]>;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Smart duration formatter optimized for subscription periods
|
|
29
|
+
*
|
|
30
|
+
* Display Rules:
|
|
31
|
+
* 1. Hours:
|
|
32
|
+
* - < 24h: show hours (e.g., "5 hours")
|
|
33
|
+
* - 24-47h: show days and hours (e.g., "1 day 5 hours")
|
|
34
|
+
* - >= 48h: convert to days
|
|
35
|
+
*
|
|
36
|
+
* 2. Days:
|
|
37
|
+
* - < 7 days: show days (e.g., "5 days")
|
|
38
|
+
* - 7-13 days: show 1 week + days (e.g., "1 week 3 days")
|
|
39
|
+
* - 14+ days: show weeks if divisible, else days (e.g., "2 weeks" or "15 days")
|
|
40
|
+
* - 30+ days: show months if divisible, else weeks/days (e.g., "1 month" or "5 weeks")
|
|
41
|
+
*
|
|
42
|
+
* 3. Weeks:
|
|
43
|
+
* - < 4 weeks: show weeks (e.g., "3 weeks")
|
|
44
|
+
* - 4-7 weeks: show 1 month + weeks (e.g., "1 month 2 weeks")
|
|
45
|
+
* - 8+ weeks: show months if divisible, else weeks (e.g., "2 months" or "9 weeks")
|
|
46
|
+
*
|
|
47
|
+
* 4. Months:
|
|
48
|
+
* - < 12: show months (e.g., "6 months")
|
|
49
|
+
* - 12+: show years if divisible, else months (e.g., "1 year" or "15 months")
|
|
50
|
+
*/
|
|
51
|
+
export const formatSmartDuration = (
|
|
52
|
+
value: number,
|
|
53
|
+
unit: TimeUnit,
|
|
54
|
+
{ t, separator = ' ' }: FormatDurationOptions
|
|
55
|
+
): string => {
|
|
56
|
+
// Format single unit
|
|
57
|
+
const formatUnit = (val: number, unitType: TimeUnit): string =>
|
|
58
|
+
`${val} ${t(`common.${unitType}${val > 1 ? 's' : ''}`).toLowerCase()}`;
|
|
59
|
+
|
|
60
|
+
// Convert to largest possible unit
|
|
61
|
+
const convertToLargest = (val: number, fromUnit: TimeUnit): [TimeUnit, number][] => {
|
|
62
|
+
switch (fromUnit) {
|
|
63
|
+
case 'hour': {
|
|
64
|
+
if (val < 24) return [['hour', val]];
|
|
65
|
+
if (val < 48)
|
|
66
|
+
return [
|
|
67
|
+
['day', Math.floor(val / 24)],
|
|
68
|
+
['hour', val % 24],
|
|
69
|
+
];
|
|
70
|
+
const days = Math.floor(val / 24);
|
|
71
|
+
return convertToLargest(days, 'day');
|
|
72
|
+
}
|
|
73
|
+
case 'day': {
|
|
74
|
+
if (val < 7) return [['day', val]];
|
|
75
|
+
if (val < 14)
|
|
76
|
+
return [
|
|
77
|
+
['week', Math.floor(val / 7)],
|
|
78
|
+
['day', val % 7],
|
|
79
|
+
];
|
|
80
|
+
if (val < 30) return val % 7 === 0 ? [['week', val / 7]] : [['day', val]];
|
|
81
|
+
const years = Math.floor(val / 365);
|
|
82
|
+
const remainingDays = val % 365;
|
|
83
|
+
const months = Math.floor(remainingDays / 30);
|
|
84
|
+
if (years > 0) {
|
|
85
|
+
return months > 0
|
|
86
|
+
? [
|
|
87
|
+
['year', years],
|
|
88
|
+
['month', months],
|
|
89
|
+
]
|
|
90
|
+
: [['year', years]];
|
|
91
|
+
}
|
|
92
|
+
return months > 0
|
|
93
|
+
? [
|
|
94
|
+
['month', months],
|
|
95
|
+
['week', Math.floor((remainingDays % 30) / 7)],
|
|
96
|
+
]
|
|
97
|
+
: [['week', Math.floor(val / 7)]];
|
|
98
|
+
}
|
|
99
|
+
case 'week': {
|
|
100
|
+
if (val < 4) return [['week', val]];
|
|
101
|
+
if (val < 8)
|
|
102
|
+
return [
|
|
103
|
+
['month', Math.floor(val / 4)],
|
|
104
|
+
['week', val % 4],
|
|
105
|
+
];
|
|
106
|
+
const months = Math.floor(val / 4);
|
|
107
|
+
return convertToLargest(months, 'month');
|
|
108
|
+
}
|
|
109
|
+
case 'month': {
|
|
110
|
+
const years = Math.floor(val / 12);
|
|
111
|
+
const months = val % 12;
|
|
112
|
+
if (years > 0) {
|
|
113
|
+
if (months > 0) {
|
|
114
|
+
return [
|
|
115
|
+
['year', years],
|
|
116
|
+
['month', months],
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
return [['year', years]];
|
|
120
|
+
}
|
|
121
|
+
return [['month', val]];
|
|
122
|
+
}
|
|
123
|
+
case 'year':
|
|
124
|
+
return [['year', val]];
|
|
125
|
+
default:
|
|
126
|
+
return [[fromUnit, val]];
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Get units and filter out zero values
|
|
131
|
+
const units = convertToLargest(value, unit).filter(([, val]) => val > 0);
|
|
132
|
+
|
|
133
|
+
// Format all units
|
|
134
|
+
return units.map(([u, val]) => formatUnit(val, u)).join(separator);
|
|
135
|
+
};
|
package/src/locales/en.tsx
CHANGED
|
@@ -25,6 +25,7 @@ export default flat({
|
|
|
25
25
|
rechargeTime: 'Recharge Time',
|
|
26
26
|
submit: 'Submit',
|
|
27
27
|
custom: 'Custom',
|
|
28
|
+
estimatedDuration: '{duration} est.',
|
|
28
29
|
},
|
|
29
30
|
admin: {
|
|
30
31
|
balances: 'Balances',
|
|
@@ -456,6 +457,9 @@ export default flat({
|
|
|
456
457
|
resume: 'Resume payment collection',
|
|
457
458
|
resumeTip:
|
|
458
459
|
'Are you sure you want to resume collecting payments? Any future invoices for this subscription will resume payment collection.',
|
|
460
|
+
paymentAddress: 'Payment Address',
|
|
461
|
+
currentBalance: 'Current Balance',
|
|
462
|
+
insufficientBalance: 'Insufficient Balance, please add funds',
|
|
459
463
|
cancel: {
|
|
460
464
|
schedule: 'Scheduled to cancel',
|
|
461
465
|
title: 'Cancel subscription',
|
|
@@ -648,38 +652,41 @@ export default flat({
|
|
|
648
652
|
error: 'Delegate failed',
|
|
649
653
|
},
|
|
650
654
|
overdraftProtection: {
|
|
651
|
-
title: '
|
|
652
|
-
setting: '
|
|
653
|
-
tip: 'To avoid service interruption due to unpaid invoices, you can enable
|
|
655
|
+
title: 'SubGuard™',
|
|
656
|
+
setting: 'Set SubGuard™',
|
|
657
|
+
tip: 'To avoid service interruption due to unpaid invoices, you can enable SubGuard™ 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
658
|
enabled: 'Enabled',
|
|
655
659
|
disabled: 'Disabled',
|
|
656
660
|
returnRemaining: 'Return Remaining Stake',
|
|
657
661
|
returnRemainingTip:
|
|
658
|
-
'Once the remaining stake is returned, the
|
|
662
|
+
'Once the remaining stake is returned, the SubGuard™ will be automatically disabled. Please confirm the action.',
|
|
659
663
|
applyRemainingSuccess: 'Stake return application successful',
|
|
660
664
|
remaining:
|
|
661
665
|
'Your current remaining stake: {amount} {symbol}, estimated required stake per cycle: {estimateAmount} {symbol}.',
|
|
662
666
|
noRemaining:
|
|
663
|
-
'No remaining stake available. Please stake at least {estimateAmount} {symbol} as soon as possible to ensure
|
|
667
|
+
'No remaining stake available. Please stake at least {estimateAmount} {symbol} as soon as possible to ensure SubGuard™ is enabled.',
|
|
664
668
|
remainingNotEnough:
|
|
665
669
|
'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
670
|
due: 'Please pay the outstanding amount first',
|
|
667
|
-
insufficient: 'Insufficient Stake to cover the next invoice',
|
|
668
|
-
insufficientTip: 'Insufficient Stake, please stake to ensure
|
|
671
|
+
insufficient: 'Insufficient Stake to cover the next invoice, please add stake',
|
|
672
|
+
insufficientTip: 'Insufficient Stake, please stake to ensure SubGuard™ is enabled.',
|
|
669
673
|
intervals: 'cycles',
|
|
670
674
|
estimatedDuration: '{duration} {unit} est.',
|
|
671
675
|
rule: 'Rule: N * ( P + Fee )',
|
|
672
676
|
ruleTip:
|
|
673
|
-
'N is the number of cycles, P is the subscription bill amount, Fee is the
|
|
677
|
+
'N is the number of cycles, P is the subscription bill amount, Fee is the SubGuard™ service fee, the single fee is {gas} {symbol}',
|
|
674
678
|
min: 'The amount must be greater or equal to {min} {symbol}',
|
|
675
|
-
settingSuccess: '
|
|
676
|
-
settingError: '
|
|
677
|
-
keepStake: '
|
|
679
|
+
settingSuccess: 'Set SubGuard™ Successful',
|
|
680
|
+
settingError: 'Set SubGuard™ Failed',
|
|
681
|
+
keepStake: 'Keep Remaining Stake For SubGuard™',
|
|
678
682
|
returnStake: 'Return Remaining Stake',
|
|
679
683
|
stake: 'Stake',
|
|
680
684
|
address: 'Staking Address',
|
|
681
685
|
total: 'Total Stake: {total} {symbol}, ',
|
|
682
686
|
disableConfirm: 'You currently have unpaid invoices, please settle your invoices first.',
|
|
687
|
+
open: 'Enable SubGuard™',
|
|
688
|
+
payerAddress: 'Payer',
|
|
689
|
+
stakingAddress: 'Staking Address',
|
|
683
690
|
},
|
|
684
691
|
unpaidInvoicesWarning: 'You currently have unpaid invoices, please settle your invoices first.',
|
|
685
692
|
unpaidInvoicesWarningTip: 'You currently have unpaid invoices, please settle your invoices promptly.',
|