payment-kit 1.19.1 → 1.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/api/src/libs/notification/template/customer-credit-grant-low-balance.ts +1 -1
  2. package/api/src/libs/notification/template/customer-credit-insufficient.ts +1 -1
  3. package/api/src/libs/security.ts +6 -3
  4. package/api/src/libs/util.ts +3 -1
  5. package/api/src/queues/credit-consume.ts +15 -2
  6. package/api/src/routes/checkout-sessions.ts +9 -4
  7. package/api/src/routes/credit-grants.ts +24 -2
  8. package/api/src/routes/customers.ts +36 -12
  9. package/api/src/routes/payment-currencies.ts +8 -0
  10. package/api/src/routes/payment-methods.ts +1 -0
  11. package/api/src/routes/webhook-endpoints.ts +0 -3
  12. package/api/src/store/migrations/20250610-billing-credit.ts +0 -3
  13. package/api/src/store/migrations/20250708-currency-precision.ts +14 -0
  14. package/api/src/store/models/webhook-attempt.ts +1 -1
  15. package/blocklet.yml +1 -1
  16. package/package.json +25 -25
  17. package/src/components/conditional-section.tsx +87 -0
  18. package/src/components/customer/credit-overview.tsx +30 -17
  19. package/src/components/customer/form.tsx +2 -1
  20. package/src/components/edit-in-line.tsx +197 -0
  21. package/src/components/metadata/form.tsx +2 -2
  22. package/src/components/meter/add-usage-dialog.tsx +2 -2
  23. package/src/components/meter/form.tsx +2 -2
  24. package/src/components/meter/products.tsx +2 -2
  25. package/src/components/payment-link/item.tsx +2 -2
  26. package/src/components/payouts/portal/list.tsx +6 -11
  27. package/src/components/price/currency-select.tsx +13 -9
  28. package/src/components/price/form.tsx +47 -16
  29. package/src/components/product/form.tsx +3 -8
  30. package/src/components/subscription/portal/list.tsx +0 -1
  31. package/src/locales/en.tsx +6 -3
  32. package/src/locales/zh.tsx +6 -3
  33. package/src/pages/admin/customers/customers/detail.tsx +5 -13
  34. package/src/pages/admin/settings/payment-methods/index.tsx +56 -85
  35. package/src/pages/customer/index.tsx +17 -15
  36. package/src/pages/customer/recharge/account.tsx +1 -1
@@ -52,7 +52,7 @@ export class CustomerCreditGrantLowBalanceEmailTemplate
52
52
  // 计算百分比
53
53
  const available = new BN(creditGrant.remaining_amount);
54
54
  const total = new BN(creditGrant.amount);
55
- const percentage = total.gt(0) ? available.mul(new BN(100)).div(total).toString() : '0';
55
+ const percentage = total.gt(new BN(0)) ? available.mul(new BN(100)).div(total).toString() : '0';
56
56
 
