payment-kit 1.13.65 → 1.13.66

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
package/api/src/index.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import 'express-async-errors';
2
2
 
3
+ import './schedule';
4
+
3
5
  import path from 'path';
4
6
 
5
7
  import fallback from '@blocklet/sdk/lib/middlewares/fallback';
@@ -14,6 +16,7 @@ import { ensureWebhookRegistered } from './integrations/stripe/setup';
14
16
  import { startCheckoutSessionQueue } from './jobs/checkout-session';
15
17
  import { startEventQueue } from './jobs/event';
16
18
  import { startInvoiceQueue } from './jobs/invoice';
19
+ import { startNotificationQueue } from './jobs/notification';
17
20
  import { startPaymentQueue } from './jobs/payment';
18
21
  import { startSubscriptionQueue } from './jobs/subscription';
19
22
  import { handlers } from './libs/auth';
@@ -101,6 +104,7 @@ export const server = app.listen(port, (err?: any) => {
101
104
  startSubscriptionQueue().then(() => logger.info('subscription queue started'));
102
105
  startEventQueue().then(() => logger.info('event queue started'));
103
106
  startCheckoutSessionQueue().then(() => logger.info('checkoutSession queue started'));
107
+ startNotificationQueue().then(() => logger.info('notification queue started'));
104
108
 
105
109
  if (process.env.BLOCKLET_MODE === 'production') {
106
110
  ensureWebhookRegistered().catch(console.error);
@@ -1,14 +1,16 @@
1
1
  import Notification from '@blocklet/sdk/service/notification';
2
2
  import { toDid } from '@ocap/util';
3
+ import { get } from 'lodash';
4
+ import type { LiteralUnion } from 'type-fest';
3
5
 
4
6
  import { blocklet } from '../../libs/auth';
5
7
  import logger from '../../libs/logger';
6
8
  import { translate } from '../../locales';
7
9
 
8
- export async function getUserLocale(userDid: string) {
10
+ export const getUserLocale = async (userDid: string): Promise<LiteralUnion<'zh' | 'en', string>> => {
9
11
  const { user } = await blocklet.getUser(userDid);
10
- return user?.locale || 'en';
11
- }
12
+ return get(user, 'locale', 'en');
13
+ };
12
14
 
13
15
  export function createInfoRows(info: any = {}) {
14
16
  const fields = Object.keys(info).reduce((list, cur) => {
@@ -0,0 +1,142 @@
1
+ import { events } from '../libs/event';
2
+ import logger from '../libs/logger';
3
+ import { Notification } from '../libs/notification';
4
+ import type { BaseEmailTemplate } from '../libs/notification/template/base';
5
+ import {
6
+ SubscriptionRenewFailedEmailTemplate,
7
+ SubscriptionRenewFailedEmailTemplateOptions,
8
+ } from '../libs/notification/template/subscription-renew-failed';
9
+ import {
10
+ SubscriptionRenewedEmailTemplate,
11
+ SubscriptionRenewedEmailTemplateOptions,
12
+ } from '../libs/notification/template/subscription-renewed';
13
+ import {
14
+ SubscriptionSucceededEmailTemplate,
15
+ SubscriptionSucceededEmailTemplateOptions,
16
+ } from '../libs/notification/template/subscription-succeeded';
17
+ import {
18
+ SubscriptionTrailStartEmailTemplate,
19
+ SubscriptionTrailStartEmailTemplateOptions,
20
+ } from '../libs/notification/template/subscription-trial-start';
21
+ import {
22
+ SubscriptionTrailWilEndEmailTemplate,
23
+ SubscriptionTrialWillEndEmailTemplateOptions,
24
+ } from '../libs/notification/template/subscription-trial-will-end';
25
+ import {
26
+ SubscriptionWillRenewEmailTemplate,
27
+ SubscriptionWillRenewEmailTemplateOptions,
28
+ } from '../libs/notification/template/subscription-will-renew';
29
+ import type { SufficientForPaymentResult } from '../libs/payment';
30
+ import createQueue from '../libs/queue';
31
+ import type { EventType, Invoice, Subscription } from '../store/models';
32
+
33
+ export type NotificationQueueJobOptions = any;
34
+ export type NotificationQueueJob = {
35
+ type: EventType | 'customer.subscription.will_renew' | 'customer.subscription.trial_will_end';
36
+ options: NotificationQueueJobOptions;
37
+ };
38
+
39
+ function getNotificationTemplate(job: NotificationQueueJob): BaseEmailTemplate {
40
+ if (job.type === 'customer.subscription.started') {
41
+ return new SubscriptionSucceededEmailTemplate(job.options as SubscriptionSucceededEmailTemplateOptions);
42
+ }
43
+ if (job.type === 'customer.subscription.renewed') {
44
+ return new SubscriptionRenewedEmailTemplate(job.options as SubscriptionRenewedEmailTemplateOptions);
45
+ }
46
+ if (job.type === 'customer.subscription.renew_failed') {
47
+ return new SubscriptionRenewFailedEmailTemplate(job.options as SubscriptionRenewFailedEmailTemplateOptions);
48
+ }
49
+ if (job.type === 'customer.subscription.trial_start') {
50
+ return new SubscriptionTrailStartEmailTemplate(job.options as SubscriptionTrailStartEmailTemplateOptions);
51
+ }
52
+ if (job.type === 'customer.subscription.will_renew') {
53
+ return new SubscriptionWillRenewEmailTemplate(job.options as SubscriptionWillRenewEmailTemplateOptions);
54
+ }
55
+ if (job.type === 'customer.subscription.trial_will_end') {
56
+ return new SubscriptionTrailWilEndEmailTemplate(job.options as SubscriptionTrialWillEndEmailTemplateOptions);
57
+ }
58
+
59
+ throw new Error(`Unknown job type: ${job.type}`);
60
+ }
61
+
62
+ async function handleNotificationJob(job: NotificationQueueJob): Promise<void> {
63
+ try {
64
+ const template = getNotificationTemplate(job);
65
+ await new Notification(template).send();
66
+ } catch (error) {
67
+ logger.error('handleNotificationJob.error.$job', job);
68
+ logger.error('handleNotificationJob.error', error);
69
+ throw error;
70
+ }
71
+ }
72
+
73
+ export const notificationQueue = createQueue<NotificationQueueJob>({
74
+ name: 'notification',
75
+ onJob: handleNotificationJob,
76
+ options: {
77
+ concurrency: 10,
78
+ maxRetries: 3,
79
+ enableScheduledJob: true,
80
+ },
81
+ });
82
+
83
+ /**
84
+ *
85
+ * @see https://team.arcblock.io/comment/discussions/0fba06ff-75b4-47d2-8c5c-f0379540ce03
86
+ * @description
87
+ * @export
88
+ */
89
+ // eslint-disable-next-line require-await
90
+ export async function startNotificationQueue() {
91
+ events.on('customer.subscription.trial_start', (subscription: Subscription) => {
92
+ notificationQueue.push({
93
+ job: {
94
+ type: 'customer.subscription.trial_start',
95
+ options: {
96
+ subscriptionId: subscription.id,
97
+ },
98
+ },
99
+ });
100
+ });
101
+
102
+ events.on('customer.subscription.started', (subscription: Subscription) => {
103
+ if (!subscription.trail_start) {
104
+ // 没有试用期的 subscription 通知
105
+ notificationQueue.push({
106
+ job: {
107
+ type: 'customer.subscription.started',
108
+ options: {
109
+ subscriptionId: subscription.id,
110
+ },
111
+ },
112
+ });
113
+ }
114
+ });
115
+
116
+ events.on('customer.subscription.renewed', (subscription: Subscription, invoice: Invoice) => {
117
+ notificationQueue.push({
118
+ job: {
119
+ type: 'customer.subscription.renewed',
120
+ options: {
121
+ subscriptionId: subscription.id,
122
+ invoiceId: invoice?.id,
123
+ } as SubscriptionRenewedEmailTemplateOptions,
124
+ },
125
+ });
126
+ });
127
+
128
+ events.on(
129
+ 'customer.subscription.renew_failed',
130
+ ({ invoice, result }: { invoice: Invoice; result: SufficientForPaymentResult }) => {
131
+ notificationQueue.push({
132
+ job: {
133
+ type: 'customer.subscription.renew_failed',
134
+ options: {
135
+ invoice,
136
+ result,
137
+ },
138
+ },
139
+ });
140
+ }
141
+ );
142
+ }
@@ -65,6 +65,16 @@ export const handlePaymentSucceed = async (paymentIntent: PaymentIntent) => {
65
65
  logger.info(`Subscription ${subscription.id} moved to active after payment done ${paymentIntent.id}`);
66
66
  }
67
67
  }
68
+
69
+ const count: number = await Invoice.count({
70
+ where: {
71
+ subscription_id: invoice.subscription_id,
72
+ status: 'paid',
73
+ },
74
+ });
75
+ if (count >= 2) {
76
+ events.emit('customer.subscription.renewed', subscription, invoice);
77
+ }
68
78
  }
69
79
 
70
80
  if (invoice.checkout_session_id) {
@@ -136,6 +146,10 @@ export const handlePayment = async (job: PaymentJob) => {
136
146
  });
137
147
  if (result.sufficient === false) {
138
148
  logger.error('PaymentIntent capture aborted on preCheck', { id: paymentIntent.id, result });
149
+ events.emit('customer.subscription.renew_failed', {
150
+ invoice,
151
+ result,
152
+ });
139
153
  // FIXME: send email to customer, pause subscription
140
154
  throw new CustomError(result.reason, 'payer balance or delegation not sufficient for this payment');
141
155
  }
@@ -174,8 +174,8 @@ export const handleSubscription = async (job: SubscriptionJob) => {
174
174
  number: await customer.getInvoiceNumber(),
175
175
  description: 'Subscription cycle',
176
176
  statement_descriptor: getStatementDescriptor(expandedItems),
177
- period_start: subscription.current_period_start,
178
- period_end: subscription.current_period_end,
177
+ period_start: setup.period.start,
178
+ period_end: setup.period.end,
179
179
 
180
180
  auto_advance: true,
181
181
  paid: false,
@@ -1,11 +1,13 @@
1
1
  import pick from 'lodash/pick';
2
+ import type { LiteralUnion } from 'type-fest';
2
3
 
4
+ import type { EventType } from '../store/models';
3
5
  import { Event } from '../store/models/event';
4
6
  import { events } from './event';
5
7
 
6
8
  const API_VERSION = '2023-09-05';
7
9
 
8
- export async function createEvent(scope: string, type: string, model: any, options: any) {
10
+ export async function createEvent(scope: string, type: LiteralUnion<EventType, string>, model: any, options: any) {
9
11
  // console.log('createEvent', scope, type, model, options);
10
12
  const data: any = {
11
13
  object: model.dataValues,
@@ -1,5 +1,8 @@
1
1
  import env from '@blocklet/sdk/lib/env';
2
2
 
3
+ export const notificationCronTime: string = process.env.NOTIFICATION_CRON_TIME || '0 5 */6 * * *'; // 默认每6个小时执行一次
4
+ export const notificationCronConcurrency: number = Number(process.env.NOTIFICATION_CRON_CONCURRENCY) || 8; // 默认并发数为 8
5
+
3
6
  export default {
4
7
  ...env,
5
8
  };
@@ -1,3 +1,12 @@
1
1
  import EventEmitter from 'events';
2
2
 
3
- export const events = new EventEmitter();
3
+ import type { LiteralUnion } from 'type-fest';
4
+
5
+ import type { EventType } from '../store/models';
6
+
7
+ interface MyEventType extends EventEmitter {
8
+ on(eventName: LiteralUnion<EventType, string | symbol>, listener: (...args: any[]) => void): this;
9
+ emit(eventName: LiteralUnion<EventType, string | symbol>, ...args: any[]): boolean;
10
+ }
11
+
12
+ export const events = new EventEmitter() as MyEventType;
@@ -0,0 +1,5 @@
1
+ import { component } from '@blocklet/sdk';
2
+
3
+ export function getCustomerInvoicePageUrl(invoiceId: string, locale: string = 'en') {
4
+ return component.getUrl(`customer/invoice/${invoiceId}?locale=${locale}`);
5
+ }
@@ -0,0 +1,23 @@
1
+ import { Notification as BlockletNotification } from '@blocklet/sdk';
2
+
3
+ import type { BaseEmailTemplate, BaseEmailTemplateType } from './template/base';
4
+
5
+ export class Notification {
6
+ template: BaseEmailTemplate;
7
+
8
+ constructor(template: BaseEmailTemplate) {
9
+ this.template = template;
10
+ }
11
+
12
+ async send() {
13
+ const template: BaseEmailTemplateType | null = await this.template.getTemplate();
14
+
15
+ if (!template) {
16
+ return;
17
+ }
18
+
19
+ const { userDid } = await this.template.getContext();
20
+
21
+ await BlockletNotification.sendToUser(userDid, template as any);
22
+ }
23
+ }
@@ -0,0 +1,12 @@
1
+ import type { TNotification, TNotificationInput } from '@blocklet/sdk/lib/types/notification';
2
+
3
+ export type BaseEmailTemplateType = TNotificationInput | TNotification;
4
+ export type BaseEmailTemplateContext = {
5
+ userDid: string;
6
+ };
7
+
8
+ export interface BaseEmailTemplate<C = BaseEmailTemplateContext> {
9
+ getTemplate(): Promise<BaseEmailTemplateType | null>;
10
+
11
+ getContext(): Promise<C>;
12
+ }
@@ -0,0 +1,286 @@
1
+ /* eslint-disable @typescript-eslint/brace-style */
2
+ /* eslint-disable @typescript-eslint/indent */
3
+ import { fromUnitToToken, toDid } from '@ocap/util';
4
+ import prettyMsI18n from 'pretty-ms-i18n';
5
+
6
+ import { getUserLocale } from '../../../integrations/blocklet/notification';
7
+ import { translate } from '../../../locales';
8
+ import {
9
+ CheckoutSession,
10
+ Customer,
11
+ Invoice,
12
+ NftMintItem,
13
+ PaymentIntent,
14
+ PaymentMethod,
15
+ Subscription,
16
+ } from '../../../store/models';
17
+ import { PaymentCurrency } from '../../../store/models/payment-currency';
18
+ import { getCustomerInvoicePageUrl } from '../../invoice';
19
+ import { SufficientForPaymentResult, getPaymentDetail } from '../../payment';
20
+ import { getMainProductName } from '../../product';
21
+ import { getCustomerSubscriptionPageUrl } from '../../subscription';
22
+ import { formatTime, getPrettyMsI18nLocale } from '../../time';
23
+ import { getExplorerLink } from '../../util';
24
+ import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
25
+
26
+ export interface SubscriptionRenewFailedEmailTemplateOptions {
27
+ invoice: Invoice;
28
+ result: SufficientForPaymentResult;
29
+ }
30
+
31
+ interface SubscriptionRenewFailedEmailTemplateContext {
32
+ locale: string;
33
+ productName: string;
34
+ at: string;
35
+ reason: string;
36
+
37
+ nftMintItem: NftMintItem | undefined;
38
+ userDid: string;
39
+ paymentInfo: string;
40
+ currentPeriodStart: string;
41
+ currentPeriodEnd: string;
42
+ duration: string;
43
+
44
+ viewSubscriptionLink: string;
45
+ viewInvoiceLink: string;
46
+ viewTxHashLink: string | undefined;
47
+ }
48
+
49
+ export class SubscriptionRenewFailedEmailTemplate
50
+ implements BaseEmailTemplate<SubscriptionRenewFailedEmailTemplateContext>
51
+ {
52
+ options: SubscriptionRenewFailedEmailTemplateOptions;
53
+
54
+ constructor(options: SubscriptionRenewFailedEmailTemplateOptions) {
55
+ this.options = options;
56
+ }
57
+
58
+ private async getReason(userDid: string, invoice: Invoice, locale: string): Promise<string> {
59
+ if (this.options.result.sufficient) {
60
+ throw new Error(`SufficientForPaymentResult.sufficient should be false: ${JSON.stringify(this.options.result)}`);
61
+ }
62
+
63
+ // 类似 NO_DID_WALLET 字符串将转成 noDidWallet
64
+ const toCamelCase = (input: string): string => {
65
+ return input.toLowerCase().replace(/_([a-z])/g, (_match, group1) => {
66
+ return group1.toUpperCase();
67
+ });
68
+ };
69
+
70
+ const i18nText = `notification.subscriptionRenewFailed.reason.${toCamelCase(this.options.result.reason as string)}`;
71
+
72
+ const paymentDetail = await getPaymentDetail(userDid, invoice);
73
+ const reason = translate(i18nText, locale, {
74
+ balance: `${paymentDetail.balance} ${paymentDetail.symbol}`,
75
+ price: `${paymentDetail.price} ${paymentDetail.symbol}`,
76
+ });
77
+
78
+ return reason;
79
+ }
80
+
81
+ async getContext(): Promise<SubscriptionRenewFailedEmailTemplateContext> {
82
+ const subscription: Subscription | null = await Subscription.findByPk(this.options.invoice.subscription_id);
83
+ if (!subscription) {
84
+ throw new Error(`Subscription not found: ${this.options.invoice.subscription_id}`);
85
+ }
86
+
87
+ const customer = await Customer.findByPk(subscription.customer_id);
88
+ if (!customer) {
89
+ throw new Error(`Customer not found: ${subscription.customer_id}`);
90
+ }
91
+
92
+ const { invoice } = this.options;
93
+ const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
94
+ const paymentCurrency = (await PaymentCurrency.findOne({
95
+ where: {
96
+ id: subscription.currency_id,
97
+ },
98
+ })) as PaymentCurrency;
99
+
100
+ const checkoutSession = await CheckoutSession.findOne({
101
+ where: {
102
+ subscription_id: subscription.id,
103
+ },
104
+ });
105
+
106
+ const locale = await getUserLocale(customer.did);
107
+ const productName = await getMainProductName(subscription.id);
108
+ const at: string = formatTime(Date.now());
109
+ const reason: string = await this.getReason(customer.did, invoice, locale);
110
+
111
+ const hasNft: boolean = checkoutSession?.nft_mint_status === 'minted';
112
+ const nftMintItem: NftMintItem | undefined = hasNft
113
+ ? checkoutSession?.nft_mint_details?.[checkoutSession?.nft_mint_details?.type as 'arcblock' | 'ethereum']
114
+ : undefined;
115
+ const paymentInfo: string = `${fromUnitToToken(invoice.amount_remaining, paymentCurrency.decimal)} ${
116
+ paymentCurrency.symbol
117
+ }`;
118
+ const currentPeriodStart: string = formatTime(invoice.period_start * 1000);
119
+ const currentPeriodEnd: string = formatTime(invoice.period_end * 1000);
120
+ const duration: string = prettyMsI18n(
121
+ new Date(currentPeriodEnd).getTime() - new Date(currentPeriodStart).getTime(),
122
+ {
123
+ locale: getPrettyMsI18nLocale(locale),
124
+ }
125
+ );
126
+
127
+ const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(invoice.default_payment_method_id);
128
+ const chainHost: string | undefined =
129
+ paymentMethod?.settings?.[checkoutSession?.nft_mint_details?.type as 'arcblock' | 'ethereum']?.api_host;
130
+ const viewSubscriptionLink = getCustomerSubscriptionPageUrl(subscription.id, locale);
131
+ const viewInvoiceLink = getCustomerInvoicePageUrl(invoice.id, locale);
132
+ const txHash: string | undefined =
133
+ paymentIntent?.payment_details?.[checkoutSession?.nft_mint_details?.type as 'arcblock' | 'ethereum']?.tx_hash;
134
+ const viewTxHashLink: string | undefined = hasNft && txHash ? getExplorerLink(chainHost, txHash, 'tx') : undefined;
135
+
136
+ return {
137
+ locale,
138
+ productName,
139
+ at,
140
+ reason,
141
+
142
+ nftMintItem,
143
+ userDid: customer.did,
144
+ paymentInfo,
145
+ currentPeriodStart,
146
+ currentPeriodEnd,
147
+ duration,
148
+
149
+ viewSubscriptionLink,
150
+ viewInvoiceLink,
151
+ viewTxHashLink,
152
+ };
153
+ }
154
+
155
+ async getTemplate(): Promise<BaseEmailTemplateType> {
156
+ const {
157
+ locale,
158
+ productName,
159
+ at,
160
+ nftMintItem,
161
+ userDid,
162
+ paymentInfo,
163
+ currentPeriodStart,
164
+ currentPeriodEnd,
165
+ duration,
166
+ reason,
167
+ viewSubscriptionLink,
168
+ viewInvoiceLink,
169
+ viewTxHashLink,
170
+ } = await this.getContext();
171
+
172
+ const template: BaseEmailTemplateType = {
173
+ title: `${translate('notification.subscriptionRenewFailed.title', locale, {
174
+ productName: `(${productName})`,
175
+ })}`,
176
+ body: `${translate('notification.subscriptionRenewFailed.body', locale, {
177
+ at,
178
+ productName: `(${productName})`,
179
+ reason: `${reason}`,
180
+ })}`,
181
+ // @ts-expect-error
182
+ attachments: [
183
+ {
184
+ type: 'section',
185
+ fields: [
186
+ {
187
+ type: 'text',
188
+ data: {
189
+ type: 'plain',
190
+ color: '#9397A1',
191
+ text: translate('notification.common.account', locale),
192
+ },
193
+ },
194
+ {
195
+ type: 'text',
196
+ data: {
197
+ type: 'plain',
198
+ text: userDid,
199
+ },
200
+ },
201
+ {
202
+ type: 'text',
203
+ data: {
204
+ type: 'plain',
205
+ color: '#9397A1',
206
+ text: translate('notification.common.product', locale),
207
+ },
208
+ },
209
+ {
210
+ type: 'text',
211
+ data: {
212
+ type: 'plain',
213
+ text: productName,
214
+ },
215
+ },
216
+ {
217
+ type: 'text',
218
+ data: {
219
+ type: 'plain',
220
+ color: '#9397A1',
221
+ text: translate('notification.common.paymentInfo', locale),
222
+ },
223
+ },
224
+ {
225
+ type: 'text',
226
+ data: {
227
+ type: 'plain',
228
+ text: paymentInfo,
229
+ },
230
+ },
231
+ {
232
+ type: 'text',
233
+ data: {
234
+ type: 'plain',
235
+ color: '#9397A1',
236
+ text: translate('notification.common.validityPeriod', locale),
237
+ },
238
+ },
239
+ {
240
+ type: 'text',
241
+ data: {
242
+ type: 'plain',
243
+ text: `${currentPeriodStart} ~ ${currentPeriodEnd}(${duration})`,
244
+ },
245
+ },
246
+ nftMintItem && {
247
+ type: 'text',
248
+ data: {
249
+ type: 'plain',
250
+ color: '#9397A1',
251
+ text: translate('notification.common.nftAddress', locale),
252
+ },
253
+ },
254
+ nftMintItem && {
255
+ type: 'text',
256
+ data: {
257
+ type: 'plain',
258
+ text: toDid(nftMintItem.address),
259
+ },
260
+ },
261
+ ].filter(Boolean),
262
+ },
263
+ ].filter(Boolean),
264
+ // @ts-ignore
265
+ actions: [
266
+ {
267
+ name: 'viewSubscription',
268
+ title: translate('notification.common.viewSubscription', locale),
269
+ link: viewSubscriptionLink,
270
+ },
271
+ {
272
+ name: 'viewSubscription',
273
+ title: translate('notification.subscriptionRenewFailed.renewNow', locale),
274
+ link: viewInvoiceLink,
275
+ },
276
+ viewTxHashLink && {
277
+ name: 'viewTxHash',
278
+ title: translate('notification.common.viewTxHash', locale),
279
+ link: viewTxHashLink as string,
280
+ },
281
+ ].filter(Boolean),
282
+ };
283
+
284
+ return template;
285
+ }
286
+ }