payment-kit 1.17.12 → 1.18.1

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 (41) hide show
  1. package/api/src/integrations/arcblock/stake.ts +0 -5
  2. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +254 -0
  3. package/api/src/libs/notification/template/customer-reward-succeeded.ts +12 -11
  4. package/api/src/libs/payment.ts +47 -2
  5. package/api/src/libs/payout.ts +24 -0
  6. package/api/src/libs/util.ts +83 -1
  7. package/api/src/locales/en.ts +16 -1
  8. package/api/src/locales/zh.ts +28 -12
  9. package/api/src/queues/notification.ts +23 -1
  10. package/api/src/routes/invoices.ts +42 -5
  11. package/api/src/routes/payment-intents.ts +14 -1
  12. package/api/src/routes/payment-links.ts +17 -0
  13. package/api/src/routes/payment-methods.ts +28 -1
  14. package/api/src/routes/payouts.ts +103 -8
  15. package/api/src/store/migrations/20250206-update-donation-products.ts +56 -0
  16. package/api/src/store/models/payout.ts +6 -2
  17. package/api/src/store/models/types.ts +2 -0
  18. package/blocklet.yml +1 -1
  19. package/package.json +4 -4
  20. package/public/methods/default.png +0 -0
  21. package/src/app.tsx +10 -0
  22. package/src/components/customer/link.tsx +11 -2
  23. package/src/components/customer/overdraft-protection.tsx +2 -2
  24. package/src/components/info-card.tsx +6 -5
  25. package/src/components/invoice/table.tsx +4 -0
  26. package/src/components/payment-method/form.tsx +4 -4
  27. package/src/components/payouts/list.tsx +17 -2
  28. package/src/components/payouts/portal/list.tsx +192 -0
  29. package/src/components/subscription/items/actions.tsx +1 -2
  30. package/src/components/uploader.tsx +1 -1
  31. package/src/libs/util.ts +42 -1
  32. package/src/locales/en.tsx +10 -0
  33. package/src/locales/zh.tsx +10 -0
  34. package/src/pages/admin/billing/invoices/detail.tsx +21 -0
  35. package/src/pages/admin/payments/payouts/detail.tsx +65 -4
  36. package/src/pages/admin/settings/payment-methods/edit.tsx +12 -1
  37. package/src/pages/customer/index.tsx +12 -25
  38. package/src/pages/customer/invoice/detail.tsx +27 -3
  39. package/src/pages/customer/payout/detail.tsx +264 -0
  40. package/src/pages/customer/recharge.tsx +2 -2
  41. package/vite.config.ts +1 -0
@@ -4,7 +4,6 @@ import assert from 'assert';
4
4
 
5
5
  import { isEthereumDid } from '@arcblock/did';
6
6
  import { toStakeAddress } from '@arcblock/did-util';
7
- import env from '@blocklet/sdk/lib/env';
8
7
  import { BN, fromUnitToToken, toBN } from '@ocap/util';
9
8
 
10
9
  import { Op } from 'sequelize';
