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.
- package/api/src/libs/notification/template/customer-credit-grant-low-balance.ts +1 -1
- package/api/src/libs/notification/template/customer-credit-insufficient.ts +1 -1
- package/api/src/libs/security.ts +6 -3
- package/api/src/libs/util.ts +3 -1
- package/api/src/queues/credit-consume.ts +15 -2
- package/api/src/routes/checkout-sessions.ts +9 -4
- package/api/src/routes/credit-grants.ts +24 -2
- package/api/src/routes/customers.ts +36 -12
- package/api/src/routes/payment-currencies.ts +8 -0
- package/api/src/routes/payment-methods.ts +1 -0
- package/api/src/routes/webhook-endpoints.ts +0 -3
- package/api/src/store/migrations/20250610-billing-credit.ts +0 -3
- package/api/src/store/migrations/20250708-currency-precision.ts +14 -0
- package/api/src/store/models/webhook-attempt.ts +1 -1
- package/blocklet.yml +1 -1
- package/package.json +25 -25
- package/src/components/conditional-section.tsx +87 -0
- package/src/components/customer/credit-overview.tsx +30 -17
- package/src/components/customer/form.tsx +2 -1
- package/src/components/edit-in-line.tsx +197 -0
- package/src/components/metadata/form.tsx +2 -2
- package/src/components/meter/add-usage-dialog.tsx +2 -2
- package/src/components/meter/form.tsx +2 -2
- package/src/components/meter/products.tsx +2 -2
- package/src/components/payment-link/item.tsx +2 -2
- package/src/components/payouts/portal/list.tsx +6 -11
- package/src/components/price/currency-select.tsx +13 -9
- package/src/components/price/form.tsx +47 -16
- package/src/components/product/form.tsx +3 -8
- package/src/components/subscription/portal/list.tsx +0 -1
- package/src/locales/en.tsx +6 -3
- package/src/locales/zh.tsx +6 -3
- package/src/pages/admin/customers/customers/detail.tsx +5 -13
- package/src/pages/admin/settings/payment-methods/index.tsx +56 -85
- package/src/pages/customer/index.tsx +17 -15
- 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;
|
package/api/src/libs/security.ts
CHANGED
|
@@ -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
|
-
|
|
110
|
-
|
|
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;
|
package/api/src/libs/util.ts
CHANGED
|
@@ -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
|
|
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:
|
|
230
|
+
required_amount: remainingToConsume.toString(),
|
|
231
231
|
available_amount: totalAvailable.toString(),
|
|
232
|
-
consumed_amount:
|
|
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
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
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
|
-
|
|
183
|
+
let customer = await Customer.findByPkOrDid(req.body.customer_id);
|
|
183
184
|
if (!customer) {
|
|
184
|
-
|
|
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',
|
|
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
|
-
|
|
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('
|
|
45
|
+
type: DataTypes.ENUM('succeeded', 'failed'),
|
|
46
46
|
allowNull: false,
|
|
47
47
|
},
|
|
48
48
|
response_status: {
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.19.
|
|
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.
|
|
47
|
-
"@arcblock/did": "^1.20.
|
|
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.
|
|
50
|
-
"@arcblock/did-util": "^1.20.
|
|
51
|
-
"@arcblock/jwt": "^1.20.
|
|
52
|
-
"@arcblock/ux": "^3.0.
|
|
53
|
-
"@arcblock/validator": "^1.20.
|
|
54
|
-
"@blocklet/did-space-js": "^1.
|
|
55
|
-
"@blocklet/js-sdk": "^1.16.
|
|
56
|
-
"@blocklet/logger": "^1.16.
|
|
57
|
-
"@blocklet/payment-react": "1.19.
|
|
58
|
-
"@blocklet/sdk": "^1.16.
|
|
59
|
-
"@blocklet/ui-react": "^3.0.
|
|
60
|
-
"@blocklet/uploader": "^0.
|
|
61
|
-
"@blocklet/xss": "^0.
|
|
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.
|
|
67
|
-
"@ocap/client": "^1.20.
|
|
68
|
-
"@ocap/mcrypto": "^1.20.
|
|
69
|
-
"@ocap/util": "^1.20.
|
|
70
|
-
"@ocap/wallet": "^1.20.
|
|
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.
|
|
123
|
+
"@abtnode/types": "^1.16.45",
|
|
124
124
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
125
|
-
"@blocklet/payment-types": "1.19.
|
|
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.
|
|
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": "
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
{
|
|
208
|
+
{filteredCreditCurrencies.map(renderCreditOverviewCard)}
|
|
196
209
|
</Box>
|
|
197
210
|
|
|
198
|
-
{
|
|
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 {
|
|
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';
|