57
57
  return {
58
58
  locale,
@@ -58,7 +58,7 @@ export class CustomerCreditInsufficientEmailTemplate
58
58
  const at = formatTime(Date.now());
59
59
 
60
60
  // 检查是否完全耗尽(可用额度为0或负数)
61
- const isExhausted = new BN(this.options.availableAmount).lte(0);
61
+ const isExhausted = new BN(this.options.availableAmount).lte(new BN(0));
62
62
 
63
63
  // 如果有订阅ID,获取订阅信息
64
64
  let productName: string | undefined;
@@ -17,6 +17,7 @@ type PermissionSpec<T extends Model> = {
17
17
  // allow record owner
18
18
  model: T;
19
19
  field: string;
20
+ findById?: (id: string) => Promise<T | null>;
20
21
  };
21
22
  mine?: boolean;
22
23
  embed?: boolean;
@@ -105,9 +106,11 @@ export function authenticate<T extends Model>({ component, roles, record, mine,
105
106
 
106
107
  // authenticate by record owner
107
108
  if (record) {
108
- const { model, field = 'customer_id' } = record;
109
- // @ts-ignore
110
- const doc: T | null = await model.findByPk(req.params.id);
109
+ const { model, field = 'customer_id', findById } = record;
110
+ const doc: T | null =
111
+ findById && typeof findById === 'function'
112
+ ? await findById(req.params.id as string)
113
+ : await (model as any).findByPk(req.params.id);
111
114
  if (doc && doc[field as keyof T]) {
112
115
  const customer = await Customer.findOne({ where: { did: req.user.did } });
113
116
  req.doc = doc;
@@ -12,6 +12,7 @@ import { joinURL, withQuery, withTrailingSlash } from 'ufo';
12
12
  import axios from 'axios';
13
13
  import { ethers } from 'ethers';
14
14
  import { fromUnitToToken } from '@ocap/util';
15
+ import get from 'lodash/get';
15
16
  import dayjs from './dayjs';
16
17
  import { blocklet, wallet } from './auth';
17
18
  import type { PaymentCurrency, PaymentMethod, Subscription } from '../store/models';
@@ -268,11 +269,12 @@ export async function getUserOrAppInfo(
268
269
  }
269
270
  const { user } = await blocklet.getUser(address);
270
271
  if (user) {
272
+ const locale = get(user, 'locale', 'en');
271
273
  return {
272
274
  name: user?.fullName,
273
275
  avatar: joinURL(process.env.BLOCKLET_APP_URL!, user?.avatar),
274
276
  type: 'user',
275
- url: getCustomerProfileUrl({ userDid: address, locale: 'en' }),
277
+ url: getCustomerProfileUrl({ userDid: address, locale }),
276
278
  };
277
279
  }
278
280
  return {
@@ -227,9 +227,9 @@ async function consumeAvailableCredits(
227
227
  metadata: {
228
228
  meter_event_id: context.meterEvent.id,
229
229
  meter_event_name: context.meterEvent.event_name,
230
- required_amount: totalRequiredAmount,
230
+ required_amount: remainingToConsume.toString(),
231
231
  available_amount: totalAvailable.toString(),
232
- consumed_amount: totalConsumed.toString(),
232
+ consumed_amount: consumed.toString(),
233
233
  pending_amount: pendingAmount,
234
234
  currency_id: currencyId,
235
235
  subscription_id: context.subscription?.id,
@@ -258,6 +258,19 @@ async function consumeAvailableCredits(
258
258
  },
259
259
  });
260
260
  }
261
+ } else if (remainingBalance === '0') {
262
+ await createEvent('Customer', 'customer.credit.insufficient', context.customer, {
263
+ metadata: {
264
+ meter_event_id: context.meterEvent.id,
265
+ meter_event_name: context.meterEvent.event_name,
266
+ required_amount: remainingToConsume.toString(),
267
+ available_amount: '0',
268
+ consumed_amount: consumed.toString(),
269
+ pending_amount: pendingAmount,
270
+ currency_id: currencyId,
271
+ subscription_id: context.subscription?.id,
272
+ },
273
+ }).catch(console.error);
261
274
  }
262
275
 
263
276
  return {
@@ -1010,7 +1010,6 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
1010
1010
  }
1011
1011
 
1012
1012
  const checkoutSession = req.doc as CheckoutSession;
1013
- logger.info('---checkoutSession---', checkoutSession.line_items);
1014
1013
  if (checkoutSession.line_items) {
1015
1014
  try {
1016
1015
  await validateInventory(checkoutSession.line_items);
@@ -1091,9 +1090,15 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
1091
1090
  } else {
1092
1091
  const updates: Record<string, any> = {};
1093
1092
  if (checkoutSession.customer_update?.name) {
1094
- updates.name = req.body.customer_name;
1095
- updates.email = req.body.customer_email;
1096
- updates.phone = req.body.customer_phone;
1093
+ if (req.body.customer_name) {
1094
+ updates.name = req.body.customer_name;
1095
+ }
1096
+ if (req.body.customer_email) {
1097
+ updates.email = req.body.customer_email;
1098
+ }
1099
+ if (req.body.customer_phone) {
1100
+ updates.phone = req.body.customer_phone;
1101
+ }
1097
1102
  }
1098
1103
  if (checkoutSession.customer_update?.address) {
1099
1104
  updates.address = Customer.formatUpdateAddress(req.body.billing_address, customer);
@@ -10,6 +10,7 @@ import { authenticate } from '../libs/security';
10
10
  import { CreditGrant, Customer, PaymentCurrency, Price, Subscription } from '../store/models';
11
11
  import { createCreditGrant } from '../libs/credit-grant';
12
12
  import { getMeterPriceIdsFromSubscription } from '../libs/subscription';
13
+ import { blocklet } from '../libs/auth';
13
14
 
14
15
  const router = Router();
15
16
  const auth = authenticate<CreditGrant>({ component: true, roles: ['owner', 'admin'] });
@@ -179,9 +180,30 @@ router.post('/', auth, async (req, res) => {
179
180
  return res.status(404).json({ error: `PaymentCurrency ${currencyId} not found` });
180
181
  }
181
182
 
182
- const customer = await Customer.findByPkOrDid(req.body.customer_id);
183
+ let customer = await Customer.findByPkOrDid(req.body.customer_id);
183
184
  if (!customer) {
184
- return res.status(404).json({ error: `Customer ${req.body.customer_id} not found` });
185
+ const { user: userInfo } = await blocklet.getUser(req.body.customer_id);
186
+ if (!userInfo) {
187
+ return res.status(404).json({ error: `User ${req.body.customer_id} not found` });
188
+ }
189
+ customer = await Customer.create({
190
+ livemode: true,
191
+ did: userInfo.did,
192
+ name: userInfo.fullName,
193
+ email: userInfo.email || '',
194
+ phone: userInfo.phone || '',
195
+ address: Customer.formatAddressFromUser(userInfo),
196
+ description: userInfo.remark || '',
197
+ metadata: {},
198
+ balance: '0',
199
+ next_invoice_sequence: 1,
200
+ delinquent: false,
201
+ invoice_prefix: Customer.getInvoicePrefix(),
202
+ });
203
+ logger.info('Customer created on credit grant', {
204
+ customerId: customer.id,
205
+ customer: customer.toJSON(),
206
+ });
185
207
  }
186
208
 
187
209
  const unitAmount = fromTokenToUnit(req.body.amount, paymentCurrency.decimal).toString();
@@ -37,6 +37,7 @@ const authPortal = authenticate<Customer>({
37
37
  // @ts-ignore
38
38
  model: Customer,
39
39
  field: 'id',
40
+ findById: (id: string) => Customer.findByPkOrDid(id),
40
41
  },
41
42
  });
42
43
 
@@ -45,9 +46,6 @@ router.get('/', auth, async (req, res) => {
45
46
  const { page, pageSize, ...query } = await schema.validateAsync(req.query, { stripUnknown: true });
46
47
  const where = getWhereFromKvQuery(query.q);
47
48
 
48
- if (typeof query.livemode === 'boolean') {
49
- where.livemode = query.livemode;
50
- }
51
49
  if (query.did) {
52
50
  where.did = query.did;
53
51
  }
@@ -220,18 +218,12 @@ router.post('/sync-to-space', sessionMiddleware(), async (req, res) => {
220
218
  });
221
219
 
222
220
  // get overdue invoices
223
- router.get('/:id/overdue/invoices', sessionMiddleware(), async (req, res) => {
224
- if (!req.user) {
225
- return res.status(403).json({ error: 'Unauthorized' });
226
- }
221
+ router.get('/:id/overdue/invoices', authPortal, async (req, res) => {
227
222
  try {
228
223
  const doc = await Customer.findByPkOrDid(req.params.id as string);
229
224
  if (!doc) {
230
225
  return res.status(404).json({ error: 'Customer not found' });
231
226
  }
232
- if (doc.did !== req.user.did && !['admin', 'owner'].includes(req.user?.role)) {
233
- return res.status(403).json({ error: 'You are not allowed to access this customer invoices' });
234
- }
235
227
  const { rows: invoices, count } = await Invoice.findAndCountAll({
236
228
  where: {
237
229
  customer_id: doc.id,
@@ -386,16 +378,48 @@ router.get('/payer-token', sessionMiddleware({ accessKey: true }), async (req, r
386
378
  });
387
379
 
388
380
  router.get('/:id', auth, async (req, res) => {
381
+ if (!req.params.id) {
382
+ return res.status(400).json({ error: 'Customer ID is required' });
383
+ }
389
384
  try {
390
385
  const doc = await Customer.findByPkOrDid(req.params.id as string);
391
386
  if (doc) {
392
387
  res.json(doc);
393
388
  } else {
394
- res.status(404).json(null);
389
+ if (req.body.create) {
390
+ if (!req.user) {
391
+ return res.status(403).json({ error: 'Unauthorized' });
392
+ }
393
+ const { user } = await blocklet.getUser(req.params.id);
394
+ if (!user) {
395
+ return res.status(404).json({ error: 'User not found' });
396
+ }
397
+ const customer = await Customer.create({
398
+ livemode: true,
399
+ did: user.did,
400
+ name: user.fullName,
401
+ email: user.email,
402
+ phone: user.phone,
403
+ address: Customer.formatAddressFromUser(user),
404
+ description: user.remark,
405
+ metadata: {},
406
+ balance: '0',
407
+ next_invoice_sequence: 1,
408
+ delinquent: false,
409
+ invoice_prefix: Customer.getInvoicePrefix(),
410
+ });
411
+ logger.info('customer created', {
412
+ customerId: customer.id,
413
+ did: customer.did,
414
+ });
415
+ return res.json(customer);
416
+ }
417
+ return res.status(404).json(null);
395
418
  }
419
+ return res.status(404).json(null);
396
420
  } catch (err) {
397
421
  logger.error(err);
398
- res.status(500).json({ error: `Failed to get customer: ${err.message}` });
422
+ return res.status(500).json({ error: `Failed to get customer: ${err.message}` });
399
423
  }
400
424
  });
401
425
 
@@ -84,6 +84,7 @@ router.post('/', auth, async (req, res) => {
84
84
  symbol: info.symbol,
85
85
  decimal: info.decimal,
86
86
  type: 'standard',
87
+ maximum_precision: 6,
87
88
 
88
89
  // FIXME: make these configurable
89
90
  minimum_payment_amount: fromTokenToUnit(0.000001, info.decimal).toString(),
@@ -121,6 +122,7 @@ router.post('/', auth, async (req, res) => {
121
122
  symbol: state.symbol,
122
123
  decimal: state.decimal,
123
124
  type: 'standard',
125
+ maximum_precision: 6,
124
126
 
125
127
  // FIXME: make these configurable
126
128
  minimum_payment_amount: fromTokenToUnit(0.000001, state.decimal).toString(),
@@ -148,6 +150,12 @@ router.get('/', auth, async (req, res) => {
148
150
  if (typeof query.livemode === 'string') {
149
151
  where.livemode = JSON.parse(query.livemode);
150
152
  }
153
+ where.type = 'standard';
154
+ if (query.credit) {
155
+ where.type = {
156
+ [Op.in]: ['standard', 'credit'],
157
+ };
158
+ }
151
159
  const list = await PaymentCurrency.findAll({
152
160
  where,
153
161
  order: [['created_at', 'DESC']],
@@ -173,6 +173,7 @@ router.post('/', auth, async (req, res) => {
173
173
  decimal: 18,
174
174
 
175
175
  minimum_payment_amount: fromTokenToUnit(0.000001, 18).toString(),
176
+ maximum_precision: 6,
176
177
  maximum_payment_amount: fromTokenToUnit(100000000, 18).toString(),
177
178
 
178
179
  contract: '',
@@ -43,9 +43,6 @@ router.get('/', auth, async (req, res) => {
43
43
  const { page, pageSize, status, ...query } = await schema.validateAsync(req.query, { stripUnknown: true });
44
44
  const where: WhereOptions<WebhookEndpoint> = {};
45
45
 
46
- if (typeof query.livemode === 'boolean') {
47
- where.livemode = query.livemode;
48
- }
49
46
  if (status) {
50
47
  where.status = status
51
48
  .split(',')
@@ -21,9 +21,6 @@ export const up: Migration = async ({ context }) => {
21
21
  },
22
22
  ],
23
23
  });
24
- await context.sequelize.query(`
25
- UPDATE payment_currencies SET maximum_precision = 2 WHERE type = 'standard';
26
- `);
27
24
 
28
25
  await context.sequelize.query(`
29
26
  UPDATE payment_currencies
@@ -0,0 +1,14 @@
1
+ import { Migration } from '../migrate';
2
+
3
+ export const up: Migration = async ({ context }) => {
4
+ await context.sequelize.query(`
5
+ UPDATE payment_currencies
6
+ SET maximum_precision = 6
7
+ WHERE payment_method_id IN (
8
+ SELECT id FROM payment_methods WHERE type IN ('arcblock', 'ethereum', 'base')
9
+ )
10
+ AND type != 'credit';
11
+ `);
12
+ };
13
+
14
+ export const down = () => {};
@@ -42,7 +42,7 @@ export class WebhookAttempt extends Model<InferAttributes<WebhookAttempt>, Infer
42
42
  allowNull: false,
43
43
  },
44
44
  status: {
45
- type: DataTypes.ENUM('enabled', 'disabled'),
45
+ type: DataTypes.ENUM('succeeded', 'failed'),
46
46
  allowNull: false,
47
47
  },
48
48
  response_status: {
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.19.1
17
+ version: 1.19.3
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.19.1",
3
+ "version": "1.19.3",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
@@ -43,31 +43,31 @@
43
43
  ]
44
44
  },
45
45
  "dependencies": {
46
- "@abtnode/cron": "^1.16.44",
47
- "@arcblock/did": "^1.20.14",
46
+ "@abtnode/cron": "^1.16.45",
47
+ "@arcblock/did": "^1.20.15",
48
48
  "@arcblock/did-auth-storage-nedb": "^1.7.1",
49
- "@arcblock/did-connect": "^3.0.1",
50
- "@arcblock/did-util": "^1.20.14",
51
- "@arcblock/jwt": "^1.20.14",
52
- "@arcblock/ux": "^3.0.1",
53
- "@arcblock/validator": "^1.20.14",
54
- "@blocklet/did-space-js": "^1.0.62",
55
- "@blocklet/js-sdk": "^1.16.44",
56
- "@blocklet/logger": "^1.16.44",
57
- "@blocklet/payment-react": "1.19.1",
58
- "@blocklet/sdk": "^1.16.44",
59
- "@blocklet/ui-react": "^3.0.1",
60
- "@blocklet/uploader": "^0.1.97",
61
- "@blocklet/xss": "^0.1.36",
49
+ "@arcblock/did-connect": "^3.0.22",
50
+ "@arcblock/did-util": "^1.20.15",
51
+ "@arcblock/jwt": "^1.20.15",
52
+ "@arcblock/ux": "^3.0.22",
53
+ "@arcblock/validator": "^1.20.15",
54
+ "@blocklet/did-space-js": "^1.1.5",
55
+ "@blocklet/js-sdk": "^1.16.45",
56
+ "@blocklet/logger": "^1.16.45",
57
+ "@blocklet/payment-react": "1.19.3",
58
+ "@blocklet/sdk": "^1.16.45",
59
+ "@blocklet/ui-react": "^3.0.22",
60
+ "@blocklet/uploader": "^0.2.4",
61
+ "@blocklet/xss": "^0.2.2",
62
62
  "@mui/icons-material": "^7.1.2",
63
63
  "@mui/lab": "7.0.0-beta.14",
64
64
  "@mui/material": "^7.1.2",
65
65
  "@mui/system": "^7.1.1",
66
- "@ocap/asset": "^1.20.14",
67
- "@ocap/client": "^1.20.14",
68
- "@ocap/mcrypto": "^1.20.14",
69
- "@ocap/util": "^1.20.14",
70
- "@ocap/wallet": "^1.20.14",
66
+ "@ocap/asset": "^1.20.15",
67
+ "@ocap/client": "^1.20.15",
68
+ "@ocap/mcrypto": "^1.20.15",
69
+ "@ocap/util": "^1.20.15",
70
+ "@ocap/wallet": "^1.20.15",
71
71
  "@stripe/react-stripe-js": "^2.9.0",
72
72
  "@stripe/stripe-js": "^2.4.0",
73
73
  "ahooks": "^3.8.5",
@@ -120,9 +120,9 @@
120
120
  "web3": "^4.16.0"
121
121
  },
122
122
  "devDependencies": {
123
- "@abtnode/types": "^1.16.44",
123
+ "@abtnode/types": "^1.16.45",
124
124
  "@arcblock/eslint-config-ts": "^0.3.3",
125
- "@blocklet/payment-types": "1.19.1",
125
+ "@blocklet/payment-types": "1.19.3",
126
126
  "@types/cookie-parser": "^1.4.9",
127
127
  "@types/cors": "^2.8.19",
128
128
  "@types/debug": "^4.1.12",
@@ -152,7 +152,7 @@
152
152
  "vite": "^7.0.0",
153
153
  "vite-node": "^3.2.4",
154
154
  "vite-plugin-babel-import": "^2.0.5",
155
- "vite-plugin-blocklet": "^0.9.33",
155
+ "vite-plugin-blocklet": "^0.10.1",
156
156
  "vite-plugin-node-polyfills": "^0.23.0",
157
157
  "vite-plugin-svgr": "^4.3.0",
158
158
  "vite-tsconfig-paths": "^5.1.4",
@@ -168,5 +168,5 @@
168
168
  "parser": "typescript"
169
169
  }
170
170
  },
171
- "gitHead": "48d5719c8ce4e89a16f8dd576ff8f72072e3909e"
171
+ "gitHead": "f274edb338c8f5a23ae46d9f7fea8458a6001cf7"
172
172
  }
@@ -0,0 +1,87 @@
1
+ import { Box } from '@mui/material';
2
+ import { useState, ReactNode, useEffect, createContext, useContext, useMemo, useRef, useCallback } from 'react';
3
+
4
+ const ConditionalSectionContext = createContext<{
5
+ hideRender: (hide?: boolean) => void;
6
+ } | null>(null);
7
+
8
+ // 导出hook供子组件使用
9
+ export const useConditionalSection = () => {
10
+ const context = useContext(ConditionalSectionContext);
11
+ return context;
12
+ };
13
+
14
+ interface ConditionalSectionProps {
15
+ skeleton: boolean;
16
+ children: ReactNode;
17
+ skeletonComponent?: ReactNode;
18
+ }
19
+
20
+ /**
21
+ * 条件渲染组件 - 完全防闪现版本
22
+ *
23
+ * 彻底解决闪现问题的方案:
24
+ * 1. skeleton=true 时显示骨架屏组件
25
+ * 2. skeleton=false 时完全隐藏(display: none)渲染子组件,让其执行逻辑
26
+ * 3. 等待子组件执行完毕,如果没有调用hideRender则显示
27
+ * 4. 整个过程用户看不到任何闪现
28
+ *
29
+ * 使用方式:
30
+ * - 在任意深度的子组件中调用 useConditionalSection()?.hideRender()
31
+ */
32
+ export default function ConditionalSection({ skeleton, children, skeletonComponent = null }: ConditionalSectionProps) {
33
+ const [renderState, setRenderState] = useState<'hidden' | 'visible' | 'none'>('hidden');
34
+ const timerRef = useRef<NodeJS.Timeout | null>(null);
35
+
36
+ const handleHideRender = useCallback((hide: boolean = true) => {
37
+ if (timerRef.current) {
38
+ clearTimeout(timerRef.current);
39
+ timerRef.current = null;
40
+ }
41
+ setRenderState(hide ? 'none' : 'visible');
42
+ }, []);
43
+
44
+ const contextValue = useMemo(() => ({ hideRender: handleHideRender }), [handleHideRender]);
45
+
46
+ useEffect(() => {
47
+ if (!skeleton) {
48
+ timerRef.current = setTimeout(() => {
49
+ setRenderState('visible');
50
+ timerRef.current = null;
51
+ }, 3000);
52
+ }
53
+
54
+ // 清理定时器
55
+ return () => {
56
+ if (timerRef.current) {
57
+ clearTimeout(timerRef.current);
58
+ timerRef.current = null;
59
+ }
60
+ };
61
+ }, [skeleton]);
62
+
63
+ if (skeleton) {
64
+ return skeletonComponent;
65
+ }
66
+
67
+ if (renderState === 'none') {
68
+ return null;
69
+ }
70
+
71
+ return (
72
+ <ConditionalSectionContext.Provider value={contextValue}>
73
+ <Box
74
+ sx={{
75
+ position: renderState === 'hidden' ? 'absolute' : 'static',
76
+ left: renderState === 'hidden' ? '-9999px' : 'auto',
77
+ top: renderState === 'hidden' ? '-9999px' : 'auto',
78
+ visibility: renderState === 'hidden' ? 'hidden' : 'visible',
79
+ width: renderState === 'hidden' ? '0' : 'auto',
80
+ height: renderState === 'hidden' ? '0' : 'auto',
81
+ overflow: 'hidden',
82
+ }}>
83
+ {children}
84
+ </Box>
85
+ </ConditionalSectionContext.Provider>
86
+ );
87
+ }
@@ -1,9 +1,10 @@
1
1
  import { formatBNStr, CreditGrantsList, CreditTransactionsList, api } from '@blocklet/payment-react';
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import { Avatar, Box, Card, CardContent, Stack, Typography, Tabs, Tab } from '@mui/material';
4
- import { useState } from 'react';
4
+ import { useMemo, useState } from 'react';
5
5
  import type { TPaymentCurrency } from '@blocklet/payment-types';
6
6
  import { useRequest } from 'ahooks';
7
+ import { useConditionalSection } from '../conditional-section';
7
8
 
8
9
  enum CreditTab {
9
10
  OVERVIEW = 'overview',
@@ -43,9 +44,30 @@ const fetchCreditSummary = async (customerId: string) => {
43
44
  export default function CreditOverview({ customerId, settings, mode = 'portal' }: CreditOverviewProps) {
44
45
  const { t } = useLocaleContext();
45
46
  const [creditTab, setCreditTab] = useState<CreditTab>(CreditTab.OVERVIEW);
47
+ const conditionalSection = useConditionalSection();
48
+
49
+ const creditCurrencies = useMemo(() => {
50
+ return (
51
+ settings?.paymentMethods
52
+ ?.filter((method: any) => method.type === 'arcblock')
53
+ ?.flatMap((method: any) => method.payment_currencies)
54
+ ?.filter((currency: TPaymentCurrency) => {
55
+ return currency.type === 'credit';
56
+ }) || []
57
+ );
58
+ }, [settings]);
59
+
46
60
  const { data: creditSummary } = useRequest(fetchCreditSummary, {
47
61
  defaultParams: [customerId],
48
62
  refreshDeps: [creditTab === CreditTab.OVERVIEW],
63
+ onSuccess: (data) => {
64
+ if (creditTab === CreditTab.OVERVIEW) {
65
+ const filteredCurrencies = creditCurrencies.filter((currency: TPaymentCurrency) => {
66
+ return data.grants?.[currency.id];
67
+ });
68
+ conditionalSection?.hideRender(filteredCurrencies.length === 0);
69
+ }
70
+ },
49
71
  });
50
72
 
51
73
  // 渲染信用概览卡片
@@ -141,20 +163,11 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
141
163
  );
142
164
  };
143
165
 
144
- // 获取有信用额度的货币
145
- const creditCurrencies =
146
- settings?.paymentMethods
147
- ?.filter((method: any) => method.type === 'arcblock')
148
- ?.flatMap((method: any) => method.payment_currencies)
149
- ?.filter((currency: TPaymentCurrency) => {
150
- const currencyId = currency.id as string;
151
- const grantData = creditSummary?.grants?.[currencyId];
152
- return grantData;
153
- }) || [];
154
-
155
- if (creditCurrencies.length === 0) {
156
- return null;
157
- }
166
+ const filteredCreditCurrencies = useMemo(() => {
167
+ return creditCurrencies.filter((currency: TPaymentCurrency) => {
168
+ return creditSummary?.grants?.[currency.id];
169
+ });
170
+ }, [creditCurrencies, creditSummary?.grants]);
158
171
 
159
172
  return (
160
173
  <Stack sx={{ width: '100%' }}>
@@ -192,10 +205,10 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
192
205
  gridTemplateColumns: 'repeat(1, 1fr)',
193
206
  },
194
207
  }}>
195
- {creditCurrencies.map(renderCreditOverviewCard)}
208
+ {filteredCreditCurrencies.map(renderCreditOverviewCard)}
196
209
  </Box>
197
210
 
198
- {creditCurrencies.length === 0 && (
211
+ {filteredCreditCurrencies.length === 0 && (
199
212
  <Box
200
213
  sx={{
201
214
  display: 'flex',
@@ -8,8 +8,9 @@ import {
8
8
  validatePhoneNumber,
9
9
  getPhoneUtil,
10
10
  validatePostalCode,
11
+ FormLabel,
11
12
  } from '@blocklet/payment-react';
12
- import { FormLabel, Stack } from '@mui/material';
13
+ import { Stack } from '@mui/material';
13
14
  import { Controller, useFormContext, useWatch } from 'react-hook-form';
14
15
  import isEmail from 'validator/es/lib/isEmail';
15
16
  import { useMount } from 'ahooks';