payment-kit 1.13.65 → 1.13.67

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 (49) hide show
  1. package/api/src/index.ts +4 -0
  2. package/api/src/integrations/blocklet/notification.ts +5 -3
  3. package/api/src/jobs/notification.ts +142 -0
  4. package/api/src/jobs/payment.ts +14 -0
  5. package/api/src/jobs/subscription.ts +2 -2
  6. package/api/src/libs/audit.ts +3 -1
  7. package/api/src/libs/env.ts +3 -0
  8. package/api/src/libs/event.ts +10 -1
  9. package/api/src/libs/invoice.ts +5 -0
  10. package/api/src/libs/notification/index.ts +23 -0
  11. package/api/src/libs/notification/template/base.ts +12 -0
  12. package/api/src/libs/notification/template/subscription-renew-failed.ts +286 -0
  13. package/api/src/libs/notification/template/subscription-renewed.ts +259 -0
  14. package/api/src/libs/notification/template/subscription-succeeded.ts +279 -0
  15. package/api/src/libs/notification/template/subscription-trial-start.ts +267 -0
  16. package/api/src/libs/notification/template/subscription-trial-will-end.ts +250 -0
  17. package/api/src/libs/notification/template/subscription-will-renew.ts +232 -0
  18. package/api/src/libs/payment.ts +100 -3
  19. package/api/src/libs/product.ts +19 -0
  20. package/api/src/libs/queue/index.ts +13 -0
  21. package/api/src/libs/subscription.ts +5 -0
  22. package/api/src/libs/time.ts +17 -0
  23. package/api/src/libs/util.ts +39 -0
  24. package/api/src/locales/en.ts +67 -0
  25. package/api/src/locales/zh.ts +64 -0
  26. package/api/src/routes/connect/collect.ts +6 -0
  27. package/api/src/schedule/index.ts +28 -0
  28. package/api/src/schedule/interface/diff.ts +9 -0
  29. package/api/src/schedule/subscription-trail-will-end.ts +197 -0
  30. package/api/src/schedule/subscription-will-renew.ts +195 -0
  31. package/api/src/store/models/subscription.ts +30 -12
  32. package/api/src/store/models/types.ts +13 -12
  33. package/api/third.d.ts +2 -0
  34. package/blocklet.yml +1 -1
  35. package/package.json +9 -7
  36. package/src/app.tsx +2 -0
  37. package/src/components/invoice/action.tsx +25 -7
  38. package/src/components/invoice/list.tsx +19 -4
  39. package/src/components/portal/invoice/list.tsx +1 -1
  40. package/src/components/portal/subscription/list.tsx +6 -5
  41. package/src/components/subscription/items/index.tsx +8 -4
  42. package/src/libs/util.ts +2 -2
  43. package/src/locales/en.tsx +5 -1
  44. package/src/locales/zh.tsx +5 -1
  45. package/src/pages/checkout/pricing-table.tsx +1 -1
  46. package/src/pages/customer/index.tsx +13 -2
  47. package/src/pages/customer/invoice.tsx +5 -4
  48. package/src/pages/customer/subscription/index.tsx +163 -0
  49. package/tsconfig.api.json +6 -1
