payment-kit 1.15.16 → 1.15.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/integrations/stripe/handlers/invoice.ts +20 -0
- package/api/src/integrations/stripe/resource.ts +2 -2
- package/api/src/libs/audit.ts +1 -1
- package/api/src/libs/invoice.ts +81 -1
- package/api/src/libs/notification/template/billing-discrepancy.ts +223 -0
- package/api/src/libs/notification/template/subscription-canceled.ts +11 -0
- package/api/src/libs/notification/template/subscription-refund-succeeded.ts +10 -2
- package/api/src/libs/notification/template/subscription-renew-failed.ts +10 -2
- package/api/src/libs/notification/template/subscription-renewed.ts +11 -3
- package/api/src/libs/notification/template/subscription-stake-slash-succeeded.ts +11 -1
- package/api/src/libs/notification/template/subscription-succeeded.ts +11 -1
- package/api/src/libs/notification/template/subscription-trial-start.ts +11 -0
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +17 -0
- package/api/src/libs/notification/template/subscription-upgraded.ts +51 -26
- package/api/src/libs/notification/template/subscription-will-canceled.ts +16 -0
- package/api/src/libs/notification/template/subscription-will-renew.ts +15 -3
- package/api/src/libs/notification/template/usage-report-empty.ts +158 -0
- package/api/src/libs/queue/index.ts +69 -19
- package/api/src/libs/queue/store.ts +28 -5
- package/api/src/libs/subscription.ts +129 -19
- package/api/src/libs/util.ts +30 -0
- package/api/src/locales/en.ts +13 -0
- package/api/src/locales/zh.ts +13 -0
- package/api/src/queues/invoice.ts +58 -20
- package/api/src/queues/notification.ts +43 -1
- package/api/src/queues/payment.ts +5 -1
- package/api/src/queues/subscription.ts +64 -15
- package/api/src/routes/checkout-sessions.ts +26 -0
- package/api/src/routes/invoices.ts +11 -31
- package/api/src/routes/subscriptions.ts +43 -7
- package/api/src/store/models/checkout-session.ts +2 -0
- package/api/src/store/models/job.ts +4 -0
- package/api/src/store/models/types.ts +22 -4
- package/api/src/store/models/usage-record.ts +5 -1
- package/api/tests/libs/subscription.spec.ts +154 -0
- package/api/tests/libs/util.spec.ts +135 -0
- package/blocklet.yml +1 -1
- package/package.json +10 -10
- package/scripts/sdk.js +37 -3
- package/src/components/invoice/list.tsx +0 -1
- package/src/components/invoice/table.tsx +7 -2
- package/src/components/subscription/items/index.tsx +26 -7
- package/src/components/subscription/items/usage-records.tsx +21 -10
- package/src/components/subscription/portal/actions.tsx +16 -14
- package/src/libs/util.ts +51 -0
- package/src/locales/en.tsx +2 -0
- package/src/locales/zh.tsx +2 -0
- package/src/pages/admin/billing/subscriptions/detail.tsx +1 -1
- package/src/pages/customer/subscription/change-plan.tsx +1 -1
- package/src/pages/customer/subscription/embed.tsx +16 -14
- package/vite-server.config.ts +8 -0
|
@@ -10,7 +10,9 @@ import {
|
|
|
10
10
|
getDataObjectFromQuery,
|
|
11
11
|
getNextRetry,
|
|
12
12
|
tryWithTimeout,
|
|
13
|
+
getSubscriptionNotificationCustomActions,
|
|
13
14
|
} from '../../src/libs/util';
|
|
15
|
+
import type { Subscription } from '../../src/store/models';
|
|
14
16
|
|
|
15
17
|
describe('createIdGenerator', () => {
|
|
16
18
|
it('should return a function that generates an ID with the specified prefix and size', () => {
|
|
@@ -240,3 +242,136 @@ describe('formatAmountPrecisionLimit', () => {
|
|
|
240
242
|
expect(result).toBe('');
|
|
241
243
|
});
|
|
242
244
|
});
|
|
245
|
+
|
|
246
|
+
describe('getSubscriptionNotificationCustomActions', () => {
|
|
247
|
+
const mockSubscription: Partial<Subscription> = {
|
|
248
|
+
service_actions: [
|
|
249
|
+
{
|
|
250
|
+
type: 'notification',
|
|
251
|
+
triggerEvents: ['customer.subscription.started', 'customer.subscription.renewed'],
|
|
252
|
+
name: 'Action 1',
|
|
253
|
+
text: { en: 'Action 1 Text', zh: '操作1文本' },
|
|
254
|
+
link: 'https://example.com/action1',
|
|
255
|
+
color: 'blue',
|
|
256
|
+
variant: 'outlined',
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
type: 'notification',
|
|
260
|
+
triggerEvents: ['customer.subscription.renewed', 'customer.subscription.upgraded'],
|
|
261
|
+
name: 'Action 2',
|
|
262
|
+
text: { en: 'Action 2 Text', zh: '操作2文本' },
|
|
263
|
+
link: 'https://example.com/action2',
|
|
264
|
+
color: 'green',
|
|
265
|
+
variant: 'contained',
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
type: 'other',
|
|
269
|
+
triggerEvents: ['customer.subscription.started'],
|
|
270
|
+
name: 'Action 3',
|
|
271
|
+
text: { en: 'Action 3 Text', zh: '操作3文本' },
|
|
272
|
+
link: 'https://example.com/action3',
|
|
273
|
+
color: 'red',
|
|
274
|
+
variant: 'text',
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
it('should return an empty array if subscription is null', () => {
|
|
280
|
+
const result = getSubscriptionNotificationCustomActions(null as any, 'customer.subscription.started', 'en');
|
|
281
|
+
expect(result).toEqual([]);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('should return an empty array if service_actions is empty', () => {
|
|
285
|
+
const result = getSubscriptionNotificationCustomActions(
|
|
286
|
+
{ service_actions: [] } as any,
|
|
287
|
+
'customer.subscription.started',
|
|
288
|
+
'en'
|
|
289
|
+
);
|
|
290
|
+
expect(result).toEqual([]);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should filter actions based on event type and return formatted actions with color and variant', () => {
|
|
294
|
+
const result = getSubscriptionNotificationCustomActions(
|
|
295
|
+
mockSubscription as any,
|
|
296
|
+
'customer.subscription.renewed',
|
|
297
|
+
'en'
|
|
298
|
+
);
|
|
299
|
+
expect(result).toEqual([
|
|
300
|
+
{
|
|
301
|
+
name: 'Action 1',
|
|
302
|
+
title: 'Action 1 Text',
|
|
303
|
+
link: 'https://example.com/action1',
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
name: 'Action 2',
|
|
307
|
+
title: 'Action 2 Text',
|
|
308
|
+
link: 'https://example.com/action2',
|
|
309
|
+
},
|
|
310
|
+
]);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should return actions with correct locale, color, and variant', () => {
|
|
314
|
+
const result = getSubscriptionNotificationCustomActions(
|
|
315
|
+
mockSubscription as any,
|
|
316
|
+
'customer.subscription.renewed',
|
|
317
|
+
'zh'
|
|
318
|
+
);
|
|
319
|
+
expect(result).toEqual([
|
|
320
|
+
{
|
|
321
|
+
name: 'Action 1',
|
|
322
|
+
title: '操作1文本',
|
|
323
|
+
link: 'https://example.com/action1',
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
name: 'Action 2',
|
|
327
|
+
title: '操作2文本',
|
|
328
|
+
link: 'https://example.com/action2',
|
|
329
|
+
},
|
|
330
|
+
]);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('should not return actions of non-notification type', () => {
|
|
334
|
+
const result = getSubscriptionNotificationCustomActions(
|
|
335
|
+
mockSubscription as any,
|
|
336
|
+
'customer.subscription.started',
|
|
337
|
+
'en'
|
|
338
|
+
);
|
|
339
|
+
expect(result).toEqual([
|
|
340
|
+
{
|
|
341
|
+
name: 'Action 1',
|
|
342
|
+
title: 'Action 1 Text',
|
|
343
|
+
link: 'https://example.com/action1',
|
|
344
|
+
},
|
|
345
|
+
]);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('should return an empty array if no matching actions found', () => {
|
|
349
|
+
const result = getSubscriptionNotificationCustomActions(
|
|
350
|
+
mockSubscription as any,
|
|
351
|
+
'customer.subscription.deleted',
|
|
352
|
+
'en'
|
|
353
|
+
);
|
|
354
|
+
expect(result).toEqual([]);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('should handle all valid NotificationActionEvents', () => {
|
|
358
|
+
const allEvents = [
|
|
359
|
+
'customer.subscription.started',
|
|
360
|
+
'customer.subscription.renewed',
|
|
361
|
+
'customer.subscription.renew_failed',
|
|
362
|
+
'refund.succeeded',
|
|
363
|
+
'subscription.stake.slash.succeeded',
|
|
364
|
+
'customer.subscription.trial_will_end',
|
|
365
|
+
'customer.subscription.trial_start',
|
|
366
|
+
'customer.subscription.upgraded',
|
|
367
|
+
'customer.subscription.will_renew',
|
|
368
|
+
'customer.subscription.will_canceled',
|
|
369
|
+
'customer.subscription.deleted',
|
|
370
|
+
];
|
|
371
|
+
|
|
372
|
+
allEvents.forEach((event) => {
|
|
373
|
+
const result = getSubscriptionNotificationCustomActions(mockSubscription as any, event, 'en');
|
|
374
|
+
expect(Array.isArray(result)).toBeTruthy();
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
});
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.15.
|
|
3
|
+
"version": "1.15.18",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -45,18 +45,18 @@
|
|
|
45
45
|
"@abtnode/cron": "^1.16.32",
|
|
46
46
|
"@arcblock/did": "^1.18.135",
|
|
47
47
|
"@arcblock/did-auth-storage-nedb": "^1.7.1",
|
|
48
|
-
"@arcblock/did-connect": "^2.10.
|
|
48
|
+
"@arcblock/did-connect": "^2.10.45",
|
|
49
49
|
"@arcblock/did-util": "^1.18.135",
|
|
50
50
|
"@arcblock/jwt": "^1.18.135",
|
|
51
|
-
"@arcblock/ux": "^2.10.
|
|
51
|
+
"@arcblock/ux": "^2.10.45",
|
|
52
52
|
"@arcblock/validator": "^1.18.135",
|
|
53
53
|
"@blocklet/js-sdk": "^1.16.32",
|
|
54
54
|
"@blocklet/logger": "^1.16.32",
|
|
55
|
-
"@blocklet/payment-react": "1.15.
|
|
55
|
+
"@blocklet/payment-react": "1.15.18",
|
|
56
56
|
"@blocklet/sdk": "^1.16.32",
|
|
57
|
-
"@blocklet/ui-react": "^2.10.
|
|
58
|
-
"@blocklet/uploader": "^0.1.
|
|
59
|
-
"@blocklet/xss": "^0.1.
|
|
57
|
+
"@blocklet/ui-react": "^2.10.45",
|
|
58
|
+
"@blocklet/uploader": "^0.1.43",
|
|
59
|
+
"@blocklet/xss": "^0.1.9",
|
|
60
60
|
"@mui/icons-material": "^5.16.6",
|
|
61
61
|
"@mui/lab": "^5.0.0-alpha.173",
|
|
62
62
|
"@mui/material": "^5.16.6",
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
"devDependencies": {
|
|
119
119
|
"@abtnode/types": "^1.16.32",
|
|
120
120
|
"@arcblock/eslint-config-ts": "^0.3.2",
|
|
121
|
-
"@blocklet/payment-types": "1.15.
|
|
121
|
+
"@blocklet/payment-types": "1.15.18",
|
|
122
122
|
"@types/cookie-parser": "^1.4.7",
|
|
123
123
|
"@types/cors": "^2.8.17",
|
|
124
124
|
"@types/debug": "^4.1.12",
|
|
@@ -144,7 +144,7 @@
|
|
|
144
144
|
"typescript": "^4.9.5",
|
|
145
145
|
"vite": "^5.3.5",
|
|
146
146
|
"vite-node": "^2.0.4",
|
|
147
|
-
"vite-plugin-blocklet": "^0.9.
|
|
147
|
+
"vite-plugin-blocklet": "^0.9.11",
|
|
148
148
|
"vite-plugin-node-polyfills": "^0.21.0",
|
|
149
149
|
"vite-plugin-svgr": "^4.2.0",
|
|
150
150
|
"vite-tsconfig-paths": "^4.3.2",
|
|
@@ -160,5 +160,5 @@
|
|
|
160
160
|
"parser": "typescript"
|
|
161
161
|
}
|
|
162
162
|
},
|
|
163
|
-
"gitHead": "
|
|
163
|
+
"gitHead": "f57a3e622d9701fa0421f1266efc2fd92bd84780"
|
|
164
164
|
}
|
package/scripts/sdk.js
CHANGED
|
@@ -86,9 +86,43 @@ const payment = require('@blocklet/payment-js').default;
|
|
|
86
86
|
// cancel_url:
|
|
87
87
|
// 'https://bbqa2t5pfyfroyobmzknmktshckzto4btkfagxyjqwy.did.abtnet.io/store/api/payment/cancel?redirect=https://bbqa2t5pfyfroyobmzknmktshckzto4btkfagxyjqwy.did.abtnet.io/maker/mint/z3CtLCSchoZj4H5gqwyATSAEdvfd1m88VnDUZ',
|
|
88
88
|
// mode: 'payment',
|
|
89
|
-
// line_items: [
|
|
90
|
-
//
|
|
91
|
-
//
|
|
89
|
+
// line_items: [
|
|
90
|
+
// { price_id: 'price_G7TE6QZbvqkIaqzDJVb2afws', quantity: 2 },
|
|
91
|
+
// { price_id: 'price_rOUdD9fQrBGqn6M3YywXdDeK', quantity: 2 },
|
|
92
|
+
// ],
|
|
93
|
+
// subscription_data: {
|
|
94
|
+
// service_actions: [
|
|
95
|
+
// {
|
|
96
|
+
// type: 'notification',
|
|
97
|
+
// text: {
|
|
98
|
+
// zh: '查看文档',
|
|
99
|
+
// en: 'View Documentation',
|
|
100
|
+
// },
|
|
101
|
+
// link: 'https://www.arcblock.io/docs/createblocklet/en/quick-start',
|
|
102
|
+
// triggerEvents: ['customer.subscription.started', 'customer.subscription.deleted'],
|
|
103
|
+
// },
|
|
104
|
+
// {
|
|
105
|
+
// type: 'notification',
|
|
106
|
+
// text: {
|
|
107
|
+
// zh: '社区提问',
|
|
108
|
+
// en: 'Ask in Community',
|
|
109
|
+
// },
|
|
110
|
+
// link: 'https://community.arcblock.io/?locale=en',
|
|
111
|
+
// triggerEvents: ['customer.subscription.started', 'customer.subscription.renewed'],
|
|
112
|
+
// },
|
|
113
|
+
// {
|
|
114
|
+
// type: 'custom',
|
|
115
|
+
// text: {
|
|
116
|
+
// zh: '查看',
|
|
117
|
+
// en: 'View',
|
|
118
|
+
// },
|
|
119
|
+
// link: 'https://www.arcblock.io/docs/createblocklet/en/quick-start',
|
|
120
|
+
// color: 'primary',
|
|
121
|
+
// variant: 'outlined',
|
|
122
|
+
// },
|
|
123
|
+
// ],
|
|
124
|
+
// },
|
|
125
|
+
// expires_at: 1729243800,
|
|
92
126
|
// });
|
|
93
127
|
// console.log('checkoutSession', checkoutSession);
|
|
94
128
|
// const product = await payment.products.create({
|
|
@@ -229,7 +229,6 @@ export default function InvoiceList({
|
|
|
229
229
|
columns.splice(3, 0, {
|
|
230
230
|
label: t('common.customer'),
|
|
231
231
|
name: 'customer_id',
|
|
232
|
-
width: 80,
|
|
233
232
|
options: {
|
|
234
233
|
customBodyRenderLite: (_: string, index: number) => {
|
|
235
234
|
const item = data.list[index] as TInvoiceExpanded;
|
|
@@ -11,6 +11,7 @@ import { styled } from '@mui/system';
|
|
|
11
11
|
import { isEmpty } from 'lodash';
|
|
12
12
|
import LineItemActions from '../subscription/items/actions';
|
|
13
13
|
import { UsageRecordDialog } from '../subscription/items/usage-records';
|
|
14
|
+
import { getInvoiceUsageReportStartEnd } from '../../libs/util';
|
|
14
15
|
|
|
15
16
|
type Props = {
|
|
16
17
|
invoice: TInvoiceExpanded;
|
|
@@ -122,6 +123,8 @@ export default function InvoiceTable({ invoice, simple, emptyNodeText }: Props)
|
|
|
122
123
|
subscriptionItemId: '',
|
|
123
124
|
});
|
|
124
125
|
|
|
126
|
+
const usageReportRange = getInvoiceUsageReportStartEnd(invoice, true);
|
|
127
|
+
|
|
125
128
|
const onOpenUsageRecords = (line: InvoiceDetailItem) => {
|
|
126
129
|
if (line.rawQuantity && line.raw.subscription_id && line.raw.subscription_item_id) {
|
|
127
130
|
setState({
|
|
@@ -271,8 +274,10 @@ export default function InvoiceTable({ invoice, simple, emptyNodeText }: Props)
|
|
|
271
274
|
subscriptionId={state.subscriptionId}
|
|
272
275
|
id={state.subscriptionItemId}
|
|
273
276
|
onConfirm={onCloseUsageRecords}
|
|
274
|
-
start={
|
|
275
|
-
end={
|
|
277
|
+
start={usageReportRange.start}
|
|
278
|
+
end={usageReportRange.end}
|
|
279
|
+
title={t('admin.subscription.usage.title')}
|
|
280
|
+
disableAddUsageQuantity
|
|
276
281
|
/>
|
|
277
282
|
)}
|
|
278
283
|
</Root>
|
|
@@ -4,6 +4,7 @@ import { formatPrice, Table, TruncatedText, useMobile } from '@blocklet/payment-
|
|
|
4
4
|
import type { TPaymentCurrency, TSubscriptionItemExpanded } from '@blocklet/payment-types';
|
|
5
5
|
import { Avatar, Stack, Typography } from '@mui/material';
|
|
6
6
|
|
|
7
|
+
import { Link } from 'react-router-dom';
|
|
7
8
|
import Copyable from '../../copyable';
|
|
8
9
|
import LineItemActions from './actions';
|
|
9
10
|
import UsageRecords from './usage-records';
|
|
@@ -23,6 +24,8 @@ const size = { width: 48, height: 48 };
|
|
|
23
24
|
export default function SubscriptionItemList({ data, currency, mode }: ListProps) {
|
|
24
25
|
const { t } = useLocaleContext();
|
|
25
26
|
const { isMobile } = useMobile();
|
|
27
|
+
const isAdmin = mode === 'admin';
|
|
28
|
+
|
|
26
29
|
const columns = [
|
|
27
30
|
{
|
|
28
31
|
label: t('admin.subscription.product'),
|
|
@@ -53,13 +56,29 @@ export default function SubscriptionItemList({ data, currency, mode }: ListProps
|
|
|
53
56
|
{item.price.product.name.slice(0, 1)}
|
|
54
57
|
</Avatar>
|
|
55
58
|
)}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
{isAdmin ? (
|
|
60
|
+
<>
|
|
61
|
+
<Typography color="text.primary" fontWeight={600}>
|
|
62
|
+
<Link to={`/admin/products/${item?.price.product_id}`}>
|
|
63
|
+
<TruncatedText text={item?.price.product.name} maxLength={isMobile ? 20 : 50} useWidth />
|
|
64
|
+
</Link>
|
|
65
|
+
</Typography>
|
|
66
|
+
<Typography color="text.secondary" whiteSpace="nowrap">
|
|
67
|
+
<Link to={`/admin/products/${item?.price.id}`}>
|
|
68
|
+
{formatPrice(item.price, currency, item?.price.product.unit_label)}
|
|
69
|
+
</Link>
|
|
70
|
+
</Typography>
|
|
71
|
+
</>
|
|
72
|
+
) : (
|
|
73
|
+
<>
|
|
74
|
+
<Typography color="text.primary" fontWeight={600}>
|
|
75
|
+
<TruncatedText text={item?.price.product.name} maxLength={isMobile ? 20 : 50} useWidth />
|
|
76
|
+
</Typography>
|
|
77
|
+
<Typography color="text.secondary" whiteSpace="nowrap">
|
|
78
|
+
{formatPrice(item.price, currency, item?.price.product.unit_label)}
|
|
79
|
+
</Typography>
|
|
80
|
+
</>
|
|
81
|
+
)}
|
|
63
82
|
</Stack>
|
|
64
83
|
);
|
|
65
84
|
},
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import Empty from '@arcblock/ux/lib/Empty';
|
|
3
3
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
4
4
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
5
|
-
import { ConfirmDialog, api, formatError, usePaymentContext } from '@blocklet/payment-react';
|
|
5
|
+
import { ConfirmDialog, api, formatError, formatTime, usePaymentContext } from '@blocklet/payment-react';
|
|
6
6
|
import type { TUsageRecord } from '@blocklet/payment-types';
|
|
7
|
-
import { Alert, Box, Button, CircularProgress, TextField } from '@mui/material';
|
|
7
|
+
import { Alert, Box, Button, CircularProgress, TextField, Typography } from '@mui/material';
|
|
8
8
|
import { useRequest } from 'ahooks';
|
|
9
9
|
import { useState } from 'react';
|
|
10
10
|
import { Bar, BarChart, Rectangle, Tooltip, XAxis, YAxis } from 'recharts';
|
|
@@ -47,17 +47,21 @@ function addUsageQuantity({
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export function UsageRecordDialog({
|
|
50
|
+
title,
|
|
50
51
|
subscriptionId,
|
|
51
52
|
id,
|
|
52
53
|
onConfirm,
|
|
53
54
|
start = 0,
|
|
54
55
|
end = 0,
|
|
56
|
+
disableAddUsageQuantity = false,
|
|
55
57
|
}: {
|
|
58
|
+
title?: string;
|
|
56
59
|
subscriptionId: string;
|
|
57
60
|
id: string;
|
|
58
61
|
onConfirm: any;
|
|
59
62
|
start?: number;
|
|
60
63
|
end?: number;
|
|
64
|
+
disableAddUsageQuantity?: boolean;
|
|
61
65
|
}) {
|
|
62
66
|
const { t } = useLocaleContext();
|
|
63
67
|
const { loading, error, data } = useRequest(() => fetchData(subscriptionId, id, start, end), {
|
|
@@ -69,7 +73,7 @@ export function UsageRecordDialog({
|
|
|
69
73
|
if (error) {
|
|
70
74
|
return (
|
|
71
75
|
<ConfirmDialog
|
|
72
|
-
title={t('admin.subscription.usage.current')}
|
|
76
|
+
title={title || t('admin.subscription.usage.current')}
|
|
73
77
|
message={<Alert severity="error">{error.message}</Alert>}
|
|
74
78
|
onConfirm={onConfirm}
|
|
75
79
|
onCancel={onConfirm}
|
|
@@ -82,7 +86,7 @@ export function UsageRecordDialog({
|
|
|
82
86
|
if (loading || !data) {
|
|
83
87
|
return (
|
|
84
88
|
<ConfirmDialog
|
|
85
|
-
title={t('admin.subscription.usage.current')}
|
|
89
|
+
title={title || t('admin.subscription.usage.current')}
|
|
86
90
|
message={<CircularProgress />}
|
|
87
91
|
onConfirm={onConfirm}
|
|
88
92
|
onCancel={onConfirm}
|
|
@@ -109,12 +113,17 @@ export function UsageRecordDialog({
|
|
|
109
113
|
};
|
|
110
114
|
return (
|
|
111
115
|
<ConfirmDialog
|
|
112
|
-
title={t('admin.subscription.usage.current')}
|
|
116
|
+
title={title || t('admin.subscription.usage.current')}
|
|
113
117
|
message={
|
|
114
118
|
<>
|
|
119
|
+
{start && end ? (
|
|
120
|
+
<Typography variant="h6" mb={2}>
|
|
121
|
+
{t('admin.subscription.usage.cycle')}: {formatTime(start * 1000)} - {formatTime(end * 1000)}
|
|
122
|
+
</Typography>
|
|
123
|
+
) : null}
|
|
115
124
|
{data.list.length > 0 ? (
|
|
116
125
|
<BarChart
|
|
117
|
-
width={
|
|
126
|
+
width={540}
|
|
118
127
|
height={240}
|
|
119
128
|
data={data.list.map((item) => ({
|
|
120
129
|
...item,
|
|
@@ -123,7 +132,7 @@ export function UsageRecordDialog({
|
|
|
123
132
|
margin={{
|
|
124
133
|
top: 5,
|
|
125
134
|
right: 5,
|
|
126
|
-
left:
|
|
135
|
+
left: 5,
|
|
127
136
|
bottom: 5,
|
|
128
137
|
}}>
|
|
129
138
|
<Tooltip />
|
|
@@ -138,8 +147,8 @@ export function UsageRecordDialog({
|
|
|
138
147
|
) : (
|
|
139
148
|
<Empty>{t('admin.usageRecord.empty')}</Empty>
|
|
140
149
|
)}
|
|
141
|
-
{!settings.livemode && window.location.pathname.includes('/admin/billing') && (
|
|
142
|
-
<Box sx={{ display: 'flex', justifyContent: 'center' }} pt={1} pb={1}>
|
|
150
|
+
{!settings.livemode && window.location.pathname.includes('/admin/billing') && !disableAddUsageQuantity && (
|
|
151
|
+
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }} pt={1} pb={1} gap={1}>
|
|
143
152
|
<TextField
|
|
144
153
|
id="add-usage-record"
|
|
145
154
|
label={t('admin.usageRecord.add.quantity')}
|
|
@@ -151,7 +160,9 @@ export function UsageRecordDialog({
|
|
|
151
160
|
value={usageQuantity}
|
|
152
161
|
onChange={(e) => setUsageQuantity(+e.target.value)}
|
|
153
162
|
/>
|
|
154
|
-
<Button onClick={handAddUsageQuantity}
|
|
163
|
+
<Button onClick={handAddUsageQuantity} sx={{ color: 'text.link' }}>
|
|
164
|
+
{t('admin.usageRecord.add.label')}
|
|
165
|
+
</Button>
|
|
155
166
|
</Box>
|
|
156
167
|
)}
|
|
157
168
|
</>
|
|
@@ -167,20 +167,22 @@ export function SubscriptionActionsInner({ subscription, showExtra, onChange, ac
|
|
|
167
167
|
{action?.text || t('admin.subscription.batchPay.button')}
|
|
168
168
|
</Button>
|
|
169
169
|
)}
|
|
170
|
-
{subscription.service_actions
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
170
|
+
{subscription.service_actions
|
|
171
|
+
?.filter((x: any) => x?.type !== 'notification')
|
|
172
|
+
.map((x) => (
|
|
173
|
+
// @ts-ignore
|
|
174
|
+
<Button
|
|
175
|
+
component={Link}
|
|
176
|
+
key={x.name}
|
|
177
|
+
variant={x?.variant || 'contained'}
|
|
178
|
+
color={x?.color || 'primary'}
|
|
179
|
+
href={x.link}
|
|
180
|
+
size="small"
|
|
181
|
+
target="_blank"
|
|
182
|
+
sx={{ textDecoration: 'none !important' }}>
|
|
183
|
+
{x.text[locale] || x.text.en || x.name}
|
|
184
|
+
</Button>
|
|
185
|
+
))}
|
|
184
186
|
{state.action === 'cancel' && state.subscription && (
|
|
185
187
|
<ConfirmDialog
|
|
186
188
|
onConfirm={handleCancel}
|
package/src/libs/util.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
import { formatCheckoutHeadlines, formatPrice, getPrefix, getPriceCurrencyOptions } from '@blocklet/payment-react';
|
|
5
5
|
import type {
|
|
6
6
|
LineItem,
|
|
7
|
+
PriceRecurring,
|
|
8
|
+
TInvoiceExpanded,
|
|
7
9
|
TLineItemExpanded,
|
|
8
10
|
TPaymentCurrency,
|
|
9
11
|
TPaymentLinkExpanded,
|
|
@@ -271,3 +273,52 @@ export function isEmptyExceptNumber(value: any): boolean {
|
|
|
271
273
|
}
|
|
272
274
|
return isEmpty(value);
|
|
273
275
|
}
|
|
276
|
+
|
|
277
|
+
export function getRecurringPeriod(recurring: PriceRecurring) {
|
|
278
|
+
const { interval } = recurring;
|
|
279
|
+
const count = +recurring.interval_count || 1;
|
|
280
|
+
const dayInMs = 24 * 60 * 60 * 1000;
|
|
281
|
+
|
|
282
|
+
switch (interval) {
|
|
283
|
+
case 'hour':
|
|
284
|
+
return 60 * 60 * 1000;
|
|
285
|
+
case 'day':
|
|
286
|
+
return count * dayInMs;
|
|
287
|
+
case 'week':
|
|
288
|
+
return count * 7 * dayInMs;
|
|
289
|
+
case 'month':
|
|
290
|
+
return count * 30 * dayInMs;
|
|
291
|
+
case 'year':
|
|
292
|
+
return count * 365 * dayInMs;
|
|
293
|
+
default:
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function getInvoiceUsageReportStartEnd(invoice: TInvoiceExpanded, showPreviousPeriod: boolean = false) {
|
|
299
|
+
const { subscription, paymentMethod } = invoice;
|
|
300
|
+
const usageReportRange = {
|
|
301
|
+
start: invoice.metadata?.usage_start || invoice.period_start,
|
|
302
|
+
end: invoice.metadata?.usage_end || invoice.period_end,
|
|
303
|
+
};
|
|
304
|
+
if (!subscription || !showPreviousPeriod) {
|
|
305
|
+
return usageReportRange;
|
|
306
|
+
}
|
|
307
|
+
if (invoice?.billing_reason === 'subscription_cancel') {
|
|
308
|
+
return usageReportRange;
|
|
309
|
+
}
|
|
310
|
+
const cycle = getRecurringPeriod(subscription.pending_invoice_item_interval);
|
|
311
|
+
let offset = 0;
|
|
312
|
+
if (['arcblock', 'ethereum'].includes(paymentMethod.type)) {
|
|
313
|
+
switch (invoice?.billing_reason) {
|
|
314
|
+
case 'subscription_cycle':
|
|
315
|
+
offset = cycle / 1000;
|
|
316
|
+
break;
|
|
317
|
+
default:
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
usageReportRange.start = invoice.period_start - offset;
|
|
322
|
+
usageReportRange.end = invoice.period_end - offset;
|
|
323
|
+
return usageReportRange;
|
|
324
|
+
}
|
package/src/locales/en.tsx
CHANGED
|
@@ -500,9 +500,11 @@ export default flat({
|
|
|
500
500
|
usage: {
|
|
501
501
|
title: 'Usage records',
|
|
502
502
|
current: 'Usage records for current period',
|
|
503
|
+
range: 'Usage records for {start} - {end}',
|
|
503
504
|
view: 'View usage',
|
|
504
505
|
vary: 'Varies with usage',
|
|
505
506
|
used: 'Unit used',
|
|
507
|
+
cycle: 'Usage cycle',
|
|
506
508
|
},
|
|
507
509
|
batchPay: {
|
|
508
510
|
button: 'Pay due invoices',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -489,9 +489,11 @@ export default flat({
|
|
|
489
489
|
usage: {
|
|
490
490
|
title: '使用记录',
|
|
491
491
|
current: '当前周期的使用记录',
|
|
492
|
+
range: '周期使用记录({start} - {end})',
|
|
492
493
|
view: '查看使用情况',
|
|
493
494
|
vary: '随使用情况变化',
|
|
494
495
|
used: '已使用单位',
|
|
496
|
+
cycle: '统计周期',
|
|
495
497
|
},
|
|
496
498
|
batchPay: {
|
|
497
499
|
button: '批量付款',
|
|
@@ -315,7 +315,7 @@ export default function SubscriptionDetail(props: { id: string }) {
|
|
|
315
315
|
<Box className="section">
|
|
316
316
|
<SectionHeader title={t('admin.product.pricing')} />
|
|
317
317
|
<Box className="section-body">
|
|
318
|
-
<SubscriptionItemList data={data.items} currency={data.paymentCurrency} />
|
|
318
|
+
<SubscriptionItemList data={data.items} currency={data.paymentCurrency} mode="admin" />
|
|
319
319
|
</Box>
|
|
320
320
|
</Box>
|
|
321
321
|
<Divider />
|
|
@@ -200,7 +200,7 @@ export default function CustomerSubscriptionChangePlan() {
|
|
|
200
200
|
};
|
|
201
201
|
|
|
202
202
|
const table = { ...data.table, currency: data.subscription.paymentCurrency };
|
|
203
|
-
table.items.forEach((x: any) => {
|
|
203
|
+
(table.items || []).forEach((x: any) => {
|
|
204
204
|
x.is_selected = x.price_id === state.priceId;
|
|
205
205
|
if (data.subscription.items.find((y) => y.price_id === x.price_id)) {
|
|
206
206
|
x.is_highlight = true;
|
|
@@ -228,20 +228,22 @@ export default function SubscriptionEmbed() {
|
|
|
228
228
|
</List>
|
|
229
229
|
</Box>
|
|
230
230
|
<Stack direction="row" justifyContent="center" spacing={2} sx={{ mt: 2 }}>
|
|
231
|
-
{subscription.service_actions
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
231
|
+
{subscription.service_actions
|
|
232
|
+
?.filter((x: any) => x?.type !== 'notification')
|
|
233
|
+
?.map((x) => (
|
|
234
|
+
// @ts-ignore
|
|
235
|
+
<Button
|
|
236
|
+
component={Link}
|
|
237
|
+
key={x.name}
|
|
238
|
+
variant={x?.variant || 'contained'}
|
|
239
|
+
color={x.color || 'primary'}
|
|
240
|
+
href={x.link}
|
|
241
|
+
size="small"
|
|
242
|
+
target="_blank"
|
|
243
|
+
sx={{ textDecoration: 'none !important' }}>
|
|
244
|
+
{x.text[locale] || x.text.en || x.name}
|
|
245
|
+
</Button>
|
|
246
|
+
))}
|
|
245
247
|
<Button
|
|
246
248
|
variant="contained"
|
|
247
249
|
sx={{ color: '#fff!important', width: subscription.service_actions?.length ? 'auto' : '100%' }}
|
package/vite-server.config.ts
CHANGED
|
@@ -6,6 +6,14 @@ import tsconfigPaths from 'vite-tsconfig-paths';
|
|
|
6
6
|
// https://vitejs.dev/config/
|
|
7
7
|
export default defineConfig(() => {
|
|
8
8
|
return {
|
|
9
|
+
resolve: {
|
|
10
|
+
alias: {
|
|
11
|
+
crypto: 'node:crypto',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
9
14
|
plugins: [tsconfigPaths()],
|
|
15
|
+
server: {
|
|
16
|
+
hmr: false,
|
|
17
|
+
},
|
|
10
18
|
};
|
|
11
19
|
});
|