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.
Files changed (36) hide show
  1. package/api/src/libs/invoice.ts +14 -4
  2. package/api/src/libs/notification/template/customer-credit-grant-granted.ts +5 -2
  3. package/api/src/libs/notification/template/customer-credit-insufficient.ts +8 -3
  4. package/api/src/libs/notification/template/customer-credit-low-balance.ts +7 -18
  5. package/api/src/libs/util.ts +32 -0
  6. package/api/src/locales/en.ts +13 -13
  7. package/api/src/queues/auto-recharge.ts +34 -0
  8. package/api/src/queues/credit-consume.ts +7 -3
  9. package/api/src/routes/auto-recharge-configs.ts +22 -3
  10. package/api/src/routes/connect/recharge-account.ts +54 -2
  11. package/api/src/routes/payment-intents.ts +1 -1
  12. package/api/src/routes/prices.ts +3 -0
  13. package/api/src/store/models/payment-currency.ts +5 -4
  14. package/api/tests/libs/subscription.spec.ts +6 -6
  15. package/blocklet.yml +1 -1
  16. package/package.json +12 -12
  17. package/src/components/customer/credit-overview.tsx +11 -18
  18. package/src/locales/en.tsx +2 -0
  19. package/src/locales/zh.tsx +2 -0
  20. package/src/pages/admin/customers/customers/credit-grant/detail.tsx +10 -3
  21. package/src/pages/admin/customers/customers/credit-transaction/detail.tsx +14 -3
  22. package/src/pages/admin/index.tsx +8 -0
  23. package/src/pages/admin/products/products/create.tsx +1 -1
  24. package/src/pages/customer/credit-grant/detail.tsx +10 -3
  25. package/src/pages/customer/credit-transaction/detail.tsx +14 -3
  26. package/src/pages/integrations/index.tsx +8 -0
  27. package/src/pages/integrations/overview.tsx +0 -9
  28. package/website/images/hero-image.png +0 -0
  29. package/website/images/r-for-ai.png +0 -0
  30. package/website/images/r-for-devr.png +0 -0
  31. package/website/images/r-for-ent.png +0 -0
  32. package/website/images/r-for-owner.png +0 -0
  33. package/website/images/r-for-web3.png +0 -0
  34. package/website/images/hero-image.webp +0 -0
  35. package/website/images/r-bus-owner.png +0 -0
  36. package/website/images/r-for-dev.png +0 -0
@@ -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]: ['uncollectible'] },
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 uncollectible invoices to retry', {
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 uncollectible invoices', {
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: `${formatNumber(fromUnitToToken(creditGrant.amount.toString(), paymentCurrency.decimal))} ${currencySymbol}`,
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 { formatNumber, getConnectQueryParam, getCustomerIndexUrl } from '../../util';
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: `${formatNumber(fromUnitToToken(this.options.requiredAmount, paymentCurrency.decimal))} ${currencySymbol}`,
97
- availableAmount: `${formatNumber(fromUnitToToken(this.options.availableAmount, paymentCurrency.decimal))} ${currencySymbol}`,
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 { formatNumber, getConnectQueryParam, getCustomerIndexUrl } from '../../util';
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 available = formatNumber(fromUnitToToken(availableAmount, paymentCurrency.decimal));
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
- currencySymbol,
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: `${availableAmount} ${currencySymbol}`,
111
+ text: availableAmount,
123
112
  },
124
113
  },
125
114
  {
@@ -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;
@@ -253,8 +253,8 @@ export default flat({
253
253
  },
254
254
 
255
255
  overdraftProtectionExhausted: {
256
- title: 'Insufficient Credit for SubGuard™',
257
- body: 'Your subscription to {productName} has insufficient staked credit for SubGuard™. Please increase your stake to maintain the service or disable it if no longer needed.',
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 Credit Amount',
278
- requiredCredit: 'Required Credit Amount',
277
+ availableCredit: 'Available Balance',
278
+ requiredCredit: 'Required Credit',
279
279
  },
280
280
 
281
281
  creditGrantGranted: {
282
- title: 'Congratulations! Your granted credit is now active',
283
- body: 'You have been granted {grantedAmount} credit, activated on {at}, valid until {expiresAt}. Enjoy your service!',
284
- bodyNoExpire: 'You have been granted {grantedAmount} credit, activated on {at}. Enjoy your service!',
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
- let description = `Consume ${fromUnitToToken(consumeAmount, context.meter.paymentCurrency.decimal)} ${context.meter.paymentCurrency.symbol}`;
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: currency.minimum_payment_amount,
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(currency.minimum_payment_amount, currency.decimal),
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(config.threshold || 0, currency.decimal),
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);
@@ -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.unit,
241
+ name: meter.name,
241
242
  description: `Credit for ${meter.unit}`,
242
243
  symbol: meter.unit,
243
- logo: getUrl('/methods/arcblock.png'), // 默认credit图标
244
+ logo: getUrl('/methods/arcblock.png'),
244
245
  decimal,
245
246
  maximum_precision: decimal,
246
- minimum_payment_amount: '1',
247
- maximum_payment_amount: '100000000000',
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
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.23.3
17
+ version: 1.23.5
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.23.3",
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.18",
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.18",
54
- "@arcblock/ux": "^3.2.18",
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.10",
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.3",
62
- "@blocklet/payment-react": "1.23.3",
63
- "@blocklet/payment-vendor": "1.23.3",
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.18",
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.3",
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.12.5",
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": "d6afc628bd9b66938eac1c45a4d5d657706bf60b"
180
+ "gitHead": "486456ed522207cc4efa54cd14b1f65c6433d179"
181
181
  }
@@ -1,4 +1,11 @@
1
- import { formatBNStr, CreditGrantsList, CreditTransactionsList, api, AutoTopupModal } from '@blocklet/payment-react';
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, 6, true)}
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
  )}
@@ -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: {
@@ -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
- {formatBNStr(data.amount, data.paymentCurrency.decimal)} {data.paymentCurrency.symbol}
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
- {formatBNStr(data.remaining_amount, data.paymentCurrency.decimal)} {data.paymentCurrency.symbol}
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 { api, formatBNStr, formatError, getCustomerAvatar, TxLink, SourceDataViewer } from '@blocklet/payment-react';
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
- -{formatBNStr(data.credit_amount, data.creditGrant?.paymentCurrency?.decimal || 0)}{' '}
150
- {data.creditGrant?.paymentCurrency?.symbol}
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: mode === 'credit' ? t('admin.creditProduct.unitLabel') : '',
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
- {formatBNStr(data.amount, data.paymentCurrency.decimal)} {data.paymentCurrency.symbol}
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
- {formatBNStr(data.remaining_amount, data.paymentCurrency.decimal)} {data.paymentCurrency.symbol}
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 { api, formatBNStr, formatTime, getCustomerAvatar, TxLink, SourceDataViewer } from '@blocklet/payment-react';
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
- -{formatBNStr(data.credit_amount, data.creditGrant?.paymentCurrency?.decimal || 0)}{' '}
163
- {data.creditGrant?.paymentCurrency?.symbol}
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