payment-kit 1.18.24 → 1.18.26

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 (41) hide show
  1. package/api/src/libs/event.ts +22 -2
  2. package/api/src/libs/invoice.ts +142 -0
  3. package/api/src/libs/notification/template/aggregated-subscription-renewed.ts +165 -0
  4. package/api/src/libs/notification/template/one-time-payment-succeeded.ts +2 -5
  5. package/api/src/libs/notification/template/subscription-canceled.ts +2 -3
  6. package/api/src/libs/notification/template/subscription-refund-succeeded.ts +7 -4
  7. package/api/src/libs/notification/template/subscription-renew-failed.ts +3 -5
  8. package/api/src/libs/notification/template/subscription-renewed.ts +2 -2
  9. package/api/src/libs/notification/template/subscription-stake-slash-succeeded.ts +2 -3
  10. package/api/src/libs/notification/template/subscription-succeeded.ts +2 -2
  11. package/api/src/libs/notification/template/subscription-upgraded.ts +5 -5
  12. package/api/src/libs/notification/template/subscription-will-renew.ts +2 -2
  13. package/api/src/libs/queue/index.ts +6 -0
  14. package/api/src/libs/queue/store.ts +13 -1
  15. package/api/src/libs/util.ts +22 -1
  16. package/api/src/locales/en.ts +5 -0
  17. package/api/src/locales/zh.ts +5 -0
  18. package/api/src/queues/invoice.ts +21 -7
  19. package/api/src/queues/notification.ts +353 -11
  20. package/api/src/queues/payment.ts +26 -10
  21. package/api/src/queues/payout.ts +21 -7
  22. package/api/src/routes/checkout-sessions.ts +26 -12
  23. package/api/src/routes/connect/recharge-account.ts +13 -1
  24. package/api/src/routes/connect/recharge.ts +13 -1
  25. package/api/src/routes/connect/shared.ts +54 -36
  26. package/api/src/routes/customers.ts +61 -0
  27. package/api/src/routes/invoices.ts +51 -1
  28. package/api/src/routes/subscriptions.ts +1 -1
  29. package/api/src/store/migrations/20250328-notification-preference.ts +29 -0
  30. package/api/src/store/models/customer.ts +42 -1
  31. package/api/src/store/models/types.ts +17 -1
  32. package/blocklet.yml +1 -1
  33. package/package.json +24 -24
  34. package/src/components/customer/form.tsx +21 -2
  35. package/src/components/customer/notification-preference.tsx +428 -0
  36. package/src/components/layout/user.tsx +1 -1
  37. package/src/locales/en.tsx +30 -0
  38. package/src/locales/zh.tsx +30 -0
  39. package/src/pages/customer/index.tsx +27 -23
  40. package/src/pages/customer/recharge/account.tsx +19 -17
  41. package/src/pages/customer/subscription/embed.tsx +25 -9
@@ -8,7 +8,7 @@ import { getGasPayerExtra } from '../../libs/payment';
8
8
  import { getTxMetadata } from '../../libs/util';
9
9
  import { ensureSubscriptionRecharge, getAuthPrincipalClaim } from './shared';
10
10
  import logger from '../../libs/logger';
11
- import { ensureRechargeInvoice } from '../../libs/invoice';
11
+ import { ensureRechargeInvoice, retryUncollectibleInvoices } from '../../libs/invoice';
12
12
  import { EVMChainType } from '../../store/models';
13
13
  import { EVM_CHAIN_TYPES } from '../../libs/constants';
14
14
 
@@ -99,6 +99,18 @@ export default {
99
99
  paymentMethod,
100
100
  customer!
101
101
  );
102
+ try {
103
+ retryUncollectibleInvoices({
104
+ currencyId: paymentCurrency.id,
105
+ subscriptionId,
106
+ });
107
+ } catch (err) {
108
+ logger.error('Failed to retry uncollectible invoices', {
109
+ error: err,
110
+ currencyId: paymentCurrency.id,
111
+ subscriptionId,
112
+ });
113
+ }
102
114
  };
