payment-kit 1.23.3 → 1.23.5
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/libs/invoice.ts +14 -4
- package/api/src/libs/notification/template/customer-credit-grant-granted.ts +5 -2
- package/api/src/libs/notification/template/customer-credit-insufficient.ts +8 -3
- package/api/src/libs/notification/template/customer-credit-low-balance.ts +7 -18
- package/api/src/libs/util.ts +32 -0
- package/api/src/locales/en.ts +13 -13
- package/api/src/queues/auto-recharge.ts +34 -0
- package/api/src/queues/credit-consume.ts +7 -3
- package/api/src/routes/auto-recharge-configs.ts +22 -3
- package/api/src/routes/connect/recharge-account.ts +54 -2
- package/api/src/routes/payment-intents.ts +1 -1
- package/api/src/routes/prices.ts +3 -0
- package/api/src/store/models/payment-currency.ts +5 -4
- package/api/tests/libs/subscription.spec.ts +6 -6
- package/blocklet.yml +1 -1
- package/package.json +12 -12
- package/src/components/customer/credit-overview.tsx +11 -18
- package/src/locales/en.tsx +2 -0
- package/src/locales/zh.tsx +2 -0
- package/src/pages/admin/customers/customers/credit-grant/detail.tsx +10 -3
- package/src/pages/admin/customers/customers/credit-transaction/detail.tsx +14 -3
- package/src/pages/admin/index.tsx +8 -0
- package/src/pages/admin/products/products/create.tsx +1 -1
- package/src/pages/customer/credit-grant/detail.tsx +10 -3
- package/src/pages/customer/credit-transaction/detail.tsx +14 -3
- package/src/pages/integrations/index.tsx +8 -0
- package/src/pages/integrations/overview.tsx +0 -9
- package/website/images/hero-image.png +0 -0
- package/website/images/r-for-ai.png +0 -0
- package/website/images/r-for-devr.png +0 -0
- package/website/images/r-for-ent.png +0 -0
- package/website/images/r-for-owner.png +0 -0
- package/website/images/r-for-web3.png +0 -0
- package/website/images/hero-image.webp +0 -0
- package/website/images/r-bus-owner.png +0 -0
- package/website/images/r-for-dev.png +0 -0
package/api/src/libs/invoice.ts
CHANGED
|
@@ -1050,6 +1050,8 @@ export async function retryUncollectibleInvoices(options: {
|
|
|
1050
1050
|
invoiceId?: string;
|
|
1051
1051
|
invoiceIds?: string[];
|
|
1052
1052
|
currencyId?: string;
|
|
1053
|
+
billingReason?: string;
|
|
1054
|
+
statusList?: string[];
|
|
1053
1055
|
}) {
|
|
1054
1056
|
const lockKey = `retry-uncollectible-${JSON.stringify(options)}`;
|
|
1055
1057
|
|
|
@@ -1065,10 +1067,12 @@ export async function retryUncollectibleInvoices(options: {
|
|
|
1065
1067
|
try {
|
|
1066
1068
|
await Lock.acquire(lockKey, dayjs().add(5, 'minutes').unix());
|
|
1067
1069
|
|
|
1068
|
-
const { customerId, subscriptionId, invoiceId, invoiceIds, currencyId } = options;
|
|
1070
|
+
const { customerId, subscriptionId, invoiceId, invoiceIds, currencyId, billingReason, statusList } = options;
|
|
1071
|
+
|
|
1072
|
+
const invoiceStatusList = statusList || ['uncollectible'];
|
|
1069
1073
|
|
|
1070
1074
|
const where: WhereOptions = {
|
|
1071
|
-
status: { [Op.in]:
|
|
1075
|
+
status: { [Op.in]: invoiceStatusList },
|
|
1072
1076
|
payment_intent_id: { [Op.ne]: null },
|
|
1073
1077
|
};
|
|
1074
1078
|
|
|
@@ -1092,6 +1096,10 @@ export async function retryUncollectibleInvoices(options: {
|
|
|
1092
1096
|
where.currency_id = currencyId;
|
|
1093
1097
|
}
|
|
1094
1098
|
|
|
1099
|
+
if (billingReason) {
|
|
1100
|
+
where.billing_reason = billingReason;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1095
1103
|
const overdueInvoices = (await Invoice.findAll({
|
|
1096
1104
|
where,
|
|
1097
1105
|
include: [{ model: PaymentIntent, as: 'paymentIntent' }],
|
|
@@ -1100,9 +1108,10 @@ export async function retryUncollectibleInvoices(options: {
|
|
|
1100
1108
|
})) as (Invoice & { paymentIntent?: PaymentIntent })[];
|
|
1101
1109
|
|
|
1102
1110
|
const startTime = Date.now();
|
|
1103
|
-
logger.info('Found
|
|
1111
|
+
logger.info('Found invoices to retry', {
|
|
1104
1112
|
count: overdueInvoices.length,
|
|
1105
1113
|
criteria: options,
|
|
1114
|
+
statusList: invoiceStatusList,
|
|
1106
1115
|
invoiceIds: overdueInvoices.map((inv) => inv.id),
|
|
1107
1116
|
});
|
|
1108
1117
|
|
|
@@ -1167,10 +1176,11 @@ export async function retryUncollectibleInvoices(options: {
|
|
|
1167
1176
|
});
|
|
1168
1177
|
|
|
1169
1178
|
const processingTime = Date.now() - startTime;
|
|
1170
|
-
logger.info('Completed retrying
|
|
1179
|
+
logger.info('Completed retrying invoices', {
|
|
1171
1180
|
totalProcessed: results.processed,
|
|
1172
1181
|
successful: results.successful.length,
|
|
1173
1182
|
failed: results.failed.length,
|
|
1183
|
+
statusList: invoiceStatusList,
|
|
1174
1184
|
processingTimeMs: processingTime,
|
|
1175
1185
|
});
|
|
1176
1186
|
|
|
@@ -5,7 +5,7 @@ import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
|
5
5
|
import { translate } from '../../../locales';
|
|
6
6
|
import { CreditGrant, Customer, PaymentCurrency } from '../../../store/models';
|
|
7
7
|
import { formatTime } from '../../time';
|
|
8
|
-
import { formatNumber, getCustomerIndexUrl } from '../../util';
|
|
8
|
+
import { formatCreditAmount, formatNumber, getCustomerIndexUrl } from '../../util';
|
|
9
9
|
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
10
10
|
|
|
11
11
|
export interface CustomerCreditGrantGrantedEmailTemplateOptions {
|
|
@@ -59,7 +59,10 @@ export class CustomerCreditGrantGrantedEmailTemplate
|
|
|
59
59
|
locale,
|
|
60
60
|
userDid,
|
|
61
61
|
currencySymbol,
|
|
62
|
-
grantedAmount:
|
|
62
|
+
grantedAmount: formatCreditAmount(
|
|
63
|
+
formatNumber(fromUnitToToken(creditGrant.amount.toString(), paymentCurrency.decimal)) || '0',
|
|
64
|
+
currencySymbol
|
|
65
|
+
),
|
|
63
66
|
expiresAt,
|
|
64
67
|
neverExpires,
|
|
65
68
|
at,
|
|
@@ -9,7 +9,7 @@ import { getMainProductName } from '../../product';
|
|
|
9
9
|
import { getCustomerSubscriptionPageUrl } from '../../subscription';
|
|
10
10
|
import { formatTime } from '../../time';
|
|
11
11
|
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
12
|
-
import {
|
|
12
|
+
import { formatCreditAmount, getConnectQueryParam, getCustomerIndexUrl, formatNumber } from '../../util';
|
|
13
13
|
import { getRechargePaymentUrl } from '../../currency';
|
|
14
14
|
|
|
15
15
|
export interface CustomerCreditInsufficientEmailTemplateOptions {
|
|
@@ -87,14 +87,19 @@ export class CustomerCreditInsufficientEmailTemplate
|
|
|
87
87
|
rechargeUrl = withQuery(rechargeUrl, { ...getConnectQueryParam({ userDid }) });
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
const formattedRequired =
|
|
91
|
+
formatNumber(fromUnitToToken(this.options.requiredAmount, paymentCurrency.decimal)) || '0';
|
|
92
|
+
const formattedAvailable =
|
|
93
|
+
formatNumber(fromUnitToToken(this.options.availableAmount, paymentCurrency.decimal)) || '0';
|
|
94
|
+
|
|
90
95
|
return {
|
|
91
96
|
locale,
|
|
92
97
|
userDid,
|
|
93
98
|
currencySymbol,
|
|
94
99
|
meterName: this.options.meterName,
|
|
95
100
|
meterEventName: this.options.meterEventName,
|
|
96
|
-
requiredAmount:
|
|
97
|
-
availableAmount:
|
|
101
|
+
requiredAmount: formatCreditAmount(formattedRequired, currencySymbol),
|
|
102
|
+
availableAmount: formatCreditAmount(formattedAvailable, currencySymbol),
|
|
98
103
|
isExhausted,
|
|
99
104
|
productName,
|
|
100
105
|
viewSubscriptionLink,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/brace-style */
|
|
2
2
|
/* eslint-disable @typescript-eslint/indent */
|
|
3
|
-
import { fromUnitToToken } from '@ocap/util';
|
|
4
3
|
import { withQuery } from 'ufo';
|
|
4
|
+
import { fromUnitToToken } from '@ocap/util';
|
|
5
5
|
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
6
6
|
import { translate } from '../../../locales';
|
|
7
7
|
import { Customer, PaymentCurrency } from '../../../store/models';
|
|
8
8
|
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
9
|
-
import {
|
|
9
|
+
import { formatCreditAmount, getConnectQueryParam, getCustomerIndexUrl, formatNumber } from '../../util';
|
|
10
10
|
import { getRechargePaymentUrl } from '../../currency';
|
|
11
11
|
|
|
12
12
|
export interface CustomerCreditLowBalanceEmailTemplateOptions {
|
|
@@ -20,7 +20,6 @@ export interface CustomerCreditLowBalanceEmailTemplateOptions {
|
|
|
20
20
|
interface CustomerCreditLowBalanceEmailTemplateContext {
|
|
21
21
|
locale: string;
|
|
22
22
|
userDid: string;
|
|
23
|
-
currencySymbol: string;
|
|
24
23
|
availableAmount: string; // formatted with symbol
|
|
25
24
|
lowBalancePercentage: string; // with % or "less than 1%"
|
|
26
25
|
currencyName: string;
|
|
@@ -51,9 +50,9 @@ export class CustomerCreditLowBalanceEmailTemplate
|
|
|
51
50
|
|
|
52
51
|
const userDid = customer.did;
|
|
53
52
|
const locale = await getUserLocale(userDid);
|
|
54
|
-
const currencySymbol = paymentCurrency.symbol;
|
|
55
53
|
|
|
56
|
-
const
|
|
54
|
+
const formattedAmount = formatNumber(fromUnitToToken(availableAmount, paymentCurrency.decimal));
|
|
55
|
+
const available = formatCreditAmount(formattedAmount || '0', paymentCurrency.symbol);
|
|
57
56
|
const percentageNum = parseFloat(percentage);
|
|
58
57
|
const isCritical = percentageNum < 1;
|
|
59
58
|
const lowBalancePercentage = isCritical
|
|
@@ -68,8 +67,7 @@ export class CustomerCreditLowBalanceEmailTemplate
|
|
|
68
67
|
return {
|
|
69
68
|
locale,
|
|
70
69
|
userDid,
|
|
71
|
-
|
|
72
|
-
availableAmount: `${available}`,
|
|
70
|
+
availableAmount: available,
|
|
73
71
|
lowBalancePercentage,
|
|
74
72
|
currencyName: paymentCurrency.name,
|
|
75
73
|
isCritical,
|
|
@@ -79,16 +77,7 @@ export class CustomerCreditLowBalanceEmailTemplate
|
|
|
79
77
|
|
|
80
78
|
async getTemplate(): Promise<BaseEmailTemplateType> {
|
|
81
79
|
const context = await this.getContext();
|
|
82
|
-
const {
|
|
83
|
-
locale,
|
|
84
|
-
userDid,
|
|
85
|
-
availableAmount,
|
|
86
|
-
lowBalancePercentage,
|
|
87
|
-
currencyName,
|
|
88
|
-
currencySymbol,
|
|
89
|
-
isCritical,
|
|
90
|
-
rechargeUrl,
|
|
91
|
-
} = context;
|
|
80
|
+
const { locale, userDid, availableAmount, lowBalancePercentage, currencyName, isCritical, rechargeUrl } = context;
|
|
92
81
|
|
|
93
82
|
const fields = [
|
|
94
83
|
{
|
|
@@ -119,7 +108,7 @@ export class CustomerCreditLowBalanceEmailTemplate
|
|
|
119
108
|
data: {
|
|
120
109
|
type: 'plain',
|
|
121
110
|
color: isCritical ? '#FF0000' : '#FF6600',
|
|
122
|
-
text:
|
|
111
|
+
text: availableAmount,
|
|
123
112
|
},
|
|
124
113
|
},
|
|
125
114
|
{
|
package/api/src/libs/util.ts
CHANGED
|
@@ -638,6 +638,38 @@ export function formatNumber(
|
|
|
638
638
|
return right ? [left, trimEnd(right, '0')].filter(Boolean).join('.') : left;
|
|
639
639
|
}
|
|
640
640
|
|
|
641
|
+
const CURRENCY_SYMBOLS: Record<string, string> = {
|
|
642
|
+
USD: '$',
|
|
643
|
+
EUR: '€',
|
|
644
|
+
GBP: '£',
|
|
645
|
+
JPY: '¥',
|
|
646
|
+
CNY: '¥',
|
|
647
|
+
KRW: '₩',
|
|
648
|
+
INR: '₹',
|
|
649
|
+
AUD: 'A$',
|
|
650
|
+
CAD: 'C$',
|
|
651
|
+
CHF: 'Fr',
|
|
652
|
+
HKD: 'HK$',
|
|
653
|
+
SGD: 'S$',
|
|
654
|
+
NZD: 'NZ$',
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Format credit amount for display in data contexts (balances, transactions, notifications)
|
|
659
|
+
* - If currency has symbol mapping (USD -> $): shows "$20"
|
|
660
|
+
* - If no symbol mapping: shows "20 minutes" or "20 arcsphere credits"
|
|
661
|
+
*
|
|
662
|
+
* @param formattedAmount - Already formatted amount string (e.g., "20", "1,000.50")
|
|
663
|
+
* @param currencySymbol - Currency symbol (USD, EUR, minutes, etc.)
|
|
664
|
+
*/
|
|
665
|
+
export function formatCreditAmount(formattedAmount: string, currencySymbol: string | undefined): string {
|
|
666
|
+
const mappedSymbol = CURRENCY_SYMBOLS[currencySymbol || ''];
|
|
667
|
+
if (mappedSymbol) {
|
|
668
|
+
return `${mappedSymbol}${formattedAmount} credits`;
|
|
669
|
+
}
|
|
670
|
+
return `${formattedAmount} ${currencySymbol || ''}`;
|
|
671
|
+
}
|
|
672
|
+
|
|
641
673
|
export function formatLinkWithLocale(url: string, locale?: string) {
|
|
642
674
|
if (!locale || !url) {
|
|
643
675
|
return url;
|
package/api/src/locales/en.ts
CHANGED
|
@@ -253,8 +253,8 @@ export default flat({
|
|
|
253
253
|
},
|
|
254
254
|
|
|
255
255
|
overdraftProtectionExhausted: {
|
|
256
|
-
title: 'Insufficient
|
|
257
|
-
body: 'Your subscription to {productName} has insufficient staked
|
|
256
|
+
title: 'Insufficient Stake for SubGuard™',
|
|
257
|
+
body: 'Your subscription to {productName} has insufficient staked amount for SubGuard™. Please increase your stake to maintain the service or disable it if no longer needed.',
|
|
258
258
|
},
|
|
259
259
|
|
|
260
260
|
aggregatedSubscriptionRenewed: {
|
|
@@ -263,25 +263,25 @@ export default flat({
|
|
|
263
263
|
},
|
|
264
264
|
|
|
265
265
|
creditInsufficient: {
|
|
266
|
-
title: 'Insufficient Credit',
|
|
266
|
+
title: 'Insufficient Credit Balance',
|
|
267
267
|
bodyWithSubscription:
|
|
268
|
-
'Your available credit ({availableAmount}) is not enough to cover your subscription to {subscriptionName}. To ensure uninterrupted service, please reload your account.',
|
|
268
|
+
'Your available credit balance ({availableAmount}) is not enough to cover your subscription to {subscriptionName}. To ensure uninterrupted service, please reload your account.',
|
|
269
269
|
bodyWithoutSubscription:
|
|
270
|
-
'Your available credit ({availableAmount}) is not enough to continue using the service. To ensure uninterrupted service, please reload your account.',
|
|
271
|
-
exhaustedTitle: 'Credit Exhausted – Please Reload',
|
|
270
|
+
'Your available credit balance ({availableAmount}) is not enough to continue using the service. To ensure uninterrupted service, please reload your account.',
|
|
271
|
+
exhaustedTitle: 'Credit Balance Exhausted – Please Reload',
|
|
272
272
|
exhaustedBodyWithSubscription:
|
|
273
|
-
'Your credit is fully exhausted and can no longer cover your subscription to {subscriptionName}. To ensure uninterrupted service, please reload your account.',
|
|
273
|
+
'Your credit balance is fully exhausted and can no longer cover your subscription to {subscriptionName}. To ensure uninterrupted service, please reload your account.',
|
|
274
274
|
exhaustedBodyWithoutSubscription:
|
|
275
|
-
'Your credit is fully exhausted. To ensure uninterrupted service, please reload your account.',
|
|
275
|
+
'Your credit balance is fully exhausted. To ensure uninterrupted service, please reload your account.',
|
|
276
276
|
meterEventName: 'Service',
|
|
277
|
-
availableCredit: 'Available
|
|
278
|
-
requiredCredit: 'Required Credit
|
|
277
|
+
availableCredit: 'Available Balance',
|
|
278
|
+
requiredCredit: 'Required Credit',
|
|
279
279
|
},
|
|
280
280
|
|
|
281
281
|
creditGrantGranted: {
|
|
282
|
-
title: 'Congratulations! Your
|
|
283
|
-
body: 'You have been granted {grantedAmount}
|
|
284
|
-
bodyNoExpire: 'You have been granted {grantedAmount}
|
|
282
|
+
title: 'Congratulations! Your credits are now active',
|
|
283
|
+
body: 'You have been granted {grantedAmount} in usage credits, activated on {at}, valid until {expiresAt}. Enjoy your service!',
|
|
284
|
+
bodyNoExpire: 'You have been granted {grantedAmount} in usage credits, activated on {at}. Enjoy your service!',
|
|
285
285
|
grantedCredit: 'Granted Credit',
|
|
286
286
|
validUntil: 'Valid Until',
|
|
287
287
|
neverExpires: 'Never Expires',
|
|
@@ -19,6 +19,7 @@ import { ensureInvoiceAndItems } from '../libs/invoice';
|
|
|
19
19
|
import dayjs from '../libs/dayjs';
|
|
20
20
|
import { invoiceQueue } from './invoice';
|
|
21
21
|
import { getPriceUintAmountByCurrency } from '../libs/price';
|
|
22
|
+
import { isDelegationSufficientForPayment } from '../libs/payment';
|
|
22
23
|
|
|
23
24
|
export interface AutoRechargeJobData {
|
|
24
25
|
customer_id: string;
|
|
@@ -228,6 +229,26 @@ async function executeAutoRecharge(
|
|
|
228
229
|
throw new Error('No payer found for auto recharge');
|
|
229
230
|
}
|
|
230
231
|
|
|
232
|
+
if (paymentMethod.type !== 'stripe') {
|
|
233
|
+
const balanceCheck = await isDelegationSufficientForPayment({
|
|
234
|
+
paymentMethod,
|
|
235
|
+
paymentCurrency: rechargeCurrency,
|
|
236
|
+
userDid: payer,
|
|
237
|
+
amount: totalAmount.toString(),
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
if (!balanceCheck.sufficient) {
|
|
241
|
+
logger.error('Insufficient balance for auto recharge', {
|
|
242
|
+
customerId: customer.id,
|
|
243
|
+
configId: config.id,
|
|
244
|
+
reason: balanceCheck.reason,
|
|
245
|
+
totalAmount: totalAmount.toString(),
|
|
246
|
+
payer,
|
|
247
|
+
});
|
|
248
|
+
throw new Error(`Insufficient balance: ${balanceCheck.reason}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
231
252
|
const invoice = await createInvoiceForAutoRecharge({
|
|
232
253
|
customer,
|
|
233
254
|
config,
|
|
@@ -276,6 +297,11 @@ export async function checkAndTriggerAutoRecharge(
|
|
|
276
297
|
currentBalance: string
|
|
277
298
|
): Promise<void> {
|
|
278
299
|
try {
|
|
300
|
+
logger.info('----- Checking and triggering auto recharge -----', {
|
|
301
|
+
customerId: customer.id,
|
|
302
|
+
currencyId,
|
|
303
|
+
currentBalance,
|
|
304
|
+
});
|
|
279
305
|
const config = await AutoRechargeConfig.findOne({
|
|
280
306
|
where: {
|
|
281
307
|
customer_id: customer.id,
|
|
@@ -292,6 +318,14 @@ export async function checkAndTriggerAutoRecharge(
|
|
|
292
318
|
return;
|
|
293
319
|
}
|
|
294
320
|
|
|
321
|
+
logger.info('Auto recharge config found', {
|
|
322
|
+
customerId: customer.id,
|
|
323
|
+
currencyId,
|
|
324
|
+
config,
|
|
325
|
+
currentBalance,
|
|
326
|
+
threshold: config.threshold,
|
|
327
|
+
});
|
|
328
|
+
|
|
295
329
|
// check if balance is above threshold
|
|
296
330
|
if (new BN(currentBalance).gte(new BN(config.threshold))) {
|
|
297
331
|
logger.debug('Balance above threshold, skipping auto recharge', {
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
TMeterExpanded,
|
|
16
16
|
} from '../store/models';
|
|
17
17
|
|
|
18
|
-
import { MAX_RETRY_COUNT, getNextRetry } from '../libs/util';
|
|
18
|
+
import { MAX_RETRY_COUNT, getNextRetry, formatCreditAmount } from '../libs/util';
|
|
19
19
|
import { getDaysUntilCancel, getDueUnit, getMeterPriceIdsFromSubscription } from '../libs/subscription';
|
|
20
20
|
import { events } from '../libs/event';
|
|
21
21
|
import { handlePastDueSubscriptionRecovery } from './payment';
|
|
@@ -391,9 +391,13 @@ async function createCreditTransaction(
|
|
|
391
391
|
meterEventId: context.meterEvent.id,
|
|
392
392
|
});
|
|
393
393
|
|
|
394
|
-
|
|
394
|
+
const formattedAmount = formatCreditAmount(
|
|
395
|
+
fromUnitToToken(consumeAmount, context.meter.paymentCurrency.decimal),
|
|
396
|
+
context.meter.paymentCurrency.symbol
|
|
397
|
+
);
|
|
398
|
+
let description = `Consume ${formattedAmount}`;
|
|
395
399
|
if (context.meterEvent.getSubscriptionId()) {
|
|
396
|
-
description += 'for Subscription';
|
|
400
|
+
description += ' for Subscription';
|
|
397
401
|
}
|
|
398
402
|
if (context.meterEvent.metadata?.description) {
|
|
399
403
|
description = context.meterEvent.metadata.description;
|
|
@@ -45,6 +45,13 @@ const getConfigSchema = Joi.object({
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
// Helper functions for validation and retrieval
|
|
48
|
+
function getMinimumThreshold(currency: PaymentCurrency): string {
|
|
49
|
+
const minimumThresholdBN = fromTokenToUnit('0.01', currency.decimal);
|
|
50
|
+
return new BN(currency.minimum_payment_amount).lt(minimumThresholdBN)
|
|
51
|
+
? minimumThresholdBN.toString()
|
|
52
|
+
: currency.minimum_payment_amount;
|
|
53
|
+
}
|
|
54
|
+
|
|
48
55
|
async function ensureCustomerExists(customerId: string): Promise<Customer> {
|
|
49
56
|
const customer = await Customer.findByPkOrDid(customerId);
|
|
50
57
|
if (!customer) {
|
|
@@ -177,12 +184,13 @@ router.get('/customer/:customerId', sessionMiddleware({ accessKey: true }), asyn
|
|
|
177
184
|
throw new CustomError(404, 'Base price not found');
|
|
178
185
|
}
|
|
179
186
|
if (!config) {
|
|
187
|
+
const minimumThreshold = getMinimumThreshold(currency);
|
|
180
188
|
const newConfig = await AutoRechargeConfig.create({
|
|
181
189
|
customer_id: customer.id, // Use customer.id instead of customerId from params
|
|
182
190
|
currency_id: currencyId,
|
|
183
191
|
livemode: currency.livemode,
|
|
184
192
|
enabled: false,
|
|
185
|
-
threshold:
|
|
193
|
+
threshold: minimumThreshold,
|
|
186
194
|
price_id: price.id,
|
|
187
195
|
quantity: 1,
|
|
188
196
|
recharge_currency_id: price.currency_id,
|
|
@@ -191,7 +199,7 @@ router.get('/customer/:customerId', sessionMiddleware({ accessKey: true }), asyn
|
|
|
191
199
|
...newConfig.toJSON(),
|
|
192
200
|
currency,
|
|
193
201
|
price,
|
|
194
|
-
threshold: fromUnitToToken(
|
|
202
|
+
threshold: fromUnitToToken(minimumThreshold, currency.decimal),
|
|
195
203
|
customer,
|
|
196
204
|
});
|
|
197
205
|
}
|
|
@@ -201,6 +209,10 @@ router.get('/customer/:customerId', sessionMiddleware({ accessKey: true }), asyn
|
|
|
201
209
|
price_id: price.id,
|
|
202
210
|
});
|
|
203
211
|
}
|
|
212
|
+
const minimumThreshold = getMinimumThreshold(currency);
|
|
213
|
+
const configThreshold = new BN(config.threshold || '0').lt(new BN(minimumThreshold))
|
|
214
|
+
? minimumThreshold
|
|
215
|
+
: config.threshold;
|
|
204
216
|
return res.json({
|
|
205
217
|
...config.toJSON(),
|
|
206
218
|
daily_limits: {
|
|
@@ -209,7 +221,7 @@ router.get('/customer/:customerId', sessionMiddleware({ accessKey: true }), asyn
|
|
|
209
221
|
},
|
|
210
222
|
currency,
|
|
211
223
|
price,
|
|
212
|
-
threshold: fromUnitToToken(
|
|
224
|
+
threshold: fromUnitToToken(configThreshold, currency.decimal),
|
|
213
225
|
customer,
|
|
214
226
|
});
|
|
215
227
|
});
|
|
@@ -352,6 +364,13 @@ router.post('/submit', async (req, res) => {
|
|
|
352
364
|
let { threshold, daily_limits: dailyLimits } = configData;
|
|
353
365
|
if (threshold) {
|
|
354
366
|
threshold = fromTokenToUnit(trimDecimals(threshold, currency.decimal), currency.decimal).toString();
|
|
367
|
+
const minimumThreshold = getMinimumThreshold(currency);
|
|
368
|
+
if (new BN(threshold).lt(new BN(minimumThreshold))) {
|
|
369
|
+
throw new CustomError(
|
|
370
|
+
400,
|
|
371
|
+
`Threshold must be greater than or equal to ${fromUnitToToken(minimumThreshold, currency.decimal)}`
|
|
372
|
+
);
|
|
373
|
+
}
|
|
355
374
|
}
|
|
356
375
|
if (dailyLimits) {
|
|
357
376
|
dailyLimits = {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Transaction } from '@ocap/client';
|
|
2
2
|
import { fromAddress } from '@ocap/wallet';
|
|
3
3
|
import { fromTokenToUnit, toBase58 } from '@ocap/util';
|
|
4
|
+
import pAll from 'p-all';
|
|
4
5
|
import { encodeTransferItx } from '../../integrations/ethereum/token';
|
|
5
6
|
import { executeEvmTransaction, waitForEvmTxConfirm } from '../../integrations/ethereum/tx';
|
|
6
7
|
import type { CallbackArgs } from '../../libs/auth';
|
|
@@ -9,9 +10,36 @@ import { getTxMetadata } from '../../libs/util';
|
|
|
9
10
|
import { ensureAccountRecharge, getAuthPrincipalClaim } from './shared';
|
|
10
11
|
import logger from '../../libs/logger';
|
|
11
12
|
import { ensureRechargeInvoice, retryUncollectibleInvoices } from '../../libs/invoice';
|
|
12
|
-
import { EVMChainType } from '../../store/models';
|
|
13
|
+
import { Customer, EVMChainType, PaymentCurrency } from '../../store/models';
|
|
13
14
|
import { EVM_CHAIN_TYPES } from '../../libs/constants';
|
|
15
|
+
import { checkAndTriggerAutoRecharge } from '../../queues/auto-recharge';
|
|
16
|
+
import { getCustomerCreditBalance } from '../../libs/credit-grant';
|
|
14
17
|
|
|
18
|
+
async function triggerAutoRecharge(customer: Customer) {
|
|
19
|
+
try {
|
|
20
|
+
const currencies = await PaymentCurrency.findAll({
|
|
21
|
+
where: {
|
|
22
|
+
type: 'credit',
|
|
23
|
+
active: true,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
if (currencies.length === 0) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
await pAll(
|
|
30
|
+
currencies.map((currency) => async () => {
|
|
31
|
+
const balanceInfo = await getCustomerCreditBalance(customer.id, currency.id);
|
|
32
|
+
await checkAndTriggerAutoRecharge(customer, currency.id, balanceInfo.totalBalance);
|
|
33
|
+
}),
|
|
34
|
+
{ concurrency: 5, stopOnError: false }
|
|
35
|
+
);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
logger.error('Failed to check and trigger auto recharge', {
|
|
38
|
+
error: err,
|
|
39
|
+
customerId: customer.id,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
15
43
|
export default {
|
|
16
44
|
action: 'recharge-account',
|
|
17
45
|
authPrincipal: false,
|
|
@@ -118,7 +146,7 @@ export default {
|
|
|
118
146
|
},
|
|
119
147
|
null,
|
|
120
148
|
paymentMethod,
|
|
121
|
-
customer
|
|
149
|
+
customer
|
|
122
150
|
);
|
|
123
151
|
try {
|
|
124
152
|
retryUncollectibleInvoices({
|
|
@@ -132,6 +160,30 @@ export default {
|
|
|
132
160
|
currencyId: paymentCurrency.id,
|
|
133
161
|
});
|
|
134
162
|
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
await retryUncollectibleInvoices({
|
|
166
|
+
customerId: customer.id,
|
|
167
|
+
currencyId: paymentCurrency.id,
|
|
168
|
+
billingReason: 'auto_recharge',
|
|
169
|
+
statusList: ['open'],
|
|
170
|
+
});
|
|
171
|
+
} catch (err) {
|
|
172
|
+
logger.error('Failed to retry auto recharge invoices', {
|
|
173
|
+
error: err,
|
|
174
|
+
customerId: customer.id,
|
|
175
|
+
currencyId: paymentCurrency.id,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
await triggerAutoRecharge(customer);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
logger.error('Failed to check and trigger auto recharge', {
|
|
183
|
+
error: err,
|
|
184
|
+
customerId: customer.id,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
135
187
|
};
|
|
136
188
|
if (paymentMethod.type === 'arcblock') {
|
|
137
189
|
try {
|
|
@@ -239,7 +239,7 @@ router.get('/:id/retry', authAdmin, async (req, res) => {
|
|
|
239
239
|
|
|
240
240
|
await doc.update({ status: 'requires_capture', last_payment_error: null });
|
|
241
241
|
await paymentQueue.pushAndWait({
|
|
242
|
-
id: doc.id
|
|
242
|
+
id: `${doc.id}-retry-${Date.now()}`,
|
|
243
243
|
job: { paymentIntentId: doc.id, retryOnError: false },
|
|
244
244
|
});
|
|
245
245
|
res.json(doc);
|
package/api/src/routes/prices.ts
CHANGED
|
@@ -197,6 +197,9 @@ export async function createPrice(payload: any) {
|
|
|
197
197
|
if (!validate) {
|
|
198
198
|
throw new Error(`currency ${notSupportCurrencies.map((x) => x.name).join(', ')} does not support recurring`);
|
|
199
199
|
}
|
|
200
|
+
if (isRecurring && raw.recurring && !raw.recurring.usage_type) {
|
|
201
|
+
raw.recurring.usage_type = 'licensed';
|
|
202
|
+
}
|
|
200
203
|
const price = await Price.insert(raw);
|
|
201
204
|
return getExpandedPrice(price.id as string);
|
|
202
205
|
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from 'sequelize';
|
|
12
12
|
import { getUrl } from '@blocklet/sdk';
|
|
13
13
|
import type { LiteralUnion } from 'type-fest';
|
|
14
|
+
import { fromTokenToUnit } from '@ocap/util';
|
|
14
15
|
import { createIdGenerator } from '../../libs/util';
|
|
15
16
|
import { RechargeConfig, VaultConfig } from './types';
|
|
16
17
|
|
|
@@ -237,14 +238,14 @@ export class PaymentCurrency extends Model<InferAttributes<PaymentCurrency>, Inf
|
|
|
237
238
|
const currency = await this.create({
|
|
238
239
|
type: 'credit',
|
|
239
240
|
payment_method_id: paymentMethodId,
|
|
240
|
-
name: meter.
|
|
241
|
+
name: meter.name,
|
|
241
242
|
description: `Credit for ${meter.unit}`,
|
|
242
243
|
symbol: meter.unit,
|
|
243
|
-
logo: getUrl('/methods/arcblock.png'),
|
|
244
|
+
logo: getUrl('/methods/arcblock.png'),
|
|
244
245
|
decimal,
|
|
245
246
|
maximum_precision: decimal,
|
|
246
|
-
minimum_payment_amount: '
|
|
247
|
-
maximum_payment_amount: '
|
|
247
|
+
minimum_payment_amount: fromTokenToUnit('0.01', decimal).toString(),
|
|
248
|
+
maximum_payment_amount: fromTokenToUnit('100000000', decimal).toString(),
|
|
248
249
|
active: true,
|
|
249
250
|
livemode: meter.livemode || false,
|
|
250
251
|
is_base_currency: false,
|
|
@@ -526,7 +526,7 @@ describe('calculateRecommendedRechargeAmount', () => {
|
|
|
526
526
|
quantity: 1,
|
|
527
527
|
price: {
|
|
528
528
|
type: 'recurring',
|
|
529
|
-
recurring: { interval: 'month', interval_count: 1 },
|
|
529
|
+
recurring: { interval: 'month', interval_count: 1, usage_type: 'licensed' },
|
|
530
530
|
unit_amount: '500',
|
|
531
531
|
currency_id: 'usd',
|
|
532
532
|
},
|
|
@@ -551,7 +551,7 @@ describe('calculateRecommendedRechargeAmount', () => {
|
|
|
551
551
|
quantity: 1,
|
|
552
552
|
price: {
|
|
553
553
|
type: 'recurring',
|
|
554
|
-
recurring: { interval: 'month', interval_count: 1 },
|
|
554
|
+
recurring: { interval: 'month', interval_count: 1, usage_type: 'licensed' },
|
|
555
555
|
unit_amount: '500',
|
|
556
556
|
currency_id: 'usd',
|
|
557
557
|
},
|
|
@@ -578,7 +578,7 @@ describe('calculateRecommendedRechargeAmount', () => {
|
|
|
578
578
|
quantity: 1,
|
|
579
579
|
price: {
|
|
580
580
|
type: 'recurring',
|
|
581
|
-
recurring: { interval: 'month', interval_count: 1 },
|
|
581
|
+
recurring: { interval: 'month', interval_count: 1, usage_type: 'licensed' },
|
|
582
582
|
unit_amount: '500',
|
|
583
583
|
currency_id: 'usd',
|
|
584
584
|
},
|
|
@@ -604,7 +604,7 @@ describe('calculateRecommendedRechargeAmount', () => {
|
|
|
604
604
|
quantity: 1,
|
|
605
605
|
price: {
|
|
606
606
|
type: 'recurring',
|
|
607
|
-
recurring: { interval: 'month', interval_count: 1 },
|
|
607
|
+
recurring: { interval: 'month', interval_count: 1, usage_type: 'licensed' },
|
|
608
608
|
unit_amount: '500',
|
|
609
609
|
currency_id: 'usd',
|
|
610
610
|
},
|
|
@@ -702,7 +702,7 @@ describe('calculateRecommendedRechargeAmount', () => {
|
|
|
702
702
|
quantity: 1,
|
|
703
703
|
price: {
|
|
704
704
|
type: 'recurring',
|
|
705
|
-
recurring: { interval: 'month', interval_count: 1 },
|
|
705
|
+
recurring: { interval: 'month', interval_count: 1, usage_type: 'licensed' },
|
|
706
706
|
unit_amount: '200',
|
|
707
707
|
currency_id: 'usd',
|
|
708
708
|
},
|
|
@@ -749,7 +749,7 @@ describe('calculateRecommendedRechargeAmount', () => {
|
|
|
749
749
|
quantity: 1,
|
|
750
750
|
price: {
|
|
751
751
|
type: 'recurring',
|
|
752
|
-
recurring: { interval: 'month', interval_count: 1 },
|
|
752
|
+
recurring: { interval: 'month', interval_count: 1, usage_type: 'licensed' },
|
|
753
753
|
currency_id: 'eth',
|
|
754
754
|
currency_options: [
|
|
755
755
|
{ currency_id: 'usd', unit_amount: '300' },
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.23.
|
|
3
|
+
"version": "1.23.5",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
|
|
@@ -46,23 +46,23 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@abtnode/cron": "^1.17.5",
|
|
48
48
|
"@arcblock/did": "^1.27.15",
|
|
49
|
-
"@arcblock/did-connect-react": "^3.2.
|
|
49
|
+
"@arcblock/did-connect-react": "^3.2.19",
|
|
50
50
|
"@arcblock/did-connect-storage-nedb": "^1.8.0",
|
|
51
51
|
"@arcblock/did-util": "^1.27.15",
|
|
52
52
|
"@arcblock/jwt": "^1.27.15",
|
|
53
|
-
"@arcblock/react-hooks": "^3.2.
|
|
54
|
-
"@arcblock/ux": "^3.2.
|
|
53
|
+
"@arcblock/react-hooks": "^3.2.19",
|
|
54
|
+
"@arcblock/ux": "^3.2.19",
|
|
55
55
|
"@arcblock/validator": "^1.27.15",
|
|
56
56
|
"@arcblock/vc": "^1.27.15",
|
|
57
|
-
"@blocklet/did-space-js": "^1.2.
|
|
57
|
+
"@blocklet/did-space-js": "^1.2.11",
|
|
58
58
|
"@blocklet/error": "^0.3.5",
|
|
59
59
|
"@blocklet/js-sdk": "^1.17.5",
|
|
60
60
|
"@blocklet/logger": "^1.17.5",
|
|
61
|
-
"@blocklet/payment-broker-client": "1.23.
|
|
62
|
-
"@blocklet/payment-react": "1.23.
|
|
63
|
-
"@blocklet/payment-vendor": "1.23.
|
|
61
|
+
"@blocklet/payment-broker-client": "1.23.5",
|
|
62
|
+
"@blocklet/payment-react": "1.23.5",
|
|
63
|
+
"@blocklet/payment-vendor": "1.23.5",
|
|
64
64
|
"@blocklet/sdk": "^1.17.5",
|
|
65
|
-
"@blocklet/ui-react": "^3.2.
|
|
65
|
+
"@blocklet/ui-react": "^3.2.19",
|
|
66
66
|
"@blocklet/uploader": "^0.3.16",
|
|
67
67
|
"@blocklet/xss": "^0.3.14",
|
|
68
68
|
"@mui/icons-material": "^7.1.2",
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
"devDependencies": {
|
|
131
131
|
"@abtnode/types": "^1.17.5",
|
|
132
132
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
133
|
-
"@blocklet/payment-types": "1.23.
|
|
133
|
+
"@blocklet/payment-types": "1.23.5",
|
|
134
134
|
"@types/cookie-parser": "^1.4.9",
|
|
135
135
|
"@types/cors": "^2.8.19",
|
|
136
136
|
"@types/debug": "^4.1.12",
|
|
@@ -161,7 +161,7 @@
|
|
|
161
161
|
"vite": "^7.0.0",
|
|
162
162
|
"vite-node": "^3.2.4",
|
|
163
163
|
"vite-plugin-babel-import": "^2.0.5",
|
|
164
|
-
"vite-plugin-blocklet": "^0.
|
|
164
|
+
"vite-plugin-blocklet": "^0.13.1",
|
|
165
165
|
"vite-plugin-node-polyfills": "^0.23.0",
|
|
166
166
|
"vite-plugin-svgr": "^4.3.0",
|
|
167
167
|
"vite-tsconfig-paths": "^5.1.4",
|
|
@@ -177,5 +177,5 @@
|
|
|
177
177
|
"parser": "typescript"
|
|
178
178
|
}
|
|
179
179
|
},
|
|
180
|
-
"gitHead": "
|
|
180
|
+
"gitHead": "486456ed522207cc4efa54cd14b1f65c6433d179"
|
|
181
181
|
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
formatCreditAmount,
|
|
3
|
+
CreditGrantsList,
|
|
4
|
+
CreditTransactionsList,
|
|
5
|
+
api,
|
|
6
|
+
AutoTopupModal,
|
|
7
|
+
formatBNStr,
|
|
8
|
+
} from '@blocklet/payment-react';
|
|
2
9
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
10
|
import { Box, Card, CardContent, Stack, Typography, Tabs, Tab, Button } from '@mui/material';
|
|
4
11
|
import { useMemo, useState, useEffect } from 'react';
|
|
@@ -217,17 +224,9 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
|
|
|
217
224
|
</Typography>
|
|
218
225
|
<Typography variant="h5" component="div" sx={{ fontWeight: 'normal' }}>
|
|
219
226
|
{totalAmount === '0' && remainingAmount === '0' ? (
|
|
220
|
-
<>0
|
|
227
|
+
<>0</>
|
|
221
228
|
) : (
|
|
222
|
-
<>
|
|
223
|
-
{formatBNStr(remainingAmount, currency.decimal, 6, true)}
|
|
224
|
-
{currency.symbol !== cardTitle && (
|
|
225
|
-
<Typography variant="body2" component="span">
|
|
226
|
-
{' '}
|
|
227
|
-
{currency.symbol}
|
|
228
|
-
</Typography>
|
|
229
|
-
)}
|
|
230
|
-
</>
|
|
229
|
+
<>{formatCreditAmount(formatBNStr(remainingAmount, currency.decimal), currency.symbol, false)}</>
|
|
231
230
|
)}
|
|
232
231
|
</Typography>
|
|
233
232
|
</Box>
|
|
@@ -248,13 +247,7 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
|
|
|
248
247
|
sx={{
|
|
249
248
|
color: 'error.main',
|
|
250
249
|
}}>
|
|
251
|
-
{formatBNStr(pendingAmount, currency.decimal,
|
|
252
|
-
{currency.symbol !== cardTitle && (
|
|
253
|
-
<Typography variant="body2" component="span">
|
|
254
|
-
{' '}
|
|
255
|
-
{currency.symbol}
|
|
256
|
-
</Typography>
|
|
257
|
-
)}
|
|
250
|
+
{formatCreditAmount(formatBNStr(pendingAmount, currency.decimal), currency.symbol, false)}
|
|
258
251
|
</Typography>
|
|
259
252
|
</Box>
|
|
260
253
|
)}
|
package/src/locales/en.tsx
CHANGED
|
@@ -128,6 +128,7 @@ export default flat({
|
|
|
128
128
|
},
|
|
129
129
|
},
|
|
130
130
|
admin: {
|
|
131
|
+
description: 'Manage your products, pricing, billing, and payments.',
|
|
131
132
|
balances: 'Balances',
|
|
132
133
|
trends: 'Trends',
|
|
133
134
|
addresses: 'Addresses',
|
|
@@ -1893,6 +1894,7 @@ export default flat({
|
|
|
1893
1894
|
},
|
|
1894
1895
|
},
|
|
1895
1896
|
integrations: {
|
|
1897
|
+
description: 'Configure and manage how Payment Kit integrates with your application.',
|
|
1896
1898
|
basicFeatures: 'Basic Features',
|
|
1897
1899
|
advancedFeatures: 'Advanced Features',
|
|
1898
1900
|
features: {
|
package/src/locales/zh.tsx
CHANGED
|
@@ -127,6 +127,7 @@ export default flat({
|
|
|
127
127
|
},
|
|
128
128
|
},
|
|
129
129
|
admin: {
|
|
130
|
+
description: '管理你的产品、定价、账单和支付流程。',
|
|
130
131
|
balances: '余额',
|
|
131
132
|
addresses: '地址',
|
|
132
133
|
trends: '趋势',
|
|
@@ -1837,6 +1838,7 @@ export default flat({
|
|
|
1837
1838
|
},
|
|
1838
1839
|
},
|
|
1839
1840
|
integrations: {
|
|
1841
|
+
description: '用于配置和管理 Payment Kit 与你应用之间的集成方式。',
|
|
1840
1842
|
basicFeatures: '基础功能',
|
|
1841
1843
|
advancedFeatures: '高级功能',
|
|
1842
1844
|
features: {
|
|
@@ -3,13 +3,14 @@ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
|
3
3
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
4
4
|
import {
|
|
5
5
|
api,
|
|
6
|
-
formatBNStr,
|
|
7
6
|
formatError,
|
|
8
7
|
formatTime,
|
|
9
8
|
CreditTransactionsList,
|
|
10
9
|
CreditStatusChip,
|
|
11
10
|
getCustomerAvatar,
|
|
12
11
|
TxLink,
|
|
12
|
+
formatCreditAmount,
|
|
13
|
+
formatBNStr,
|
|
13
14
|
} from '@blocklet/payment-react';
|
|
14
15
|
import type { TCreditGrantExpanded } from '@blocklet/payment-types';
|
|
15
16
|
import { ArrowBackOutlined } from '@mui/icons-material';
|
|
@@ -196,7 +197,10 @@ export default function AdminCreditGrantDetail({ id }: { id: string }) {
|
|
|
196
197
|
alt={data.paymentCurrency?.symbol}
|
|
197
198
|
/>
|
|
198
199
|
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
|
199
|
-
{
|
|
200
|
+
{formatCreditAmount(
|
|
201
|
+
formatBNStr(data.amount, data.paymentCurrency.decimal),
|
|
202
|
+
data.paymentCurrency.symbol
|
|
203
|
+
)}
|
|
200
204
|
</Typography>
|
|
201
205
|
</Stack>
|
|
202
206
|
}
|
|
@@ -212,7 +216,10 @@ export default function AdminCreditGrantDetail({ id }: { id: string }) {
|
|
|
212
216
|
alt={data.paymentCurrency?.symbol}
|
|
213
217
|
/>
|
|
214
218
|
<Typography variant="body2" color={isDepleted ? 'error.main' : 'text.secondary'}>
|
|
215
|
-
{
|
|
219
|
+
{formatCreditAmount(
|
|
220
|
+
formatBNStr(data.remaining_amount, data.paymentCurrency.decimal),
|
|
221
|
+
data.paymentCurrency.symbol
|
|
222
|
+
)}
|
|
216
223
|
</Typography>
|
|
217
224
|
</Stack>
|
|
218
225
|
}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
api,
|
|
4
|
+
formatBNStr,
|
|
5
|
+
formatError,
|
|
6
|
+
getCustomerAvatar,
|
|
7
|
+
TxLink,
|
|
8
|
+
SourceDataViewer,
|
|
9
|
+
formatCreditAmount,
|
|
10
|
+
} from '@blocklet/payment-react';
|
|
3
11
|
import type { TCreditTransactionExpanded } from '@blocklet/payment-types';
|
|
4
12
|
import { ArrowBackOutlined } from '@mui/icons-material';
|
|
5
13
|
import { Alert, Avatar, Box, Button, Chip, CircularProgress, Divider, Stack, Typography } from '@mui/material';
|
|
@@ -146,8 +154,11 @@ export default function AdminCreditTransactionDetail({ id }: { id: string }) {
|
|
|
146
154
|
value={
|
|
147
155
|
<Stack direction="row" alignItems="center" spacing={0.5}>
|
|
148
156
|
<Typography variant="body2" sx={{ color: 'error.main' }}>
|
|
149
|
-
-
|
|
150
|
-
{
|
|
157
|
+
-
|
|
158
|
+
{formatCreditAmount(
|
|
159
|
+
formatBNStr(data.credit_amount, data.creditGrant?.paymentCurrency?.decimal || 0),
|
|
160
|
+
data.creditGrant?.paymentCurrency?.symbol || ''
|
|
161
|
+
)}
|
|
151
162
|
</Typography>
|
|
152
163
|
</Stack>
|
|
153
164
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
+
import { useAppInfo } from '@blocklet/ui-react/lib/Dashboard';
|
|
2
3
|
import { Switch, usePaymentContext } from '@blocklet/payment-react';
|
|
3
4
|
import { Chip, Stack } from '@mui/material';
|
|
4
5
|
import React, { isValidElement, useEffect } from 'react';
|
|
@@ -43,6 +44,7 @@ const renderNav = (group: string, onChange: Function, groups: { label: string; v
|
|
|
43
44
|
function Admin() {
|
|
44
45
|
const navigate = useNavigate();
|
|
45
46
|
const { t } = useLocaleContext();
|
|
47
|
+
const { updateAppInfo } = useAppInfo();
|
|
46
48
|
const settings = usePaymentContext();
|
|
47
49
|
const { group = 'overview' } = useParams();
|
|
48
50
|
const { isPending, startTransition } = useTransitionContext();
|
|
@@ -72,6 +74,12 @@ function Admin() {
|
|
|
72
74
|
|
|
73
75
|
const group2 = [{ label: t('admin.developers'), value: 'developers' }];
|
|
74
76
|
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
updateAppInfo({
|
|
79
|
+
description: t('admin.description'),
|
|
80
|
+
});
|
|
81
|
+
}, [t]);
|
|
82
|
+
|
|
75
83
|
return (
|
|
76
84
|
<>
|
|
77
85
|
<ProgressBar pending={isPending} />
|
|
@@ -77,7 +77,7 @@ export default function ProductsCreate({
|
|
|
77
77
|
description: mode === 'credit' ? t('admin.creditProduct.defaultDescription') : '',
|
|
78
78
|
images: [],
|
|
79
79
|
statement_descriptor: '',
|
|
80
|
-
unit_label:
|
|
80
|
+
unit_label: '',
|
|
81
81
|
features: [],
|
|
82
82
|
prices: [
|
|
83
83
|
mode === 'credit'
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
3
|
import {
|
|
4
4
|
api,
|
|
5
|
-
formatBNStr,
|
|
6
5
|
formatTime,
|
|
7
6
|
CreditTransactionsList,
|
|
8
7
|
CreditStatusChip,
|
|
9
8
|
getCustomerAvatar,
|
|
10
9
|
TxLink,
|
|
10
|
+
formatCreditAmount,
|
|
11
|
+
formatBNStr,
|
|
11
12
|
} from '@blocklet/payment-react';
|
|
12
13
|
import type { TCreditGrantExpanded } from '@blocklet/payment-types';
|
|
13
14
|
import { ArrowBackOutlined } from '@mui/icons-material';
|
|
@@ -173,7 +174,10 @@ export default function CustomerCreditGrantDetail() {
|
|
|
173
174
|
alt={data.paymentCurrency?.symbol}
|
|
174
175
|
/>
|
|
175
176
|
<Typography variant="body2">
|
|
176
|
-
{
|
|
177
|
+
{formatCreditAmount(
|
|
178
|
+
formatBNStr(data.amount, data.paymentCurrency.decimal),
|
|
179
|
+
data.paymentCurrency.symbol
|
|
180
|
+
)}
|
|
177
181
|
</Typography>
|
|
178
182
|
</Stack>
|
|
179
183
|
}
|
|
@@ -189,7 +193,10 @@ export default function CustomerCreditGrantDetail() {
|
|
|
189
193
|
alt={data.paymentCurrency?.symbol}
|
|
190
194
|
/>
|
|
191
195
|
<Typography variant="body2">
|
|
192
|
-
{
|
|
196
|
+
{formatCreditAmount(
|
|
197
|
+
formatBNStr(data.remaining_amount, data.paymentCurrency.decimal),
|
|
198
|
+
data.paymentCurrency.symbol
|
|
199
|
+
)}
|
|
193
200
|
</Typography>
|
|
194
201
|
</Stack>
|
|
195
202
|
}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
api,
|
|
4
|
+
formatTime,
|
|
5
|
+
getCustomerAvatar,
|
|
6
|
+
TxLink,
|
|
7
|
+
SourceDataViewer,
|
|
8
|
+
formatCreditAmount,
|
|
9
|
+
formatBNStr,
|
|
10
|
+
} from '@blocklet/payment-react';
|
|
3
11
|
import type { TCreditTransactionExpanded } from '@blocklet/payment-types';
|
|
4
12
|
import { ArrowBackOutlined } from '@mui/icons-material';
|
|
5
13
|
import { Alert, Avatar, Box, Button, Chip, CircularProgress, Divider, Stack, Typography } from '@mui/material';
|
|
@@ -159,8 +167,11 @@ export default function CustomerCreditTransactionDetail() {
|
|
|
159
167
|
value={
|
|
160
168
|
<Stack direction="row" alignItems="center" spacing={0.5}>
|
|
161
169
|
<Typography variant="body2" sx={{ color: 'error.main' }}>
|
|
162
|
-
-
|
|
163
|
-
{
|
|
170
|
+
-
|
|
171
|
+
{formatCreditAmount(
|
|
172
|
+
formatBNStr(data.credit_amount, data.creditGrant?.paymentCurrency?.decimal || 0),
|
|
173
|
+
data.creditGrant?.paymentCurrency?.symbol || ''
|
|
174
|
+
)}
|
|
164
175
|
</Typography>
|
|
165
176
|
</Stack>
|
|
166
177
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
+
import { useAppInfo } from '@blocklet/ui-react/lib/Dashboard';
|
|
2
3
|
import { Stack } from '@mui/material';
|
|
3
4
|
import React, { useEffect } from 'react';
|
|
4
5
|
import { useNavigate, useParams } from 'react-router-dom';
|
|
@@ -16,6 +17,7 @@ const pages = {
|
|
|
16
17
|
function Integrations() {
|
|
17
18
|
const navigate = useNavigate();
|
|
18
19
|
const { t } = useLocaleContext();
|
|
20
|
+
const { updateAppInfo } = useAppInfo();
|
|
19
21
|
const { group = 'overview' } = useParams();
|
|
20
22
|
const { isPending, startTransition } = useTransitionContext();
|
|
21
23
|
|
|
@@ -32,6 +34,12 @@ function Integrations() {
|
|
|
32
34
|
{ label: t('admin.donate.title'), value: 'donations' },
|
|
33
35
|
];
|
|
34
36
|
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
updateAppInfo({
|
|
39
|
+
description: t('integrations.description'),
|
|
40
|
+
});
|
|
41
|
+
}, [t]);
|
|
42
|
+
|
|
35
43
|
return (
|
|
36
44
|
<>
|
|
37
45
|
<ProgressBar pending={isPending} />
|
|
@@ -99,15 +99,6 @@ export default function Overview() {
|
|
|
99
99
|
sx={{
|
|
100
100
|
mb: 4,
|
|
101
101
|
}}>
|
|
102
|
-
<Typography
|
|
103
|
-
variant="h2"
|
|
104
|
-
gutterBottom
|
|
105
|
-
sx={{
|
|
106
|
-
fontWeight: 'bold',
|
|
107
|
-
mb: 1,
|
|
108
|
-
}}>
|
|
109
|
-
{t('common.welcome')}
|
|
110
|
-
</Typography>
|
|
111
102
|
<Typography
|
|
112
103
|
variant="body2"
|
|
113
104
|
sx={{
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|