payment-kit 1.21.14 → 1.21.16

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.
@@ -4,6 +4,8 @@ import { VendorFulfillmentService } from '../../libs/vendor-util/fulfillment';
4
4
  import { CheckoutSession } from '../../store/models';
5
5
  import { VendorInfo } from './fulfillment-coordinator';
6
6
 
7
+ export const MAX_RETURN_RETRY = 3;
8
+
7
9
  type ReturnProcessorJob = {
8
10
  checkoutSessionId: string;
9
11
  };
@@ -39,13 +41,14 @@ async function handleReturnProcessorJob(job: ReturnProcessorJob): Promise<void>
39
41
  let i = -1;
40
42
  for (const vendor of vendorInfoList) {
41
43
  i++;
42
- // Only process vendors with 'completed' status
43
- if (vendor.status !== 'completed') {
44
- logger.info('Skipping vendor return because status is not completed', {
44
+ const returnRetry = vendor.returnRetry ? vendor.returnRetry + 1 : 1;
45
+ if (vendor.status === 'returned') {
46
+ logger.info('Skipping vendor return because status is returned', {
45
47
  checkoutSessionId,
46
48
  vendorId: vendor.vendor_id,
47
49
  orderId: vendor.order_id,
48
50
  status: vendor.status,
51
+ returnRetry,
49
52
  });
50
53
  // eslint-disable-next-line no-continue
51
54
  continue;
@@ -56,53 +59,51 @@ async function handleReturnProcessorJob(job: ReturnProcessorJob): Promise<void>
56
59
  checkoutSessionId,
57
60
  vendorId: vendor.vendor_id,
58
61
  orderId: vendor.order_id,
62
+ returnRetry,
59
63
  });
60
64
 
61
65
  // eslint-disable-next-line no-await-in-loop
62
- const returnResult = await callVendorReturn(vendor, checkoutSession);
63
-
64
- if (returnResult.success) {
65
- // Return successful, update status to 'returned'
66
- vendorInfoList[i] = {
67
- ...vendor,
68
- status: 'returned',
69
- lastAttemptAt: new Date().toISOString(),
70
- };
71
- hasChanges = true;
72
-
73
- logger.info('Vendor return successful', {
74
- checkoutSessionId,
75
- vendorId: vendor.vendor_id,
76
- orderId: vendor.order_id,
77
- });
78
- } else {
79
- // Return failed, keep 'completed' status for next scan retry
80
- vendorInfoList[i] = {
81
- ...vendor,
82
- lastAttemptAt: new Date().toISOString(),
83
- error_message: returnResult.message || 'Return request failed',
84
- };
85
-
86
- logger.warn('Vendor return failed', {
87
- checkoutSessionId,
88
- vendorId: vendor.vendor_id,
89
- orderId: vendor.order_id,
90
- error: returnResult.message,
91
- });
92
- }
66
+ await callVendorReturn(vendor, checkoutSession);
67
+
68
+ // Return successful, update status to 'returned'
69
+ vendorInfoList[i] = {
70
+ ...vendor,
71
+ status: 'returned',
72
+ lastAttemptAt: new Date().toISOString(),
73
+ };
74
+ hasChanges = true;
75
+
76
+ logger.info('Vendor return successful', {
77
+ checkoutSessionId,
78
+ vendorId: vendor.vendor_id,
79
+ orderId: vendor.order_id,
80
+ returnRetry,
81
+ });
93
82
  } catch (error: any) {
94
83
  logger.error('Error processing vendor return', {
95
84
  checkoutSessionId,
96
85
  vendorId: vendor.vendor_id,
97
86
  orderId: vendor.order_id,
98
- error: error.message,
87
+ error,
88
+ returnRetry,
99
89
  });
100
90
 
91
+ if (returnRetry >= MAX_RETURN_RETRY) {
92
+ logger.warn('Skipping vendor return because return retry is greater than 5', {
93
+ checkoutSessionId,
94
+ vendorId: vendor.vendor_id,
95
+ orderId: vendor.order_id,
96
+ returnRetry,
97
+ });
98
+ }
99
+
101
100
  // Record error but keep status unchanged for retry
102
101
  vendorInfoList[i] = {
103
102
  ...vendor,
103
+ status: returnRetry >= MAX_RETURN_RETRY ? 'returned' : vendor.status,
104
104
  lastAttemptAt: new Date().toISOString(),
105
105
  error_message: error.message,
106
+ returnRetry,
106
107
  };
107
108
  hasChanges = true;
108
109
  }
@@ -110,14 +111,14 @@ async function handleReturnProcessorJob(job: ReturnProcessorJob): Promise<void>
110
111
 
111
112
  // Update vendor_info if there are changes
112
113
  if (hasChanges) {
113
- await checkoutSession.update({ vendor_info: vendorInfoList });
114
+ await CheckoutSession.update({ vendor_info: vendorInfoList }, { where: { id: checkoutSessionId } });
114
115
  }
115
116
 
116
117
  // Check if all vendors have been returned
117
118
  const allReturned = vendorInfoList.every((vendor) => vendor.status === 'returned');
118
119
 
119
120
  if (allReturned && checkoutSession.fulfillment_status !== 'returned') {
120
- await checkoutSession.update({ fulfillment_status: 'returned' });
121
+ await CheckoutSession.update({ fulfillment_status: 'returned' }, { where: { id: checkoutSessionId } });
121
122
 
122
123
  logger.info('All vendors returned, updated fulfillment status to returned', {
123
124
  checkoutSessionId,
@@ -140,44 +141,20 @@ async function handleReturnProcessorJob(job: ReturnProcessorJob): Promise<void>
140
141
  }
141
142
  }
142
143
 
143
- async function callVendorReturn(
144
- vendor: VendorInfo,
145
- checkoutSession: CheckoutSession
146
- ): Promise<{ success: boolean; message?: string }> {
147
- try {
148
- const vendorAdapter = await VendorFulfillmentService.getVendorAdapter(vendor.vendor_key);
149
-
150
- if (!vendorAdapter) {
151
- return {
152
- success: false,
153
- message: `No adapter found for vendor: ${vendor.vendor_id}`,
154
- };
155
- }
144
+ async function callVendorReturn(vendor: VendorInfo, checkoutSession: CheckoutSession) {
145
+ const vendorAdapter = await VendorFulfillmentService.getVendorAdapter(vendor.vendor_key);
156
146
 
157
- const returnResult = await vendorAdapter.requestReturn({
158
- orderId: vendor.order_id,
159
- reason: 'Subscription canceled',
160
- customParams: {
161
- checkoutSessionId: checkoutSession.id,
162
- subscriptionId: checkoutSession.subscription_id,
163
- vendorKey: vendor.vendor_key,
164
- },
165
- });
166
-
167
- return {
168
- success: returnResult.success || false,
169
- message: returnResult.message,
170
- };
171
- } catch (error: any) {
172
- logger.error('Failed to call vendor return API', {
173
- vendorId: vendor.vendor_id,
174
- orderId: vendor.order_id,
175
- error: error.message,
176
- });
177
-
178
- return {
179
- success: false,
180
- message: error.message,
181
- };
147
+ if (!vendorAdapter) {
148
+ throw new Error(`No adapter found for vendor: ${vendor.vendor_id}`);
182
149
  }
150
+
151
+ return vendorAdapter.requestReturn({
152
+ orderId: vendor.order_id,
153
+ reason: 'Subscription canceled',
154
+ customParams: {
155
+ checkoutSessionId: checkoutSession.id,
156
+ subscriptionId: checkoutSession.subscription_id,
157
+ vendorKey: vendor.vendor_key,
158
+ },
159
+ });
183
160
  }
@@ -1,9 +1,11 @@
1
1
  import { Op } from 'sequelize';
2
+ import dayjs from 'dayjs';
2
3
  import logger from '../../libs/logger';
3
4
  import createQueue from '../../libs/queue';
4
5
  import { CheckoutSession, Subscription } from '../../store/models';
5
6
  import { vendorReturnProcessorQueue } from './return-processor';
6
7
  import { VendorInfo } from './fulfillment-coordinator';
8
+ import { events } from '../../libs/event';
7
9
 
8
10
  export const vendorReturnScannerQueue = createQueue({
9
11
  name: 'vendor-return-scanner',
@@ -52,8 +54,10 @@ async function handleReturnScannerJob(): Promise<void> {
52
54
  async function findSessionsNeedingVendorReturn(): Promise<CheckoutSession[]> {
53
55
  try {
54
56
  // First, find canceled subscriptions
57
+ const oneWeekAgo = dayjs().subtract(7, 'day').unix();
58
+
55
59
  const canceledSubscriptions = await Subscription.findAll({
56
- where: { status: 'canceled' },
60
+ where: { status: 'canceled', canceled_at: { [Op.gt]: oneWeekAgo } },
57
61
  attributes: ['id'],
58
62
  });
59
63
 
@@ -62,7 +66,7 @@ async function findSessionsNeedingVendorReturn(): Promise<CheckoutSession[]> {
62
66
  // Find checkout sessions with completed fulfillment and canceled subscriptions
63
67
  const readyToReturnSessions = await CheckoutSession.findAll({
64
68
  where: {
65
- fulfillment_status: 'completed',
69
+ fulfillment_status: { [Op.notIn]: ['returning', 'returned', 'failed'] },
66
70
  subscription_id: { [Op.in]: canceledSubscriptionIds },
67
71
  },
68
72
  order: [['updated_at', 'DESC']],
@@ -102,7 +106,10 @@ async function findSessionsNeedingVendorReturn(): Promise<CheckoutSession[]> {
102
106
  if (!vendorInfoList || vendorInfoList.length === 0) {
103
107
  return false;
104
108
  }
105
- const hasVendorNeedingReturn = vendorInfoList.some((vendor) => vendor.status === 'completed');
109
+
110
+ const hasVendorNeedingReturn = vendorInfoList.some(
111
+ (vendor) => !['cancelled', 'return_requested', 'returned'].includes(vendor.status)
112
+ );
106
113
  return hasVendorNeedingReturn;
107
114
  });
108
115
 
@@ -117,3 +124,31 @@ export function scheduleVendorReturnScan(): void {
117
124
  const scanId = `scan-${Date.now()}`;
118
125
  vendorReturnScannerQueue.push({ id: scanId, job: {} });
119
126
  }
127
+
128
+ events.on('customer.subscription.deleted', async (subscription: Subscription) => {
129
+ logger.info('Customer subscription deleted', { subscription });
130
+ if (subscription.status !== 'canceled') {
131
+ logger.info('Subscription is not canceled, skipping vendor return process[customer.subscription.deleted]', {
132
+ subscriptionId: subscription.id,
133
+ });
134
+ return;
135
+ }
136
+
137
+ const session = await CheckoutSession.findOne({
138
+ where: { subscription_id: subscription.id },
139
+ });
140
+
141
+ if (session) {
142
+ const id = `vendor-return-process-${session.id}`;
143
+ // eslint-disable-next-line no-await-in-loop
144
+ const exists = await vendorReturnProcessorQueue.get(id);
145
+ if (!exists) {
146
+ vendorReturnProcessorQueue.push({
147
+ id,
148
+ job: {
149
+ checkoutSessionId: session.id,
150
+ },
151
+ });
152
+ }
153
+ }
154
+ });
@@ -5,6 +5,7 @@ import Joi from 'joi';
5
5
 
6
6
  // eslint-disable-next-line import/no-extraneous-dependencies
7
7
  import { gte } from 'semver';
8
+ import { Op } from 'sequelize';
8
9
  import { MetadataSchema } from '../libs/api';
9
10
  import { wallet } from '../libs/auth';
10
11
  import dayjs from '../libs/dayjs';
@@ -31,19 +32,15 @@ const createVendorSchema = Joi.object({
31
32
  name: Joi.string().max(255).required(),
32
33
  description: Joi.string().max(1000).allow('').optional(),
33
34
  app_url: Joi.string().uri().max(512).required(),
34
- app_pid: Joi.string().max(255).allow('').optional(),
35
- app_logo: Joi.string().max(512).allow('').optional(),
36
35
  status: Joi.string().valid('active', 'inactive').default('active'),
37
36
  metadata: MetadataSchema,
38
- }).unknown(false);
37
+ }).unknown(true);
39
38
 
40
39
  const updateVendorSchema = Joi.object({
41
40
  vendor_type: Joi.string().valid('launcher', 'didnames').optional(),
42
41
  name: Joi.string().max(255).optional(),
43
42
  description: Joi.string().max(1000).allow('').optional(),
44
43
  app_url: Joi.string().uri().max(512).optional(),
45
- app_pid: Joi.string().max(255).allow('').optional(),
46
- app_logo: Joi.string().max(512).allow('').optional(),
47
44
  status: Joi.string().valid('active', 'inactive').optional(),
48
45
  metadata: MetadataSchema,
49
46
  }).unknown(true);
@@ -56,6 +53,10 @@ const sessionIdParamSchema = Joi.object({
56
53
  sessionId: Joi.string().max(100).required(),
57
54
  });
58
55
 
56
+ const sessionIdsParamSchema = Joi.object({
57
+ sessionIds: Joi.array().items(Joi.string().max(100)).required(),
58
+ });
59
+
59
60
  const subscriptionIdParamSchema = Joi.object({
60
61
  subscriptionId: Joi.string().max(100).required(),
61
62
  });
@@ -134,6 +135,51 @@ async function getVendorInfo(req: any, res: any) {
134
135
  }
135
136
  }
136
137
 
138
+ async function prepareVendorData(appUrlInput: string, vendorType: 'launcher' | 'didnames', metadata: any = {}) {
139
+ let appUrl = '';
140
+ let blockletJson = null;
141
+ try {
142
+ appUrl = new URL(appUrlInput).origin;
143
+ blockletJson = await getBlockletJson(appUrl);
144
+ } catch (error) {
145
+ logger.error('Failed to get blocklet json', {
146
+ appUrlInput,
147
+ error,
148
+ });
149
+ return { error: `Invalid app URL: ${appUrlInput}, get blocklet json failed` as const };
150
+ }
151
+
152
+ if (!blockletJson?.appId || !blockletJson?.appPk) {
153
+ return { error: `Invalid app URL: ${appUrl}, the appId or appPk is required in the target app` as const };
154
+ }
155
+
156
+ const vendorDid = VENDOR_DID[vendorType];
157
+ const component = blockletJson?.componentMountPoints?.find((item: any) => item.did === vendorDid);
158
+
159
+ if (!component) {
160
+ return { error: `Invalid app URL: ${appUrl}, the ${vendorType} did is not found in the target server` as const };
161
+ }
162
+
163
+ const mountPoint = component.mountPoint || '/';
164
+ return {
165
+ vendor_did: vendorDid,
166
+ app_url: appUrl,
167
+ // Both appPid and appId can be used here for transfer purposes, with did being recommended.
168
+ // Keeping appPid for now due to extensive changes required
169
+ app_pid: blockletJson.appId,
170
+ app_logo: blockletJson.appLogo,
171
+ metadata: {
172
+ ...metadata,
173
+ mountPoint,
174
+ },
175
+ extends: {
176
+ mountPoint,
177
+ appId: blockletJson.appId,
178
+ appPk: blockletJson.appPk,
179
+ },
180
+ };
181
+ }
182
+
137
183
  async function createVendor(req: any, res: any) {
138
184
  try {
139
185
  const { error, value } = createVendorSchema.validate(req.body);
@@ -144,26 +190,9 @@ async function createVendor(req: any, res: any) {
144
190
  });
145
191
  }
146
192
 
147
- const {
148
- vendor_key: vendorKey,
149
- vendor_type: type,
150
- name,
151
- description,
152
- metadata,
153
- app_pid: appPid,
154
- app_logo: appLogo,
155
- status,
156
- } = value;
157
-
158
- let appUrl = '';
159
- try {
160
- appUrl = new URL(value.app_url).origin;
161
- } catch {
162
- return res.status(400).json({ error: 'Invalid app URL' });
163
- }
193
+ const { vendor_key: vendorKey, vendor_type: type, name, description, metadata, status } = value;
164
194
 
165
195
  const vendorType = (type || 'launcher') as 'launcher' | 'didnames';
166
- const vendorDid = VENDOR_DID[vendorType];
167
196
 
168
197
  const existingVendor = await ProductVendor.findOne({
169
198
  where: { vendor_key: vendorKey },
@@ -172,30 +201,18 @@ async function createVendor(req: any, res: any) {
172
201
  return res.status(400).json({ error: 'Vendor key already exists' });
173
202
  }
174
203
 
175
- const blockletJson = await getBlockletJson(appUrl);
176
-
177
- const mountPoint =
178
- blockletJson?.componentMountPoints?.find((item: any) => item.did === vendorDid)?.mountPoint || '/';
204
+ const preparedData = await prepareVendorData(value.app_url, vendorType, metadata);
205
+ if ('error' in preparedData) {
206
+ return res.status(400).json({ error: preparedData.error });
207
+ }
179
208
 
180
209
  const vendor = await ProductVendor.create({
210
+ ...preparedData,
181
211
  vendor_key: vendorKey,
182
212
  vendor_type: vendorType,
183
213
  name,
184
214
  description,
185
- app_url: appUrl,
186
- vendor_did: vendorDid,
187
215
  status: status || 'active',
188
- app_pid: appPid,
189
- app_logo: appLogo,
190
- metadata: {
191
- ...metadata,
192
- mountPoint,
193
- },
194
- extends: {
195
- mountPoint,
196
- appId: blockletJson?.appId,
197
- appPk: blockletJson?.appPk,
198
- },
199
216
  created_by: req.user?.did || 'admin',
200
217
  });
201
218
 
@@ -224,50 +241,30 @@ async function updateVendor(req: any, res: any) {
224
241
  });
225
242
  }
226
243
 
227
- const { vendor_type: type, name, description, status, metadata, app_pid: appPid, app_logo: appLogo } = value;
228
-
229
- let appUrl = '';
230
- try {
231
- appUrl = new URL(value.app_url).origin;
232
- } catch {
233
- return res.status(400).json({ error: 'Invalid app URL' });
234
- }
235
-
236
- const vendorType = (type || 'launcher') as 'launcher' | 'didnames';
237
- const vendorDid = VENDOR_DID[vendorType];
238
-
239
- const blockletJson = await getBlockletJson(appUrl);
240
-
241
- const mountPoint =
242
- blockletJson?.componentMountPoints?.find((item: any) => item.did === vendorDid)?.mountPoint || '/';
244
+ const { vendor_type: type, vendor_key: vendorKey, name, description, status, metadata } = value;
243
245
 
244
- if (req.body.vendorKey && req.body.vendorKey !== vendor.vendor_key) {
246
+ if (vendorKey && vendorKey !== vendor.vendor_key) {
245
247
  const existingVendor = await ProductVendor.findOne({
246
- where: { vendor_key: req.body.vendorKey },
248
+ where: { vendor_key: vendorKey },
247
249
  });
248
250
  if (existingVendor) {
249
251
  return res.status(400).json({ error: 'Vendor key already exists' });
250
252
  }
251
253
  }
254
+
255
+ const vendorType = (type || 'launcher') as 'launcher' | 'didnames';
256
+ const preparedData = await prepareVendorData(value.app_url, vendorType, metadata);
257
+ if ('error' in preparedData) {
258
+ return res.status(400).json({ error: preparedData.error });
259
+ }
260
+
252
261
  const updates = {
262
+ ...preparedData,
253
263
  vendor_type: vendorType,
264
+ vendor_key: vendorKey,
254
265
  name,
255
266
  description,
256
- app_url: appUrl,
257
- vendor_did: vendorDid,
258
267
  status,
259
- metadata: {
260
- ...metadata,
261
- mountPoint,
262
- },
263
- app_pid: appPid,
264
- app_logo: appLogo,
265
- vendor_key: req.body.vendor_key,
266
- extends: {
267
- mountPoint,
268
- appId: blockletJson?.appId,
269
- appPk: blockletJson?.appPk,
270
- },
271
268
  };
272
269
 
273
270
  await vendor.update(Object.fromEntries(Object.entries(updates).filter(([, v]) => v !== undefined)));
@@ -561,6 +558,36 @@ async function redirectToVendor(req: any, res: any) {
561
558
  }
562
559
  }
563
560
 
561
+ async function getCancelledSessions(req: any, res: any) {
562
+ const { error } = sessionIdsParamSchema.validate(req.body);
563
+ if (error) {
564
+ return res.status(400).json({ error: error.message });
565
+ }
566
+
567
+ const { sessionIds = [] } = req.body;
568
+
569
+ const allCheckoutSessions = await CheckoutSession.findAll({
570
+ where: { id: { [Op.in]: sessionIds } },
571
+ attributes: ['id', 'subscription_id', 'fulfillment_status', 'vendor_info'],
572
+ });
573
+
574
+ const subscriptionIds = allCheckoutSessions.map((item) => item.subscription_id!).filter((item) => !!item);
575
+
576
+ const cancelledSubscriptions = await Subscription.findAll({
577
+ where: {
578
+ id: { [Op.in]: subscriptionIds },
579
+ status: 'canceled',
580
+ },
581
+ attributes: ['id'],
582
+ });
583
+
584
+ const cancelledSubIds = cancelledSubscriptions.map((item) => item.id);
585
+ const cancelledSessions = allCheckoutSessions.filter(
586
+ (item) => item.subscription_id && cancelledSubIds.includes(item.subscription_id)
587
+ );
588
+ return res.json({ cancelledSessions });
589
+ }
590
+
564
591
  async function getVendorSubscription(req: any, res: any) {
565
592
  const { sessionId } = req.params;
566
593
 
@@ -636,6 +663,7 @@ router.get('/order/:sessionId/detail', loginAuth, validateParams(sessionIdParamS
636
663
 
637
664
  // Those for Vendor Call
638
665
  router.get('/connectTest', ensureVendorAuth, getVendorConnectTest);
666
+ router.post('/subscription/cancelled', ensureVendorAuth, getCancelledSessions);
639
667
  router.get('/subscription/:sessionId/redirect', handleSubscriptionRedirect);
640
668
  router.get('/subscription/:sessionId', ensureVendorAuth, getVendorSubscription);
641
669
 
@@ -239,6 +239,7 @@ export class CheckoutSession extends Model<InferAttributes<CheckoutSession>, Inf
239
239
 
240
240
  attempts?: number;
241
241
  lastAttemptAt?: string;
242
+ returnRetry?: number;
242
243
  completedAt?: string;
243
244
  commissionAmount?: string;
244
245
 
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.21.14
17
+ version: 1.21.16
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.21.14",
3
+ "version": "1.21.16",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
@@ -56,9 +56,9 @@
56
56
  "@blocklet/error": "^0.2.5",
57
57
  "@blocklet/js-sdk": "^1.16.53-beta-20251011-054719-4ed2f6b7",
58
58
  "@blocklet/logger": "^1.16.53-beta-20251011-054719-4ed2f6b7",
59
- "@blocklet/payment-broker-client": "1.21.14",
60
- "@blocklet/payment-react": "1.21.14",
61
- "@blocklet/payment-vendor": "1.21.14",
59
+ "@blocklet/payment-broker-client": "1.21.16",
60
+ "@blocklet/payment-react": "1.21.16",
61
+ "@blocklet/payment-vendor": "1.21.16",
62
62
  "@blocklet/sdk": "^1.16.53-beta-20251011-054719-4ed2f6b7",
63
63
  "@blocklet/ui-react": "^3.1.46",
64
64
  "@blocklet/uploader": "^0.2.15",
@@ -128,7 +128,7 @@
128
128
  "devDependencies": {
129
129
  "@abtnode/types": "^1.16.53-beta-20251011-054719-4ed2f6b7",
130
130
  "@arcblock/eslint-config-ts": "^0.3.3",
131
- "@blocklet/payment-types": "1.21.14",
131
+ "@blocklet/payment-types": "1.21.16",
132
132
  "@types/cookie-parser": "^1.4.9",
133
133
  "@types/cors": "^2.8.19",
134
134
  "@types/debug": "^4.1.12",
@@ -175,5 +175,5 @@
175
175
  "parser": "typescript"
176
176
  }
177
177
  },
178
- "gitHead": "31f93a8310fe5184be8dd4ff23b362906c8a66cf"
178
+ "gitHead": "16509d9abd2da2f52587972c863c79ba9e4cd49d"
179
179
  }
@@ -13,9 +13,9 @@ import {
13
13
  } from '@blocklet/payment-react';
14
14
  import type { TInvoiceExpanded } from '@blocklet/payment-types';
15
15
  import { Avatar, CircularProgress, Typography } from '@mui/material';
16
- import { useLocalStorageState } from 'ahooks';
17
16
  import { useEffect, useState } from 'react';
18
17
  import { Link, useSearchParams } from 'react-router-dom';
18
+ import { useCacheState } from '../../hooks/cache-state';
19
19
  import CustomerLink from '../customer/link';
20
20
  import FilterToolbar from '../filter-toolbar';
21
21
  import InvoiceActions from './action';
@@ -129,11 +129,7 @@ export default function InvoiceList({
129
129
  const { t, locale } = useLocaleContext();
130
130
  const defaultPageSize = useDefaultPageSize(20);
131
131
 
132
- const urlStatus = searchParams.get('status');
133
- const urlCurrencyId = searchParams.get('currency_id');
134
- const urlCustomerId = searchParams.get('customer_id');
135
-
136
- const [search, setSearch] = useLocalStorageState<
132
+ const [search, setSearch] = useCacheState<
137
133
  SearchProps & { ignore_zero?: boolean; include_staking?: boolean; include_return_staking?: boolean }
138
134
  >(listKey, {
139
135
  defaultValue: {
@@ -147,19 +143,21 @@ export default function InvoiceList({
147
143
  include_staking: !!include_staking,
148
144
  include_return_staking: !!include_return_staking,
149
145
  },
146
+ getUrlParams: () => {
147
+ const params: Record<string, any> = {};
148
+ if (searchParams.has('status')) {
149
+ params.status = searchParams.get('status');
150
+ }
151
+ if (searchParams.has('currency_id')) {
152
+ params.currency_id = searchParams.get('currency_id');
153
+ }
154
+ if (searchParams.has('customer_id')) {
155
+ params.customer_id = searchParams.get('customer_id');
156
+ }
157
+ return params;
158
+ },
150
159
  });
151
160
 
152
- useEffect(() => {
153
- if (urlStatus || urlCurrencyId || urlCustomerId) {
154
- setSearch((prev) => ({
155
- ...prev!,
156
- ...(urlStatus && { status: urlStatus }),
157
- ...(urlCurrencyId && { currency_id: urlCurrencyId }),
158
- ...(urlCustomerId && { customer_id: urlCustomerId }),
159
- }));
160
- }
161
- }, [urlStatus, urlCurrencyId, urlCustomerId, setSearch]);
162
-
163
161
  const [data, setData] = useState({}) as any;
164
162
 
165
163
  const refresh = () =>
@@ -11,11 +11,11 @@ import {
11
11
  } from '@blocklet/payment-react';
12
12
  import type { TPaymentIntentExpanded } from '@blocklet/payment-types';
13
13
  import { Avatar, CircularProgress, Typography } from '@mui/material';
14
- import { useLocalStorageState } from 'ahooks';
15
14
  import { useEffect, useState } from 'react';
16
15
  import { Link, useSearchParams } from 'react-router-dom';
17
16
 
18
17
  import { debounce } from '../../libs/util';
18
+ import { useCacheState } from '../../hooks/cache-state';
19
19
  import CustomerLink from '../customer/link';
20
20
  import FilterToolbar from '../filter-toolbar';
21
21
  import PaymentIntentActions from './actions';
@@ -81,11 +81,7 @@ export default function PaymentList({
81
81
  const listKey = getListKey({ customer_id, invoice_id });
82
82
  const defaultPageSize = useDefaultPageSize(20);
83
83
 
84
- const urlStatus = searchParams.get('status');
85
- const urlCurrencyId = searchParams.get('currency_id');
86
- const urlCustomerId = searchParams.get('customer_id');
87
-
88
- const [search, setSearch] = useLocalStorageState<SearchProps>(listKey, {
84
+ const [search, setSearch] = useCacheState<SearchProps>(listKey, {
89
85
  defaultValue: {
90
86
  status: '',
91
87
  customer_id,
@@ -93,19 +89,21 @@ export default function PaymentList({
93
89
  pageSize: defaultPageSize,
94
90
  page: 1,
95
91
  },
92
+ getUrlParams: () => {
93
+ const params: Record<string, any> = {};
94
+ if (searchParams.has('status')) {
95
+ params.status = searchParams.get('status');
96
+ }
97
+ if (searchParams.has('currency_id')) {
98
+ params.currency_id = searchParams.get('currency_id');
99
+ }
100
+ if (searchParams.has('customer_id')) {
101
+ params.customer_id = searchParams.get('customer_id');
102
+ }
103
+ return params;
104
+ },
96
105
  });
97
106
 
98
- useEffect(() => {
99
- if (urlStatus || urlCurrencyId || urlCustomerId) {
100
- setSearch((prev) => ({
101
- ...prev!,
102
- ...(urlStatus && { status: urlStatus }),
103
- ...(urlCurrencyId && { currency_id: urlCurrencyId }),
104
- ...(urlCustomerId && { customer_id: urlCustomerId }),
105
- }));
106
- }
107
- }, [urlStatus, urlCurrencyId, urlCustomerId, setSearch]);
108
-
109
107
  const [data, setData] = useState({}) as any;
110
108
 
111
109
  const fetchListData = () => {
@@ -11,13 +11,13 @@ import {
11
11
  } from '@blocklet/payment-react';
12
12
  import type { TPayoutExpanded } from '@blocklet/payment-types';
13
13
  import { Avatar, CircularProgress, Typography } from '@mui/material';
14
- import { useLocalStorageState } from 'ahooks';
15
14
  import { useEffect, useState } from 'react';
16
15
  import { Link, useSearchParams } from 'react-router-dom';
17
16
 
18
17
  import DID from '@arcblock/ux/lib/DID';
19
18
  import ShortenLabel from '@arcblock/ux/lib/UserCard/Content/shorten-label';
20
19
  import { debounce, getAppInfo } from '../../libs/util';
20
+ import { useCacheState } from '../../hooks/cache-state';
21
21
  import CustomerLink from '../customer/link';
22
22
  import FilterToolbar from '../filter-toolbar';
23
23
  import PayoutActions from './actions';
@@ -86,11 +86,7 @@ export default function PayoutList({
86
86
  const listKey = getListKey({ customer_id, payment_intent_id });
87
87
  const defaultPageSize = useDefaultPageSize(20);
88
88
 
89
- const urlStatus = searchParams.get('status');
90
- const urlCurrencyId = searchParams.get('currency_id');
91
- const urlCustomerId = searchParams.get('customer_id');
92
-
93
- const [search, setSearch] = useLocalStorageState<SearchProps>(listKey, {
89
+ const [search, setSearch] = useCacheState<SearchProps>(listKey, {
94
90
  defaultValue: {
95
91
  status: status as string,
96
92
  customer_id,
@@ -98,19 +94,21 @@ export default function PayoutList({
98
94
  pageSize: defaultPageSize,
99
95
  page: 1,
100
96
  },
97
+ getUrlParams: () => {
98
+ const params: Record<string, any> = {};
99
+ if (searchParams.has('status')) {
100
+ params.status = searchParams.get('status');
101
+ }
102
+ if (searchParams.has('currency_id')) {
103
+ params.currency_id = searchParams.get('currency_id');
104
+ }
105
+ if (searchParams.has('customer_id')) {
106
+ params.customer_id = searchParams.get('customer_id');
107
+ }
108
+ return params;
109
+ },
101
110
  });
102
111
 
103
- useEffect(() => {
104
- if (urlStatus || urlCurrencyId || urlCustomerId) {
105
- setSearch((prev) => ({
106
- ...prev!,
107
- ...(urlStatus && { status: urlStatus }),
108
- ...(urlCurrencyId && { currency_id: urlCurrencyId }),
109
- ...(urlCustomerId && { customer_id: urlCustomerId }),
110
- }));
111
- }
112
- }, [urlStatus, urlCurrencyId, urlCustomerId, setSearch]);
113
-
114
112
  const [data, setData] = useState({}) as any;
115
113
 
116
114
  useEffect(() => {
@@ -11,7 +11,6 @@ import {
11
11
  } from '@blocklet/payment-react';
12
12
  import type { TRefundExpanded } from '@blocklet/payment-types';
13
13
  import { Avatar, CircularProgress, Typography } from '@mui/material';
14
- import { useLocalStorageState } from 'ahooks';
15
14
  import { useEffect, useState } from 'react';
16
15
  import { Link, useSearchParams } from 'react-router-dom';
17
16
 
@@ -19,6 +18,7 @@ import { capitalize, toLower } from 'lodash';
19
18
  import CustomerLink from '../customer/link';
20
19
  import FilterToolbar from '../filter-toolbar';
21
20
  import RefundActions from './actions';
21
+ import { useCacheState } from '../../hooks/cache-state';
22
22
 
23
23
  const fetchData = (params: Record<string, any> = {}): Promise<{ list: TRefundExpanded[]; count: number }> => {
24
24
  const search = new URLSearchParams();
@@ -96,11 +96,7 @@ export default function RefundList({
96
96
  const listKey = getListKey({ customer_id, invoice_id, subscription_id, payment_intent_id });
97
97
  const defaultPageSize = useDefaultPageSize(20);
98
98
 
99
- const urlStatus = searchParams.get('status');
100
- const urlCurrencyId = searchParams.get('currency_id');
101
- const urlCustomerId = searchParams.get('customer_id');
102
-
103
- const [search, setSearch] = useLocalStorageState<SearchProps>(listKey, {
99
+ const [search, setSearch] = useCacheState<SearchProps>(listKey, {
104
100
  defaultValue: {
105
101
  status: status as string,
106
102
  customer_id,
@@ -110,19 +106,21 @@ export default function RefundList({
110
106
  pageSize: defaultPageSize,
111
107
  page: 1,
112
108
  },
109
+ getUrlParams: () => {
110
+ const params: Record<string, any> = {};
111
+ if (searchParams.has('status')) {
112
+ params.status = searchParams.get('status');
113
+ }
114
+ if (searchParams.has('currency_id')) {
115
+ params.currency_id = searchParams.get('currency_id');
116
+ }
117
+ if (searchParams.has('customer_id')) {
118
+ params.customer_id = searchParams.get('customer_id');
119
+ }
120
+ return params;
121
+ },
113
122
  });
114
123
 
115
- useEffect(() => {
116
- if (urlStatus || urlCurrencyId || urlCustomerId) {
117
- setSearch((prev) => ({
118
- ...prev!,
119
- ...(urlStatus && { status: urlStatus }),
120
- ...(urlCurrencyId && { currency_id: urlCurrencyId }),
121
- ...(urlCustomerId && { customer_id: urlCustomerId }),
122
- }));
123
- }
124
- }, [urlStatus, urlCurrencyId, urlCustomerId, setSearch]);
125
-
126
124
  const [data, setData] = useState({}) as any;
127
125
 
128
126
  const refresh = () =>
@@ -3,10 +3,10 @@ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import { Status, api, formatTime, Table, useDefaultPageSize } from '@blocklet/payment-react';
4
4
  import type { TSubscriptionExpanded } from '@blocklet/payment-types';
5
5
  import { CircularProgress } from '@mui/material';
6
- import { useLocalStorageState } from 'ahooks';
7
6
  import { useEffect, useState } from 'react';
8
7
  import { Link, useSearchParams } from 'react-router-dom';
9
8
 
9
+ import { useCacheState } from '../../hooks/cache-state';
10
10
  import CustomerLink from '../customer/link';
11
11
  import FilterToolbar from '../filter-toolbar';
12
12
  import SubscriptionActions from './actions';
@@ -70,10 +70,7 @@ export default function SubscriptionList({
70
70
  const { t } = useLocaleContext();
71
71
  const defaultPageSize = useDefaultPageSize(20);
72
72
 
73
- const urlStatus = searchParams.get('status');
74
- const urlCustomerId = searchParams.get('customer_id');
75
-
76
- const [search, setSearch] = useLocalStorageState<SearchProps>(listKey, {
73
+ const [search, setSearch] = useCacheState<SearchProps>(listKey, {
77
74
  defaultValue: {
78
75
  status: (status || 'active') as string,
79
76
  customer_id,
@@ -81,18 +78,18 @@ export default function SubscriptionList({
81
78
  page: 1,
82
79
  price_id: '',
83
80
  },
81
+ getUrlParams: () => {
82
+ const params: Record<string, any> = {};
83
+ if (searchParams.has('status')) {
84
+ params.status = searchParams.get('status');
85
+ }
86
+ if (searchParams.has('customer_id')) {
87
+ params.customer_id = searchParams.get('customer_id');
88
+ }
89
+ return params;
90
+ },
84
91
  });
85
92
 
86
- useEffect(() => {
87
- if (urlStatus || urlCustomerId) {
88
- setSearch((prev) => ({
89
- ...prev!,
90
- ...(urlStatus && { status: urlStatus }),
91
- ...(urlCustomerId && { customer_id: urlCustomerId }),
92
- }));
93
- }
94
- }, [urlStatus, urlCustomerId, setSearch]);
95
-
96
93
  const [data, setData] = useState({}) as any;
97
94
 
98
95
  const refresh = () =>
@@ -0,0 +1,84 @@
1
+ import { useCallback, useState } from 'react';
2
+
3
+ type StorageType = 'localStorage' | 'sessionStorage' | 'cookie';
4
+
5
+ interface CacheStateOptions<T> {
6
+ defaultValue: T;
7
+ storage?: StorageType;
8
+ getUrlParams?: () => Partial<T>;
9
+ clearUrlParams?: () => void;
10
+ }
11
+
12
+ const getStorageValue = <T>(key: string, storage: StorageType): T | undefined => {
13
+ try {
14
+ if (storage === 'cookie') {
15
+ const matches = document.cookie.match(
16
+ new RegExp(`(?:^|; )${key.replace(/([.$?*|{}()[\]\\/+^])/g, '\\$1')}=([^;]*)`)
17
+ );
18
+ return matches?.[1] ? JSON.parse(decodeURIComponent(matches[1])) : undefined;
19
+ }
20
+ const storageObj = storage === 'sessionStorage' ? sessionStorage : localStorage;
21
+ const value = storageObj.getItem(key);
22
+ return value ? JSON.parse(value) : undefined;
23
+ } catch {
24
+ return undefined;
25
+ }
26
+ };
27
+
28
+ const setStorageValue = <T>(key: string, value: T, storage: StorageType): void => {
29
+ try {
30
+ const serialized = JSON.stringify(value);
31
+ if (storage === 'cookie') {
32
+ document.cookie = `${encodeURIComponent(key)}=${encodeURIComponent(serialized)}; path=/`;
33
+ return;
34
+ }
35
+ const storageObj = storage === 'sessionStorage' ? sessionStorage : localStorage;
36
+ storageObj.setItem(key, serialized);
37
+ } catch (error) {
38
+ console.error(`Failed to set ${storage} value:`, error);
39
+ }
40
+ };
41
+
42
+ const getInitialValue = <T>(
43
+ key: string,
44
+ storage: StorageType,
45
+ defaultValue: T,
46
+ getUrlParams?: () => Partial<T>,
47
+ clearUrlParams?: () => void
48
+ ): T => {
49
+ const cachedValue = getStorageValue<T>(key, storage);
50
+ const urlParams = getUrlParams?.();
51
+
52
+ if (urlParams && Object.keys(urlParams).length > 0) {
53
+ const mergedValue = {
54
+ ...(cachedValue || defaultValue),
55
+ ...urlParams,
56
+ };
57
+ setStorageValue(key, mergedValue, storage);
58
+ clearUrlParams?.();
59
+ return mergedValue;
60
+ }
61
+
62
+ return cachedValue !== undefined ? cachedValue : defaultValue;
63
+ };
64
+
65
+ export const useCacheState = <T = any>(key: string, options: CacheStateOptions<T>) => {
66
+ const { defaultValue, storage = 'localStorage', getUrlParams, clearUrlParams } = options;
67
+
68
+ const [state, setState] = useState<T>(() =>
69
+ getInitialValue(key, storage, defaultValue, getUrlParams, clearUrlParams)
70
+ );
71
+
72
+ const setStateAndStorage = useCallback(
73
+ (value: T | ((prev: T) => T)) => {
74
+ setState((prevState) => {
75
+ const nextState = typeof value === 'function' ? (value as (prev: T) => T)(prevState) : value;
76
+ setStorageValue(key, nextState, storage);
77
+ return nextState;
78
+ });
79
+ },
80
+ [key, storage]
81
+ );
82
+
83
+ return [state, setStateAndStorage] as const;
84
+ };
@@ -1135,7 +1135,7 @@ export default function Overview() {
1135
1135
  }}>
1136
1136
  <Typography
1137
1137
  component={Link}
1138
- to={metric.link}
1138
+ to={`${metric.link}?status=`}
1139
1139
  sx={{
1140
1140
  fontSize: '1.5rem',
1141
1141
  fontWeight: 600,
@@ -5,23 +5,21 @@ import { AddOutlined } from '@mui/icons-material';
5
5
  import {
6
6
  Button,
7
7
  CircularProgress,
8
- Stack,
9
- TextField,
10
- FormControlLabel,
11
- Switch,
12
8
  FormControl,
9
+ FormControlLabel,
13
10
  InputLabel,
14
- Select,
15
11
  MenuItem,
12
+ Select,
13
+ Stack,
14
+ Switch,
15
+ TextField,
16
16
  } from '@mui/material';
17
17
  import { useState } from 'react';
18
- import { useForm, Controller, FormProvider } from 'react-hook-form';
18
+ import { Controller, FormProvider, useForm } from 'react-hook-form';
19
19
  import { dispatch } from 'use-bus';
20
20
 
21
- import { joinURL, withQuery } from 'ufo';
22
21
  import DrawerForm from '../../../../components/drawer-form';
23
22
  import MetadataForm from '../../../../components/metadata/form';
24
- import { formatProxyUrl } from '../../../../libs/util';
25
23
 
26
24
  interface Vendor {
27
25
  id: string;
@@ -84,8 +82,6 @@ export default function VendorCreate({
84
82
  app_url: '',
85
83
  status: 'inactive' as const,
86
84
  metadata: [{ key: 'blockletMetaUrl', value: '' }],
87
- app_pid: '',
88
- app_logo: '',
89
85
  };
90
86
 
91
87
  const methods = useForm<VendorFormData>({
@@ -147,36 +143,6 @@ export default function VendorCreate({
147
143
  metadata: metadataObj,
148
144
  };
149
145
 
150
- // 如果状态为启用,则检测应用地址可用性
151
- if (submitData.status === 'active') {
152
- try {
153
- const response = await fetch(
154
- formatProxyUrl(withQuery(joinURL(submitData.app_url, '__blocklet__.js'), { type: 'json' })),
155
- {
156
- method: 'GET',
157
- headers: { 'Content-Type': 'application/json' },
158
- }
159
- );
160
-
161
- if (!response.ok) {
162
- Toast.error(t('admin.vendor.addressCheckFailed'));
163
- setLoading(false);
164
- return;
165
- }
166
-
167
- // 从响应中获取appPid和appLogo
168
- const blockletInfo = await response.json();
169
- if (blockletInfo) {
170
- submitData.app_pid = blockletInfo.pid || blockletInfo.appPid;
171
- submitData.app_logo = blockletInfo.logo || blockletInfo.appLogo;
172
- }
173
- } catch (error) {
174
- Toast.error(t('admin.vendor.addressCheckFailed'));
175
- setLoading(false);
176
- return;
177
- }
178
- }
179
-
180
146
  if (isEditMode && vendorData) {
181
147
  // 编辑模式:更新供应商
182
148
  await api.put(`/api/vendors/${vendorData.id}`, submitData);
@@ -6,6 +6,7 @@ import { ContentCopy } from '@mui/icons-material';
6
6
  import { Box, Chip, CircularProgress, IconButton, Tooltip, Typography } from '@mui/material';
7
7
  import { useEffect, useState } from 'react';
8
8
  import useBus from 'use-bus';
9
+ import omit from 'lodash/omit';
9
10
 
10
11
  import { useLocalStorageState } from 'ahooks';
11
12
  import FilterToolbar from '../../../../components/filter-toolbar';
@@ -320,7 +321,10 @@ export default function VendorsList() {
320
321
  setSelectedVendor(null);
321
322
  refresh();
322
323
  }}
323
- vendorData={selectedVendor}
324
+ vendorData={{
325
+ ...selectedVendor,
326
+ metadata: omit(selectedVendor?.metadata || {}, 'mountPoint'),
327
+ }}
324
328
  />
325
329
  )}
326
330
  </>