@@ -0,0 +1,9 @@
1
+ import type { ManipulateType } from 'dayjs';
2
+
3
+ export interface Diff {
4
+ value: number;
5
+ unit: ManipulateType;
6
+
7
+ // 这个 job 必须跑吗?
8
+ required?: false | true;
9
+ }
@@ -0,0 +1,197 @@
1
+ /* eslint-disable no-console */
2
+ import dayjs from 'dayjs';
3
+ import { clone } from 'lodash';
4
+ import pAll from 'p-all';
5
+ import { Op } from 'sequelize';
6
+
7
+ import { NotificationQueueJob, notificationQueue } from '../jobs/notification';
8
+ import { notificationCronConcurrency } from '../libs/env';
9
+ import logger from '../libs/logger';
10
+ import type { SubscriptionTrialWillEndEmailTemplateOptions } from '../libs/notification/template/subscription-trial-will-end';
11
+ import { Subscription } from '../store/models';
12
+ import type { Diff } from './interface/diff';
13
+
14
+ interface SubscriptionForTrailWillEnd extends Subscription {
15
+ diffs: Diff[];
16
+ }
17
+
18
+ export class SubscriptionTrailWillEndSchedule {
19
+ private start: number;
20
+
21
+ private end: number;
22
+
23
+ constructor() {
24
+ this.start = Date.now();
25
+ this.end = dayjs(this.start).add(1, 'M').add(1, 'd').toDate().getTime();
26
+ }
27
+
28
+ async run() {
29
+ const label: string = new Date().toISOString();
30
+
31
+ logger.info(`${label}: SubscriptionTrailWillEndSchedule.run start`);
32
+
33
+ const subscriptions = await this.getSubscriptions();
34
+ const subscriptionForWillRenew = this.getSubscriptionsForWillRenew(subscriptions);
35
+
36
+ await this.addTaskToQueue(subscriptionForWillRenew);
37
+
38
+ logger.info(`${label}: SubscriptionTrailWillEndSchedule.run end`);
39
+ }
40
+
41
+ async getSubscriptions(): Promise<Subscription[]> {
42
+ const subscriptions = await Subscription.findAll({
43
+ where: {
44
+ // @note: 之所以除 1000,是因为时间的精确度是 s
45
+ current_period_start: {
46
+ [Op.lte]: this.start / 1000,
47
+ },
48
+ current_period_end: {
49
+ [Op.lt]: this.end / 1000,
50
+ },
51
+ trail_start: {
52
+ [Op.gt]: 0,
53
+ },
54
+ },
55
+ attributes: ['id', 'current_period_start', 'current_period_end'],
56
+ raw: true,
57
+ });
58
+
59
+ return subscriptions;
60
+ }
61
+
62
+ static DIFFS: Diff[] = [
63
+ {
64
+ value: 1,
65
+ unit: 'M',
66
+ },
67
+ {
68
+ value: 7,
69
+ unit: 'd',
70
+ },
71
+ {
72
+ value: 3,
73
+ unit: 'd',
74
+ },
75
+ {
76
+ value: 1,
77
+ unit: 'd',
78
+ },
79
+ {
80
+ value: 6,
81
+ unit: 'h',
82
+ },
83
+ ];
84
+
85
+ /**
86
+ * @see https://github.com/blocklet/payment-kit/issues/236#issuecomment-1824129965
87
+ * @description
88
+ * @param {Subscription[]} subscriptions
89
+ * @return {*} {SubscriptionForWillRenew[]}
90
+ * @memberof SubscriptionWillRenewSchedule
91
+ */
92
+ getSubscriptionsForWillRenew(subscriptions: Subscription[]): SubscriptionForTrailWillEnd[] {
93
+ return subscriptions.map((subscription: Subscription): SubscriptionForTrailWillEnd => {
94
+ const s: SubscriptionForTrailWillEnd = clone(subscription) as SubscriptionForTrailWillEnd;
95
+ const currentPeriodStart: number = s.current_period_start * 1000;
96
+ const currentPeriodEnd: number = s.current_period_end * 1000;
97
+
98
+ if (
99
+ dayjs(currentPeriodEnd).diff(this.start, 'M') >= 1 &&
100
+ dayjs(currentPeriodEnd).diff(currentPeriodStart, 'M') > 1
101
+ ) {
102
+ s.diffs = SubscriptionTrailWillEndSchedule.DIFFS.slice(0);
103
+ } else if (
104
+ dayjs(currentPeriodEnd).diff(this.start, 'd') >= 7 &&
105
+ dayjs(currentPeriodEnd).diff(currentPeriodStart, 'd') > 7
106
+ ) {
107
+ s.diffs = SubscriptionTrailWillEndSchedule.DIFFS.slice(1);
108
+ } else if (
109
+ dayjs(currentPeriodEnd).diff(this.start, 'd') >= 3 &&
110
+ dayjs(currentPeriodEnd).diff(currentPeriodStart, 'd') > 3
111
+ ) {
112
+ s.diffs = SubscriptionTrailWillEndSchedule.DIFFS.slice(2);
113
+ } else if (
114
+ dayjs(currentPeriodEnd).diff(this.start, 'd') >= 1 &&
115
+ dayjs(currentPeriodEnd).diff(currentPeriodStart, 'd') > 1
116
+ ) {
117
+ s.diffs = SubscriptionTrailWillEndSchedule.DIFFS.slice(3);
118
+ } else if (
119
+ dayjs(currentPeriodEnd).diff(this.start, 'h') >= 6 &&
120
+ dayjs(currentPeriodEnd).diff(currentPeriodStart, 'h') > 6
121
+ ) {
122
+ s.diffs = SubscriptionTrailWillEndSchedule.DIFFS.slice(4);
123
+ } else {
124
+ s.diffs = [];
125
+ }
126
+
127
+ // 每一个周期都至少收到一封邮件
128
+ if (dayjs(currentPeriodEnd).diff(currentPeriodStart, 'M') > 1) {
129
+ s.diffs.forEach((x) => {
130
+ if (x.value === 7 && x.unit === 'd') {
131
+ x.required = true;
132
+ }
133
+ });
134
+ } else if (dayjs(currentPeriodEnd).diff(currentPeriodStart, 'd') > 7) {
135
+ s.diffs.forEach((x) => {
136
+ if (x.value === 3 && x.unit === 'd') {
137
+ x.required = true;
138
+ }
139
+ });
140
+ } else if (dayjs(currentPeriodEnd).diff(currentPeriodStart, 'h') >= 6) {
141
+ s.diffs.forEach((x) => {
142
+ if (x.value === 6 && x.unit === 'h') {
143
+ x.required = true;
144
+ }
145
+ });
146
+ }
147
+
148
+ return s;
149
+ });
150
+ }
151
+
152
+ async addTaskToQueue(subscriptions: SubscriptionForTrailWillEnd[]): Promise<void> {
153
+ const tasks: {
154
+ id: string;
155
+ job: { type: NotificationQueueJob['type']; options: SubscriptionTrialWillEndEmailTemplateOptions };
156
+ delay: number;
157
+ }[] = [];
158
+
159
+ for (const subscription of subscriptions) {
160
+ for (const diff of subscription.diffs) {
161
+ const id: string = `${subscription.id}.${diff.value}.${diff.unit}`;
162
+ const job: { type: NotificationQueueJob['type']; options: SubscriptionTrialWillEndEmailTemplateOptions } = {
163
+ type: 'customer.subscription.trial_will_end',
164
+ options: {
165
+ subscriptionId: subscription.id,
166
+ willRenewValue: diff.value,
167
+ willRenewUnit: diff.unit,
168
+ required: !!diff.required,
169
+ },
170
+ };
171
+ const delay: number = dayjs(subscription.current_period_end * 1000)
172
+ .subtract(diff.value, diff.unit)
173
+ .diff(this.start, 's');
174
+
175
+ tasks.push({
176
+ id,
177
+ job,
178
+ delay,
179
+ });
180
+ }
181
+ }
182
+
183
+ await pAll(
184
+ tasks.map((x) => {
185
+ return async () => {
186
+ const job = await notificationQueue.get(x.id);
187
+ if (job) {
188
+ return;
189
+ }
190
+
191
+ notificationQueue.push(x);
192
+ };
193
+ }),
194
+ { concurrency: notificationCronConcurrency }
195
+ );
196
+ }
197
+ }
@@ -0,0 +1,195 @@
1
+ /* eslint-disable no-console */
2
+ import dayjs from 'dayjs';
3
+ import { clone } from 'lodash';
4
+ import pAll from 'p-all';
5
+ import { Op } from 'sequelize';
6
+
7
+ import { NotificationQueueJob, notificationQueue } from '../jobs/notification';
8
+ import { notificationCronConcurrency } from '../libs/env';
9
+ import logger from '../libs/logger';
10
+ import type { SubscriptionWillRenewEmailTemplateOptions } from '../libs/notification/template/subscription-will-renew';
11
+ import { Subscription } from '../store/models';
12
+ import type { Diff } from './interface/diff';
13
+
14
+ interface SubscriptionForWillRenew extends Subscription {
15
+ diffs: Diff[];
16
+ }
17
+
18
+ export class SubscriptionWillRenewSchedule {
19
+ private start: number;
20
+
21
+ private end: number;
22
+
23
+ constructor() {
24
+ this.start = Date.now();
25
+ this.end = dayjs(this.start).add(1, 'M').add(1, 'd').toDate().getTime();
26
+ }
27
+
28
+ async run() {
29
+ const label: string = new Date().toISOString();
30
+
31
+ logger.info(`${label}: SubscriptionWillRenewSchedule.run start`);
32
+
33
+ const subscriptions = await this.getSubscriptions();
34
+ const subscriptionForWillRenew = this.getSubscriptionsForWillRenew(subscriptions);
35
+
36
+ await this.addTaskToQueue(subscriptionForWillRenew);
37
+
38
+ logger.info(`${label}: SubscriptionWillRenewSchedule.run end`);
39
+ }
40
+
41
+ async getSubscriptions(): Promise<Subscription[]> {
42
+ const subscriptions = await Subscription.findAll({
43
+ where: {
44
+ // @note: 之所以除 1000,是因为时间的精确度是 s
45
+ current_period_start: {
46
+ [Op.lte]: this.start / 1000,
47
+ },
48
+ current_period_end: {
49
+ [Op.lt]: this.end / 1000,
50
+ },
51
+ trail_start: 0,
52
+ },
53
+ attributes: ['id', 'current_period_start', 'current_period_end'],
54
+ raw: true,
55
+ });
56
+
57
+ return subscriptions;
58
+ }
59
+
60
+ static DIFFS: Diff[] = [
61
+ {
62
+ value: 1,
63
+ unit: 'M',
64
+ },
65
+ {
66
+ value: 7,
67
+ unit: 'd',
68
+ },
69
+ {
70
+ value: 3,
71
+ unit: 'd',
72
+ },
73
+ {
74
+ value: 1,
75
+ unit: 'd',
76
+ },
77
+ {
78
+ value: 6,
79
+ unit: 'h',
80
+ },
81
+ ];
82
+
83
+ /**
84
+ * @see https://github.com/blocklet/payment-kit/issues/236#issuecomment-1824129965
85
+ * @description
86
+ * @param {Subscription[]} subscriptions
87
+ * @return {*} {SubscriptionForWillRenew[]}
88
+ * @memberof SubscriptionWillRenewSchedule
89
+ */
90
+ getSubscriptionsForWillRenew(subscriptions: Subscription[]): SubscriptionForWillRenew[] {
91
+ return subscriptions.map((subscription: Subscription): SubscriptionForWillRenew => {
92
+ const s: SubscriptionForWillRenew = clone(subscription) as SubscriptionForWillRenew;
93
+ const currentPeriodStart: number = s.current_period_start * 1000;
94
+ const currentPeriodEnd: number = s.current_period_end * 1000;
95
+
96
+ if (
97
+ dayjs(currentPeriodEnd).diff(this.start, 'M') >= 1 &&
98
+ dayjs(currentPeriodEnd).diff(currentPeriodStart, 'M') > 1
99
+ ) {
100
+ s.diffs = SubscriptionWillRenewSchedule.DIFFS.slice(0);
101
+ } else if (
102
+ dayjs(currentPeriodEnd).diff(this.start, 'd') >= 7 &&
103
+ dayjs(currentPeriodEnd).diff(currentPeriodStart, 'd') > 7
104
+ ) {
105
+ s.diffs = SubscriptionWillRenewSchedule.DIFFS.slice(1);
106
+ } else if (
107
+ dayjs(currentPeriodEnd).diff(this.start, 'd') >= 3 &&
108
+ dayjs(currentPeriodEnd).diff(currentPeriodStart, 'd') > 3
109
+ ) {
110
+ s.diffs = SubscriptionWillRenewSchedule.DIFFS.slice(2);
111
+ } else if (
112
+ dayjs(currentPeriodEnd).diff(this.start, 'd') >= 1 &&
113
+ dayjs(currentPeriodEnd).diff(currentPeriodStart, 'd') > 1
114
+ ) {
115
+ s.diffs = SubscriptionWillRenewSchedule.DIFFS.slice(3);
116
+ } else if (
117
+ dayjs(currentPeriodEnd).diff(this.start, 'h') >= 6 &&
118
+ dayjs(currentPeriodEnd).diff(currentPeriodStart, 'h') > 6
119
+ ) {
120
+ s.diffs = SubscriptionWillRenewSchedule.DIFFS.slice(4);
121
+ } else {
122
+ s.diffs = [];
123
+ }
124
+
125
+ // 每一个周期都至少收到一封邮件
126
+ if (dayjs(currentPeriodEnd).diff(currentPeriodStart, 'M') > 1) {
127
+ s.diffs.forEach((x) => {
128
+ if (x.value === 7 && x.unit === 'd') {
129
+ x.required = true;
130
+ }
131
+ });
132
+ } else if (dayjs(currentPeriodEnd).diff(currentPeriodStart, 'd') > 7) {
133
+ s.diffs.forEach((x) => {
134
+ if (x.value === 3 && x.unit === 'd') {
135
+ x.required = true;
136
+ }
137
+ });
138
+ } else if (dayjs(currentPeriodEnd).diff(currentPeriodStart, 'h') >= 6) {
139
+ s.diffs.forEach((x) => {
140
+ if (x.value === 6 && x.unit === 'h') {
141
+ x.required = true;
142
+ }
143
+ });
144
+ }
145
+
146
+ return s;
147
+ });
148
+ }
149
+
150
+ async addTaskToQueue(subscriptions: SubscriptionForWillRenew[]): Promise<void> {
151
+ const tasks: {
152
+ id: string;
153
+ job: { type: NotificationQueueJob['type']; options: SubscriptionWillRenewEmailTemplateOptions };
154
+ delay: number;
155
+ }[] = [];
156
+
157
+ for (const subscription of subscriptions) {
158
+ for (const diff of subscription.diffs) {
159
+ const id: string = `${subscription.id}.${diff.value}.${diff.unit}`;
160
+ const job: { type: NotificationQueueJob['type']; options: SubscriptionWillRenewEmailTemplateOptions } = {
161
+ type: 'customer.subscription.will_renew',
162
+ options: {
163
+ subscriptionId: subscription.id,
164
+ willRenewValue: diff.value,
165
+ willRenewUnit: diff.unit,
166
+ required: !!diff.required,
167
+ },
168
+ };
169
+ const delay: number = dayjs(subscription.current_period_end * 1000)
170
+ .subtract(diff.value, diff.unit)
171
+ .diff(this.start, 's');
172
+
173
+ tasks.push({
174
+ id,
175
+ job,
176
+ delay,
177
+ });
178
+ }
179
+ }
180
+
181
+ await pAll(
182
+ tasks.map((x) => {
183
+ return async () => {
184
+ const job = await notificationQueue.get(x.id);
185
+ if (job) {
186
+ return;
187
+ }
188
+
189
+ notificationQueue.push(x);
190
+ };
191
+ }),
192
+ { concurrency: notificationCronConcurrency }
193
+ );
194
+ }
195
+ }
@@ -5,6 +5,7 @@ import type { LiteralUnion } from 'type-fest';
5
5
 
