payment-kit 1.14.37 → 1.14.39

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.
@@ -189,6 +189,7 @@ export async function getStakeSummaryByDid(did: string, livemode: boolean = true
189
189
  return {};
190
190
  }
191
191
 
192
+ // FIXME: should use listStakes to find all stakes and summarize here
192
193
  const address = toStakeAddress(did, wallet.address);
193
194
  const results: GroupedBN = {};
194
195
  await Promise.all(
@@ -44,7 +44,7 @@ export async function ensureStripeProduct(internal: Product, method: PaymentMeth
44
44
  attrs.unit_label = internal.unit_label;
45
45
  }
46
46
  if (internal.statement_descriptor) {
47
- attrs.statement_descriptor_suffix = internal.statement_descriptor;
47
+ attrs.statement_descriptor_suffix = '';
48
48
  }
49
49
 
50
50
  const product = await client.products.create(attrs);
@@ -176,7 +176,7 @@ export async function ensureStripePaymentIntent(
176
176
  enabled: true,
177
177
  allow_redirects: 'never',
178
178
  },
179
- statement_descriptor_suffix: internal.statement_descriptor,
179
+ statement_descriptor_suffix: '',
180
180
  metadata: {
181
181
  appPid: env.appPid,
182
182
  id: internal.id,
@@ -163,13 +163,13 @@ function validateMetadataValue(value: any, helpers: any) {
163
163
  export const MetadataSchema = Joi.alternatives()
164
164
  .try(
165
165
  Joi.object()
166
- .pattern(Joi.string().max(64), Joi.any().custom(validateMetadataValue, 'Custom Validation'))
166
+ .pattern(Joi.string().max(40), Joi.any().custom(validateMetadataValue, 'Custom Validation'))
167
167
  .min(0)
168
168
  .allow(null),
169
169
  Joi.array()
170
170
  .items(
171
171
  Joi.object({
172
- key: Joi.string().max(64).required(),
172
+ key: Joi.string().max(40).required(),
173
173
  value: Joi.any().custom(validateMetadataValue, 'Custom Validation').required(),
174
174
  })
175
175
  )
@@ -30,7 +30,7 @@ import dayjs from './dayjs';
30
30
  import env from './env';
31
31
  import logger from './logger';
32
32
  import { getPriceCurrencyOptions, getPriceUintAmountByCurrency, getRecurringPeriod } from './session';
33
- import { getConnectQueryParam } from './util';
33
+ import { getConnectQueryParam, getCustomerStakeAddress } from './util';
34
34
 
35
35
  export function getCustomerSubscriptionPageUrl({
36
36
  subscriptionId,
@@ -700,16 +700,25 @@ export async function getSubscriptionRemainingStakeSetup(
700
700
  paymentMethod: PaymentMethod,
701
701
  action: 'return' | 'slash' = 'return'
702
702
  ) {
703
+ const currency = await PaymentCurrency.findByPk(subscription.currency_id);
704
+ if (!currency) {
705
+ return {
706
+ total: '0',
707
+ return_amount: '0',
708
+ sender: '',
709
+ };
710
+ }
711
+
703
712
  const client = paymentMethod.getOcapClient();
704
713
  const { state } = await client.getStakeState({ address });
705
- const currency = await PaymentCurrency.findByPk(subscription.currency_id);
706
- if (!state.tokens || !currency) {
714
+ if (!state) {
707
715
  return {
708
716
  total: '0',
709
717
  return_amount: '0',
710
718
  sender: '',
711
719
  };
712
720
  }
721
+
713
722
  let total = new BN(state.tokens.find((x: any) => x.address === currency.contract)?.value || '0');
714
723
  if (action === 'slash') {
715
724
  // add revoked tokens to total
@@ -763,17 +772,18 @@ export async function checkRemainingStake(
763
772
  revoked: '0',
764
773
  };
765
774
  }
775
+
766
776
  const client = paymentMethod.getOcapClient();
767
777
  const { state } = await client.getStakeState({ address });
768
-
769
778
  if (!state) {
770
- logger.warn('getStakeState failed in checkRemainingStake', { address, paymentMethod, paymentCurrency });
779
+ logger.warn('getStakeState failed in checkRemainingStake', { address });
771
780
  return {
772
781
  enough: false,
773
782
  staked: '0',
774
783
  revoked: '0',
775
784
  };
776
785
  }
786
+
777
787
  const staked = state.tokens?.find((x: any) => x.address === paymentCurrency.contract);
778
788
  const revoked = state.revokedTokens?.find((x: any) => x.address === paymentCurrency.contract);
779
789
  let total = new BN(0);
@@ -783,6 +793,7 @@ export async function checkRemainingStake(
783
793
  if (revoked) {
784
794
  total = total.add(new BN(revoked?.value || '0'));
785
795
  }
796
+
786
797
  return {
787
798
  enough: total.gte(new BN(amount)),
788
799
  staked,
@@ -805,3 +816,10 @@ export function getSubscriptionTrialSetup(data: Partial<SubscriptionData>, curre
805
816
  trialEnd,
806
817
  };
807
818
  }
819
+
820
+ export async function getSubscriptionStakeAddress(subscription: Subscription, customerDid: string) {
821
+ return (
822
+ subscription.payment_details?.arcblock?.staking?.address ||
823
+ (await getCustomerStakeAddress(customerDid, subscription.id))
824
+ );
825
+ }
@@ -2,11 +2,14 @@ import crypto from 'crypto';
2
2
 
3
3
  import { getUrl } from '@blocklet/sdk/lib/component';
4
4
  import env from '@blocklet/sdk/lib/env';
5
+ import { getWalletDid } from '@blocklet/sdk/lib/did';
6
+ import { toStakeAddress } from '@arcblock/did-util';
5
7
  import { customAlphabet } from 'nanoid';
6
8
  import type { LiteralUnion } from 'type-fest';
7
9
  import { withQuery } from 'ufo';
8
10
 
9
11
  import dayjs from './dayjs';
12
+ import { blocklet, wallet } from './auth';
10
13
 
11
14
  export const OCAP_PAYMENT_TX_TYPE = 'fg:t:transfer_v2';
12
15
 
@@ -260,3 +263,12 @@ export function formatAmountPrecisionLimit(
260
263
  }
261
264
  return '';
262
265
  }
266
+
267
+ export async function getCustomerStakeAddress(customerDid: string, nonce?: string) {
268
+ const { user } = await blocklet.getUser(customerDid, { enableConnectedAccount: true });
269
+ if (user) {
270
+ return toStakeAddress(getWalletDid(user), wallet.address, nonce);
271
+ }
272
+
273
+ return toStakeAddress(customerDid, wallet.address, nonce);
274
+ }
@@ -1,6 +1,5 @@
1
1
  import isEmpty from 'lodash/isEmpty';
2
2
 
3
- import { toStakeAddress } from '@arcblock/did-util';
4
3
  import { ensureStakedForGas } from '../integrations/arcblock/stake';
5
4
  import { transferErc20FromUser } from '../integrations/ethereum/token';
6
5
  import { createEvent } from '../libs/audit';
@@ -19,6 +18,7 @@ import {
19
18
  getMaxRetryCount,
20
19
  getMinRetryMail,
21
20
  getSubscriptionCreateSetup,
21
+ getSubscriptionStakeAddress,
22
22
  shouldCancelSubscription,
23
23
  } from '../libs/subscription';
24
24
  import { MAX_RETRY_COUNT, MIN_RETRY_MAIL, getNextRetry } from '../libs/util';
@@ -402,7 +402,7 @@ const handleStakeSlash = async (
402
402
  return;
403
403
  }
404
404
 
405
- const address = toStakeAddress(customer.did, wallet.address, subscription.id);
405
+ const address = await getSubscriptionStakeAddress(subscription, customer.did);
406
406
  const slashAmount = paymentIntent.amount;
407
407
  const stakeEnough = await checkRemainingStake(paymentMethod, paymentCurrency, address, slashAmount);
408
408
  if (!stakeEnough.enough) {
@@ -432,7 +432,7 @@ const handleStakeSlash = async (
432
432
  const signed = await client.signSlashStakeTx({
433
433
  tx: {
434
434
  itx: {
435
- address: toStakeAddress(customer.did, wallet.address, subscription.id),
435
+ address,
436
436
  outputs: [{ owner: wallet.address, tokens: [{ address: paymentCurrency.contract, value: slashAmount }] }],
437
437
  message: 'stake_slash_on_subscription_cancel',
438
438
  data: {
@@ -1,6 +1,5 @@
1
- import { toStakeAddress } from '@arcblock/did-util';
2
- import { isRefundReasonSupportedByStripe } from '@api/libs/refund';
3
- import { checkRemainingStake } from '@api/libs/subscription';
1
+ import { isRefundReasonSupportedByStripe } from '../libs/refund';
2
+ import { checkRemainingStake, getSubscriptionStakeAddress } from '../libs/subscription';
4
3
  import { sendErc20ToUser } from '../integrations/ethereum/token';
5
4
  import { wallet } from '../libs/auth';
6
5
  import CustomError from '../libs/error';
@@ -14,6 +13,7 @@ import { PaymentCurrency } from '../store/models/payment-currency';
14
13
  import { PaymentIntent } from '../store/models/payment-intent';
15
14
  import { PaymentMethod } from '../store/models/payment-method';
16
15
  import { Refund } from '../store/models/refund';
16
+ import { Subscription } from '../store/models/subscription';
17
17
  import type { PaymentError } from '../store/models/types';
18
18
 
19
19
  type RefundJob = {
@@ -313,7 +313,8 @@ const handleStakeReturnJob = async (
313
313
  return;
314
314
  }
315
315
  const client = paymentMethod.getOcapClient();
316
- const address = toStakeAddress(customer.did, wallet.address, refund.subscription_id);
316
+ const subscription = await Subscription.findByPk(refund.subscription_id);
317
+ const address = await getSubscriptionStakeAddress(subscription!, customer.did);
317
318
  const stakeEnough = await checkRemainingStake(paymentMethod, paymentCurrency, address, refund.amount);
318
319
  if (!stakeEnough.enough) {
319
320
  logger.warn('Stake return aborted because stake is not enough ', {
@@ -1,4 +1,3 @@
1
- import { toStakeAddress } from '@arcblock/did-util';
2
1
  import type { LiteralUnion } from 'type-fest';
3
2
 
4
3
  import { ensurePassportRevoked } from '../integrations/blocklet/passport';
@@ -15,6 +14,7 @@ import {
15
14
  checkRemainingStake,
16
15
  getSubscriptionCycleAmount,
17
16
  getSubscriptionCycleSetup,
17
+ getSubscriptionStakeAddress,
18
18
  getSubscriptionStakeReturnSetup,
19
19
  getSubscriptionStakeSlashSetup,
20
20
  shouldCancelSubscription,
@@ -329,7 +329,7 @@ const handleStakeSlashAfterCancel = async (subscription: Subscription) => {
329
329
 
330
330
  // check the staking
331
331
  const client = method.getOcapClient();
332
- const address = toStakeAddress(customer.did, wallet.address, subscription.id);
332
+ const address = await getSubscriptionStakeAddress(subscription, customer.did);
333
333
  const { state } = await client.getStakeState({ address });
334
334
  if (!state || !state.data?.value) {
335
335
  logger.warn('Stake slashing aborted because no staking state', {
@@ -374,7 +374,7 @@ const handleStakeSlashAfterCancel = async (subscription: Subscription) => {
374
374
  const signed = await client.signSlashStakeTx({
375
375
  tx: {
376
376
  itx: {
377
- address: toStakeAddress(customer.did, wallet.address, subscription.id),
377
+ address,
378
378
  outputs: [{ owner: wallet.address, tokens: [{ address: currency.contract, value: invoice.amount_remaining }] }],
379
379
  message: 'uncollectible_past_due_invoice',
380
380
  data: {
@@ -530,7 +530,6 @@ const slashStakeOnCancel = async (subscription: Subscription) => {
530
530
  });
531
531
  return;
532
532
  }
533
- const address = toStakeAddress(customer.did, wallet.address, subscription.id);
534
533
  const currency = await PaymentCurrency.findByPk(subscription.currency_id);
535
534
  if (!currency) {
536
535
  logger.warn('Stake slashing skipped because currency not found', {
@@ -539,6 +538,7 @@ const slashStakeOnCancel = async (subscription: Subscription) => {
539
538
  });
540
539
  return;
541
540
  }
541
+ const address = await getSubscriptionStakeAddress(subscription, customer.did);
542
542
  const result = await getSubscriptionStakeSlashSetup(subscription, address, paymentMethod);
543
543
  const stakeEnough = await checkRemainingStake(paymentMethod, currency, address, result.return_amount);
544
544
  if (!stakeEnough.enough) {
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable @typescript-eslint/indent */
2
2
  /* eslint-disable prettier/prettier */
3
3
  import { toTypeInfo } from '@arcblock/did';
4
- import { toDelegateAddress, toStakeAddress } from '@arcblock/did-util';
4
+ import { toDelegateAddress } from '@arcblock/did-util';
5
5
  import type { Transaction } from '@ocap/client';
6
6
  import { BN, fromTokenToUnit, toBase58 } from '@ocap/util';
7
7
  import { fromPublicKey } from '@ocap/wallet';
@@ -21,7 +21,7 @@ import {
21
21
  getSubscriptionItemPrice,
22
22
  getSubscriptionStakeSetup,
23
23
  } from '../../libs/subscription';
24
- import { OCAP_PAYMENT_TX_TYPE } from '../../libs/util';
24
+ import { getCustomerStakeAddress, OCAP_PAYMENT_TX_TYPE } from '../../libs/util';
25
25
  import { invoiceQueue } from '../../queues/invoice';
26
26
  import type { TLineItemExpanded } from '../../store/models';
27
27
  import { CheckoutSession } from '../../store/models/checkout-session';
@@ -745,7 +745,7 @@ export async function getStakeTxClaim({
745
745
  if (paymentMethod.type === 'arcblock') {
746
746
  // create staking data
747
747
  const client = paymentMethod.getOcapClient();
748
- const address = toStakeAddress(userDid, wallet.address, subscription.id);
748
+ const address = await getCustomerStakeAddress(userDid, subscription.id);
749
749
  const { state } = await client.getStakeState({ address });
750
750
  const data = {
751
751
  type: 'json',
@@ -1026,7 +1026,7 @@ export async function executeOcapTransactions(
1026
1026
  type: 'delegate',
1027
1027
  staking: {
1028
1028
  tx_hash: stakingTxHash,
1029
- address: toStakeAddress(userDid, wallet.address, nonce),
1029
+ address: await getCustomerStakeAddress(userDid, nonce),
1030
1030
  },
1031
1031
  };
1032
1032
  }
@@ -21,11 +21,19 @@ const auth = authenticate<Product>({ component: true, roles: ['owner', 'admin']
21
21
  const ProductAndPriceSchema = Joi.object({
22
22
  name: Joi.string().max(64).empty('').optional(),
23
23
  type: Joi.string().valid('service', 'good').empty('').optional(),
24
- description: Joi.string().max(256).empty('').optional(),
24
+ description: Joi.string().max(250).empty('').optional(),
25
25
  images: Joi.any().optional(),
26
26
  metadata: MetadataSchema,
27
- statement_descriptor: Joi.string().max(32).empty('').optional(),
28
- unit_label: Joi.string().max(32).empty('').optional(),
27
+ statement_descriptor: Joi.string()
28
+ .max(22)
29
+ .pattern(/^(?=.*[A-Za-z])[^\u4e00-\u9fa5<>"’\\]*$/)
30
+ .messages({
31
+ 'string.pattern.base':
32
+ 'statement_descriptor should be at least one letter and cannot include Chinese characters and special characters such as <, >、"、’ or \\',
33
+ })
34
+ .empty('')
35
+ .optional(),
36
+ unit_label: Joi.string().max(12).empty('').optional(),
29
37
  nft_factory: Joi.string().max(40).allow(null).empty('').optional(),
30
38
  features: Joi.array()
31
39
  .items(Joi.object({ name: Joi.string().max(64).empty('').optional() }).unknown(true))
@@ -177,13 +177,11 @@ describe('MetadataSchema', () => {
177
177
 
178
178
  it('should invalidate an object with a key longer than 64 characters', () => {
179
179
  const data = {
180
- ['a'.repeat(65)]: 'value1',
180
+ ['a'.repeat(41)]: 'value1',
181
181
  };
182
182
  const { error } = MetadataSchema.validate(data);
183
183
  expect(error).toBeDefined();
184
- expect(error?.details?.[0]?.message).toMatch(
185
- /"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" is not allowed/
186
- );
184
+ expect(error?.details?.[0]?.message).toMatch(/"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" is not allowed/);
187
185
  });
188
186
 
189
187
  it('should invalidate an array with an object missing the key field', () => {
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.14.37
17
+ version: 1.14.39
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.14.37",
3
+ "version": "1.14.39",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -43,34 +43,34 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@abtnode/cron": "1.16.30",
46
- "@arcblock/did": "^1.18.132",
46
+ "@arcblock/did": "^1.18.135",
47
47
  "@arcblock/did-auth-storage-nedb": "^1.7.1",
48
- "@arcblock/did-connect": "^2.10.23",
49
- "@arcblock/did-util": "^1.18.132",
50
- "@arcblock/jwt": "^1.18.132",
51
- "@arcblock/ux": "^2.10.23",
52
- "@arcblock/validator": "^1.18.132",
48
+ "@arcblock/did-connect": "^2.10.25",
49
+ "@arcblock/did-util": "^1.18.135",
50
+ "@arcblock/jwt": "^1.18.135",
51
+ "@arcblock/ux": "2.10.24",
52
+ "@arcblock/validator": "^1.18.135",
53
53
  "@blocklet/js-sdk": "1.16.30",
54
54
  "@blocklet/logger": "1.16.30",
55
- "@blocklet/payment-react": "1.14.37",
55
+ "@blocklet/payment-react": "1.14.39",
56
56
  "@blocklet/sdk": "1.16.30",
57
- "@blocklet/ui-react": "^2.10.23",
57
+ "@blocklet/ui-react": "^2.10.25",
58
58
  "@blocklet/uploader": "^0.1.27",
59
59
  "@mui/icons-material": "^5.16.6",
60
60
  "@mui/lab": "^5.0.0-alpha.173",
61
61
  "@mui/material": "^5.16.6",
62
62
  "@mui/styles": "^5.16.6",
63
63
  "@mui/system": "^5.16.6",
64
- "@ocap/asset": "^1.18.132",
65
- "@ocap/client": "^1.18.132",
66
- "@ocap/mcrypto": "^1.18.132",
67
- "@ocap/util": "^1.18.132",
68
- "@ocap/wallet": "^1.18.132",
64
+ "@ocap/asset": "^1.18.135",
65
+ "@ocap/client": "^1.18.135",
66
+ "@ocap/mcrypto": "^1.18.135",
67
+ "@ocap/util": "^1.18.135",
68
+ "@ocap/wallet": "^1.18.135",
69
69
  "@react-pdf/renderer": "^3.4.4",
70
70
  "@stripe/react-stripe-js": "^2.7.3",
71
71
  "@stripe/stripe-js": "^2.4.0",
72
72
  "ahooks": "^3.8.0",
73
- "axios": "^1.7.2",
73
+ "axios": "^1.7.5",
74
74
  "body-parser": "^1.20.2",
75
75
  "cls-hooked": "^4.2.2",
76
76
  "cookie-parser": "^1.4.6",
@@ -119,7 +119,7 @@
119
119
  "devDependencies": {
120
120
  "@abtnode/types": "1.16.30",
121
121
  "@arcblock/eslint-config-ts": "^0.3.2",
122
- "@blocklet/payment-types": "1.14.37",
122
+ "@blocklet/payment-types": "1.14.39",
123
123
  "@types/cookie-parser": "^1.4.7",
124
124
  "@types/cors": "^2.8.17",
125
125
  "@types/debug": "^4.1.12",
@@ -139,13 +139,13 @@
139
139
  "npm-run-all": "^4.1.5",
140
140
  "prettier": "^3.3.3",
141
141
  "prettier-plugin-import-sort": "^0.0.7",
142
- "ts-jest": "^29.2.3",
142
+ "ts-jest": "^29.2.5",
143
143
  "ts-node": "^10.9.2",
144
144
  "type-fest": "^4.23.0",
145
145
  "typescript": "^4.9.5",
146
146
  "vite": "^5.3.5",
147
147
  "vite-node": "^2.0.4",
148
- "vite-plugin-blocklet": "^0.9.1",
148
+ "vite-plugin-blocklet": "^0.9.3",
149
149
  "vite-plugin-node-polyfills": "^0.21.0",
150
150
  "vite-plugin-svgr": "^4.2.0",
151
151
  "vite-tsconfig-paths": "^4.3.2",
@@ -161,5 +161,5 @@
161
161
  "parser": "typescript"
162
162
  }
163
163
  },
164
- "gitHead": "645ae0bdbab463eec878af0521eb982b51499639"
164
+ "gitHead": "a0f45877c7288ccd7a7f73b3fedc464c19cc0163"
165
165
  }
@@ -1,6 +1,7 @@
1
1
  import type { TCustomer } from '@blocklet/payment-types';
2
2
  import { Link } from 'react-router-dom';
3
3
 
4
+ import { getCustomerAvatar } from '@blocklet/payment-react';
4
5
  import InfoCard from '../info-card';
5
6
 
6
7
  export default function CustomerLink({ customer, linked }: { customer: TCustomer; linked?: boolean }) {
@@ -11,7 +12,11 @@ export default function CustomerLink({ customer, linked }: { customer: TCustomer
11
12
  return (
12
13
  <Link to={`/admin/customers/${customer.id}`}>
13
14
  <InfoCard
14
- logo={`/.well-known/service/user/avatar/${customer.did}?imageFilter=resize&w=48&h=48`}
15
+ logo={getCustomerAvatar(
16
+ customer?.did,
17
+ customer?.updated_at ? new Date(customer.updated_at).toISOString() : '',
18
+ 48
19
+ )}
15
20
  name={customer.email}
16
21
  description={`${customer.did.slice(0, 6)}...${customer.did.slice(-6)}`}
17
22
  />
@@ -21,7 +26,7 @@ export default function CustomerLink({ customer, linked }: { customer: TCustomer
21
26
 
22
27
  return (
23
28
  <InfoCard
24
- logo={`/.well-known/service/user/avatar/${customer.did}?imageFilter=resize&w=48&h=48`}
29
+ logo={getCustomerAvatar(customer.did, customer.updated_at ? new Date(customer.updated_at).toISOString() : '', 48)}
25
30
  name={customer.email}
26
31
  description={<span style={{ wordBreak: 'break-all' }}>{customer?.did}</span>}
27
32
  />
@@ -1,5 +1,5 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
- import { api, useMobile, usePaymentContext } from '@blocklet/payment-react';
2
+ import { api, getCustomerAvatar, useMobile, usePaymentContext } from '@blocklet/payment-react';
3
3
  import type { TCustomer } from '@blocklet/payment-types';
4
4
  import { Add, Close } from '@mui/icons-material';
5
5
  import { Button, Menu, MenuItem } from '@mui/material';
@@ -300,7 +300,7 @@ function SearchCustomers({ setSearch, search }: Pick<Props, 'setSearch' | 'searc
300
300
  setShow(null);
301
301
  }}>
302
302
  <InfoCard
303
- logo={`/.well-known/service/user/avatar/${x.did}?imageFilter=resize&w=48&h=48`}
303
+ logo={getCustomerAvatar(x?.did, x?.updated_at, 48)}
304
304
  name={x.email}
305
305
  key={x.id}
306
306
  description={`${x.did.slice(0, 6)}...${x.did.slice(-6)}`}
@@ -75,7 +75,7 @@ export default function MetadataForm({
75
75
  required: t('payment.checkout.required'),
76
76
  maxLength: {
77
77
  value: 64,
78
- message: t('common.maxLength', { len: 64 }),
78
+ message: t('common.maxLength', { len: 40 }),
79
79
  },
80
80
  }}
81
81
  placeholder="Key"
@@ -83,7 +83,7 @@ export default function MetadataForm({
83
83
  // @ts-ignore
84
84
  ref={errors?.metadata?.[index]?.key ? errorRef : null}
85
85
  inputProps={{
86
- maxLength: 64,
86
+ maxLength: 40,
87
87
  }}
88
88
  />
89
89
  <FormInput
@@ -104,6 +104,7 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
104
104
  control,
105
105
  setValue,
106
106
  formState: { errors },
107
+ trigger,
107
108
  } = useFormContext();
108
109
  const getFieldError = (name: string) => {
109
110
  const names = name?.split('.');
@@ -111,7 +112,6 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
111
112
  };
112
113
  const { settings, livemode } = usePaymentContext();
113
114
  const currencies = useFieldArray({ control, name: getFieldName('currency_options') });
114
-
115
115
  const priceLocked = useWatch({ control, name: getFieldName('locked') });
116
116
  const isRecurring = useWatch({ control, name: getFieldName('type') }) === 'recurring';
117
117
  const isMetered = useWatch({ control, name: getFieldName('recurring.usage_type') }) === 'metered';
@@ -138,6 +138,11 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
138
138
  return true;
139
139
  };
140
140
 
141
+ const handleRemoveCurrency = async (index: number) => {
142
+ await currencies.remove(index);
143
+ trigger(getFieldName('recurring.interval_config'));
144
+ };
145
+
141
146
  return (
142
147
  <Root direction="column" alignItems="flex-start" spacing={2}>
143
148
  {isLocked && <Alert severity="info">{t('admin.price.locked')}</Alert>}
@@ -280,7 +285,7 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
280
285
  );
281
286
  }}
282
287
  />
283
- <IconButton size="small" disabled={isLocked} onClick={() => currencies.remove(index)}>
288
+ <IconButton size="small" disabled={isLocked} onClick={() => handleRemoveCurrency(index)}>
284
289
  <DeleteOutlineOutlined color="error" sx={{ opacity: 0.75 }} />
285
290
  </IconButton>
286
291
  </Stack>
@@ -322,6 +327,19 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
322
327
  name={getFieldName('recurring.interval_config')}
323
328
  control={control}
324
329
  disabled={isLocked}
330
+ rules={{
331
+ validate: (val) => {
332
+ const hasStripe = currencies.fields?.some((x: any) => {
333
+ return !!settings.paymentMethods.find(
334
+ (y) => y?.type === 'stripe' && x?.currency_id === y?.default_currency_id
335
+ );
336
+ });
337
+ if (val === 'hour_1' && hasStripe) {
338
+ return t('admin.price.recurring.stripeTip');
339
+ }
340
+ return true;
341
+ },
342
+ }}
325
343
  render={({ field }) => (
326
344
  <Box>
327
345
  <FormLabel sx={{ color: 'text.primary' }}>{t('admin.price.recurring.interval')}</FormLabel>
@@ -333,8 +351,10 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
333
351
  setValue(getFieldName('recurring.interval'), interval);
334
352
  setValue(getFieldName('recurring.interval_count'), +count);
335
353
  setValue(getFieldName('recurring.interval_config'), e.target.value);
354
+ trigger(getFieldName('recurring.interval_config'));
336
355
  }}
337
356
  sx={{ width: INPUT_WIDTH }}
357
+ error={!!get(errors, getFieldName('recurring.interval_config'))}
338
358
  size="small">
339
359
  {!livemode && <MenuItem value="hour_1">{t('common.hourly')}</MenuItem>}
340
360
  <MenuItem value="day_1">{t('common.daily')}</MenuItem>
@@ -345,6 +365,12 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
345
365
  <MenuItem value="year_1">{t('common.yearly')}</MenuItem>
346
366
  <MenuItem value="month_2">{t('common.custom')}</MenuItem>
347
367
  </Select>
368
+ {get(errors, getFieldName('recurring.interval_config'))?.message && (
369
+ <Typography color="error" sx={{ fontSize: '0.75rem', mt: 0.5, ml: 1.75 }}>
370
+ {/* @ts-ignore */}
371
+ {get(errors, getFieldName('recurring.interval_config')).message}
372
+ </Typography>
373
+ )}
348
374
  </Box>
349
375
  )}
350
376
  />
@@ -57,8 +57,8 @@ export default function ProductForm(props: Props) {
57
57
  rules={{
58
58
  required: t('admin.product.description.required'),
59
59
  maxLength: {
60
- value: 256,
61
- message: t('common.maxLength', { len: 256 }),
60
+ value: 250,
61
+ message: t('common.maxLength', { len: 250 }),
62
62
  },
63
63
  }}
64
64
  label={t('admin.product.description.label')}
@@ -68,21 +68,27 @@ export default function ProductForm(props: Props) {
68
68
  multiline
69
69
  minRows={2}
70
70
  maxRows={4}
71
- inputProps={{ maxLength: 256 }}
71
+ inputProps={{ maxLength: 250 }}
72
72
  />
73
73
  <Collapse trigger={t('admin.product.additional')}>
74
74
  <Stack spacing={2} alignItems="flex-start">
75
75
  <FormInput
76
76
  name="statement_descriptor"
77
77
  label={t('admin.product.statement_descriptor.label')}
78
- rules={{ maxLength: { value: 32, message: t('common.maxLength', { len: 32 }) } }}
79
- inputProps={{ maxLength: 32 }}
78
+ rules={{
79
+ maxLength: { value: 22, message: t('common.maxLength', { len: 22 }) },
80
+ pattern: {
81
+ value: /^(?=.*[A-Za-z])[^\u4e00-\u9fa5<>"’\\]*$/,
82
+ message: t('common.latinOnly'),
83
+ },
84
+ }}
80
85
  />
81
86
  <FormInput
82
87
  name="unit_label"
83
88
  label={t('admin.product.unit_label.label')}
84
- rules={{ maxLength: { value: 32, message: t('common.maxLength', { len: 32 }) } }}
85
- inputProps={{ maxLength: 32 }}
89
+ rules={{
90
+ maxLength: { value: 12, message: t('common.maxLength', { len: 12 }) },
91
+ }}
86
92
  />
87
93
  {!props.simple && <ProductFeatures />}
88
94
  {!props.simple && <MetadataForm title={t('common.metadata.label')} />}
@@ -18,6 +18,9 @@ export default flat({
18
18
  exit: 'Exit',
19
19
  maxLength: 'Max {len} characters',
20
20
  minLength: 'Min {len} characters',
21
+ invalidCharacters: 'Invalid characters',
22
+ latinOnly:
23
+ 'At least one letter and cannot include Chinese characters and special characters such as <, >、"、’ or \\',
21
24
  loading: 'Loading...',
22
25
  },
23
26
  admin: {
@@ -148,6 +151,7 @@ export default flat({
148
151
  'Metered billing lets you charge customers based on reported usage at the end of each billing period.',
149
152
  aggregate: 'Charge for metered usage by',
150
153
  intervalCountTip: 'Billing interval must be a positive integer',
154
+ stripeTip: 'Stripe requires the billing period to be greater than or equal to 1 day',
151
155
  },
152
156
  currency: {
153
157
  add: 'Add more currencies',
@@ -18,6 +18,8 @@ export default flat({
18
18
  exit: '退出',
19
19
  maxLength: '最多输入{len}个字符',
20
20
  minLength: '最少输入{len}个字符',
21
+ invalidCharacters: '无效字符',
22
+ latinOnly: '至少包含一个字母,并且不能包含中文字符和特殊字符如 <, >、"、’ 或 \\',
21
23
  loading: '加载中...',
22
24
  },
23
25
  admin: {
@@ -144,6 +146,7 @@ export default flat({
144
146
  meteredTip: '计量计费允许您根据每个计费周期结束时的报告的使用情况向客户收费。',
145
147
  aggregate: '按何种方式收费计量使用',
146
148
  intervalCountTip: '计费周期必须是正整数',
149
+ stripeTip: 'Stripe要求计费周期不能为小时',
147
150
  },
148
151
  currency: {
149
152
  add: '添加更多货币',
@@ -2,7 +2,15 @@
2
2
  import DidAddress from '@arcblock/ux/lib/DID';
3
3
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
4
  import Toast from '@arcblock/ux/lib/Toast';
5
- import { api, formatBNStr, formatError, formatTime, useMobile, usePaymentContext } from '@blocklet/payment-react';
5
+ import {
6
+ api,
7
+ formatBNStr,
8
+ formatError,
9
+ formatTime,
10
+ getCustomerAvatar,
11
+ useMobile,
12
+ usePaymentContext,
13
+ } from '@blocklet/payment-react';
6
14
  import type { GroupedBN, TCustomerExpanded, TPaymentMethodExpanded } from '@blocklet/payment-types';
7
15
  import { ArrowBackOutlined } from '@mui/icons-material';
8
16
  import { Alert, Avatar, Box, Button, CircularProgress, Divider, Stack, Typography } from '@mui/material';
@@ -196,7 +204,11 @@ export default function CustomerDetail(props: { id: string }) {
196
204
  <Stack direction="row" alignItems="center" spacing={1}>
197
205
  <Avatar
198
206
  title={data.customer.name}
199
- src={`/.well-known/service/user/avatar/${data?.customer?.did}`}
207
+ src={getCustomerAvatar(
208
+ data.customer?.did,
209
+ data.customer?.updated_at ? new Date(data.customer.updated_at).toISOString() : '',
210
+ 52
211
+ )}
200
212
  variant="square"
201
213
  sx={{ width: 52, height: 52, borderRadius: 'var(--radius-s, 4px)' }}
202
214
  />
@@ -2,7 +2,7 @@
2
2
  import { getDurableData } from '@arcblock/ux/lib/Datatable';
3
3
  import DidAddress from '@arcblock/ux/lib/DID';
4
4
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
5
- import { api, formatTime, Table } from '@blocklet/payment-react';
5
+ import { api, formatTime, getCustomerAvatar, Table } from '@blocklet/payment-react';
6
6
  import type { TCustomer } from '@blocklet/payment-types';
7
7
  import { Avatar, CircularProgress, Stack, Typography } from '@mui/material';
8
8
  import { useEffect, useState } from 'react';
@@ -60,7 +60,11 @@ export default function CustomersList() {
60
60
  <Link to={`/admin/customers/${item.id}`}>
61
61
  <Stack direction="row" alignItems="center" spacing={1}>
62
62
  <Avatar
63
- src={`/.well-known/service/user/avatar/${item.did}?imageFilter=resize&w=48&h=48`}
63
+ src={getCustomerAvatar(
64
+ item?.did,
65
+ item?.updated_at ? new Date(item.updated_at).toISOString() : '',
66
+ 48
67
+ )}
64
68
  variant="square"
65
69
  sx={{ borderRadius: 'var(--radius-m, 8px)' }}
66
70
  />
@@ -5,6 +5,7 @@ import {
5
5
  CustomerInvoiceList,
6
6
  formatBNStr,
7
7
  formatError,
8
+ getCustomerAvatar,
8
9
  getPrefix,
9
10
  TruncatedText,
10
11
  useMobile,
@@ -337,7 +338,7 @@ export default function CustomerHome() {
337
338
  <Box display="flex" alignItems="center" gap={1} flexWrap="wrap" sx={{ mb: 3 }}>
338
339
  <Avatar
339
340
  title={data?.name}
340
- src={`/.well-known/service/user/avatar/${data?.did}`}
341
+ src={getCustomerAvatar(data?.did, data?.updated_at ? new Date(data.updated_at).toISOString() : '', 48)}
341
342
  variant="circular"
342
343
  sx={{ width: 48, height: 48 }}
343
344
  />