payment-kit 1.22.30 → 1.22.32

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.
@@ -14,6 +14,29 @@ import { WebhookAttempt } from '../store/models/webhook-attempt';
14
14
  import { WebhookEndpoint } from '../store/models/webhook-endpoint';
15
15
  import { addNotificationJob } from './notification';
16
16
 
17
+ async function decrementPendingWebhooks(event: Event, webhookId: string) {
18
+ const existingSuccessCount = await WebhookAttempt.count({
19
+ where: {
20
+ event_id: event.id,
21
+ webhook_endpoint_id: webhookId,
22
+ status: 'succeeded',
23
+ },
24
+ });
25
+
26
+ if (existingSuccessCount === 0) {
27
+ if (event.pending_webhooks > 0) {
28
+ await event.decrement('pending_webhooks');
29
+ logger.info('pending_webhooks decremented', { eventId: event.id, newCount: event.pending_webhooks, webhookId });
30
+ } else {
31
+ logger.warn('Attempted to decrement pending_webhooks below 0', {
32
+ eventId: event.id,
33
+ currentCount: event.pending_webhooks,
34
+ webhookId,
35
+ });
36
+ }
37
+ }
38
+ }
39
+
17
40
  type WebhookJob = {
18
41
  eventId: string;
19
42
  webhookId: string;
@@ -80,8 +103,7 @@ export const handleWebhook = async (job: WebhookJob) => {
80
103
  });
81
104
  logger.info('WebhookAttempt created successfully', { eventId: event.id, webhookId: webhook.id });
82
105
 
83
- await event.decrement('pending_webhooks');
84
- logger.info('pending_webhooks decremented', { eventId: event.id, newCount: event.pending_webhooks });
106
+ await decrementPendingWebhooks(event, webhook.id);
85
107
 
86
108
  logger.info('webhook attempt success', { ...job, retryCount });
87
109
  } catch (err: any) {
@@ -113,15 +135,10 @@ export const handleWebhook = async (job: WebhookJob) => {
113
135
  addWebhookJob(event.id, webhook.id, {
114
136
  runAt: getNextRetry(retryCount, event.created_at),
115
137
  persist: false,
116
- skipExistCheck: true,
117
138
  });
118
139
  });
119
140
  } else {
120
- await event.decrement('pending_webhooks');
121
- logger.info('Max retries reached, pending_webhooks decremented', {
122
- eventId: event.id,
123
- newCount: event.pending_webhooks,
124
- });
141
+ await decrementPendingWebhooks(event, webhook.id);
125
142
  }
126
143
  }
127
144
  };