6
6
  import { createCustomEvent, createEvent, createStatusEvent } from '../../libs/audit';
7
7
  import { createIdGenerator } from '../../libs/util';
8
+ import { Invoice } from './invoice';
8
9
  import type { PaymentDetails, PaymentSettings, PriceRecurring } from './types';
9
10
 
10
11
  const nextId = createIdGenerator('sub', 24);
@@ -266,10 +267,36 @@ export class Subscription extends Model<InferAttributes<Subscription>, InferCrea
266
267
  createdAt: 'created_at',
267
268
  updatedAt: 'updated_at',
268
269
  hooks: {
269
- afterCreate: (model: Subscription, options) =>
270
- createEvent('Subscription', 'customer.subscription.created', model, options).catch(console.error),
271
- afterUpdate: (model: Subscription, options) => {
270
+ afterCreate: (model: Subscription, options) => {
271
+ createEvent('Subscription', 'customer.subscription.created', model, options).catch(console.error);
272
+ },
273
+ afterUpdate: async (model: Subscription, options) => {
272
274
  createEvent('Subscription', 'customer.subscription.updated', model, options).catch(console.error);
275
+
276
+ if (model.trail_start) {
277
+ const previousLatestInvoiceId = model.previous('latest_invoice_id');
278
+
279
+ if (!previousLatestInvoiceId && model.latest_invoice_id) {
280
+ createEvent('Subscription', 'customer.subscription.trial_start', model, options).catch(console.error);
281
+ } else if (
282
+ previousLatestInvoiceId &&
283
+ model.latest_invoice_id &&
284
+ previousLatestInvoiceId !== model.latest_invoice_id
285
+ ) {
286
+ const count: number = await Invoice.count({
287
+ where: {
288
+ subscription_id: model.id,
289
+ },
290
+ });
291
+ if (count === 2) {
292
+ // 当且仅当 有试用期的订阅更新了 && 恰好有 2 次发票,此时订阅的试用期刚好结束
293
+ createEvent('Subscription', 'customer.subscription.trial_end', model, options).catch(console.error);
294
+ }
295
+ }
296
+ } else if (!model.previous('latest_invoice_id') && model.latest_invoice_id) {
297
+ createEvent('Subscription', 'customer.subscription.started', model, options).catch(console.error);
298
+ }
299
+
273
300
  createStatusEvent('Subscription', 'customer.subscription', { canceled: 'deleted' }, model, options).catch(
274
301
  console.error
275
302
  );
@@ -313,15 +340,6 @@ export function getSubscriptionEventType(current: TSubscription, previous: Parti
313
340
  if (!current.pause_collection && previous.pause_collection) {
314
341
  return 'resumed';
315
342
  }
316
- if (
317
- current.status === 'active' &&
318
- current.current_period_end &&
319
- current.current_period_end !== previous.current_period_end &&
320
- current.current_period_start &&
321
- current.current_period_start !== previous.current_period_start
322
- ) {
323
- return 'renewed';
324
- }
325
343
 
326
344
  return '';
327
345
  }
@@ -276,20 +276,17 @@ export type NftMintSettings = {
276
276
  inputs?: Record<string, string>;
277
277
  };
278
278
 
279
+ export interface NftMintItem {
280
+ tx_hash?: string;
281
+ address: string;
282
+ owner: string;
283
+ error?: string;
284
+ }
285
+
279
286
  export type NftMintDetails = {
280
287
  type: LiteralUnion<'arcblock' | 'ethereum' | 'bitcoin', string>;
281
- arcblock?: {
282
- tx_hash?: string;
283
- address: string;
284
- owner: string;
285
- error?: string;
286
- };
287
- ethereum?: {
288
- tx_hash?: string;
289
- address: string;
290
- owner: string;
291
- error?: string;
292
- };
288
+ arcblock?: NftMintItem;
289
+ ethereum?: NftMintItem;
293
290
  };
294
291
 
295
292
  // Very similar to PaymentLink
@@ -406,7 +403,11 @@ export type EventType = LiteralUnion<
406
403
  | 'customer.subscription.pending_update_expired'
407
404
  | 'customer.subscription.resumed'
408
405
  | 'customer.subscription.renewed'
406
+ | 'customer.subscription.renew_failed'
407
+ | 'customer.subscription.trial_start'
409
408
  | 'customer.subscription.trial_will_end'
409
+ | 'customer.subscription.trial_end'
410
+ | 'customer.subscription.started'
410
411
  | 'customer.subscription.updated'
411
412
  | 'customer.tax_id.created'
412
413
  | 'customer.tax_id.deleted'
package/api/third.d.ts CHANGED
@@ -30,3 +30,5 @@ namespace Express {
30
30
  stripeClient?: any;
31
31
  }
32
32
  }
33
+
34
+ declare module 'pretty-ms-i18n';
package/blocklet.yml CHANGED
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.13.65
17
+ version: 1.13.67
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.13.65",
3
+ "version": "1.13.67",
4
4
  "scripts": {
5
5
  "dev": "COMPONENT_STORE_URL=https://test.store.blocklet.dev blocklet dev",
6
6
  "eject": "vite eject",
@@ -40,6 +40,7 @@
40
40
  ]
41
41
  },
42
42
  "dependencies": {
43
+ "@abtnode/cron": "^1.16.19",
43
44
  "@arcblock/did": "^1.18.103",
44
45
  "@arcblock/did-auth-storage-nedb": "^1.7.1",
45
46
  "@arcblock/did-connect": "^2.8.20",
@@ -51,10 +52,10 @@
51
52
  "@blocklet/ui-react": "^2.8.20",
52
53
  "@blocklet/uploader": "^0.0.38",
53
54
  "@mui/icons-material": "^5.14.19",
54
- "@mui/lab": "^5.0.0-alpha.154",
55
- "@mui/material": "^5.14.19",
56
- "@mui/styles": "^5.14.19",
57
- "@mui/system": "^5.14.19",
55
+ "@mui/lab": "^5.0.0-alpha.155",
56
+ "@mui/material": "^5.14.20",
57
+ "@mui/styles": "^5.14.20",
58
+ "@mui/system": "^5.14.20",
58
59
  "@ocap/asset": "^1.18.103",
59
60
  "@ocap/client": "^1.18.103",
60
61
  "@ocap/mcrypto": "^1.18.103",
@@ -82,6 +83,7 @@
82
83
  "lodash": "^4.17.21",
83
84
  "morgan": "^1.10.0",
84
85
  "nanoid": "3",
86
+ "p-all": "3.0.0",
85
87
  "p-wait-for": "3",
86
88
  "pretty-ms-i18n": "^1.0.3",
87
89
  "react": "^18.2.0",
@@ -104,7 +106,7 @@
104
106
  "@abtnode/types": "^1.16.19",
105
107
  "@arcblock/eslint-config": "^0.2.4",
106
108
  "@arcblock/eslint-config-ts": "^0.2.4",
107
- "@did-pay/types": "1.13.65",
109
+ "@did-pay/types": "1.13.67",
108
110
  "@types/cookie-parser": "^1.4.6",
109
111
  "@types/cors": "^2.8.17",
110
112
  "@types/dotenv-flow": "^3.3.3",
@@ -141,5 +143,5 @@
141
143
  "parser": "typescript"
142
144
  }
143
145
  },
