payment-kit 1.13.249 → 1.13.251
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/ethereum/token.ts +0 -27
- package/api/src/integrations/ethereum/tx.ts +37 -0
- package/api/src/libs/notification/template/subscription-will-renew.ts +99 -19
- package/api/src/libs/payment.ts +1 -1
- package/api/src/libs/ws.ts +7 -7
- package/api/src/locales/en.ts +9 -3
- package/api/src/locales/zh.ts +8 -2
- package/api/src/routes/checkout-sessions.ts +21 -0
- package/api/src/routes/connect/change-payment.ts +1 -2
- package/api/src/routes/connect/change-plan.ts +1 -2
- package/api/src/routes/connect/pay.ts +2 -2
- package/api/src/routes/connect/setup.ts +3 -2
- package/api/src/routes/connect/subscribe.ts +3 -2
- package/blocklet.yml +1 -1
- package/package.json +4 -4
- package/src/components/product/form.tsx +7 -2
- package/src/components/subscription/metrics.tsx +1 -1
- package/src/components/uploader.tsx +13 -5
- package/src/pages/customer/invoice/detail.tsx +16 -7
- package/src/pages/customer/subscription/change-plan.tsx +13 -6
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { JsonRpcProvider, TransactionReceipt, ethers } from 'ethers';
|
|
2
2
|
|
|
3
3
|
import { ethWallet } from '../../libs/auth';
|
|
4
|
-
import logger from '../../libs/logger';
|
|
5
|
-
import type { PaymentMethod } from '../../store/models/payment-method';
|
|
6
4
|
import { getApproveFunction } from './contract';
|
|
7
5
|
import erc20Abi from './erc20-abi.json';
|
|
8
|
-
import { waitForEvmTxReceipt } from './tx';
|
|
9
6
|
|
|
10
7
|
export async function fetchErc20Meta(provider: JsonRpcProvider, contractAddress: string) {
|
|
11
8
|
const contract = new ethers.Contract(contractAddress, erc20Abi, provider);
|
|
@@ -125,27 +122,3 @@ export async function sendErc20ToUser(
|
|
|
125
122
|
const receipt = await res.wait();
|
|
126
123
|
return receipt;
|
|
127
124
|
}
|
|
128
|
-
|
|
129
|
-
export async function executeEvmTransaction(
|
|
130
|
-
type: string,
|
|
131
|
-
userDid: string,
|
|
132
|
-
claims: any[],
|
|
133
|
-
paymentMethod: PaymentMethod
|
|
134
|
-
) {
|
|
135
|
-
const client = paymentMethod.getEvmClient();
|
|
136
|
-
const claim = claims.find((x) => x.type === 'signature');
|
|
137
|
-
logger.info('executeEvmTransaction', { type, userDid, claim });
|
|
138
|
-
const receipt = await waitForEvmTxReceipt(client, claim.hash);
|
|
139
|
-
if (!receipt.status) {
|
|
140
|
-
throw new Error(`EVM Transaction failed: ${claim.hash}`);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return {
|
|
144
|
-
type,
|
|
145
|
-
tx_hash: claim.hash,
|
|
146
|
-
payer: userDid,
|
|
147
|
-
block_height: receipt.blockNumber.toString(),
|
|
148
|
-
gas_used: receipt.gasUsed.toString(),
|
|
149
|
-
gas_price: receipt.gasPrice.toString(),
|
|
150
|
-
};
|
|
151
|
-
}
|
|
@@ -2,6 +2,8 @@ import type { JsonRpcProvider, TransactionReceipt, TransactionResponse } from 'e
|
|
|
2
2
|
import waitFor from 'p-wait-for';
|
|
3
3
|
|
|
4
4
|
import logger from '../../libs/logger';
|
|
5
|
+
import { broadcast } from '../../libs/ws';
|
|
6
|
+
import type { PaymentMethod } from '../../store/models/payment-method';
|
|
5
7
|
|
|
6
8
|
export async function waitForEvmTxReceipt(provider: JsonRpcProvider, txHash: string) {
|
|
7
9
|
let mined: TransactionResponse;
|
|
@@ -41,3 +43,38 @@ export async function waitForEvmTxConfirm(provider: JsonRpcProvider, height: num
|
|
|
41
43
|
{ interval: 3000, timeout: 30 * 60 * 1000 }
|
|
42
44
|
);
|
|
43
45
|
}
|
|
46
|
+
|
|
47
|
+
export async function executeEvmTransaction(
|
|
48
|
+
type: string,
|
|
49
|
+
userDid: string,
|
|
50
|
+
claims: any[],
|
|
51
|
+
paymentMethod: PaymentMethod
|
|
52
|
+
) {
|
|
53
|
+
const client = paymentMethod.getEvmClient();
|
|
54
|
+
const claim = claims.find((x) => x.type === 'signature');
|
|
55
|
+
logger.info('executeEvmTransaction', { type, userDid, claim });
|
|
56
|
+
const receipt = await waitForEvmTxReceipt(client, claim.hash);
|
|
57
|
+
if (!receipt.status) {
|
|
58
|
+
throw new Error(`EVM Transaction failed: ${claim.hash}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
type,
|
|
63
|
+
tx_hash: claim.hash,
|
|
64
|
+
payer: userDid,
|
|
65
|
+
block_height: receipt.blockNumber.toString(),
|
|
66
|
+
gas_used: receipt.gasUsed.toString(),
|
|
67
|
+
gas_price: receipt.gasPrice.toString(),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function broadcastEvmTransaction(checkoutSessionId: string, status: string, claims: any[]) {
|
|
72
|
+
const claim = claims.find((x) => x.type === 'signature');
|
|
73
|
+
if (claim?.hash) {
|
|
74
|
+
broadcast('checkout.session.evm_transaction', {
|
|
75
|
+
id: checkoutSessionId,
|
|
76
|
+
txHash: claim.hash,
|
|
77
|
+
status,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -4,10 +4,11 @@ import { fromUnitToToken } from '@ocap/util';
|
|
|
4
4
|
import type { ManipulateType } from 'dayjs';
|
|
5
5
|
import dayjs from 'dayjs';
|
|
6
6
|
import prettyMsI18n from 'pretty-ms-i18n';
|
|
7
|
+
import type { LiteralUnion } from 'type-fest';
|
|
7
8
|
|
|
8
9
|
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
9
10
|
import { translate } from '../../../locales';
|
|
10
|
-
import { Customer, Invoice, PaymentMethod, Subscription } from '../../../store/models';
|
|
11
|
+
import { Customer, Invoice, PaymentMethod, Price, Subscription, SubscriptionItem } from '../../../store/models';
|
|
11
12
|
import { PaymentCurrency } from '../../../store/models/payment-currency';
|
|
12
13
|
import { PaymentDetail, getPaymentDetail } from '../../payment';
|
|
13
14
|
import { getMainProductName } from '../../product';
|
|
@@ -29,6 +30,7 @@ interface SubscriptionWillRenewEmailTemplateContext {
|
|
|
29
30
|
at: string;
|
|
30
31
|
willRenewDuration: string;
|
|
31
32
|
paymentDetail: PaymentDetail;
|
|
33
|
+
paidType: string;
|
|
32
34
|
|
|
33
35
|
userDid: string;
|
|
34
36
|
paymentInfo: string;
|
|
@@ -37,7 +39,7 @@ interface SubscriptionWillRenewEmailTemplateContext {
|
|
|
37
39
|
duration: string;
|
|
38
40
|
|
|
39
41
|
viewSubscriptionLink: string;
|
|
40
|
-
|
|
42
|
+
addFundsLink: string;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
export class SubscriptionWillRenewEmailTemplate
|
|
@@ -83,14 +85,23 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
83
85
|
const willRenewDuration: string =
|
|
84
86
|
locale === 'en' ? this.getWillRenewDuration(locale) : this.getWillRenewDuration(locale).split(' ').join('');
|
|
85
87
|
|
|
86
|
-
const paymentDetail: PaymentDetail = await getPaymentDetail(userDid, invoice);
|
|
87
88
|
const upcomingInvoiceAmount = await getUpcomingInvoiceAmount(subscription.id);
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
const paymentDetail: PaymentDetail = await getPaymentDetail(userDid, invoice);
|
|
90
|
+
paymentDetail.price = +fromUnitToToken(+upcomingInvoiceAmount.amount, upcomingInvoiceAmount.currency?.decimal);
|
|
91
|
+
|
|
92
|
+
const { isPrePaid, interval } = await this.getPaymentCategory({
|
|
93
|
+
subscriptionId: subscription.id,
|
|
94
|
+
});
|
|
95
|
+
const paidType: string = isPrePaid
|
|
96
|
+
? translate('notification.common.prepaid', locale)
|
|
97
|
+
: translate('notification.common.postpaid', locale);
|
|
98
|
+
const paymentInfo: string = `${paymentDetail.price} ${paymentCurrency.symbol}`;
|
|
99
|
+
const currentPeriodStart: string = isPrePaid
|
|
100
|
+
? formatTime(invoice.period_end * 1000)
|
|
101
|
+
: formatTime(invoice.period_start * 1000);
|
|
102
|
+
const currentPeriodEnd: string = isPrePaid
|
|
103
|
+
? formatTime(dayjs(invoice.period_end * 1000).add(1, interval as ManipulateType))
|
|
104
|
+
: formatTime(invoice.period_end * 1000);
|
|
94
105
|
const duration: string = prettyMsI18n(
|
|
95
106
|
new Date(currentPeriodEnd).getTime() - new Date(currentPeriodStart).getTime(),
|
|
96
107
|
{
|
|
@@ -106,7 +117,7 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
106
117
|
const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
|
|
107
118
|
// @ts-ignore
|
|
108
119
|
const chainHost: string | undefined = paymentMethod?.settings?.[paymentMethod.type]?.api_host;
|
|
109
|
-
const
|
|
120
|
+
const addFundsLink: string = getExplorerLink({
|
|
110
121
|
type: 'account',
|
|
111
122
|
did: userDid,
|
|
112
123
|
chainHost,
|
|
@@ -121,6 +132,7 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
121
132
|
at,
|
|
122
133
|
willRenewDuration,
|
|
123
134
|
paymentDetail,
|
|
135
|
+
paidType,
|
|
124
136
|
|
|
125
137
|
userDid,
|
|
126
138
|
paymentInfo,
|
|
@@ -129,7 +141,28 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
129
141
|
duration,
|
|
130
142
|
|
|
131
143
|
viewSubscriptionLink,
|
|
132
|
-
|
|
144
|
+
addFundsLink,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
async getPaymentCategory({ subscriptionId }: { subscriptionId: string }): Promise<{
|
|
148
|
+
isPrePaid: boolean;
|
|
149
|
+
interval: LiteralUnion<'hour' | 'day' | 'week' | 'month' | 'year', string> | undefined;
|
|
150
|
+
}> {
|
|
151
|
+
const subscriptionItems = await SubscriptionItem.findAll({
|
|
152
|
+
where: {
|
|
153
|
+
subscription_id: subscriptionId,
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const lineItemExpanded = await Price.expand(subscriptionItems);
|
|
158
|
+
const metered = lineItemExpanded.find((lineItem) => lineItem.price.recurring?.usage_type === 'metered');
|
|
159
|
+
|
|
160
|
+
const isPrePaid = !metered;
|
|
161
|
+
const interval = metered?.price.recurring?.interval;
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
isPrePaid,
|
|
165
|
+
interval,
|
|
133
166
|
};
|
|
134
167
|
}
|
|
135
168
|
|
|
@@ -184,12 +217,16 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
184
217
|
at,
|
|
185
218
|
willRenewDuration,
|
|
186
219
|
paymentDetail,
|
|
220
|
+
paidType,
|
|
187
221
|
|
|
188
222
|
userDid,
|
|
189
223
|
paymentInfo,
|
|
224
|
+
currentPeriodStart,
|
|
225
|
+
currentPeriodEnd,
|
|
226
|
+
duration,
|
|
190
227
|
|
|
191
228
|
viewSubscriptionLink,
|
|
192
|
-
|
|
229
|
+
addFundsLink,
|
|
193
230
|
} = await this.getContext();
|
|
194
231
|
|
|
195
232
|
const canPay: boolean = paymentDetail.balance >= paymentDetail.price;
|
|
@@ -197,6 +234,10 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
197
234
|
// 当余额足够支付并且本封邮件不是必须发送时,可以不发送邮件
|
|
198
235
|
return null;
|
|
199
236
|
}
|
|
237
|
+
if (!paymentDetail.price && paymentDetail.symbol !== 'USD') {
|
|
238
|
+
// 如果预估的价格是 0 并且货币不是 USD,那么直接不发送
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
200
241
|
|
|
201
242
|
const template: BaseEmailTemplateType = {
|
|
202
243
|
title: `${translate('notification.subscriptionWillRenew.title', locale, {
|
|
@@ -214,8 +255,14 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
214
255
|
at,
|
|
215
256
|
productName,
|
|
216
257
|
willRenewDuration,
|
|
217
|
-
|
|
218
|
-
|
|
258
|
+
reason: `<span style="color: red;">${translate(
|
|
259
|
+
'notification.subscriptionWillRenew.unableToPayReason',
|
|
260
|
+
locale,
|
|
261
|
+
{
|
|
262
|
+
balance: `${paymentDetail.balance} ${paymentDetail.symbol}`,
|
|
263
|
+
price: `${paymentDetail.price} ${paymentDetail.symbol}`,
|
|
264
|
+
}
|
|
265
|
+
)}</span>`,
|
|
219
266
|
})}`,
|
|
220
267
|
// @ts-expect-error
|
|
221
268
|
attachments: [
|
|
@@ -252,6 +299,21 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
252
299
|
text: productName,
|
|
253
300
|
},
|
|
254
301
|
},
|
|
302
|
+
{
|
|
303
|
+
type: 'text',
|
|
304
|
+
data: {
|
|
305
|
+
type: 'plain',
|
|
306
|
+
color: '#9397A1',
|
|
307
|
+
text: translate('notification.subscriptionWillRenew.renewAmount', locale),
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
type: 'text',
|
|
312
|
+
data: {
|
|
313
|
+
type: 'plain',
|
|
314
|
+
text: paymentInfo,
|
|
315
|
+
},
|
|
316
|
+
},
|
|
255
317
|
{
|
|
256
318
|
type: 'text',
|
|
257
319
|
data: {
|
|
@@ -264,6 +326,9 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
264
326
|
type: 'text',
|
|
265
327
|
data: {
|
|
266
328
|
type: 'plain',
|
|
329
|
+
...(!canPay && {
|
|
330
|
+
color: 'red',
|
|
331
|
+
}),
|
|
267
332
|
text: `${paymentDetail.balance} ${paymentDetail.symbol}`,
|
|
268
333
|
},
|
|
269
334
|
},
|
|
@@ -272,14 +337,29 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
272
337
|
data: {
|
|
273
338
|
type: 'plain',
|
|
274
339
|
color: '#9397A1',
|
|
275
|
-
text: translate('notification.
|
|
340
|
+
text: translate('notification.common.paidType', locale),
|
|
276
341
|
},
|
|
277
342
|
},
|
|
278
343
|
{
|
|
279
344
|
type: 'text',
|
|
280
345
|
data: {
|
|
281
346
|
type: 'plain',
|
|
282
|
-
text:
|
|
347
|
+
text: `${paidType}`,
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
type: 'text',
|
|
352
|
+
data: {
|
|
353
|
+
type: 'plain',
|
|
354
|
+
color: '#9397A1',
|
|
355
|
+
text: translate('notification.common.paymentPeriod', locale),
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
type: 'text',
|
|
360
|
+
data: {
|
|
361
|
+
type: 'plain',
|
|
362
|
+
text: `${currentPeriodStart} ~ ${currentPeriodEnd}(${duration})`,
|
|
283
363
|
},
|
|
284
364
|
},
|
|
285
365
|
].filter(Boolean),
|
|
@@ -288,9 +368,9 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
288
368
|
// @ts-ignore
|
|
289
369
|
actions: [
|
|
290
370
|
!canPay && {
|
|
291
|
-
name: translate('notification.common.
|
|
292
|
-
title: translate('notification.common.
|
|
293
|
-
link:
|
|
371
|
+
name: translate('notification.common.addFunds', locale),
|
|
372
|
+
title: translate('notification.common.addFunds', locale),
|
|
373
|
+
link: addFundsLink,
|
|
294
374
|
},
|
|
295
375
|
{
|
|
296
376
|
name: translate('notification.common.viewSubscription', locale),
|
package/api/src/libs/payment.ts
CHANGED
|
@@ -172,7 +172,7 @@ export function getGasPayerExtra(txBuffer: Buffer, headers?: { [key: string]: st
|
|
|
172
172
|
export interface PaymentDetail {
|
|
173
173
|
balance: number;
|
|
174
174
|
price: number;
|
|
175
|
-
symbol: string
|
|
175
|
+
symbol: LiteralUnion<'ABT' | 'USD', string>;
|
|
176
176
|
}
|
|
177
177
|
export async function getPaymentDetail(userDid: string, invoice: Invoice): Promise<PaymentDetail> {
|
|
178
178
|
const defaultResult = {
|
package/api/src/libs/ws.ts
CHANGED
|
@@ -3,23 +3,23 @@ import { sendToRelay } from '@blocklet/sdk/service/notification';
|
|
|
3
3
|
import type { CheckoutSession, Invoice, PaymentIntent } from '../store/models';
|
|
4
4
|
import { events } from './event';
|
|
5
5
|
|
|
6
|
-
export function broadcast(
|
|
7
|
-
sendToRelay(
|
|
8
|
-
console.error(`Failed to broadcast
|
|
6
|
+
export function broadcast(eventName: string, data: any) {
|
|
7
|
+
sendToRelay('events', eventName, data).catch((err: any) => {
|
|
8
|
+
console.error(`Failed to broadcast event: ${eventName}`, err);
|
|
9
9
|
});
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export function initEventBroadcast() {
|
|
13
13
|
events.on('payment_intent.succeeded', (data: PaymentIntent) => {
|
|
14
|
-
broadcast('
|
|
14
|
+
broadcast('payment_intent.succeeded', data);
|
|
15
15
|
});
|
|
16
16
|
events.on('checkout.session.completed', (data: CheckoutSession) => {
|
|
17
|
-
broadcast('
|
|
17
|
+
broadcast('checkout.session.completed', data);
|
|
18
18
|
});
|
|
19
19
|
events.on('checkout.session.nft_minted', (data: CheckoutSession) => {
|
|
20
|
-
broadcast('
|
|
20
|
+
broadcast('checkout.session.nft_minted', data);
|
|
21
21
|
});
|
|
22
22
|
events.on('invoice.paid', (data: Invoice) => {
|
|
23
|
-
broadcast('
|
|
23
|
+
broadcast('invoice.paid', data);
|
|
24
24
|
});
|
|
25
25
|
}
|
package/api/src/locales/en.ts
CHANGED
|
@@ -9,6 +9,7 @@ export default flat({
|
|
|
9
9
|
refundAmount: 'Refund amount',
|
|
10
10
|
refundPeriod: 'Refund period',
|
|
11
11
|
validityPeriod: 'Service period',
|
|
12
|
+
paymentPeriod: 'Payment period',
|
|
12
13
|
trialPeriod: 'Trial period',
|
|
13
14
|
viewSubscription: 'View subscription',
|
|
14
15
|
viewInvoice: 'View invoice',
|
|
@@ -25,11 +26,14 @@ export default flat({
|
|
|
25
26
|
minutes: 'minutes',
|
|
26
27
|
cancellationReason: 'Cancellation reason',
|
|
27
28
|
renewNow: 'Pay now',
|
|
28
|
-
|
|
29
|
+
addFunds: 'Add funds',
|
|
29
30
|
amount: 'Amount',
|
|
30
31
|
rewardAmount: 'Reward amount',
|
|
31
32
|
rewardDetail: 'Reward detail',
|
|
32
33
|
currentBalance: 'Current balance',
|
|
34
|
+
prepaid: 'Prepaid',
|
|
35
|
+
postpaid: 'Postpaid',
|
|
36
|
+
paidType: 'Paid type',
|
|
33
37
|
},
|
|
34
38
|
|
|
35
39
|
sendTo: 'Sent to',
|
|
@@ -69,8 +73,10 @@ export default flat({
|
|
|
69
73
|
title: '{productName} automatic payment reminder',
|
|
70
74
|
body: 'Your subscription to {productName} is scheduled for automatic payment on {at}({willRenewDuration} later). If you have any questions or need assistance, please feel free to contact us.',
|
|
71
75
|
unableToPayBody:
|
|
72
|
-
'Your subscription to {productName} is scheduled for automatic payment on {at}({willRenewDuration} later).
|
|
73
|
-
|
|
76
|
+
'Your subscription to {productName} is scheduled for automatic payment on {at}({willRenewDuration} later). If you have any questions or need assistance, please feel free to contact us.\r\n{reason}',
|
|
77
|
+
unableToPayReason:
|
|
78
|
+
'The estimated payment amount is {price}, but the current balance is insufficient ({balance}), please ensure that your account has enough balance to avoid payment failure.',
|
|
79
|
+
renewAmount: 'Payment amount',
|
|
74
80
|
},
|
|
75
81
|
|
|
76
82
|
subscriptionRenewed: {
|
package/api/src/locales/zh.ts
CHANGED
|
@@ -9,6 +9,7 @@ export default flat({
|
|
|
9
9
|
refundAmount: '退款金额',
|
|
10
10
|
refundPeriod: '退款周期',
|
|
11
11
|
validityPeriod: '服务周期',
|
|
12
|
+
paymentPeriod: '扣款周期',
|
|
12
13
|
trialPeriod: '试用期',
|
|
13
14
|
viewSubscription: '查看订阅',
|
|
14
15
|
viewInvoice: '查看账单',
|
|
@@ -25,11 +26,14 @@ export default flat({
|
|
|
25
26
|
minutes: '分钟',
|
|
26
27
|
cancellationReason: '取消原因',
|
|
27
28
|
renewNow: '立即付款',
|
|
28
|
-
|
|
29
|
+
addFunds: '立即充值',
|
|
29
30
|
amount: '金额',
|
|
30
31
|
rewardAmount: '打赏金额',
|
|
31
32
|
rewardDetail: '打赏详情',
|
|
32
33
|
currentBalance: '当前余额',
|
|
34
|
+
prepaid: '预付费',
|
|
35
|
+
postpaid: '后付费',
|
|
36
|
+
paidType: '付费类型',
|
|
33
37
|
},
|
|
34
38
|
|
|
35
39
|
sendTo: '发送给',
|
|
@@ -69,7 +73,9 @@ export default flat({
|
|
|
69
73
|
title: '{productName} 自动扣费提醒',
|
|
70
74
|
body: '您订阅的 {productName} 将在 {at}({willRenewDuration}后) 发起自动扣费。若有任何疑问或需要帮助,请随时与我们联系。',
|
|
71
75
|
unableToPayBody:
|
|
72
|
-
'您订阅的 {productName} 将在 {at}({willRenewDuration}后)
|
|
76
|
+
'您订阅的 {productName} 将在 {at}({willRenewDuration}后) 发起自动扣费,若有任何疑问或者需要帮助,请随时与我们联系。\r\n{reason}',
|
|
77
|
+
unableToPayReason:
|
|
78
|
+
'预计扣款金额为 {price},但当前余额不足(余额为 {balance}),请确保您的账户余额充足,避免扣费失败。',
|
|
73
79
|
renewAmount: '扣费金额',
|
|
74
80
|
},
|
|
75
81
|
|
|
@@ -1107,6 +1107,7 @@ const schema = Joi.object<{
|
|
|
1107
1107
|
customer_id?: string;
|
|
1108
1108
|
customer_did?: string;
|
|
1109
1109
|
payment_intent_id?: string;
|
|
1110
|
+
payment_link_id?: string;
|
|
1110
1111
|
subscription_id?: string;
|
|
1111
1112
|
livemode?: boolean;
|
|
1112
1113
|
}>({
|
|
@@ -1118,6 +1119,7 @@ const schema = Joi.object<{
|
|
|
1118
1119
|
customer_id: Joi.string().empty(''),
|
|
1119
1120
|
customer_did: Joi.string().empty(''),
|
|
1120
1121
|
payment_intent_id: Joi.string().empty(''),
|
|
1122
|
+
payment_link_id: Joi.string().empty(''),
|
|
1121
1123
|
subscription_id: Joi.string().empty(''),
|
|
1122
1124
|
livemode: Joi.boolean().empty(''),
|
|
1123
1125
|
});
|
|
@@ -1141,6 +1143,9 @@ router.get('/', auth, async (req, res) => {
|
|
|
1141
1143
|
if (query.payment_intent_id) {
|
|
1142
1144
|
where.payment_intent_id = query.payment_intent_id;
|
|
1143
1145
|
}
|
|
1146
|
+
if (query.payment_link_id) {
|
|
1147
|
+
where.payment_link_id = query.payment_link_id;
|
|
1148
|
+
}
|
|
1144
1149
|
if (query.subscription_id) {
|
|
1145
1150
|
where.subscription_id = query.subscription_id;
|
|
1146
1151
|
}
|
|
@@ -1191,4 +1196,20 @@ router.get('/', auth, async (req, res) => {
|
|
|
1191
1196
|
}
|
|
1192
1197
|
});
|
|
1193
1198
|
|
|
1199
|
+
router.put('/:id', auth, async (req, res) => {
|
|
1200
|
+
const doc = await CheckoutSession.findByPk(req.params.id);
|
|
1201
|
+
|
|
1202
|
+
if (!doc) {
|
|
1203
|
+
return res.status(404).json({ error: 'CheckoutSession not found' });
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
const raw = pick(req.body, ['metadata']);
|
|
1207
|
+
if (raw.metadata) {
|
|
1208
|
+
raw.metadata = formatMetadata(raw.metadata);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
await doc.update(raw);
|
|
1212
|
+
res.json(doc);
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1194
1215
|
export default router;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { executeEvmTransaction } from '../../integrations/ethereum/
|
|
2
|
-
import { waitForEvmTxConfirm } from '../../integrations/ethereum/tx';
|
|
1
|
+
import { executeEvmTransaction, waitForEvmTxConfirm } from '../../integrations/ethereum/tx';
|
|
3
2
|
import type { CallbackArgs } from '../../libs/auth';
|
|
4
3
|
import { isDelegationSufficientForPayment } from '../../libs/payment';
|
|
5
4
|
import { getFastCheckoutAmount } from '../../libs/session';
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { executeEvmTransaction } from '../../integrations/ethereum/
|
|
2
|
-
import { waitForEvmTxConfirm } from '../../integrations/ethereum/tx';
|
|
1
|
+
import { executeEvmTransaction, waitForEvmTxConfirm } from '../../integrations/ethereum/tx';
|
|
3
2
|
import type { CallbackArgs } from '../../libs/auth';
|
|
4
3
|
import { isDelegationSufficientForPayment } from '../../libs/payment';
|
|
5
4
|
import { getFastCheckoutAmount } from '../../libs/session';
|
|
@@ -2,8 +2,8 @@ import type { Transaction, TransferV3Tx } from '@ocap/client';
|
|
|
2
2
|
import { toBase58 } from '@ocap/util';
|
|
3
3
|
import { fromAddress } from '@ocap/wallet';
|
|
4
4
|
|
|
5
|
-
import { encodeTransferItx
|
|
6
|
-
import { waitForEvmTxConfirm } from '../../integrations/ethereum/tx';
|
|
5
|
+
import { encodeTransferItx } from '../../integrations/ethereum/token';
|
|
6
|
+
import { executeEvmTransaction, waitForEvmTxConfirm } from '../../integrations/ethereum/tx';
|
|
7
7
|
import { CallbackArgs, ethWallet } from '../../libs/auth';
|
|
8
8
|
import logger from '../../libs/logger';
|
|
9
9
|
import { getGasPayerExtra } from '../../libs/payment';
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { executeEvmTransaction } from '../../integrations/ethereum/
|
|
2
|
-
import { waitForEvmTxConfirm } from '../../integrations/ethereum/tx';
|
|
1
|
+
import { broadcastEvmTransaction, executeEvmTransaction, waitForEvmTxConfirm } from '../../integrations/ethereum/tx';
|
|
3
2
|
import type { CallbackArgs } from '../../libs/auth';
|
|
4
3
|
import dayjs from '../../libs/dayjs';
|
|
5
4
|
import logger from '../../libs/logger';
|
|
@@ -182,10 +181,12 @@ export default {
|
|
|
182
181
|
|
|
183
182
|
if (paymentMethod.type === 'ethereum') {
|
|
184
183
|
await prepareTxExecution();
|
|
184
|
+
broadcastEvmTransaction(checkoutSessionId, 'pending', claims);
|
|
185
185
|
const paymentDetails = await executeEvmTransaction('approve', userDid, claims, paymentMethod);
|
|
186
186
|
waitForEvmTxConfirm(paymentMethod.getEvmClient(), +paymentDetails.block_height, paymentMethod.confirmation.block)
|
|
187
187
|
.then(async () => {
|
|
188
188
|
await afterTxExecution(paymentDetails);
|
|
189
|
+
broadcastEvmTransaction(checkoutSessionId, 'confirmed', claims);
|
|
189
190
|
})
|
|
190
191
|
.catch(console.error);
|
|
191
192
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { executeEvmTransaction } from '../../integrations/ethereum/
|
|
2
|
-
import { waitForEvmTxConfirm } from '../../integrations/ethereum/tx';
|
|
1
|
+
import { broadcastEvmTransaction, executeEvmTransaction, waitForEvmTxConfirm } from '../../integrations/ethereum/tx';
|
|
3
2
|
import type { CallbackArgs } from '../../libs/auth';
|
|
4
3
|
import dayjs from '../../libs/dayjs';
|
|
5
4
|
import logger from '../../libs/logger';
|
|
@@ -165,6 +164,7 @@ export default {
|
|
|
165
164
|
if (paymentMethod.type === 'ethereum') {
|
|
166
165
|
await prepareTxExecution();
|
|
167
166
|
const { invoice } = await ensureInvoiceForCheckout({ checkoutSession, customer, subscription });
|
|
167
|
+
broadcastEvmTransaction(checkoutSessionId, 'pending', claims);
|
|
168
168
|
|
|
169
169
|
const paymentDetails = await executeEvmTransaction('approve', userDid, claims, paymentMethod);
|
|
170
170
|
waitForEvmTxConfirm(
|
|
@@ -174,6 +174,7 @@ export default {
|
|
|
174
174
|
)
|
|
175
175
|
.then(async () => {
|
|
176
176
|
await afterTxExecution(invoice!, paymentDetails);
|
|
177
|
+
broadcastEvmTransaction(checkoutSessionId, 'confirmed', claims);
|
|
177
178
|
})
|
|
178
179
|
.catch(console.error);
|
|
179
180
|
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.251",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"@arcblock/ux": "^2.9.77",
|
|
52
52
|
"@arcblock/validator": "^1.18.116",
|
|
53
53
|
"@blocklet/logger": "1.16.26",
|
|
54
|
-
"@blocklet/payment-react": "1.13.
|
|
54
|
+
"@blocklet/payment-react": "1.13.251",
|
|
55
55
|
"@blocklet/sdk": "1.16.26",
|
|
56
56
|
"@blocklet/ui-react": "^2.9.77",
|
|
57
57
|
"@blocklet/uploader": "^0.1.6",
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
"devDependencies": {
|
|
117
117
|
"@abtnode/types": "1.16.26",
|
|
118
118
|
"@arcblock/eslint-config-ts": "^0.3.0",
|
|
119
|
-
"@blocklet/payment-types": "1.13.
|
|
119
|
+
"@blocklet/payment-types": "1.13.251",
|
|
120
120
|
"@types/cookie-parser": "^1.4.7",
|
|
121
121
|
"@types/cors": "^2.8.17",
|
|
122
122
|
"@types/dotenv-flow": "^3.3.3",
|
|
@@ -155,5 +155,5 @@
|
|
|
155
155
|
"parser": "typescript"
|
|
156
156
|
}
|
|
157
157
|
},
|
|
158
|
-
"gitHead": "
|
|
158
|
+
"gitHead": "46caad7baca15f2ba3876018589f78ba11179f59"
|
|
159
159
|
}
|
|
@@ -25,8 +25,13 @@ export default function ProductForm(props: Props) {
|
|
|
25
25
|
const images = useWatch({ control, name: 'images' });
|
|
26
26
|
|
|
27
27
|
const onUploaded = (result: any) => {
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
console.warn('onUploaded', result);
|
|
29
|
+
if (result.url) {
|
|
30
|
+
const tmp = new URL(result.url);
|
|
31
|
+
setValue('images', [tmp.pathname]);
|
|
32
|
+
} else {
|
|
33
|
+
setValue('images', []);
|
|
34
|
+
}
|
|
30
35
|
};
|
|
31
36
|
|
|
32
37
|
return (
|
|
@@ -38,7 +38,7 @@ export default function SubscriptionMetrics({ subscription }: Props) {
|
|
|
38
38
|
divider
|
|
39
39
|
/>
|
|
40
40
|
)}
|
|
41
|
-
{upcoming && upcoming.amount !== '0' && (
|
|
41
|
+
{upcoming?.amount && upcoming.amount !== '0' && (
|
|
42
42
|
<InfoMetric
|
|
43
43
|
label={t('admin.subscription.nextInvoiceAmount')}
|
|
44
44
|
value={`${formatBNStr(upcoming.amount, subscription.paymentCurrency.decimal)} ${
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
2
|
import { Box, Button, Typography } from '@mui/material';
|
|
3
3
|
import { styled } from '@mui/system';
|
|
4
4
|
import { lazy, useCallback, useEffect, useRef } from 'react';
|
|
@@ -14,12 +14,17 @@ type Props = {
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
export default function Uploader({ onUploaded, preview, maxFileSize, maxNumberOfFiles, allowedFileExts }: Props) {
|
|
17
|
+
const { t } = useLocaleContext();
|
|
17
18
|
const uploaderRef = useRef<any>(null);
|
|
18
19
|
const handleOpen = useCallback(() => {
|
|
19
20
|
if (!uploaderRef.current) return;
|
|
20
21
|
uploaderRef.current.open();
|
|
21
22
|
}, []);
|
|
22
23
|
|
|
24
|
+
const handleRemove = () => {
|
|
25
|
+
onUploaded({ url: '' });
|
|
26
|
+
};
|
|
27
|
+
|
|
23
28
|
useEffect(() => {
|
|
24
29
|
if (uploaderRef.current) {
|
|
25
30
|
const uploader = uploaderRef.current.getUploader();
|
|
@@ -33,17 +38,20 @@ export default function Uploader({ onUploaded, preview, maxFileSize, maxNumberOf
|
|
|
33
38
|
display="flex"
|
|
34
39
|
alignItems={preview ? 'flex-end' : 'center'}
|
|
35
40
|
justifyContent="center"
|
|
36
|
-
onClick={handleOpen}
|
|
37
41
|
style={{
|
|
38
42
|
backgroundImage: preview ? `url(${preview})` : 'none',
|
|
39
43
|
backgroundRepeat: 'no-repeat',
|
|
40
44
|
backgroundSize: 'contain',
|
|
41
45
|
backgroundPosition: 'center',
|
|
42
46
|
}}>
|
|
43
|
-
<Button
|
|
44
|
-
<
|
|
45
|
-
<Typography>{preview ? 'Change' : 'Upload'}</Typography>
|
|
47
|
+
<Button variant={preview ? 'contained' : 'text'} color="inherit" size="small" onClick={handleOpen}>
|
|
48
|
+
<Typography>{t(`common.${preview ? 'change' : 'upload'}`)}</Typography>
|
|
46
49
|
</Button>
|
|
50
|
+
{preview && (
|
|
51
|
+
<Button variant="contained" color="error" size="small" onClick={handleRemove}>
|
|
52
|
+
<Typography>{t('common.remove')}</Typography>
|
|
53
|
+
</Button>
|
|
54
|
+
)}
|
|
47
55
|
</Div>
|
|
48
56
|
<UploaderComponent
|
|
49
57
|
// @ts-ignore
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
|
2
2
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
3
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
Status,
|
|
6
|
+
TxGas,
|
|
7
|
+
TxLink,
|
|
8
|
+
api,
|
|
9
|
+
formatError,
|
|
10
|
+
formatTime,
|
|
11
|
+
getInvoiceStatusColor,
|
|
12
|
+
usePaymentContext,
|
|
13
|
+
} from '@blocklet/payment-react';
|
|
5
14
|
import type { TInvoiceExpanded } from '@blocklet/payment-types';
|
|
6
15
|
import { ArrowBackOutlined } from '@mui/icons-material';
|
|
7
16
|
import { Alert, Box, Button, CircularProgress, Stack, Typography } from '@mui/material';
|
|
@@ -16,7 +25,6 @@ import InfoRow from '../../../components/info-row';
|
|
|
16
25
|
import { Download } from '../../../components/invoice-pdf/pdf';
|
|
17
26
|
import InvoiceTable from '../../../components/invoice/table';
|
|
18
27
|
import SectionHeader from '../../../components/section/header';
|
|
19
|
-
import { useSessionContext } from '../../../contexts/session';
|
|
20
28
|
import { goBackOrFallback } from '../../../libs/util';
|
|
21
29
|
import CustomerRefundList from '../refund/list';
|
|
22
30
|
|
|
@@ -27,7 +35,7 @@ const fetchData = (id: string): Promise<TInvoiceExpanded> => {
|
|
|
27
35
|
export default function CustomerInvoiceDetail() {
|
|
28
36
|
const { t } = useLocaleContext();
|
|
29
37
|
const [searchParams] = useSearchParams();
|
|
30
|
-
const {
|
|
38
|
+
const { connect } = usePaymentContext();
|
|
31
39
|
const params = useParams<{ id: string }>();
|
|
32
40
|
const [state, setState] = useSetState({
|
|
33
41
|
downloading: false,
|
|
@@ -39,8 +47,9 @@ export default function CustomerInvoiceDetail() {
|
|
|
39
47
|
|
|
40
48
|
const onPay = () => {
|
|
41
49
|
setState({ paying: true });
|
|
42
|
-
|
|
50
|
+
connect.open({
|
|
43
51
|
action: 'collect',
|
|
52
|
+
saveConnect: false,
|
|
44
53
|
messages: {
|
|
45
54
|
scan: '',
|
|
46
55
|
title: t(`payment.customer.invoice.${action || 'pay'}`),
|
|
@@ -50,11 +59,11 @@ export default function CustomerInvoiceDetail() {
|
|
|
50
59
|
} as any,
|
|
51
60
|
extraParams: { invoiceId: params.id, action },
|
|
52
61
|
onSuccess: async () => {
|
|
53
|
-
|
|
62
|
+
connect.close();
|
|
54
63
|
await runAsync();
|
|
55
64
|
},
|
|
56
65
|
onClose: () => {
|
|
57
|
-
|
|
66
|
+
connect.close();
|
|
58
67
|
setState({ paying: false });
|
|
59
68
|
},
|
|
60
69
|
onError: (err: any) => {
|
|
@@ -66,7 +75,7 @@ export default function CustomerInvoiceDetail() {
|
|
|
66
75
|
|
|
67
76
|
const closePay = () => {
|
|
68
77
|
setState({ paying: true });
|
|
69
|
-
|
|
78
|
+
connect.close();
|
|
70
79
|
};
|
|
71
80
|
|
|
72
81
|
useEffect(() => {
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
/* eslint-disable react/no-unstable-nested-components */
|
|
2
2
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
3
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
PricingTable,
|
|
6
|
+
api,
|
|
7
|
+
formatBNStr,
|
|
8
|
+
formatError,
|
|
9
|
+
formatPrice,
|
|
10
|
+
formatTime,
|
|
11
|
+
usePaymentContext,
|
|
12
|
+
} from '@blocklet/payment-react';
|
|
5
13
|
import type { TLineItemExpanded, TPricingTableExpanded, TSubscriptionExpanded } from '@blocklet/payment-types';
|
|
6
14
|
import { ArrowBackOutlined } from '@mui/icons-material';
|
|
7
15
|
import { LoadingButton } from '@mui/lab';
|
|
@@ -12,7 +20,6 @@ import { useNavigate, useParams } from 'react-router-dom';
|
|
|
12
20
|
import InfoCard from '../../../components/info-card';
|
|
13
21
|
import SectionHeader from '../../../components/section/header';
|
|
14
22
|
import SubscriptionDescription from '../../../components/subscription/description';
|
|
15
|
-
import { useSessionContext } from '../../../contexts/session';
|
|
16
23
|
import { goBackOrFallback } from '../../../libs/util';
|
|
17
24
|
|
|
18
25
|
const fetchData = async (
|
|
@@ -45,7 +52,7 @@ export default function CustomerSubscriptionChangePlan() {
|
|
|
45
52
|
const navigate = useNavigate();
|
|
46
53
|
const { id } = useParams() as { id: string };
|
|
47
54
|
const { t, locale } = useLocaleContext();
|
|
48
|
-
const {
|
|
55
|
+
const { connect } = usePaymentContext();
|
|
49
56
|
|
|
50
57
|
const { loading, error, data } = useRequest(() => fetchData(id));
|
|
51
58
|
const [state, setState] = useSetState({
|
|
@@ -142,7 +149,7 @@ export default function CustomerSubscriptionChangePlan() {
|
|
|
142
149
|
setState({ paying: true });
|
|
143
150
|
try {
|
|
144
151
|
setState({ paying: true });
|
|
145
|
-
|
|
152
|
+
connect.open({
|
|
146
153
|
action: result.data.connectAction,
|
|
147
154
|
saveConnect: false,
|
|
148
155
|
messages: {
|
|
@@ -156,12 +163,12 @@ export default function CustomerSubscriptionChangePlan() {
|
|
|
156
163
|
onSuccess: () => {
|
|
157
164
|
setState({ paid: true, paying: false });
|
|
158
165
|
setTimeout(() => {
|
|
159
|
-
|
|
166
|
+
connect.close();
|
|
160
167
|
handleBack();
|
|
161
168
|
}, 2000);
|
|
162
169
|
},
|
|
163
170
|
onClose: () => {
|
|
164
|
-
|
|
171
|
+
connect.close();
|
|
165
172
|
setState({ paying: false });
|
|
166
173
|
},
|
|
167
174
|
onError: (err: any) => {
|