@@ -26,10 +25,6 @@ export async function ensureStakedForGas() {
26
25
 
27
26
  try {
28
27
  const { state: account } = await client.getAccountState({ address: wallet.address });
29
- if (!account) {
30
- const hash = await client.declare({ moniker: env.appNameSlug, wallet });
31
- logger.info(`declared app on chain ${host}`, { hash });
32
- }
33
28
 
34
29
  const address = toStakeAddress(wallet.address, wallet.address);
35
30
  const { state: stake } = await client.getStakeState({ address });
@@ -0,0 +1,254 @@
1
+ /* eslint-disable @typescript-eslint/brace-style */
2
+ /* eslint-disable @typescript-eslint/indent */
3
+ import { fromUnitToToken } from '@ocap/util';
4
+ import { getUrl } from '@blocklet/sdk';
5
+ import { getUserLocale } from '../../../integrations/blocklet/notification';
6
+ import { translate } from '../../../locales';
7
+ import { CheckoutSession, Customer, PaymentLink, PaymentMethod, Payout } from '../../../store/models';
8
+ import { PaymentCurrency } from '../../../store/models/payment-currency';
9
+ import { formatTime } from '../../time';
10
+ import { getCustomerProfileUrl, getExplorerLink, getUserOrAppInfo } from '../../util';
11
+ import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
12
+ import { getCustomerPayoutPageUrl } from '../../payout';
13
+ import { PaymentIntent } from '../../../store/models/payment-intent';
14
+
15
+ export interface CustomerRevenueSucceededEmailTemplateOptions {
16
+ payoutId: string;
17
+ }
18
+
19
+ interface CustomerRevenueSucceededEmailTemplateContext {
20
+ locale: string;
21
+ at: string;
22
+ paymentIntentId: string;
23
+
24
+ chainHost: string | undefined;
25
+ userDid: string;
26
+ paymentInfo: string;
27
+ user: string;
28
+ revenueDetail: {
29
+ url: string;
30
+ title: string;
31
+ appDID: string;
32
+ logo?: string;
33
+ desc?: string;
34
+ };
35
+
36
+ viewPayoutLink: string;
37
+ viewTxHashLink: string;
38
+ type: 'payment' | 'donate';
39
+ }
40
+
41
+ /**
42
+ * @description
43
+ * @export
44
+ * @class CustomerRevenueSucceededEmailTemplate
45
+ * @implements {BaseEmailTemplate<CustomerRevenueSucceededEmailTemplateContext>}
46
+ */
47
+ export class CustomerRevenueSucceededEmailTemplate
48
+ implements BaseEmailTemplate<CustomerRevenueSucceededEmailTemplateContext>
49
+ {
50
+ options: CustomerRevenueSucceededEmailTemplateOptions;
51
+
52
+ constructor(options: CustomerRevenueSucceededEmailTemplateOptions) {
53
+ this.options = options;
54
+ }
55
+
56
+ async getContext(): Promise<CustomerRevenueSucceededEmailTemplateContext> {
57
+ const payout = (await Payout.findByPk(this.options.payoutId, {
58
+ include: [
59
+ {
60
+ model: PaymentIntent,
61
+ as: 'paymentIntent',
62
+ },
63
+ ],
64
+ })) as Payout & { paymentIntent: PaymentIntent };
65
+ if (!payout) {
66
+ throw new Error(`Payout(${this.options.payoutId}) not found`);
67
+ }
68
+
69
+ if (!payout.paymentIntent) {
70
+ throw new Error(`PaymentIntent not found for payout: ${this.options.payoutId}`);
71
+ }
72
+
73
+ const customer = await Customer.findByPk(payout.customer_id);
74
+ if (!customer) {
75
+ throw new Error(`Customer not found: ${payout.customer_id}`);
76
+ }
77
+
78
+ const paymentCurrency = (await PaymentCurrency.findOne({
79
+ where: {
80
+ id: payout.currency_id,
81
+ },
82
+ })) as PaymentCurrency;
83
+
84
+ const userDid: string = customer.did;
85
+ const locale = await getUserLocale(userDid);
86
+ const at: string = formatTime(payout.created_at);
87
+ let type: 'payment' | 'donate' = 'payment';
88
+
89
+ try {
90
+ const checkoutSession = await CheckoutSession.findOne({
91
+ where: {
92
+ payment_intent_id: payout.payment_intent_id,
93
+ },
94
+ attributes: ['id', 'submit_type'],
95
+ });
96
+ if (checkoutSession && checkoutSession.submit_type === 'donate') {
97
+ type = 'donate';
98
+ }
99
+ } catch (error) {
100
+ console.error(error);
101
+ }
102
+
103
+ const payer = await Customer.findByPk(payout.paymentIntent?.customer_id);
104
+ if (!payer) {
105
+ throw new Error(`Payer not found for paymentIntent: ${payout.paymentIntent?.customer_id}`);
106
+ }
107
+
108
+ const paymentInfo: string = `${fromUnitToToken(payout.amount, paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
109
+ const userInfo = await getUserOrAppInfo(payer.did);
110
+ const revenueDetail = {
111
+ url: getCustomerProfileUrl({ userDid: payer.did, locale }),
112
+ title: translate('notification.customerRevenueSucceeded.sended', locale, {
113
+ address: payer.name || payer.did,
114
+ amount: paymentInfo,
115
+ }),
116
+ logo: userInfo?.avatar || getUrl('/methods/default.png'),
117
+ appDID: payer.did,
118
+ desc: '',
119
+ };
120
+
121
+ const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(payout.payment_method_id);
122
+ // @ts-expect-error
123
+ const chainHost: string | undefined = paymentMethod?.settings?.[paymentMethod.type]?.api_host;
124
+ const viewPayoutLink = getCustomerPayoutPageUrl({
125
+ payoutId: this.options.payoutId,
126
+ userDid,
127
+ locale,
128
+ });
129
+
130
+ // @ts-expect-error
131
+ const txHash: string | undefined = payout?.payment_details?.[paymentMethod.type]?.tx_hash;
132
+ const viewTxHashLink: string =
133
+ (txHash &&
134
+ getExplorerLink({
135
+ type: 'tx',
136
+ did: txHash,
137
+ chainHost,
138
+ })) ||
139
+ '';
140
+
141
+ return {
142
+ locale,
143
+ at,
144
+ type,
145
+ user: payer.name || userInfo?.name || payer.did,
146
+ paymentIntentId: payout.payment_intent_id,
147
+ userDid,
148
+ chainHost,
149
+ paymentInfo,
150
+ revenueDetail,
151
+
152
+ viewPayoutLink,
153
+ viewTxHashLink,
154
+ };
155
+ }
156
+
157
+ async getReference(paymentIntentId: string): Promise<string> {
158
+ try {
159
+ const checkoutSession = (await CheckoutSession.findOne({
160
+ where: {
161
+ payment_intent_id: paymentIntentId,
162
+ },
163
+ attributes: ['id', 'payment_link_id'],
164
+ })) as CheckoutSession;
165
+ if (!checkoutSession) {
166
+ throw new Error(`CheckoutSession not found for paymentIntentId: ${paymentIntentId}`);
167
+ }
168
+ if (!checkoutSession.payment_link_id) {
169
+ throw new Error(`Payment link cannot be found for checkoutSession: ${checkoutSession.id}`);
170
+ }
171
+ const paymentLink = await PaymentLink.findByPk(checkoutSession.payment_link_id);
172
+ if (!paymentLink) {
173
+ throw new Error(`Payment link cannot be found for payment_link_id(${checkoutSession.payment_link_id})`);
174
+ }
175
+ return paymentLink.donation_settings?.reference || '';
176
+ } catch (error) {
177
+ console.error(error);
178
+ return '';
179
+ }
180
+ }
181
+
182
+ async getTemplate(): Promise<BaseEmailTemplateType> {
183
+ const {
184
+ locale,
185
+ at,
186
+ user,
187
+ type,
188
+ paymentIntentId,
189
+ paymentInfo,
190
+ revenueDetail,
191
+
192
+ viewPayoutLink,
193
+ viewTxHashLink,
194
+ } = await this.getContext();
195
+
196
+ const reference = await this.getReference(paymentIntentId);
197
+
198
+ const template: BaseEmailTemplateType = {
199
+ title: translate(`notification.customerRevenueSucceeded.${type}.title`, locale),
200
+ body: translate(`notification.customerRevenueSucceeded.${type}.body`, locale, {
201
+ at,
202
+ amount: paymentInfo,
203
+ user,
204
+ }),
205
+ // @ts-expect-error
206
+ attachments: [
207
+ {
208
+ type: 'section',
209
+ fields: [
210
+ {
211
+ type: 'text',
212
+ data: {
213
+ type: 'plain',
214
+ color: '#9397A1',
215
+ text: translate('notification.customerRevenueSucceeded.sender', locale),
216
+ },
217
+ },
218
+ {
219
+ type: 'text',
220
+ data: {
221
+ type: 'plain',
222
+ text: ' ',
223
+ },
224
+ },
225
+ ].filter(Boolean),
226
+ },
227
+ {
228
+ type: 'dapp',
229
+ data: revenueDetail,
230
+ },
231
+ ].filter(Boolean),
232
+ // @ts-expect-error
233
+ actions: [
234
+ viewPayoutLink && {
235
+ name: translate('notification.customerRevenueSucceeded.viewDetail', locale),
236
+ title: translate('notification.customerRevenueSucceeded.viewDetail', locale),
237
+ link: viewPayoutLink,
238
+ },
239
+ viewTxHashLink && {
240
+ name: translate('notification.common.viewTxHash', locale),
241
+ title: translate('notification.common.viewTxHash', locale),
242
+ link: viewTxHashLink,
243
+ },
244
+ reference && {
245
+ name: translate('notification.customerRevenueSucceeded.donate.tipDetail', locale),
246
+ title: translate('notification.customerRevenueSucceeded.donate.tipDetail', locale),
247
+ link: reference,
248
+ },
249
+ ].filter(Boolean),
250
+ };
251
+
252
+ return template;
253
+ }
254
+ }
@@ -5,7 +5,7 @@ import isEmpty from 'lodash/isEmpty';
5
5
  import pWaitFor from 'p-wait-for';
6
6
  import type { LiteralUnion } from 'type-fest';
7
7
 
8
- import { joinURL } from 'ufo';
8
+ import { getUrl } from '@blocklet/sdk';
9
9
  import { getUserLocale } from '../../../integrations/blocklet/notification';
10
10
  import { translate } from '../../../locales';
11
11
  import {
@@ -22,9 +22,8 @@ import { PaymentCurrency } from '../../../store/models/payment-currency';
22
22
  import { getCustomerInvoicePageUrl } from '../../invoice';
23
23
  import logger from '../../logger';
24
24
  import { formatTime } from '../../time';
25
- import { getCustomerProfileUrl, getExplorerLink } from '../../util';
25
+ import { getBlockletJson, getCustomerProfileUrl, getExplorerLink, getUserOrAppInfo } from '../../util';
26
26
  import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
27
- import { blocklet } from '../../auth';
28
27
 
29
28
  export interface CustomerRewardSucceededEmailTemplateOptions {
30
29
  checkoutSessionId: string;
@@ -197,8 +196,9 @@ export class CustomerRewardSucceededEmailTemplate
197
196
  return null;
198
197
  }
199
198
 
199
+ const blockletJson = await getBlockletJson();
200
200
  const promises = paymentIntent.beneficiaries!.map((x: PaymentBeneficiary) => {
201
- return blocklet.getUser(x.address);
201
+ return getUserOrAppInfo(x.address, blockletJson);
202
202
  });
203
203
  const users = await Promise.all(promises);
204
204
  if (!users.length) {
@@ -207,15 +207,12 @@ export class CustomerRewardSucceededEmailTemplate
207
207
  }
208
208
  const rewardDetail = paymentIntent.beneficiaries!.map((x: PaymentBeneficiary, index: number) => {
209
209
  return {
210
- url: getCustomerProfileUrl({ userDid: x.address, locale }),
210
+ url: users[index]?.url || getCustomerProfileUrl({ userDid: x.address, locale }),
211
211
  title: translate('notification.customerRewardSucceeded.received', locale, {
212
- address: users[index]?.user?.fullName || x.address,
212
+ address: users[index]?.name || x.address,
213
213
  amount: `${fromUnitToToken(x.share, paymentCurrency.decimal)} ${paymentCurrency.symbol}`,
214
214
  }),
215
- logo:
216
- process.env.BLOCKLET_APP_URL && users[index]?.user?.avatar
217
- ? joinURL(process.env.BLOCKLET_APP_URL, users[index]?.user?.avatar as string)
218
- : '',
215
+ logo: users[index]?.avatar ? users[index]?.avatar : getUrl('/methods/default.png'),
219
216
  appDID: x.address,
220
217
  desc: '',
221
218
  };
@@ -246,7 +243,6 @@ export class CustomerRewardSucceededEmailTemplate
246
243
  body: `${translate('notification.customerRewardSucceeded.body', locale, {
247
244
  at,
248
245
  amount: paymentInfo,
249
- subject: `<${donationSettings.title}(link:${donationSettings.reference})>`,
250
246
  })}`,
251
247
  // @ts-expect-error
252
248
  attachments: [
@@ -327,6 +323,11 @@ export class CustomerRewardSucceededEmailTemplate
327
323
  title: translate('notification.common.viewTxHash', locale),
328
324
  link: viewTxHashLink,
329
325
  },
326
+ donationSettings.reference && {
327
+ name: translate('notification.customerRewardSucceeded.viewDetail', locale),
328
+ title: translate('notification.customerRewardSucceeded.viewDetail', locale),
329
+ link: donationSettings.reference,
330
+ },
330
331
  ].filter(Boolean),
331
332
  };
332
333
 
@@ -10,11 +10,19 @@ import cloneDeep from 'lodash/cloneDeep';
10
10
  import type { LiteralUnion } from 'type-fest';
11
11
 
12
12
  import { fetchErc20Allowance, fetchErc20Balance, fetchEtherBalance } from '../integrations/ethereum/token';
13
- import { Invoice, PaymentCurrency, PaymentIntent, PaymentMethod, TCustomer, TLineItemExpanded } from '../store/models';
13
+ import {
14
+ Invoice,
15
+ PaymentCurrency,
16
+ PaymentIntent,
17
+ PaymentLink,
18
+ PaymentMethod,
19
+ TCustomer,
20
+ TLineItemExpanded,
21
+ } from '../store/models';
14
22
  import type { TPaymentCurrency } from '../store/models/payment-currency';
15
23
  import { blocklet, ethWallet, wallet } from './auth';
16
24
  import logger from './logger';
17
- import { OCAP_PAYMENT_TX_TYPE } from './util';
25
+ import { getBlockletJson, getUserOrAppInfo, OCAP_PAYMENT_TX_TYPE } from './util';
18
26
  import { CHARGE_SUPPORTED_CHAIN_TYPES, EVM_CHAIN_TYPES } from './constants';
19
27
 
20
28
  export interface SufficientForPaymentResult {
@@ -345,3 +353,40 @@ export async function isBalanceSufficientForRefund(args: {
345
353
 
346
354
  throw new Error(`isBalanceSufficientForRefund: Payment method ${paymentMethod.type} not supported`);
347
355
  }
356
+
357
+ export async function getDonationBenefits(paymentLink: PaymentLink, url?: string) {
358
+ const { donation_settings: donationSettings } = paymentLink;
359
+ if (!donationSettings) {
360
+ return [];
361
+ }
362
+ const { beneficiaries } = donationSettings;
363
+ const total = beneficiaries.reduce((t, x) => t + Number(x.share), 0);
364
+ if (total === 0) {
365
+ return beneficiaries;
366
+ }
367
+ const blockletJson = await getBlockletJson(url);
368
+ const result = await Promise.all(
369
+ beneficiaries.map(async (beneficiary) => {
370
+ const { address, share, name, avatar } = beneficiary;
371
+ try {
372
+ const info = await getUserOrAppInfo(address, blockletJson);
373
+ return {
374
+ address,
375
+ percent: (Number(share) * 100) / total,
376
+ name: name || info?.name || '',
377
+ avatar: avatar || info?.avatar || '',
378
+ url: info?.url || '',
379
+ type: info?.type || 'user',
380
+ };
381
+ } catch (error) {
382
+ return {
383
+ address,
384
+ percent: (Number(share) * 100) / total,
385
+ name: name || '',
386
+ avatar: avatar || '',
387
+ };
388
+ }
389
+ })
390
+ );
391
+ return result;
392
+ }
@@ -0,0 +1,24 @@
1
+ import { component } from '@blocklet/sdk';
2
+ import type { LiteralUnion } from 'type-fest';
3
+ import { withQuery } from 'ufo';
4
+ import { getConnectQueryParam } from './util';
5
+
6
+ export function getCustomerPayoutPageUrl({
7
+ payoutId,
8
+ userDid,
9
+ locale = 'en',
10
+ action = '',
11
+ }: {
12
+ payoutId: string;
13
+ userDid: string;
14
+ locale: LiteralUnion<'en' | 'zh', string>;
15
+ action?: LiteralUnion<'pay', string>;
16
+ }) {
17
+ return component.getUrl(
18
+ withQuery(`customer/payout/${payoutId}`, {
19
+ locale,
20
+ action,
21
+ ...getConnectQueryParam({ userDid }),
22
+ })
23
+ );
24
+ }
@@ -6,8 +6,9 @@ import { getWalletDid } from '@blocklet/sdk/lib/did';
6
6
  import { toStakeAddress } from '@arcblock/did-util';
7
7
  import { customAlphabet } from 'nanoid';
8
8
  import type { LiteralUnion } from 'type-fest';
9
- import { joinURL, withQuery } from 'ufo';
9
+ import { joinURL, withQuery, withTrailingSlash } from 'ufo';
10
10
 
11
+ import axios from 'axios';
11
12
  import dayjs from './dayjs';
12
13
  import { blocklet, wallet } from './auth';
13
14
  import type { Subscription } from '../store/models';
@@ -72,6 +73,10 @@ export const STRIPE_EVENTS: any[] = [
72
73
  'refund.updated',
73
74
  ];
74
75
 
76
+ const api = axios.create({
77
+ timeout: 10 * 1000,
78
+ });
79
+
75
80
  export function md5(input: string) {
76
81
  return crypto.createHash('md5').update(input).digest('hex');
77
82
  }
@@ -177,6 +182,83 @@ export function getDataObjectFromQuery(
177
182
  return result;
178
183
  }
179
184
 
185
+ export function safeJsonParse(input: any, defaultValue: any) {
186
+ try {
187
+ return JSON.parse(input);
188
+ } catch {
189
+ if (defaultValue === undefined) {
190
+ return input;
191
+ }
192
+ return defaultValue;
193
+ }
194
+ }
195
+
196
+ const cachedBlockletJsonResult = new Map<string, { data: any; expiry: number }>();
197
+ const CACHE_TTL = 60 * 60 * 1000; // 1 hour
198
+
199
+ export async function getBlockletJson(url?: string) {
200
+ const blockletKey = url || process.env.BLOCKLET_APP_URL || 'default';
201
+ const now = Date.now();
202
+
203
+ if (cachedBlockletJsonResult.has(blockletKey)) {
204
+ const cached = cachedBlockletJsonResult.get(blockletKey);
205
+ if (cached && now < cached.expiry) {
206
+ return cached.data;
207
+ }
208
+ }
209
+ const scriptUrl = new URL('__blocklet__.js?type=json', withTrailingSlash(url || process.env.BLOCKLET_APP_URL));
210
+ try {
211
+ const { data: blockletMeta } = await api.get(scriptUrl.href);
212
+ cachedBlockletJsonResult.set(blockletKey, { data: blockletMeta, expiry: now + CACHE_TTL });
213
+ return blockletMeta;
214
+ } catch (err) {
215
+ logger.error(`getBlockletJson error for ${scriptUrl}`, err);
216
+ if (process.env.BLOCKLET_MOUNT_POINTS) {
217
+ const BLOCKLET_MOUNT_POINTS = safeJsonParse(process.env.BLOCKLET_MOUNT_POINTS, []);
218
+ return {
219
+ componentMountPoints: BLOCKLET_MOUNT_POINTS,
220
+ appId: process.env.BLOCKLET_APP_ID,
221
+ appName: process.env.BLOCKLET_APP_NAME,
222
+ appLogo: joinURL(process.env.BLOCKLET_APP_URL!, '.well-known/service/blocklet/logo'),
223
+ appUrl: process.env.BLOCKLET_APP_URL,
224
+ };
225
+ }
226
+ return null;
227
+ }
228
+ }
229
+
230
+ export async function getUserOrAppInfo(
231
+ address: string,
232
+ blockletJson?: any
233
+ ): Promise<{ name: string; avatar: string; type: 'dapp' | 'user'; url: string } | null> {
234
+ if (blockletJson) {
235
+ if (blockletJson?.appId === address) {
236
+ return {
237
+ name: blockletJson?.appName,
238
+ avatar: blockletJson?.appLogo,
239
+ type: 'dapp',
240
+ url: blockletJson?.appUrl,
241
+ };
242
+ }
243
+ const appInfo = blockletJson?.componentMountPoints?.find((x: any) => x.appId === address);
244
+ if (appInfo) {
245
+ return {
246
+ name: appInfo.name,
247
+ avatar: joinURL(process.env.BLOCKLET_APP_URL!, `.well-known/service/blocklet/logo-bundle/${appInfo.did}`),
248
+ type: 'dapp',
249
+ url: joinURL(process.env.BLOCKLET_APP_URL!, appInfo.mountPoint),
250
+ };
251
+ }
252
+ }
253
+ const { user } = await blocklet.getUser(address);
254
+ return {
255
+ name: user?.fullName,
256
+ avatar: joinURL(process.env.BLOCKLET_APP_URL!, user?.avatar),
257
+ type: 'user',
258
+ url: getCustomerProfileUrl({ userDid: address, locale: 'en' }),
259
+ };
260
+ }
261
+
180
262
  // @FIXME: 这个应该封装在某个通用类库里面 @jianchao @wangshijun
181
263
  export function getExplorerLink({
182
264
  type,
@@ -156,10 +156,25 @@ export default flat({
156
156
 
157
157
  customerRewardSucceeded: {
158
158
  title: 'Thanks for your reward of {amount}',
159
- body: 'Thanks for your reward on {at} for {subject}, the amount of reward is {amount}. Your support is our driving force, thanks for your generous support!',
159
+ body: 'Thanks for your reward on {at}, the amount of reward is {amount}. Your support is our driving force, thanks for your generous support!',
160
160
  received: '{address} has received {amount}',
161
+ viewDetail: 'View Reference',
161
162
  },
162
163
 
164
+ customerRevenueSucceeded: {
165
+ donate: {
166
+ title: 'You received a tip',
167
+ body: 'Congratulations! On {at}, you received a tip of {amount} from {user}.',
168
+ tipDetail: 'View Tip Reference',
169
+ },
170
+ payment: {
171
+ title: 'You received a payment',
172
+ body: 'Congratulations! On {at}, you received a payment of {amount} from {user}.',
173
+ },
174
+ sender: 'Payer',
175
+ viewDetail: 'View Details',
176
+ sended: '{address} has sent {amount}',
177
+ },
163
178
  subscriptWillCanceled: {
164
179
  title: '{productName} subscription is about to be cancelled ',
165
180
  pastDue:
@@ -93,7 +93,7 @@ export default flat({
93
93
  },
94
94
 
95
95
  oneTimePaymentSucceeded: {
96
- title: '恭喜!您已购买 {productName} 成功',
96
+ title: '恭喜!您已成功购买 {productName}',
97
97
  body: '感谢您于 {at} 成功购买了 {productName}。我们将竭诚为您提供优质的服务,祝您使用愉快!',
98
98
  },
99
99
 
@@ -122,16 +122,16 @@ export default flat({
122
122
  title: '{productName} 扣费失败',
123
123
  body: '很抱歉地通知您,您的 {productName} 于 {at} 扣费失败。如有任何疑问,请及时联系我们。谢谢!',
124
124
  reason: {
125
- noDidWallet: '您尚未绑定 DID Wallet,请绑定 DID Wallet,确保余额充足',
126
- noDelegation: '您的 DID Wallet 尚未授权,请更新授权',
127
- noTransferPermission: '您的 DID Wallet 未授予应用转账权限,请更新授权',
128
- noTokenPermission: '您的 DID Wallet 未授予应用对应通证的转账权限,请更新授权',
129
- noTransferTo: '您的 DID Wallet 未授予应用扣费权限,请更新授权',
130
- noEnoughAllowance: '扣款金额超出单笔转账限额,请更新授权',
131
- noToken: '您的账户没有任何代币,请充值代币',
132
- noEnoughToken: '您的账户代币余额为 {balance},不足 {price},请充值代币',
133
- noSupported: '不支持使用代币扣费,请检查您的套餐',
134
- txSendFailed: '扣费交易发送失败',
125
+ noDidWallet: '您尚未绑定 DID Wallet,请绑定 DID Wallet,确保余额充足。',
126
+ noDelegation: '您的 DID Wallet 尚未授权,请更新授权。',
127
+ noTransferPermission: '您的 DID Wallet 未授予应用转账权限,请更新授权。',
128
+ noTokenPermission: '您的 DID Wallet 未授予应用对应通证的转账权限,请更新授权。',
129
+ noTransferTo: '您的 DID Wallet 未授予应用扣费权限,请更新授权。',
130
+ noEnoughAllowance: '扣款金额超出单笔转账限额,请更新授权。',
131
+ noToken: '您的账户没有任何代币,请充值代币。',
132
+ noEnoughToken: '您的账户代币余额为 {balance},不足 {price},请充值代币。',
133
+ noSupported: '不支持使用代币扣费,请检查您的套餐。',
134
+ txSendFailed: '扣费交易发送失败。',
135
135
  },
136
136
  },
137
137
 
@@ -152,8 +152,24 @@ export default flat({
152
152
 
153
153
  customerRewardSucceeded: {
154
154
  title: '感谢您打赏的 {amount}',
155
- body: '感谢您于 {at} {subject} 下的打赏,打赏金额为 {amount}。您的支持是我们前行的动力,谢谢您的大力支持!',
155
+ body: '感谢您于 {at} 的打赏,打赏金额为 {amount}。您的支持是我们前行的动力,谢谢您的大力支持!',
156
156
  received: '{address} 收到了 {amount}',
157
+ viewDetail: '查看详情',
158
+ },
159
+
160
+ customerRevenueSucceeded: {
161
+ donate: {
162
+ title: '您收到了一笔打赏',
163
+ body: '恭喜您于 {at} 收到了一笔来自 {user} 的打赏,打赏金额为 {amount}。',
164
+ tipDetail: '查看打赏原文',
165
+ },
166
+ payment: {
167
+ title: '您收到了一笔付款',
168
+ body: '恭喜您于 {at} 收到一笔来自 {user} 的 {amount}。',
169
+ },
170
+ sender: '付款方',
171
+ viewDetail: '查看详情',
172
+ sended: '{address} 付款给您 {amount}',
157
173
  },
158
174
 
159
175
  subscriptWillCanceled: {
@@ -56,7 +56,7 @@ import {
56
56
  SubscriptionStakeSlashSucceededEmailTemplateOptions,
57
57
  } from '../libs/notification/template/subscription-stake-slash-succeeded';
58
58
  import createQueue from '../libs/queue';
59
- import { CheckoutSession, EventType, Invoice, PaymentLink, Refund, Subscription } from '../store/models';
59
+ import { CheckoutSession, EventType, Invoice, PaymentLink, Payout, Refund, Subscription } from '../store/models';
60
60
  import {
61
61
  UsageReportEmptyEmailTemplate,
62
62
  UsageReportEmptyEmailTemplateOptions,
@@ -69,6 +69,10 @@ import {
69
69
  OverdraftProtectionExhaustedEmailTemplate,
70
70
  OverdraftProtectionExhaustedEmailTemplateOptions,
71
71
  } from '../libs/notification/template/subscription-overdraft-protection-exhausted';
72
+ import {
73
+ CustomerRevenueSucceededEmailTemplate,
74
+ CustomerRevenueSucceededEmailTemplateOptions,
75
+ } from '../libs/notification/template/customer-revenue-succeeded';
72
76
 
73
77
  export type NotificationQueueJobOptions = any;
74
78
 
@@ -141,6 +145,10 @@ function getNotificationTemplate(job: NotificationQueueJob): BaseEmailTemplate {
141
145
  );
142
146
  }
143
147
 
148
+ if (job.type === 'payout.paid') {
149
+ return new CustomerRevenueSucceededEmailTemplate(job.options as CustomerRevenueSucceededEmailTemplateOptions);
150
+ }
151
+
144
152
  throw new Error(`Unknown job type: ${job.type}`);
145
153
  }
146
154
 
@@ -339,4 +347,18 @@ export async function startNotificationQueue() {
339
347
  },
340
348
  });
341
349
  });
350
+
351
+ events.on('payout.paid', (payout: Payout) => {
352
+ if (payout.customer_id) {
353
+ notificationQueue.push({
354
+ id: `payout.paid.${payout.id}`,
355
+ job: {
356
+ type: 'payout.paid',
357
+ options: {
358
+ payoutId: payout.id,
359
+ },
360
+ },
361
+ });
362
+ }
363
+ });
342
364
  }