144
- "gitHead": "8af4c34686bae8cf09c114b4e3afce2671f320f0"
146
+ "gitHead": "37b1d2ce195a256f8743a6cc53fd24d3d01579b0"
145
147
  }
package/src/app.tsx CHANGED
@@ -19,6 +19,7 @@ const CheckoutPage = React.lazy(() => import('./pages/checkout'));
19
19
  const AdminPage = React.lazy(() => import('./pages/admin'));
20
20
  const CustomerHome = React.lazy(() => import('./pages/customer/index'));
21
21
  const CustomerInvoice = React.lazy(() => import('./pages/customer/invoice'));
22
+ const CustomerSubscription = React.lazy(() => import('./pages/customer/subscription'));
22
23
 
23
24
  const theme = createTheme({
24
25
  typography: {
@@ -48,6 +49,7 @@ function App() {
48
49
  <Route key="admin-sub" path="/admin/:group/:page" element={<AdminPage />} />,
49
50
  <Route key="admin-fallback" path="/admin/*" element={<AdminPage />} />,
50
51
  <Route key="customer-home" path="/customer" element={<CustomerHome />} />,
52
+ <Route key="customer-subscription" path="/customer/subscription/:id" element={<CustomerSubscription />} />
51
53
  <Route key="customer-invoice" path="/customer/invoice/:id" element={<CustomerInvoice />} />,
52
54
  <Route key="customer-fallback" path="/customer/*" element={<Navigate to="/customer" />} />,
53
55
  <Route path="*" element={<Navigate to="/" />} />
@@ -15,13 +15,17 @@ type Props = {
15
15
  data: TInvoiceExpanded;
16
16
  variant?: LiteralUnion<'compact' | 'normal', string>;
17
17
  onChange: (action: string) => void;
18
+
19
+ mode?: 'admin' | 'customer';
18
20
  };
19
21
 
20
22
  InvoiceActions.defaultProps = {
21
23
  variant: 'compact',
24
+
25
+ mode: 'admin',
22
26
  };
23
27
 
24
- export default function InvoiceActions({ data, variant, onChange }: Props) {
28
+ export default function InvoiceActions({ data, variant, onChange, mode }: Props) {
25
29
  const { t } = useLocaleContext();
26
30
  const navigate = useNavigate();
27
31
  const [state, setState] = useSetState({
@@ -43,6 +47,8 @@ export default function InvoiceActions({ data, variant, onChange }: Props) {
43
47
  }
44
48
  };
45
49
 
50
+ const isAdmin = mode === 'admin';
51
+
46
52
  const actions = [
47
53
  {
48
54
  label: t('admin.invoice.download'),
@@ -50,13 +56,13 @@ export default function InvoiceActions({ data, variant, onChange }: Props) {
50
56
  color: 'primary',
51
57
  disabled: data.status !== 'paid',
52
58
  },
53
- {
59
+ isAdmin && {
54
60
  label: t('admin.invoice.edit'),
55
61
  handler: () => setState({ action: 'edit' }),
56
62
  color: 'primary',
57
63
  disabled: data.status !== 'draft',
58
64
  },
59
- {
65
+ isAdmin && {
60
66
  label: t('admin.invoice.duplicate'),
61
67
  handler: () => setState({ action: 'duplicate' }),
62
68
  color: 'primary',
@@ -65,22 +71,34 @@ export default function InvoiceActions({ data, variant, onChange }: Props) {
65
71
  },
66
72
  {
67
73
  label: t('admin.customer.view'),
68
- handler: () => navigate(`/admin/customers/${data.customer_id}`),
74
+ handler: () => {
75
+ if (isAdmin) {
76
+ navigate(`/admin/customers/${data.customer_id}`);
77
+ } else {
78
+ navigate('/customer');
79
+ }
80
+ },
69
81
  color: 'primary',
70
82
  },
71
- ];
83
+ ].filter(Boolean);
72
84
 
73
85
  if (variant === 'compact') {
74
86
  actions.push({
75
87
  label: t('admin.invoice.view'),
76
- handler: () => navigate(`/admin/billing/${data.id}`),
88
+ handler: () => {
89
+ if (isAdmin) {
90
+ navigate(`/admin/billing/${data.id}`);
91
+ } else {
92
+ navigate(`/customer/invoice/${data.id}`);
93
+ }
94
+ },
77
95
  color: 'primary',
78
96
  });
79
97
  }
80
98
 
81
99
  return (
82
100
  <ClickBoundary>
83
- <Actions variant={variant} actions={actions} />
101
+ <Actions variant={variant} actions={actions as any} />
84
102
  {state.action === 'xxx' && (
85
103
  <ConfirmDialog
86
104
  onConfirm={handleAction}