@@ -188,18 +205,20 @@ export async function addWebhookJob(
188
205
  options: {
189
206
  runAt?: number;
190
207
  persist?: boolean;
191
- skipExistCheck?: boolean;
208
+ replace?: boolean;
192
209
  } = {}
193
210
  ) {
194
- const { runAt, persist = false, skipExistCheck = false } = options;
211
+ const { runAt, persist = false, replace = true } = options;
195
212
  const jobId = getWebhookJobId(eventId, webhookId);
213
+ const exist = await webhookQueue.get(jobId);
196
214
 
197
- if (!skipExistCheck) {
198
- const exist = await webhookQueue.get(jobId);
199
- if (exist) {
200
- logger.info('Webhook job already exists, skipping', { eventId, webhookId, jobId });
201
- return false;
202
- }
215
+ if (exist && !replace) {
216
+ logger.info('Webhook job already exists, skipping', { eventId, webhookId, jobId });
217
+ return false;
218
+ }
219
+
220
+ if (exist) {
221
+ await webhookQueue.delete(jobId);
203
222
  }
204
223
 
205
224
  webhookQueue.push({
@@ -1,6 +1,6 @@
1
1
  import { Router } from 'express';
2
2
  import Joi from 'joi';
3
- import { Op, QueryTypes } from 'sequelize';
3
+ import { Op, QueryTypes, Sequelize } from 'sequelize';
4
4
 
5
5
  import { fromTokenToUnit } from '@ocap/util';
6
6
  import { createListParamSchema, getOrder, getWhereFromKvQuery, MetadataSchema } from '../libs/api';
@@ -95,7 +95,10 @@ router.get('/', authMine, async (req, res) => {
95
95
  }
96
96
 
97
97
  if (query.customer_id) {
98
- where['payload.customer_id'] = query.customer_id;
98
+ where[Op.and] = where[Op.and] || [];
99
+ where[Op.and].push(
100
+ Sequelize.where(Sequelize.literal("json_extract(payload, '$.customer_id')"), query.customer_id)
101
+ );
99
102
  }
100
103
 
101
104
  if (query.start || query.end) {
@@ -171,7 +174,7 @@ router.get('/stats', authMine, async (req, res) => {
171
174
  };
172
175
 
173
176
  if (customerId) {
174
- whereClause += " AND payload->>'customer_id' = :customerId";
177
+ whereClause += " AND json_extract(payload, '$.customer_id') = :customerId";
175
178
  replacements.customerId = customerId;
176
179
  }
177
180
 
@@ -0,0 +1,129 @@
1
+ /* eslint-disable no-console */
2
+ import { createIndexIfNotExists, type Migration } from '../migrate';
3
+
4
+ export const up: Migration = async ({ context }) => {
5
+ console.log('🚀 Starting database performance optimization...');
6
+
7
+ // meter_events
8
+ await createIndexIfNotExists(context, 'meter_events', ['status', 'created_at'], 'idx_meter_events_status_created');
9
+ await createIndexIfNotExists(
10
+ context,
11
+ 'meter_events',
12
+ ['event_name', 'livemode', 'created_at'],
13
+ 'idx_meter_events_event_livemode_created'
14
+ );
15
+
16
+ await context.sequelize.query(
17
+ "CREATE INDEX IF NOT EXISTS idx_meter_events_customer_status ON meter_events(json_extract(payload, '$.customer_id'), status, livemode, created_at)"
18
+ );
19
+ await context.sequelize.query(
20
+ "CREATE INDEX IF NOT EXISTS idx_meter_events_subscription_status ON meter_events(json_extract(payload, '$.subscription_id'), status, livemode, created_at)"
21
+ );
22
+
23
+ // credit_transactions
24
+ await createIndexIfNotExists(context, 'credit_transactions', ['created_at'], 'idx_credit_transactions_created');
25
+ await createIndexIfNotExists(
26
+ context,
27
+ 'credit_transactions',
28
+ ['customer_id', 'created_at'],
29
+ 'idx_credit_transactions_customer_created'
30
+ );
31
+ await createIndexIfNotExists(
32
+ context,
33
+ 'credit_transactions',
34
+ ['meter_id', 'created_at'],
35
+ 'idx_credit_transactions_meter_created'
36
+ );
37
+ await createIndexIfNotExists(
38
+ context,
39
+ 'credit_transactions',
40
+ ['meter_event_name', 'created_at'],
41
+ 'idx_credit_transactions_event_created'
42
+ );
43
+ await createIndexIfNotExists(
44
+ context,
45
+ 'credit_transactions',
46
+ ['subscription_id', 'created_at'],
47
+ 'idx_credit_transactions_subscription_created'
48
+ );
49
+
50
+ // credit_grants
51
+ await createIndexIfNotExists(
52
+ context,
53
+ 'credit_grants',
54
+ ['customer_id', 'currency_id', 'status'],
55
+ 'idx_credit_grants_customer_currency_status'
56
+ );
57
+ await createIndexIfNotExists(context, 'credit_grants', ['customer_id'], 'idx_credit_grants_customer');
58
+ await createIndexIfNotExists(context, 'credit_grants', ['currency_id'], 'idx_credit_grants_currency');
59
+ await createIndexIfNotExists(context, 'credit_grants', ['status'], 'idx_credit_grants_status');
60
+
61
+ // customers
62
+ await createIndexIfNotExists(context, 'customers', ['did'], 'idx_customers_did');
63
+
64
+ // subscriptions
65
+ await createIndexIfNotExists(context, 'subscriptions', ['customer_id'], 'idx_subscriptions_customer_id');
66
+
67
+ // meters
68
+ await createIndexIfNotExists(context, 'meters', ['livemode', 'status'], 'idx_meters_livemode_status');
69
+
70
+ // events
71
+ await createIndexIfNotExists(context, 'events', ['type', 'created_at'], 'idx_events_type_created');
72
+ await createIndexIfNotExists(
73
+ context,
74
+ 'events',
75
+ ['object_type', 'object_id', 'created_at'],
76
+ 'idx_events_object_type_id_created'
77
+ );
78
+ await createIndexIfNotExists(context, 'events', ['object_id', 'created_at'], 'idx_events_object_id_created');
79
+
80
+ // jobs
81
+ await createIndexIfNotExists(context, 'jobs', ['queue', 'will_run_at'], 'idx_jobs_queue_run_at');
82
+ await createIndexIfNotExists(context, 'jobs', ['cancelled', 'will_run_at'], 'idx_jobs_cancelled_run_at');
83
+
84
+ // invoices
85
+ await createIndexIfNotExists(context, 'invoices', ['status'], 'idx_invoices_status');
86
+
87
+ // checkout_sessions
88
+ await createIndexIfNotExists(
89
+ context,
90
+ 'checkout_sessions',
91
+ ['payment_link_id', 'status', 'livemode'],
92
+ 'idx_checkout_sessions_payment_link_status_livemode'
93
+ );
94
+
95
+ console.log('✅ Performance optimization completed');
96
+ };
97
+
98
+ export const down: Migration = async ({ context }) => {
99
+ console.log('🔄 Rolling back performance optimization...');
100
+
101
+ await context.removeIndex('meter_events', 'idx_meter_events_status_created');
102
+ await context.removeIndex('meter_events', 'idx_meter_events_event_livemode_created');
103
+ await context.removeIndex('meter_events', 'idx_meter_events_customer_status');
104
+ await context.removeIndex('meter_events', 'idx_meter_events_subscription_status');
105
+
106
+ await context.removeIndex('credit_transactions', 'idx_credit_transactions_created');
107
+ await context.removeIndex('credit_transactions', 'idx_credit_transactions_customer_created');
108
+ await context.removeIndex('credit_transactions', 'idx_credit_transactions_meter_created');
109
+ await context.removeIndex('credit_transactions', 'idx_credit_transactions_event_created');
110
+ await context.removeIndex('credit_transactions', 'idx_credit_transactions_subscription_created');
111
+
112
+ await context.removeIndex('credit_grants', 'idx_credit_grants_customer_currency_status');
113
+ await context.removeIndex('credit_grants', 'idx_credit_grants_customer');
114
+ await context.removeIndex('credit_grants', 'idx_credit_grants_currency');
115
+ await context.removeIndex('credit_grants', 'idx_credit_grants_status');
116
+
117
+ await context.removeIndex('customers', 'idx_customers_did');
118
+ await context.removeIndex('subscriptions', 'idx_subscriptions_customer_id');
119
+ await context.removeIndex('meters', 'idx_meters_livemode_status');
120
+ await context.removeIndex('events', 'idx_events_type_created');
121
+ await context.removeIndex('events', 'idx_events_object_type_id_created');
122
+ await context.removeIndex('events', 'idx_events_object_id_created');
123
+ await context.removeIndex('jobs', 'idx_jobs_queue_run_at');
124
+ await context.removeIndex('jobs', 'idx_jobs_cancelled_run_at');
125
+ await context.removeIndex('invoices', 'idx_invoices_status');
126
+ await context.removeIndex('checkout_sessions', 'idx_checkout_sessions_payment_link_status_livemode');
127
+
128
+ console.log('✅ Rollback completed');
129
+ };
@@ -427,9 +427,18 @@ export class CreditGrant extends Model<InferAttributes<CreditGrant>, InferCreati
427
427
  targetCurrencyIds = grantsWithCurrency.map((grant: any) => grant.currency_id);
428
428
  }
429
429
 
430
+ const currencies = await PaymentCurrency.scope('withRechargeConfig').findAll({
431
+ where: {
432
+ id: {
433
+ [Op.in]: Array.from(new Set(targetCurrencyIds)),
434
+ },
435
+ },
436
+ });
437
+ const currencyMap = new Map(currencies.map((c) => [c.id, c]));
438
+
430
439
  await Promise.all(
431
440
  targetCurrencyIds.map(async (currencyId: string) => {
432
- const paymentCurrency = await PaymentCurrency.scope('withRechargeConfig').findByPk(currencyId);
441
+ const paymentCurrency = currencyMap.get(currencyId);
433
442
  if (!paymentCurrency) {
434
443
  return null;
435
444
  }
@@ -7,6 +7,7 @@ import {
7
7
  Model,
8
8
  Op,
9
9
  QueryTypes,
10
+ Sequelize,
10
11
  WhereOptions,
11
12
  } from 'sequelize';
12
13
  import type { LiteralUnion } from 'type-fest';
@@ -372,27 +373,52 @@ export class MeterEvent extends Model<InferAttributes<MeterEvent>, InferCreation
372
373
  const summary: GroupedBN = {};
373
374
  const detail: GroupedStrList = {};
374
375
 
375
- await Promise.all(
376
- events.map(async (event) => {
377
- const meter = await Meter.getMeterByEventName(event.event_name);
378
- if (!meter) {
379
- return;
380
- }
381
- const paymentCurrency = await PaymentCurrency.findByPk(meter.currency_id);
382
- if (!paymentCurrency) {
383
- return;
384
- }
385
- const currencyId = meter.currency_id as string;
386
- if (searchCurrencyId && searchCurrencyId !== currencyId) {
387
- return;
388
- }
389
- if (!detail[currencyId]) {
390
- detail[currencyId] = [];
391
- }
392
- summary[currencyId] = new BN(summary[currencyId] || '0').add(new BN(event.credit_pending)).toString();
393
- detail[currencyId]!.push(event.id);
394
- })
395
- );
376
+ if (events.length === 0) {
377
+ return [summary, detail, events];
378
+ }
379
+
380
+ const eventNames = [...new Set(events.map((e) => e.event_name))];
381
+ const meters = await Meter.findAll({
382
+ where: {
383
+ event_name: {
384
+ [Op.in]: eventNames,
385
+ },
386
+ },
387
+ });
388
+ const meterMap = new Map(meters.map((m) => [m.event_name, m]));
389
+
390
+ const currencyIds = [...new Set(meters.map((m) => m.currency_id).filter((id): id is string => Boolean(id)))];
391
+ const currencies =
392
+ currencyIds.length > 0
393
+ ? await PaymentCurrency.findAll({
394
+ where: {
395
+ id: {
396
+ [Op.in]: currencyIds,
397
+ },
398
+ },
399
+ })
400
+ : [];
401
+ const currencyMap = new Map(currencies.map((c) => [c.id, c]));
402
+
403
+ events.forEach((event) => {
404
+ const meter = meterMap.get(event.event_name);
405
+ if (!meter || !meter.currency_id) {
406
+ return;
407
+ }
408
+ const paymentCurrency = currencyMap.get(meter.currency_id);
409
+ if (!paymentCurrency) {
410
+ return;
411
+ }
412
+ const currencyId = meter.currency_id as string;
413
+ if (searchCurrencyId && searchCurrencyId !== currencyId) {
414
+ return;
415
+ }
416
+ if (!detail[currencyId]) {
417
+ detail[currencyId] = [];
418
+ }
419
+ summary[currencyId] = new BN(summary[currencyId] || '0').add(new BN(event.credit_pending)).toString();
420
+ detail[currencyId]!.push(event.id);
421
+ });
396
422
 
397
423
  return [summary, detail, events];
398
424
  }
@@ -419,11 +445,16 @@ export class MeterEvent extends Model<InferAttributes<MeterEvent>, InferCreation
419
445
  if (status) {
420
446
  where.status = Array.isArray(status) ? { [Op.in]: status } : status;
421
447
  }
448
+
422
449
  if (subscriptionId) {
423
- where['payload.subscription_id'] = subscriptionId;
450
+ where[Op.and] = where[Op.and] || [];
451
+ where[Op.and].push(
452
+ Sequelize.where(Sequelize.literal("json_extract(payload, '$.subscription_id')"), subscriptionId)
453
+ );
424
454
  }
425
455
  if (customerId) {
426
- where['payload.customer_id'] = customerId;
456
+ where[Op.and] = where[Op.and] || [];
457
+ where[Op.and].push(Sequelize.where(Sequelize.literal("json_extract(payload, '$.customer_id')"), customerId));
427
458
  }
428
459
 
429
460
  return this._getPendingAmounts(where, currencyId);
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.22.30
17
+ version: 1.22.32
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.22.30",
3
+ "version": "1.22.32",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
@@ -44,35 +44,35 @@
44
44
  ]
45
45
  },
46
46
  "dependencies": {
47
- "@abtnode/cron": "^1.17.4-beta-20251204-152224-243ff54f",
48
- "@arcblock/did": "^1.27.13",
47
+ "@abtnode/cron": "^1.17.4",
48
+ "@arcblock/did": "^1.27.14",
49
49
  "@arcblock/did-connect-react": "^3.2.11",
50
50
  "@arcblock/did-connect-storage-nedb": "^1.8.0",
51
- "@arcblock/did-util": "^1.27.13",
52
- "@arcblock/jwt": "^1.27.13",
51
+ "@arcblock/did-util": "^1.27.14",
52
+ "@arcblock/jwt": "^1.27.14",
53
53
  "@arcblock/react-hooks": "^3.2.11",
54
54
  "@arcblock/ux": "^3.2.11",
55
- "@arcblock/validator": "^1.27.13",
56
- "@blocklet/did-space-js": "^1.2.8",
55
+ "@arcblock/validator": "^1.27.14",
56
+ "@blocklet/did-space-js": "^1.2.9",
57
57
  "@blocklet/error": "^0.3.4",
58
- "@blocklet/js-sdk": "^1.17.4-beta-20251204-152224-243ff54f",
59
- "@blocklet/logger": "^1.17.4-beta-20251204-152224-243ff54f",
60
- "@blocklet/payment-broker-client": "1.22.30",
61
- "@blocklet/payment-react": "1.22.30",
62
- "@blocklet/payment-vendor": "1.22.30",
63
- "@blocklet/sdk": "^1.17.4-beta-20251204-152224-243ff54f",
58
+ "@blocklet/js-sdk": "^1.17.4",
59
+ "@blocklet/logger": "^1.17.4",
60
+ "@blocklet/payment-broker-client": "1.22.32",
61
+ "@blocklet/payment-react": "1.22.32",
62
+ "@blocklet/payment-vendor": "1.22.32",
63
+ "@blocklet/sdk": "^1.17.4",
64
64
  "@blocklet/ui-react": "^3.2.11",
65
- "@blocklet/uploader": "^0.3.13",
66
- "@blocklet/xss": "^0.3.11",
65
+ "@blocklet/uploader": "^0.3.14",
66
+ "@blocklet/xss": "^0.3.12",
67
67
  "@mui/icons-material": "^7.1.2",
68
68
  "@mui/lab": "7.0.0-beta.14",
69
69
  "@mui/material": "^7.1.2",
70
70
  "@mui/system": "^7.1.1",
71
- "@ocap/asset": "^1.27.13",
72
- "@ocap/client": "^1.27.13",
73
- "@ocap/mcrypto": "^1.27.13",
74
- "@ocap/util": "^1.27.13",
75
- "@ocap/wallet": "^1.27.13",
71
+ "@ocap/asset": "^1.27.14",
72
+ "@ocap/client": "^1.27.14",
73
+ "@ocap/mcrypto": "^1.27.14",
74
+ "@ocap/util": "^1.27.14",
75
+ "@ocap/wallet": "^1.27.14",
76
76
  "@stripe/react-stripe-js": "^2.9.0",
77
77
  "@stripe/stripe-js": "^2.4.0",
78
78
  "ahooks": "^3.8.5",
@@ -127,9 +127,9 @@
127
127
  "web3": "^4.16.0"
128
128
  },
129
129
  "devDependencies": {
130
- "@abtnode/types": "^1.17.4-beta-20251204-152224-243ff54f",
130
+ "@abtnode/types": "^1.17.4",
131
131
  "@arcblock/eslint-config-ts": "^0.3.3",
132
- "@blocklet/payment-types": "1.22.30",
132
+ "@blocklet/payment-types": "1.22.32",
133
133
  "@types/cookie-parser": "^1.4.9",
134
134
  "@types/cors": "^2.8.19",
135
135
  "@types/debug": "^4.1.12",
@@ -176,5 +176,5 @@
176
176
  "parser": "typescript"
177
177
  }
178
178
  },
179
- "gitHead": "78cac71748e6836069582ed293945b41b7c7af66"
179
+ "gitHead": "4b5eac2af9a6a10a4f971f7dcbe220e8049d0cc7"
180
180
  }
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable react/no-unstable-nested-components */
2
2
  import CodeBlock from '@arcblock/ux/lib/CodeBlock';
3
3
  import Toast from '@arcblock/ux/lib/Toast';
4
- import { api, formatTime } from '@blocklet/payment-react';
4
+ import { api, formatTime, ConfirmDialog } from '@blocklet/payment-react';
5
5
  import type { Paginated, TEvent, TWebhookAttemptExpanded } from '@blocklet/payment-types';
6
6
  import { CheckCircleOutlined, ErrorOutlined, RefreshOutlined } from '@mui/icons-material';
7
7
  import {
@@ -18,7 +18,8 @@ import {
18
18
  Stack,
19
19
  Typography,
20
20
  } from '@mui/material';
21
- import { useInfiniteScroll, useRequest } from 'ahooks';
21
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
22
+ import { useInfiniteScroll, useRequest, useSetState } from 'ahooks';
22
23
  import React, { useEffect, useState } from 'react';
23
24
 
24
25
  import { isEmpty } from 'lodash';
@@ -52,6 +53,7 @@ type Props = {
52
53
  };
53
54
 
54
55
  export default function WebhookAttempts({ event_id = '', webhook_endpoint_id = '', event = undefined }: Props) {
56
+ const { t } = useLocaleContext();
55
57
  const { data, loadMore, loadingMore, loading, reload } = useInfiniteScroll<Paginated<TWebhookAttemptExpanded>>(
56
58
  (d) => {
57
59
  const size = 15;
@@ -69,6 +71,11 @@ export default function WebhookAttempts({ event_id = '', webhook_endpoint_id = '
69
71
  const [selected, setSelected] = useState<
70
72
  (TWebhookAttemptExpanded & { event: TEvent & { requestInfo?: RequestInfo } }) | null
71
73
  >(null);
74
+
75
+ const [state, setState] = useSetState({
76
+ retryEventId: '',
77
+ });
78
+
72
79
  const groupedAttempts = groupAttemptsByDate(attempts);
73
80
 
74
81
  const { loading: retrying, run: retryWebhook } = useRequest(
@@ -76,11 +83,11 @@ export default function WebhookAttempts({ event_id = '', webhook_endpoint_id = '
76
83
  {
77
84
  manual: true,
78
85
  onSuccess: (result) => {
79
- Toast.success(result.message || 'Webhook scheduled for retry');
86
+ Toast.success(result.message || t('admin.event.retryOptions.success'));
80
87
  reload();
81
88
  },
82
89
  onError: (err: any) => {
83
- Toast.error(err.response?.data?.error || 'Failed to retry webhook');
90
+ Toast.error(err.response?.data?.error || t('admin.event.retryOptions.error'));
84
91
  },
85
92
  }
86
93
  );
@@ -95,6 +102,17 @@ export default function WebhookAttempts({ event_id = '', webhook_endpoint_id = '
95
102
  setSelected(attempt);
96
103
  };
97
104
 
105
+ const handleRetryClick = (eventId: string) => {
106
+ setState({ retryEventId: eventId });
107
+ };
108
+
109
+ const handleConfirmRetry = () => {
110
+ if (state.retryEventId) {
111
+ retryWebhook(state.retryEventId);
112
+ setState({ retryEventId: '' });
113
+ }
114
+ };
115
+
98
116
  if (loading) {
99
117
  return <CircularProgress />;
100
118
  }
@@ -109,7 +127,7 @@ export default function WebhookAttempts({ event_id = '', webhook_endpoint_id = '
109
127
  sx={{
110
128
  color: 'text.secondary',
111
129
  }}>
112
- No Attempt
130
+ {t('admin.event.noAttempts')}
113
131
  </Typography>
114
132
  ) : (
115
133
  <>
@@ -155,7 +173,7 @@ export default function WebhookAttempts({ event_id = '', webhook_endpoint_id = '
155
173
  </List>
156
174
  {hasMore && (
157
175
  <Button variant="text" type="button" color="inherit" onClick={loadMore} disabled={loadingMore}>
158
- {loadingMore ? 'Loading more...' : 'Load more'}
176
+ {loadingMore ? t('common.loading') : t('common.loadMore')}
159
177
  </Button>
160
178
  )}
161
179
  {!hasMore && (
@@ -163,7 +181,7 @@ export default function WebhookAttempts({ event_id = '', webhook_endpoint_id = '
163
181
  sx={{
164
182
  color: 'text.secondary',
165
183
  }}>
166
- No more data
184
+ {t('common.noMoreData')}
167
185
  </Typography>
168
186
  )}
169
187
  </>
@@ -182,13 +200,25 @@ export default function WebhookAttempts({ event_id = '', webhook_endpoint_id = '
182
200
  variant="outlined"
183
201
  size="small"
184
202
  startIcon={<RefreshOutlined />}
185
- onClick={() => retryWebhook(selected.event_id)}
203
+ onClick={() => handleRetryClick(selected.event_id)}
186
204
  disabled={retrying}>
187
- {retrying ? 'Retrying...' : 'Retry'}
205
+ {retrying ? t('admin.event.retrying') : t('admin.event.retry')}
188
206
  </Button>
189
207
  </Stack>
208
+ {state.retryEventId && (
209
+ <ConfirmDialog
210
+ onConfirm={handleConfirmRetry}
211
+ onCancel={() => setState({ retryEventId: '' })}
212
+ title={t('admin.event.retryOptions.title')}
213
+ message={t('admin.event.retryOptions.confirmMessage')}
214
+ loading={retrying}
215
+ color="primary"
216
+ />
217
+ )}
190
218
  <Box>
191
- <Typography variant="h6">Response ({selected.response_status})</Typography>
219
+ <Typography variant="h6">
220
+ {t('admin.event.response')} ({selected.response_status})
221
+ </Typography>
192
222
  {/* @ts-ignore */}
193
223
  <CodeBlock language="json">{JSON.stringify(selected.response_body, null, 2)}</CodeBlock>
194
224
  </Box>
@@ -199,7 +229,7 @@ export default function WebhookAttempts({ event_id = '', webhook_endpoint_id = '
199
229
  sx={{
200
230
  alignItems: 'center',
201
231
  }}>
202
- <Typography variant="h6">Request</Typography>
232
+ <Typography variant="h6">{t('admin.event.request')}</Typography>
203
233
  <RequestInfoPopper
204
234
  // @ts-ignore
205
235
  requestInfo={selected?.event?.requestInfo}
@@ -219,7 +249,7 @@ export default function WebhookAttempts({ event_id = '', webhook_endpoint_id = '
219
249
  sx={{
220
250
  alignItems: 'center',
221
251
  }}>
222
- <Typography variant="h6">Event Data</Typography>
252
+ <Typography variant="h6">{t('admin.event.eventData')}</Typography>
223
253
  <RequestInfoPopper
224
254
  // @ts-ignore
225
255
  requestInfo={event.requestInfo}
@@ -31,6 +31,8 @@ export default flat({
31
31
  latinOnly:
32
32
  'Must contain at least one letter and cannot include Chinese characters or special characters such as <, >, ", \' or \\',
33
33
  loading: 'Loading...',
34
+ loadMore: 'Load more',
35
+ noMoreData: 'No more data',
34
36
  rechargeTime: 'Recharge Time',
35
37
  submit: 'Submit',
36
38
  custom: 'Custom',
@@ -1217,6 +1219,19 @@ export default flat({
1217
1219
  webhooks: 'Webhook Attempts',
1218
1220
  type: 'Type',
1219
1221
  pendingWebhooks: 'Pending Webhooks',
1222
+ noAttempts: 'No Attempt',
1223
+ retry: 'Retry',
1224
+ retrying: 'Retrying...',
1225
+ retryOptions: {
1226
+ title: 'Retry Webhook',
1227
+ confirmMessage:
1228
+ 'Are you sure you want to retry this webhook? This will send another request to the webhook endpoint.',
1229
+ success: 'Webhook scheduled for retry',
1230
+ error: 'Failed to retry webhook',
1231
+ },
1232
+ response: 'Response',
1233
+ request: 'Request',
1234
+ eventData: 'Event Data',
1220
1235
  },
1221
1236
  invoice: {
1222
1237
  view: 'View invoice',
@@ -29,6 +29,8 @@ export default flat({
29
29
  invalidCharacters: '无效字符',
30
30
  latinOnly: '至少包含一个字母,并且不能包含中文字符和特殊字符如 <, >、"、’ 或 \\',
31
31
  loading: '加载中...',
32
+ loadMore: '加载更多',
33
+ noMoreData: '没有更多数据',
32
34
  rechargeTime: '充值时间',
33
35
  submit: '提交',
34
36
  custom: '自定义',
@@ -1171,6 +1173,18 @@ export default flat({
1171
1173
  webhooks: '钩子调用',
1172
1174
  type: '类型',
1173
1175
  pendingWebhooks: '待处理的钩子',
1176
+ noAttempts: '无调用记录',
1177
+ retry: '重试',
1178
+ retrying: '重试中...',
1179
+ retryOptions: {
1180
+ title: '重试 Webhook',
1181
+ confirmMessage: '确定要重试此 webhook 吗?这将向 webhook 端点发送另一个请求。',
1182
+ success: 'Webhook 已安排重试',
1183
+ error: '重试 webhook 失败',
1184
+ },
1185
+ response: '响应',
1186
+ request: '请求',
1187
+ eventData: '事件数据',
1174
1188
  events: {
1175
1189
  dailyEventCount: '每日事件数量',
1176
1190
  dailyTotalValue: '每日总使用量',