103
115
  if (paymentMethod.type === 'arcblock') {
104
116
  try {
@@ -1105,6 +1105,29 @@ export async function ensureSubscriptionForOverdraftProtection(subscriptionId: s
1105
1105
  };
1106
1106
  }
1107
1107
 
1108
+
1109
+ async function executeSingleTransaction(
1110
+ client: any,
1111
+ claim: any,
1112
+ type: 'Delegate' | 'Stake',
1113
+ userDid: string,
1114
+ userPk: string,
1115
+ gasPayerHeaders: Record<string, string>
1116
+ ): Promise<string> {
1117
+ if (!claim) return '';
1118
+
1119
+ const tx: Partial<Transaction> = client.decodeTx(claim.finalTx || claim.origin);
1120
+ if (claim.sig) {
1121
+ tx.signature = claim.sig;
1122
+ }
1123
+
1124
+ const { buffer } = await client[`encode${type}Tx`]({ tx });
1125
+ return client[`send${type}Tx`](
1126
+ { tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid)) },
1127
+ getGasPayerExtra(buffer, gasPayerHeaders)
1128
+ );
1129
+ }
1130
+
1108
1131
  export async function executeOcapTransactions(
1109
1132
  userDid: string,
1110
1133
  userPk: string,
@@ -1116,16 +1139,14 @@ export async function executeOcapTransactions(
1116
1139
  nonce?: string
1117
1140
  ) {
1118
1141
  const client = paymentMethod.getOcapClient();
1119
- logger.info('start executeOcapTransactions', claims);
1120
- const delegation = claims.find((x) => x.type === 'signature' && x.meta?.purpose === 'delegation');
1121
- const staking = claims.find((x) => x.type === 'prepareTx' && x.meta?.purpose === 'staking');
1122
- const transactions = [
1123
- [delegation, 'Delegate'],
1124
- [staking, 'Stake'],
1125
- ];
1126
-
1127
- const stakingAmount =
1128
- staking?.requirement?.tokens?.find((x: any) => x.address === paymentCurrencyContract)?.value || '0';
1142
+ logger.info('start executeOcapTransactions', { userDid, claims });
1143
+
1144
+ const delegation = claims.find(x => x.type === 'signature' && x.meta?.purpose === 'delegation');
1145
+ const staking = claims.find(x => x.type === 'prepareTx' && x.meta?.purpose === 'staking');
1146
+
1147
+ const stakingAmount = staking?.requirement?.tokens?.find(
1148
+ (x: any) => x?.address === paymentCurrencyContract
1149
+ )?.value || '0';
1129
1150
 
1130
1151
  try {
1131
1152
  const getHeaders = (index: number): Record<string, string> => {
@@ -1145,30 +1166,27 @@ export async function executeOcapTransactions(
1145
1166
  return {};
1146
1167
  };
1147
1168
 
1148
- const [delegationTxHash, stakingTxHash] = await Promise.all(
1149
- transactions.map(async ([claim, type], index) => {
1150
- if (!claim) {
1151
- return '';
1152
- }
1153
-
1154
- const tx: Partial<Transaction> = client.decodeTx(claim.finalTx || claim.origin);
1155
- if (claim.sig) {
1156
- tx.signature = claim.sig;
1157
- }
1158
-
1159
- // @ts-ignore
1160
- const { buffer } = await client[`encode${type}Tx`]({ tx });
1161
- const gasPayerHeaders = getHeaders(index);
1162
- // @ts-ignore
1163
- const txHash = await client[`send${type}Tx`](
1164
- // @ts-ignore
1165
- { tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid)) },
1166
- getGasPayerExtra(buffer, gasPayerHeaders)
1167
- );
1168
-
1169
- return txHash;
1170
- })
1171
- );
1169
+ const transactions = [
1170
+ { claim: delegation, type: 'Delegate' },
1171
+ { claim: staking, type: 'Stake' }
1172
+ ];
1173
+
1174
+ const txHashes = [];
1175
+ for (let i = 0; i < transactions.length; i++) {
1176
+ const { claim, type } = transactions[i]!;
1177
+ // eslint-disable-next-line no-await-in-loop
1178
+ const hash = await executeSingleTransaction(
1179
+ client,
1180
+ claim,
1181
+ type as 'Delegate' | 'Stake',
1182
+ userDid,
1183
+ userPk,
1184
+ getHeaders(i)
1185
+ );
1186
+ txHashes.push(hash);
1187
+ }
1188
+
1189
+ const [delegationTxHash, stakingTxHash] = txHashes;
1172
1190
 
1173
1191
  return {
1174
1192
  tx_hash: delegationTxHash,
@@ -1176,12 +1194,12 @@ export async function executeOcapTransactions(
1176
1194
  type: 'delegate',
1177
1195
  staking: {
1178
1196
  tx_hash: stakingTxHash,
1179
- address: await getCustomerStakeAddress(userDid, nonce || subscriptionId || ''),
1197
+ address: await getCustomerStakeAddress(userDid, nonce || subscriptionId || ''),
1180
1198
  },
1181
1199
  stakingAmount,
1182
1200
  };
1183
1201
  } catch (err) {
1184
- logger.error('executeOcapTransactions failed', err);
1202
+ logger.error('executeOcapTransactions failed', { error: err, userDid });
1185
1203
  throw err;
1186
1204
  }
1187
1205
  }
@@ -24,6 +24,7 @@ import {
24
24
  } from '../store/models';
25
25
  import { getSubscriptionPaymentAddress, calculateRecommendedRechargeAmount } from '../libs/subscription';
26
26
  import { expandLineItems } from '../libs/session';
27
+ import { handleNotificationPreferenceChange } from '../queues/notification';
27
28
 
28
29
  const router = Router();
29
30
  const auth = authenticate<Customer>({ component: true, roles: ['owner', 'admin'] });
@@ -181,6 +182,9 @@ router.get('/:id/overdue/invoices', sessionMiddleware(), async (req, res) => {
181
182
  if (!doc) {
182
183
  return res.status(404).json({ error: 'Customer not found' });
183
184
  }
185
+ if (doc.did !== req.user.did && !['admin', 'owner'].includes(req.user?.role)) {
186
+ return res.status(403).json({ error: 'You are not allowed to access this customer invoices' });
187
+ }
184
188
  const { rows: invoices, count } = await Invoice.findAndCountAll({
185
189
  where: {
186
190
  customer_id: doc.id,
@@ -367,6 +371,63 @@ router.get('/:id/summary', auth, async (req, res) => {
367
371
  }
368
372
  });
369
373
 
374
+ const updatePreferenceSchema = Joi.object({
375
+ notification: Joi.object({
376
+ frequency: Joi.string().valid('default', 'daily', 'weekly', 'monthly').required(),
377
+ schedule: Joi.object({
378
+ time: Joi.string().pattern(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/),
379
+ date: Joi.alternatives().conditional('..frequency', {
380
+ switch: [
381
+ { is: 'weekly', then: Joi.number().min(0).max(6).required() },
382
+ { is: 'monthly', then: Joi.number().min(1).max(31).required() },
383
+ { is: Joi.valid('daily', 'default'), then: Joi.number().optional() },
384
+ ],
385
+ }),
386
+ }).when('frequency', {
387
+ is: 'default',
388
+ then: Joi.optional(),
389
+ otherwise: Joi.required(),
390
+ }),
391
+ }).optional(),
392
+ }).unknown(false);
393
+
394
+ router.put('/preference', sessionMiddleware(), async (req, res) => {
395
+ try {
396
+ if (!req.user) {
397
+ return res.status(403).json({ error: 'Unauthorized' });
398
+ }
399
+
400
+ const doc = await Customer.findByPkOrDid(req.user.did as string);
401
+ if (!doc) {
402
+ return res.status(404).json({ error: 'Customer not found' });
403
+ }
404
+
405
+ const { error, value } = updatePreferenceSchema.validate(req.body);
406
+ if (error) {
407
+ return res.status(400).json({ error: error.message });
408
+ }
409
+
410
+ // Get old preference before update
411
+ const oldPreference = doc.preference?.notification;
412
+
413
+ await doc.update({ preference: value });
414
+
415
+ // Handle notification queue updates if notification preference changed
416
+ if (
417
+ oldPreference?.frequency !== value.notification?.frequency ||
418
+ oldPreference?.schedule?.time !== value.notification?.schedule?.time ||
419
+ oldPreference?.schedule?.date !== value.notification?.schedule?.date
420
+ ) {
421
+ await handleNotificationPreferenceChange(doc.id, value.notification);
422
+ }
423
+
424
+ return res.json(doc);
425
+ } catch (err) {
426
+ logger.error('Failed to update customer preference', err);
427
+ return res.status(400).json({ error: `Failed to update preference: ${err.message}` });
428
+ }
429
+ });
430
+
370
431
  const updateCustomerSchema = Joi.object({
371
432
  metadata: MetadataSchema,
372
433
  name: Joi.string().min(2).max(30).empty(''),
@@ -21,7 +21,7 @@ import { PaymentMethod } from '../store/models/payment-method';
21
21
  import { Price } from '../store/models/price';
22
22
  import { Product } from '../store/models/product';
23
23
  import { Subscription } from '../store/models/subscription';
24
- import { getReturnStakeInvoices, getStakingInvoices } from '../libs/invoice';
24
+ import { getReturnStakeInvoices, getStakingInvoices, retryUncollectibleInvoices } from '../libs/invoice';
25
25
  import { CheckoutSession, PaymentLink, TInvoiceExpanded } from '../store/models';
26
26
  import logger from '../libs/logger';
27
27
 
@@ -253,6 +253,56 @@ router.get('/search', authMine, async (req, res) => {
253
253
  res.json({ count, list, paging: { page, pageSize } });
254
254
  });
255
255
 
256
+ const retryUncollectibleSchema = Joi.object({
257
+ customerId: Joi.string().trim().allow('').optional(),
258
+ subscriptionId: Joi.string().trim().allow('').optional(),
259
+ invoiceId: Joi.string().trim().allow('').optional(),
260
+ invoiceIds: Joi.alternatives()
261
+ .try(
262
+ Joi.array().items(Joi.string().trim()),
263
+ Joi.string()
264
+ .trim()
265
+ .custom((value) => {
266
+ if (!value) return undefined;
267
+ return value
268
+ .split(',')
269
+ .map((id: string) => id.trim())
270
+ .filter(Boolean);
271
+ })
272
+ )
273
+ .optional(),
274
+ currencyId: Joi.string().trim().allow('').optional(),
275
+ });
276
+ router.get('/retry-uncollectible', authAdmin, async (req, res) => {
277
+ try {
278
+ const { error, value } = retryUncollectibleSchema.validate(req.query, {
279
+ stripUnknown: true,
280
+ });
281
+
282
+ if (error) {
283
+ return res.status(400).json({ error: error.message });
284
+ }
285
+
286
+ const { customerId, subscriptionId, invoiceId, invoiceIds, currencyId } = value;
287
+
288
+ const result = await retryUncollectibleInvoices({
289
+ customerId,
290
+ subscriptionId,
291
+ invoiceId,
292
+ invoiceIds,
293
+ currencyId,
294
+ });
295
+
296
+ return res.json(result);
297
+ } catch (error) {
298
+ logger.error('Failed to retry uncollectible invoices', { error });
299
+ return res.status(500).json({
300
+ error: 'Failed to retry uncollectible invoices',
301
+ message: error.message,
302
+ });
303
+ }
304
+ });
305
+
256
306
  router.get('/:id', authPortal, async (req, res) => {
257
307
  try {
258
308
  const doc = (await Invoice.findOne({
@@ -1906,7 +1906,7 @@ router.get('/:id/overdue/invoices', authPortal, async (req, res) => {
1906
1906
  return res.status(404).json({ error: 'Subscription not found' });
1907
1907
  }
1908
1908
  // @ts-ignore
1909
- if (subscription.customer?.did !== req.user?.did) {
1909
+ if (subscription.customer?.did !== req.user?.did && !['admin', 'owner'].includes(req.user?.role)) {
1910
1910
  return res.status(403).json({ error: 'You are not allowed to access this subscription' });
1911
1911
  }
1912
1912
  const { rows: invoices, count } = await Invoice.findAndCountAll({
@@ -0,0 +1,29 @@
1
+ import { DataTypes } from 'sequelize';
2
+
3
+ import { Migration, safeApplyColumnChanges } from '../migrate';
4
+
5
+ export const up: Migration = async ({ context }) => {
6
+ await safeApplyColumnChanges(context, {
7
+ customers: [
8
+ {
9
+ name: 'preference',
10
+ field: {
11
+ type: DataTypes.JSON,
12
+ defaultValue: JSON.stringify({
13
+ notification: {
14
+ frequency: 'monthly',
15
+ schedule: {
16
+ time: '10:00',
17
+ date: 1,
18
+ },
19
+ },
20
+ }),
21
+ },
22
+ },
23
+ ],
24
+ });
25
+ };
26
+
27
+ export const down: Migration = async ({ context }) => {
28
+ await context.removeColumn('customers', 'preference');
29
+ };
@@ -12,11 +12,12 @@ import {
12
12
  Op,
13
13
  } from 'sequelize';
14
14
 
15
+ import merge from 'lodash/merge';
15
16
  import { createEvent } from '../../libs/audit';
16
17
  import CustomError from '../../libs/error';
17
18
  import { getLock } from '../../libs/lock';
18
19
  import { createCodeGenerator, createIdGenerator } from '../../libs/util';
19
- import type { CustomerAddress, CustomerShipping } from './types';
20
+ import type { CustomerAddress, CustomerPreferences, CustomerShipping } from './types';
20
21
 
21
22
  export const nextCustomerId = createIdGenerator('cus', 14);
22
23
  export const nextInvoicePrefix = createCodeGenerator('', 8);
@@ -64,6 +65,7 @@ export class Customer extends Model<InferAttributes<Customer>, InferCreationAttr
64
65
  declare created_at: CreationOptional<Date>;
65
66
  declare updated_at: CreationOptional<Date>;
66
67
  declare last_sync_at?: number;
68
+ declare preference?: CustomerPreferences;
67
69
 
68
70
  public static readonly GENESIS_ATTRIBUTES = {
69
71
  id: {
@@ -233,6 +235,19 @@ export class Customer extends Model<InferAttributes<Customer>, InferCreationAttr
233
235
  type: DataTypes.INTEGER,
234
236
  allowNull: true,
235
237
  },
238
+ preference: {
239
+ type: DataTypes.JSON,
240
+ allowNull: true,
241
+ defaultValue: {
242
+ notification: {
243
+ frequency: 'monthly',
244
+ schedule: {
245
+ time: '10:00',
246
+ date: 1,
247
+ },
248
+ },
249
+ },
250
+ },
236
251
  },
237
252
  {
238
253
  sequelize,
@@ -304,6 +319,32 @@ export class Customer extends Model<InferAttributes<Customer>, InferCreationAttr
304
319
  postalCode: customer.address?.postal_code || '',
305
320
  };
306
321
  }
322
+
323
+ public static formatUpdateAddress(address: CustomerAddress, customer: Customer): CustomerAddress {
324
+ const defaultAddress = {
325
+ country: 'us',
326
+ state: '',
327
+ city: '',
328
+ line1: '',
329
+ line2: '',
330
+ postal_code: '',
331
+ };
332
+
333
+ return merge(
334
+ defaultAddress,
335
+ customer.address || {},
336
+ address
337
+ ? {
338
+ country: address.country?.toLowerCase(),
339
+ state: address.state,
340
+ city: address.city,
341
+ line1: address.line1,
342
+ line2: address.line2,
343
+ postal_code: address.postal_code,
344
+ }
345
+ : {}
346
+ );
347
+ }
307
348
  }
308
349
 
309
350
  export type TCustomer = InferAttributes<Customer>;
@@ -442,7 +442,7 @@ export type PricingTableItem = {
442
442
  // Enables user redeemable promotion codes.
443
443
  allow_promotion_codes: boolean;
444
444
 
445
- // Configuration for collecting the customers billing address.
445
+ // Configuration for collecting the customer's billing address.
446
446
  billing_address_collection?: LiteralUnion<'auto' | 'required', string>;
447
447
 
448
448
  is_highlight: boolean;
@@ -704,3 +704,19 @@ export type EventType = LiteralUnion<
704
704
  export type StripeRefundReason = 'duplicate' | 'fraudulent' | 'requested_by_customer';
705
705
 
706
706
  export type SettingType = LiteralUnion<'donate', string>;
707
+
708
+ export type NotificationFrequency = 'default' | 'daily' | 'weekly' | 'monthly';
709
+ export type NotificationSchedule = {
710
+ time: string; // HH:mm 格式
711
+ date?: number; // weekly: 0-6, monthly: 1-31
712
+ };
713
+
714
+ export type NotificationSettings = {
715
+ frequency: NotificationFrequency;
716
+ schedule?: NotificationSchedule;
717
+ };
718
+
719
+ export type CustomerPreferences = {
720
+ notification?: NotificationSettings;
721
+ // support more preferences
722
+ };
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.18.24
17
+ version: 1.18.26
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.18.24",
3
+ "version": "1.18.26",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -43,30 +43,30 @@
43
43
  ]
44
44
  },
45
45
  "dependencies": {
46
- "@abtnode/cron": "^1.16.40",
47
- "@arcblock/did": "^1.19.15",
46
+ "@abtnode/cron": "^1.16.41",
47
+ "@arcblock/did": "^1.19.19",
48
48
  "@arcblock/did-auth-storage-nedb": "^1.7.1",
49
- "@arcblock/did-connect": "^2.12.48",
50
- "@arcblock/did-util": "^1.19.15",
51
- "@arcblock/jwt": "^1.19.15",
52
- "@arcblock/ux": "^2.12.48",
53
- "@arcblock/validator": "^1.19.15",
54
- "@blocklet/js-sdk": "^1.16.40",
55
- "@blocklet/logger": "^1.16.40",
56
- "@blocklet/payment-react": "1.18.24",
57
- "@blocklet/sdk": "^1.16.40",
58
- "@blocklet/ui-react": "^2.12.48",
59
- "@blocklet/uploader": "^0.1.81",
60
- "@blocklet/xss": "^0.1.30",
49
+ "@arcblock/did-connect": "^2.12.60",
50
+ "@arcblock/did-util": "^1.19.19",
51
+ "@arcblock/jwt": "^1.19.19",
52
+ "@arcblock/ux": "^2.12.60",
53
+ "@arcblock/validator": "^1.19.19",
54
+ "@blocklet/js-sdk": "^1.16.41",
55
+ "@blocklet/logger": "^1.16.41",
56
+ "@blocklet/payment-react": "1.18.26",
57
+ "@blocklet/sdk": "^1.16.41",
58
+ "@blocklet/ui-react": "^2.12.60",
59
+ "@blocklet/uploader": "^0.1.82",
60
+ "@blocklet/xss": "^0.1.31",
61
61
  "@mui/icons-material": "^5.16.6",
62
62
  "@mui/lab": "^5.0.0-alpha.173",
63
63
  "@mui/material": "^5.16.6",
64
64
  "@mui/system": "^5.16.6",
65
- "@ocap/asset": "^1.19.15",
66
- "@ocap/client": "^1.19.15",
67
- "@ocap/mcrypto": "^1.19.15",
68
- "@ocap/util": "^1.19.15",
69
- "@ocap/wallet": "^1.19.15",
65
+ "@ocap/asset": "^1.19.19",
66
+ "@ocap/client": "^1.19.19",
67
+ "@ocap/mcrypto": "^1.19.19",
68
+ "@ocap/util": "^1.19.19",
69
+ "@ocap/wallet": "^1.19.19",
70
70
  "@stripe/react-stripe-js": "^2.7.3",
71
71
  "@stripe/stripe-js": "^2.4.0",
72
72
  "ahooks": "^3.8.0",
@@ -119,9 +119,9 @@
119
119
  "web3": "^4.16.0"
120
120
  },
121
121
  "devDependencies": {
122
- "@abtnode/types": "^1.16.40",
122
+ "@abtnode/types": "^1.16.41",
123
123
  "@arcblock/eslint-config-ts": "^0.3.3",
124
- "@blocklet/payment-types": "1.18.24",
124
+ "@blocklet/payment-types": "1.18.26",
125
125
  "@types/cookie-parser": "^1.4.7",
126
126
  "@types/cors": "^2.8.17",
127
127
  "@types/debug": "^4.1.12",
@@ -151,7 +151,7 @@
151
151
  "vite": "^5.3.5",
152
152
  "vite-node": "^2.0.4",
153
153
  "vite-plugin-babel-import": "^2.0.5",
154
- "vite-plugin-blocklet": "^0.9.29",
154
+ "vite-plugin-blocklet": "^0.9.31",
155
155
  "vite-plugin-node-polyfills": "^0.21.0",
156
156
  "vite-plugin-svgr": "^4.2.0",
157
157
  "vite-tsconfig-paths": "^4.3.2",
@@ -167,5 +167,5 @@
167
167
  "parser": "typescript"
168
168
  }
169
169
  },
170
- "gitHead": "6c36b088743e696ce250cda11f8502b3886df0ef"
170
+ "gitHead": "7f95dbeba13242d2b7e77ac3f4e69a7f764ed213"
171
171
  }
@@ -1,14 +1,29 @@
1
1
  import 'react-international-phone/style.css';
2
2
 
3
3
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
- import { FormInput, PhoneInput, CountrySelect, validatePhoneNumber } from '@blocklet/payment-react';
4
+ import {
5
+ FormInput,
6
+ PhoneInput,
7
+ CountrySelect,
8
+ validatePhoneNumber,
9
+ getPhoneUtil,
10
+ validatePostalCode,
11
+ } from '@blocklet/payment-react';
5
12
  import { FormLabel, Stack } from '@mui/material';
6
- import { Controller, useFormContext } from 'react-hook-form';
13
+ import { Controller, useFormContext, useWatch } from 'react-hook-form';
7
14
  import isEmail from 'validator/es/lib/isEmail';
15
+ import { useMount } from 'ahooks';
8
16
 
9
17
  export default function CustomerForm() {
10
18
  const { t } = useLocaleContext();
11
19
  const { control } = useFormContext();
20
+ useMount(() => {
21
+ getPhoneUtil().catch((err) => {
22
+ console.error('Failed to preload phone validator:', err);
23
+ });
24
+ });
25
+
26
+ const country = useWatch({ control, name: 'address.country' });
12
27
 
13
28
  return (
14
29
  <Stack
@@ -148,6 +163,10 @@ export default function CustomerForm() {
148
163
  value: 20,
149
164
  message: t('common.maxLength', { len: 20 }),
150
165
  },
166
+ validate: (x: string) => {
167
+ const isValid = validatePostalCode(x, country);
168
+ return isValid ? true : t('payment.checkout.invalid');
169
+ },
151
170
  }}
152
171
  />
153
172
  </Stack>