payment-kit 1.21.2 → 1.21.4
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/vendor-util/adapters/didnames-adapter.ts +6 -0
- package/api/src/libs/vendor-util/adapters/launcher-adapter.ts +6 -0
- package/api/src/libs/vendor-util/adapters/types.ts +1 -0
- package/api/src/libs/vendor-util/adapters/util.ts +2 -2
- package/api/src/routes/checkout-sessions.ts +16 -0
- package/api/src/routes/meter-events.ts +2 -2
- package/api/src/routes/meters.ts +1 -1
- package/api/src/routes/vendor.ts +51 -13
- package/api/src/store/models/meter-event.ts +1 -1
- package/api/src/store/models/meter.ts +2 -5
- package/blocklet.yml +6 -6
- package/package.json +6 -6
- package/screenshots/SCREENSHOTS.md +33 -0
- package/src/components/vendor/actions.tsx +33 -0
- package/src/locales/en.tsx +14 -11
- package/src/locales/zh.tsx +3 -0
- package/src/pages/integrations/overview.tsx +1 -1
- package/src/libs/meter-utils.ts +0 -196
- /package/screenshots/{7ea8ef758865ecf6edb712d3534d2974.png → admin-payment-integrations.png} +0 -0
- /package/screenshots/{3a4cab81c52c29662db8794b05ccc7c7.png → checkout-payment-form.png} +0 -0
- /package/screenshots/{77ac49b79ae920f0f253ce8c694ffd65.png → customer-billing-dashboard.png} +0 -0
- /package/screenshots/{0ffe164ebe4aa2eb43f8d87f87683f7f.png → payment-transactions-list.png} +0 -0
- /package/screenshots/{1ef9e15ac36d4af5bef34941000ba3af.png → subscription-details-view.png} +0 -0
|
@@ -409,4 +409,10 @@ export class DidnamesAdapter implements VendorAdapter {
|
|
|
409
409
|
|
|
410
410
|
return data;
|
|
411
411
|
}
|
|
412
|
+
|
|
413
|
+
connectTest(): Promise<any> {
|
|
414
|
+
const url = formatVendorUrl(this.vendorConfig!, '/api/vendor/health');
|
|
415
|
+
const { headers } = VendorAuth.signRequestWithHeaders({});
|
|
416
|
+
return fetch(url, { headers }).then((res) => res.json());
|
|
417
|
+
}
|
|
412
418
|
}
|
|
@@ -267,4 +267,10 @@ export class LauncherAdapter implements VendorAdapter {
|
|
|
267
267
|
|
|
268
268
|
return doRequestVendorData(vendor, orderId, url, options);
|
|
269
269
|
}
|
|
270
|
+
|
|
271
|
+
connectTest(): Promise<any> {
|
|
272
|
+
const url = formatVendorUrl(this.vendorConfig!, '/api/vendor/health');
|
|
273
|
+
const { headers } = VendorAuth.signRequestWithHeaders({});
|
|
274
|
+
return fetch(url, { headers }).then((res) => res.json());
|
|
275
|
+
}
|
|
270
276
|
}
|
|
@@ -89,4 +89,5 @@ export interface VendorAdapter {
|
|
|
89
89
|
checkOrderStatus(params: CheckOrderStatusParams): Promise<CheckOrderStatusResult>;
|
|
90
90
|
getOrder(vendor: ProductVendor, orderId: string, option?: Record<string, any>): Promise<any>;
|
|
91
91
|
getOrderStatus(vendor: ProductVendor, orderId: string, option?: Record<string, any>): Promise<any>;
|
|
92
|
+
connectTest(): Promise<any>;
|
|
92
93
|
}
|
|
@@ -2,6 +2,6 @@ import { joinURL } from 'ufo';
|
|
|
2
2
|
|
|
3
3
|
import type { VendorConfig } from './types';
|
|
4
4
|
|
|
5
|
-
export const formatVendorUrl = (vendorConfig: VendorConfig,
|
|
6
|
-
return joinURL(vendorConfig.app_url, vendorConfig.metadata?.mountPoint || '',
|
|
5
|
+
export const formatVendorUrl = (vendorConfig: VendorConfig, ...args: string[]) => {
|
|
6
|
+
return joinURL(vendorConfig.app_url, vendorConfig.metadata?.mountPoint || '', ...args);
|
|
7
7
|
};
|
|
@@ -1329,6 +1329,11 @@ router.get('/retrieve/:id', user, async (req, res) => {
|
|
|
1329
1329
|
});
|
|
1330
1330
|
});
|
|
1331
1331
|
|
|
1332
|
+
async function checkVendorConfig(items: TLineItemExpanded[]) {
|
|
1333
|
+
const lineItems = await Price.expand(items, { upsell: true });
|
|
1334
|
+
return lineItems?.some((item: TLineItemExpanded) => !!item?.price?.product?.vendor_config?.length);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1332
1337
|
// submit order
|
|
1333
1338
|
router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
1334
1339
|
try {
|
|
@@ -1336,6 +1341,17 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
1336
1341
|
return res.status(403).json({ code: 'REQUIRE_LOGIN', error: 'Please login to continue' });
|
|
1337
1342
|
}
|
|
1338
1343
|
|
|
1344
|
+
const hasVendorConfig = await checkVendorConfig(req.doc.line_items);
|
|
1345
|
+
if (hasVendorConfig) {
|
|
1346
|
+
const { user: userDetail } = await blocklet.getUser(req.user.did, { enableConnectedAccount: true });
|
|
1347
|
+
if (!userDetail?.sourceAppPid) {
|
|
1348
|
+
return res.status(403).json({
|
|
1349
|
+
code: 'UNIFIED_APP_REQUIRED',
|
|
1350
|
+
error: 'This action requires a unified account. Please switch accounts and try again.',
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1339
1355
|
const checkoutSession = req.doc as CheckoutSession;
|
|
1340
1356
|
if (checkoutSession.line_items) {
|
|
1341
1357
|
try {
|
|
@@ -222,7 +222,7 @@ router.post('/', auth, async (req, res) => {
|
|
|
222
222
|
return res.status(400).json({ error: `Event with identifier "${req.body.identifier}" already exists` });
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
-
const meter = await Meter.getMeterByEventName(req.body.event_name
|
|
225
|
+
const meter = await Meter.getMeterByEventName(req.body.event_name);
|
|
226
226
|
if (!meter) {
|
|
227
227
|
return res
|
|
228
228
|
.status(400)
|
|
@@ -361,7 +361,7 @@ router.get('/:id', authMine, async (req, res) => {
|
|
|
361
361
|
if (customer.did !== req.user?.did && !['owner', 'admin'].includes(req.user?.role || '')) {
|
|
362
362
|
return res.status(403).json({ error: 'You are not allowed to access this resource' });
|
|
363
363
|
}
|
|
364
|
-
const meter = await Meter.getMeterByEventName(event.event_name
|
|
364
|
+
const meter = await Meter.getMeterByEventName(event.event_name);
|
|
365
365
|
let paymentCurrency = null;
|
|
366
366
|
if (meter) {
|
|
367
367
|
paymentCurrency = await PaymentCurrency.findByPk(meter.currency_id);
|
package/api/src/routes/meters.ts
CHANGED
|
@@ -68,7 +68,7 @@ router.post('/', auth, async (req, res) => {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
const existing = await Meter.findOne({
|
|
71
|
-
where: { event_name: req.body.event_name
|
|
71
|
+
where: { event_name: req.body.event_name },
|
|
72
72
|
});
|
|
73
73
|
if (existing) {
|
|
74
74
|
return res.status(409).json({ error: `Meter with event_name "${req.body.event_name}" already exists` });
|
package/api/src/routes/vendor.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { middleware } from '@blocklet/payment-vendor';
|
|
1
2
|
import { getUrl } from '@blocklet/sdk/lib/component';
|
|
2
3
|
import { Router } from 'express';
|
|
3
4
|
import Joi from 'joi';
|
|
4
|
-
import { middleware } from '@blocklet/payment-vendor';
|
|
5
5
|
|
|
6
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
7
|
+
import { gte } from 'semver';
|
|
6
8
|
import { MetadataSchema } from '../libs/api';
|
|
7
9
|
import { wallet } from '../libs/auth';
|
|
8
10
|
import dayjs from '../libs/dayjs';
|
|
11
|
+
import env from '../libs/env';
|
|
9
12
|
import logger from '../libs/logger';
|
|
10
13
|
import { authenticate } from '../libs/security';
|
|
11
14
|
import { formatToShortUrl } from '../libs/url';
|
|
@@ -298,18 +301,40 @@ async function testVendorConnection(req: any, res: any) {
|
|
|
298
301
|
try {
|
|
299
302
|
const vendor = await ProductVendor.findByPk(req.params.id);
|
|
300
303
|
if (!vendor) {
|
|
301
|
-
return res.status(404).json({
|
|
304
|
+
return res.status(404).json({
|
|
305
|
+
code: 'VENDOR_NOT_FOUND',
|
|
306
|
+
error: 'Vendor not found',
|
|
307
|
+
});
|
|
302
308
|
}
|
|
303
309
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
310
|
+
const vendorAdapter = await VendorFulfillmentService.getVendorAdapter(vendor.vendor_key);
|
|
311
|
+
const result = await vendorAdapter.connectTest();
|
|
312
|
+
|
|
313
|
+
if (result.connected) {
|
|
314
|
+
if (env.appId !== result.did) {
|
|
315
|
+
return res.status(400).json({
|
|
316
|
+
code: 'VENDOR_CONNECT_TEST_FAILED',
|
|
317
|
+
error: 'Vendor connection test failed! Please check the DID in the vendor preferences config',
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
return res.status(200).json({ ...result });
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (result.error) {
|
|
324
|
+
return res.status(400).json({
|
|
325
|
+
code: 'VENDOR_CONNECT_TEST_FAILED',
|
|
326
|
+
error: result.error,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (!result.sdkVersion) {
|
|
331
|
+
return res.status(400).json({
|
|
332
|
+
code: 'VENDOR_CONNECT_TEST_FAILED',
|
|
333
|
+
error: 'Vendor SDK version is too low. Please upgrade to the latest version (>=1.21.4)',
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return res.status(400).json({ ...result });
|
|
313
338
|
} catch (error: any) {
|
|
314
339
|
logger.error('Failed to test vendor connection', { error, id: req.params.id });
|
|
315
340
|
return res.status(500).json({ error: 'Internal server error' });
|
|
@@ -588,22 +613,35 @@ async function handleSubscriptionRedirect(req: any, res: any) {
|
|
|
588
613
|
return res.redirect(getUrl(`/customer/subscription/${checkoutSession.subscription_id}`));
|
|
589
614
|
}
|
|
590
615
|
|
|
616
|
+
function getVendorConnectTest(req: any, res: any) {
|
|
617
|
+
const sdkVersion = req.headers['x-broker-vendor-version'];
|
|
618
|
+
if (sdkVersion && gte(sdkVersion, '1.21.4')) {
|
|
619
|
+
return res.json({ connected: true, sdkVersion });
|
|
620
|
+
}
|
|
621
|
+
return res.json({
|
|
622
|
+
connected: false,
|
|
623
|
+
sdkVersion,
|
|
624
|
+
error: 'Vendor SDK version is too low, please upgrade to the latest version (>=1.21.4)',
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
591
628
|
const router = Router();
|
|
592
629
|
|
|
593
630
|
const ensureVendorAuth = middleware.ensureVendorAuth((vendorPk: string) =>
|
|
594
631
|
ProductVendor.findOne({ where: { 'extends.appPk': vendorPk } }).then((v) => v as any)
|
|
595
632
|
);
|
|
596
633
|
|
|
597
|
-
// FIXME: Authentication not yet added, awaiting implementation @Pengfei
|
|
598
634
|
router.get('/order/:sessionId/status', loginAuth, validateParams(sessionIdParamSchema), getVendorFulfillmentStatus);
|
|
599
635
|
router.get('/order/:sessionId/detail', loginAuth, validateParams(sessionIdParamSchema), getVendorFulfillmentDetail);
|
|
600
636
|
|
|
637
|
+
// Those for Vendor Call
|
|
638
|
+
router.get('/connectTest', ensureVendorAuth, getVendorConnectTest);
|
|
601
639
|
router.get('/subscription/:sessionId/redirect', handleSubscriptionRedirect);
|
|
602
640
|
router.get('/subscription/:sessionId', ensureVendorAuth, getVendorSubscription);
|
|
603
641
|
|
|
604
642
|
router.get(
|
|
605
643
|
'/open/:subscriptionId',
|
|
606
|
-
|
|
644
|
+
loginAuth,
|
|
607
645
|
validateParams(subscriptionIdParamSchema),
|
|
608
646
|
validateQuery(vendorRedirectQuerySchema),
|
|
609
647
|
redirectToVendor
|
|
@@ -374,7 +374,7 @@ export class MeterEvent extends Model<InferAttributes<MeterEvent>, InferCreation
|
|
|
374
374
|
|
|
375
375
|
await Promise.all(
|
|
376
376
|
events.map(async (event) => {
|
|
377
|
-
const meter = await Meter.getMeterByEventName(event.event_name
|
|
377
|
+
const meter = await Meter.getMeterByEventName(event.event_name);
|
|
378
378
|
if (!meter) {
|
|
379
379
|
return;
|
|
380
380
|
}
|
|
@@ -146,11 +146,8 @@ export class Meter extends Model<InferAttributes<Meter>, InferCreationAttributes
|
|
|
146
146
|
});
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
public static getMeterByEventName(eventName: string
|
|
150
|
-
const whereClause: any = { event_name: eventName
|
|
151
|
-
if (livemode !== undefined) {
|
|
152
|
-
whereClause.livemode = livemode;
|
|
153
|
-
}
|
|
149
|
+
public static getMeterByEventName(eventName: string): Promise<Meter | null> {
|
|
150
|
+
const whereClause: any = { event_name: eventName };
|
|
154
151
|
return this.findOne({ where: whereClause });
|
|
155
152
|
}
|
|
156
153
|
|
package/blocklet.yml
CHANGED
|
@@ -14,7 +14,7 @@ repository:
|
|
|
14
14
|
type: git
|
|
15
15
|
url: git+https://github.com/blocklet/payment-kit.git
|
|
16
16
|
specVersion: 1.2.8
|
|
17
|
-
version: 1.21.
|
|
17
|
+
version: 1.21.4
|
|
18
18
|
logo: logo.png
|
|
19
19
|
files:
|
|
20
20
|
- dist
|
|
@@ -84,11 +84,11 @@ capabilities:
|
|
|
84
84
|
clusterMode: false
|
|
85
85
|
component: true
|
|
86
86
|
screenshots:
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
-
|
|
87
|
+
- checkout-payment-form.png
|
|
88
|
+
- customer-billing-dashboard.png
|
|
89
|
+
- subscription-details-view.png
|
|
90
|
+
- admin-payment-integrations.png
|
|
91
|
+
- payment-transactions-list.png
|
|
92
92
|
components:
|
|
93
93
|
- name: image-bin
|
|
94
94
|
source:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.4",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
|
|
@@ -56,9 +56,9 @@
|
|
|
56
56
|
"@blocklet/error": "^0.2.5",
|
|
57
57
|
"@blocklet/js-sdk": "^1.16.52-beta-20250912-112002-e3499e9c",
|
|
58
58
|
"@blocklet/logger": "^1.16.52-beta-20250912-112002-e3499e9c",
|
|
59
|
-
"@blocklet/payment-broker-client": "1.21.
|
|
60
|
-
"@blocklet/payment-react": "1.21.
|
|
61
|
-
"@blocklet/payment-vendor": "1.21.
|
|
59
|
+
"@blocklet/payment-broker-client": "1.21.4",
|
|
60
|
+
"@blocklet/payment-react": "1.21.4",
|
|
61
|
+
"@blocklet/payment-vendor": "1.21.4",
|
|
62
62
|
"@blocklet/sdk": "^1.16.52-beta-20250912-112002-e3499e9c",
|
|
63
63
|
"@blocklet/ui-react": "^3.1.43",
|
|
64
64
|
"@blocklet/uploader": "^0.2.12",
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
"devDependencies": {
|
|
129
129
|
"@abtnode/types": "^1.16.52-beta-20250912-112002-e3499e9c",
|
|
130
130
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
131
|
-
"@blocklet/payment-types": "1.21.
|
|
131
|
+
"@blocklet/payment-types": "1.21.4",
|
|
132
132
|
"@types/cookie-parser": "^1.4.9",
|
|
133
133
|
"@types/cors": "^2.8.19",
|
|
134
134
|
"@types/debug": "^4.1.12",
|
|
@@ -175,5 +175,5 @@
|
|
|
175
175
|
"parser": "typescript"
|
|
176
176
|
}
|
|
177
177
|
},
|
|
178
|
-
"gitHead": "
|
|
178
|
+
"gitHead": "d650fbb4c391c1d58bc7ebbd9dedf11901db9f89"
|
|
179
179
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Payment Kit Screenshots
|
|
2
|
+
|
|
3
|
+
This document provides a visual overview of the Payment Kit application, a decentralized Stripe-like payment solution for blocklets. Below are screenshots showcasing the key features and interfaces of the application.
|
|
4
|
+
|
|
5
|
+
## Checkout Payment Form
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
This screenshot displays the checkout payment interface where customers complete their purchases. The form shows an order summary for "DID Names Service" priced at 5 USD, alongside payment details where users can select their preferred payment method (PLAY3, TBA, ABT, or USD). The interface includes fields for customer information such as name, email, and postal code, with a prominent "Pay" button to complete the transaction.
|
|
10
|
+
|
|
11
|
+
## Customer Billing Dashboard
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
The customer billing dashboard provides users with a comprehensive overview of their account status and subscriptions. It displays wallet balances across multiple cryptocurrencies (TBA and PLAY3), showing balance, spent, and stake amounts. The credits section tracks token usage and transactions. Below that, users can view and manage their active subscriptions including "Usage Payment Service," "Test Service A," "Notification Service," and "Test Service B," each with renewal dates and management options.
|
|
16
|
+
|
|
17
|
+
## Subscription Details View
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
This detailed subscription management screen shows comprehensive information about "Test Service A." It displays the subscription status (Active), next invoice date, amount, current balance, and subdcard information. The interface includes customer details, billing information, payment method (ArcBlock Beta), and state transaction data. The Products section shows the subscription item (0.01 TBA/month), and the Invoice History table provides a complete record of past payments with amounts, payment methods, invoice numbers, and statuses.
|
|
22
|
+
|
|
23
|
+
## Admin Payment Integrations
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
The admin integrations screen shows the configuration interface for payment methods and services. This view displays the vault settings page where administrators can configure app bundles and buffer thresholds for managing payment-related vault transfers. The right panel shows the available payment methods including ArcBlock (with ArcBlock Beta for testing), ETHng (with Ether payments via Alchemy Network), and BASE (with Base Mainnet and Sepolia options), each with toggle switches to enable or disable the payment method.
|
|
28
|
+
|
|
29
|
+
## Payment Transactions List
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
The payment transactions overview displays a comprehensive table of all payment activities in the system. The list shows transaction amounts (ranging from 0.1 TBA to 1 TBA), payment methods (ArcBlock Beta), customer information (primarily showing "Xiao Fang"), descriptions (subscription cycles, creation, and purchase confirmations), creation and update timestamps, and transaction statuses (marked as "Succeeded"). This view provides administrators with complete visibility into all payment processing activities within the platform.
|
|
@@ -4,6 +4,7 @@ import { ConfirmDialog, api, formatError } from '@blocklet/payment-react';
|
|
|
4
4
|
import { useSetState } from 'ahooks';
|
|
5
5
|
import type { LiteralUnion } from 'type-fest';
|
|
6
6
|
|
|
7
|
+
import { useEffect, useRef } from 'react';
|
|
7
8
|
import Actions from '../actions';
|
|
8
9
|
import ClickBoundary from '../click-boundary';
|
|
9
10
|
|
|
@@ -62,6 +63,33 @@ export default function VendorActions({ data, variant = 'compact', onChange }: V
|
|
|
62
63
|
}
|
|
63
64
|
};
|
|
64
65
|
|
|
66
|
+
const onTestConnectionRef = useRef(async () => {
|
|
67
|
+
try {
|
|
68
|
+
setState({ loading: true });
|
|
69
|
+
const result = await api.post(`/api/vendors/${data.id}/test-connection`).then((res: any) => res.data);
|
|
70
|
+
if (result.error) {
|
|
71
|
+
Toast.error(result.error);
|
|
72
|
+
} else {
|
|
73
|
+
Toast.success(t('admin.vendor.testConnectionSuccess'));
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error(err);
|
|
77
|
+
if (err.response?.data?.code === 'VENDOR_CONNECT_TEST_FAILED') {
|
|
78
|
+
Toast.error(err.response?.data?.error);
|
|
79
|
+
} else {
|
|
80
|
+
Toast.error(formatError(err));
|
|
81
|
+
}
|
|
82
|
+
} finally {
|
|
83
|
+
setState({ loading: false, action: '' });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (state.action === 'testConnection') {
|
|
89
|
+
onTestConnectionRef.current();
|
|
90
|
+
}
|
|
91
|
+
}, [state.action]);
|
|
92
|
+
|
|
65
93
|
return (
|
|
66
94
|
<ClickBoundary>
|
|
67
95
|
<Actions
|
|
@@ -84,6 +112,11 @@ export default function VendorActions({ data, variant = 'compact', onChange }: V
|
|
|
84
112
|
color: 'error',
|
|
85
113
|
divider: true,
|
|
86
114
|
},
|
|
115
|
+
{
|
|
116
|
+
label: t('admin.vendor.testConnection'),
|
|
117
|
+
handler: () => setState({ action: 'testConnection' }),
|
|
118
|
+
color: 'primary',
|
|
119
|
+
},
|
|
87
120
|
].filter(Boolean)}
|
|
88
121
|
/>
|
|
89
122
|
{state.action === 'toggle_status' && (
|
package/src/locales/en.tsx
CHANGED
|
@@ -463,7 +463,7 @@ export default flat({
|
|
|
463
463
|
commissionType: 'Commission Type',
|
|
464
464
|
commissionRate: 'Commission Rate',
|
|
465
465
|
commissionRateRequired: 'Commission rate is required',
|
|
466
|
-
commissionRateMin: 'Commission rate must be
|
|
466
|
+
commissionRateMin: 'Commission rate must be at least 0',
|
|
467
467
|
commissionRateMax: 'Commission rate cannot exceed 100%',
|
|
468
468
|
amount: 'Fixed Amount',
|
|
469
469
|
noVendor: 'No vendor available',
|
|
@@ -501,7 +501,7 @@ export default flat({
|
|
|
501
501
|
unit_amount: {
|
|
502
502
|
required: 'Price is required',
|
|
503
503
|
positive: 'Price must be positive',
|
|
504
|
-
stripeTip: 'Stripe requires the price to be
|
|
504
|
+
stripeTip: 'Stripe requires the price to be at least 0.5',
|
|
505
505
|
},
|
|
506
506
|
nickname: {
|
|
507
507
|
label: 'Price description',
|
|
@@ -518,8 +518,8 @@ export default flat({
|
|
|
518
518
|
meteredTip:
|
|
519
519
|
'Metered billing lets you charge customers based on reported usage at the end of each billing period.',
|
|
520
520
|
aggregate: 'Charge for metered usage by',
|
|
521
|
-
intervalCountTip: 'Billing interval must be a positive
|
|
522
|
-
stripeTip: 'Stripe requires the billing period to be
|
|
521
|
+
intervalCountTip: 'Billing interval must be a positive number',
|
|
522
|
+
stripeTip: 'Stripe requires the billing period to be at least 1 day',
|
|
523
523
|
},
|
|
524
524
|
currency: {
|
|
525
525
|
add: 'Add more currencies',
|
|
@@ -566,7 +566,7 @@ export default flat({
|
|
|
566
566
|
choosePricingModel: 'Choose your pricing model',
|
|
567
567
|
usage: 'Usage',
|
|
568
568
|
usageDesc:
|
|
569
|
-
'Price by number of users, units, or seats.
|
|
569
|
+
'Price by number of users, units, or seats. Requires a record for Stripe to track customer service usage.',
|
|
570
570
|
meter: 'Meter',
|
|
571
571
|
billingPeriod: 'Billing period',
|
|
572
572
|
aggregate: {
|
|
@@ -581,14 +581,14 @@ export default flat({
|
|
|
581
581
|
tip: '',
|
|
582
582
|
},
|
|
583
583
|
quantity: {
|
|
584
|
-
tip: 'Quantity must be
|
|
584
|
+
tip: 'Quantity must be at least 0',
|
|
585
585
|
},
|
|
586
586
|
quantityAvailable: {
|
|
587
587
|
label: 'Available quantity',
|
|
588
588
|
placeholder: '0 means unlimited',
|
|
589
589
|
format: 'Available {num} pieces',
|
|
590
590
|
noLimit: 'No limit on available quantity',
|
|
591
|
-
valid: 'Available quantity must be
|
|
591
|
+
valid: 'Available quantity must be at least the sold quantity',
|
|
592
592
|
description: 'Enter the number of units that can be sold, 0 means unlimited',
|
|
593
593
|
},
|
|
594
594
|
quantitySold: {
|
|
@@ -644,7 +644,7 @@ export default flat({
|
|
|
644
644
|
promotionCodes: 'Promotion Codes',
|
|
645
645
|
promotionCodesHelp: 'Promotion codes will be created after the coupon is saved.',
|
|
646
646
|
addPromotionCode: 'Add promotion code',
|
|
647
|
-
eligibleFirstTime: 'Eligible for first-time order only
|
|
647
|
+
eligibleFirstTime: 'Eligible for first-time order only (each user can only use one coupon discount)',
|
|
648
648
|
limitNumberRedemptions: 'Limit the number of times this code can be redeemed',
|
|
649
649
|
addExpirationDate: 'Add an expiration date',
|
|
650
650
|
requireMinimumOrder: 'Require minimum order value',
|
|
@@ -1112,6 +1112,9 @@ export default flat({
|
|
|
1112
1112
|
saved: 'Vendor saved successfully',
|
|
1113
1113
|
test: 'Test',
|
|
1114
1114
|
testConnection: 'Test Connection',
|
|
1115
|
+
testConnectionTip: 'Are you sure you want to test the connection for vendor "{name}"?',
|
|
1116
|
+
testConnectionSuccess: 'Connection test successful',
|
|
1117
|
+
testConnectionFailed: 'Connection test failed',
|
|
1115
1118
|
testSuccess: 'Connection test successful',
|
|
1116
1119
|
testAfterSave: 'Vendor saved successfully. You can test the connection to verify the configuration.',
|
|
1117
1120
|
testToEnable: 'Vendor saved successfully. Test the connection to enable the vendor.',
|
|
@@ -1158,7 +1161,7 @@ export default flat({
|
|
|
1158
1161
|
commission: 'Commission',
|
|
1159
1162
|
commissionRate: 'Commission Rate',
|
|
1160
1163
|
commissionRateRequired: 'Commission rate is required',
|
|
1161
|
-
commissionRateMin: 'Commission rate must be
|
|
1164
|
+
commissionRateMin: 'Commission rate must be at least 0',
|
|
1162
1165
|
commissionRateMax: 'Commission rate is too high',
|
|
1163
1166
|
commissionType: 'Commission Type',
|
|
1164
1167
|
commissionRateHelp: 'Commission rate as percentage',
|
|
@@ -1472,7 +1475,7 @@ export default flat({
|
|
|
1472
1475
|
bufferThreshold: 'Buffer Threshold',
|
|
1473
1476
|
bufferThresholdHelp:
|
|
1474
1477
|
'Only when the amount exceeding the deposit threshold reaches the buffer threshold will the collection operation be triggered.',
|
|
1475
|
-
bufferThresholdInvalid: 'Buffer threshold must be
|
|
1478
|
+
bufferThresholdInvalid: 'Buffer threshold must be at least 0',
|
|
1476
1479
|
edit: 'Configure',
|
|
1477
1480
|
enable: 'Enable',
|
|
1478
1481
|
editTitle: 'Configure {currency} Vault Settings',
|
|
@@ -1488,7 +1491,7 @@ export default flat({
|
|
|
1488
1491
|
noLimit: 'No limit',
|
|
1489
1492
|
withdrawThresholdNoLimit: '0 means no withdrawal limit',
|
|
1490
1493
|
depositThresholdRequired: 'Deposit threshold must be greater than 0',
|
|
1491
|
-
withdrawThresholdInvalid: 'Withdrawal threshold must be
|
|
1494
|
+
withdrawThresholdInvalid: 'Withdrawal threshold must be at least 0',
|
|
1492
1495
|
enableSuccess: 'Successfully enabled vault wallet for {currency}',
|
|
1493
1496
|
disableSuccess: 'Successfully disabled vault wallet for {currency}',
|
|
1494
1497
|
updateSuccess: 'Successfully updated vault wallet settings for {currency}',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -1084,6 +1084,9 @@ export default flat({
|
|
|
1084
1084
|
saved: '供应商保存成功',
|
|
1085
1085
|
test: '测试',
|
|
1086
1086
|
testConnection: '测试连接',
|
|
1087
|
+
testConnectionTip: '确定要测试供应商 "{name}" 的连接吗?',
|
|
1088
|
+
testConnectionSuccess: '连接测试成功',
|
|
1089
|
+
testConnectionFailed: '连接测试失败',
|
|
1087
1090
|
testSuccess: '连接测试成功',
|
|
1088
1091
|
testAfterSave: '供应商保存成功。您可以测试连接以验证配置是否正确。',
|
|
1089
1092
|
testToEnable: '供应商保存成功。请测试连接以启用供应商。',
|
|
@@ -261,7 +261,7 @@ export default function Overview() {
|
|
|
261
261
|
actions={
|
|
262
262
|
<Box sx={{ display: 'flex', gap: 2 }}>
|
|
263
263
|
<Button onClick={() => setOpenMeterDialog(false)}>{t('common.cancel')}</Button>
|
|
264
|
-
<Button onClick={() => navigate('/admin/meters')} variant="contained">
|
|
264
|
+
<Button onClick={() => navigate('/admin/billing/meters')} variant="contained">
|
|
265
265
|
{t('common.goToConfigure')}
|
|
266
266
|
</Button>
|
|
267
267
|
</Box>
|
package/src/libs/meter-utils.ts
DELETED
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
import { api } from '@blocklet/payment-react';
|
|
2
|
-
import type { TMeter, TCustomer, TPaymentCurrency, TSubscription } from '@blocklet/payment-types';
|
|
3
|
-
|
|
4
|
-
export interface MeterInfo {
|
|
5
|
-
meter?: TMeter & { paymentCurrency?: TPaymentCurrency };
|
|
6
|
-
customer?: TCustomer;
|
|
7
|
-
paymentCurrency?: TPaymentCurrency;
|
|
8
|
-
subscription?: TSubscription;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface GetMeterInfoOptions {
|
|
12
|
-
meterId?: string;
|
|
13
|
-
customerId?: string;
|
|
14
|
-
eventName?: string;
|
|
15
|
-
includeCustomer?: boolean;
|
|
16
|
-
includePaymentCurrency?: boolean;
|
|
17
|
-
includeSubscription?: boolean;
|
|
18
|
-
subscriptionId?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* 获取meter相关信息的通用方法
|
|
23
|
-
* @param options 配置选项
|
|
24
|
-
* @returns Promise<MeterInfo>
|
|
25
|
-
*/
|
|
26
|
-
export async function getMeterInfo(options: GetMeterInfoOptions = {}): Promise<MeterInfo> {
|
|
27
|
-
const {
|
|
28
|
-
meterId,
|
|
29
|
-
customerId,
|
|
30
|
-
eventName,
|
|
31
|
-
includeCustomer = false,
|
|
32
|
-
includePaymentCurrency = false,
|
|
33
|
-
includeSubscription = false,
|
|
34
|
-
subscriptionId,
|
|
35
|
-
} = options;
|
|
36
|
-
|
|
37
|
-
const result: MeterInfo = {};
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
// 获取meter信息
|
|
41
|
-
if (meterId) {
|
|
42
|
-
const { data: meter } = await api.get(`/api/meters/${meterId}`);
|
|
43
|
-
result.meter = meter;
|
|
44
|
-
|
|
45
|
-
// 如果meter包含paymentCurrency且需要包含
|
|
46
|
-
if (includePaymentCurrency && meter.paymentCurrency) {
|
|
47
|
-
result.paymentCurrency = meter.paymentCurrency;
|
|
48
|
-
}
|
|
49
|
-
} else if (eventName) {
|
|
50
|
-
// 通过event_name查找meter
|
|
51
|
-
const { data: meters } = await api.get(`/api/meters?event_name=${encodeURIComponent(eventName)}`);
|
|
52
|
-
if (meters.list && meters.list.length > 0) {
|
|
53
|
-
const [firstMeter] = meters.list;
|
|
54
|
-
result.meter = firstMeter;
|
|
55
|
-
|
|
56
|
-
if (includePaymentCurrency && firstMeter.paymentCurrency) {
|
|
57
|
-
result.paymentCurrency = firstMeter.paymentCurrency;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// 获取customer信息
|
|
63
|
-
if (includeCustomer && customerId) {
|
|
64
|
-
const { data: customer } = await api.get(`/api/customers/${customerId}`);
|
|
65
|
-
result.customer = customer;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// 获取subscription信息
|
|
69
|
-
if (includeSubscription && subscriptionId) {
|
|
70
|
-
const { data: subscription } = await api.get(`/api/subscriptions/${subscriptionId}`);
|
|
71
|
-
result.subscription = subscription;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// 如果需要paymentCurrency但还没有获取到,尝试从meter的currency_id获取
|
|
75
|
-
if (includePaymentCurrency && !result.paymentCurrency && result.meter?.currency_id) {
|
|
76
|
-
try {
|
|
77
|
-
const { data: paymentCurrency } = await api.get(`/api/payment-currencies/${result.meter.currency_id}`);
|
|
78
|
-
result.paymentCurrency = paymentCurrency;
|
|
79
|
-
} catch (err) {
|
|
80
|
-
console.warn('Failed to fetch payment currency:', err);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return result;
|
|
85
|
-
} catch (error) {
|
|
86
|
-
console.error('Error fetching meter info:', error);
|
|
87
|
-
throw error;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* 获取meter的完整信息(包含所有关联数据)
|
|
93
|
-
* @param meterId meter ID
|
|
94
|
-
* @returns Promise<MeterInfo>
|
|
95
|
-
*/
|
|
96
|
-
export function getMeterFullInfo(meterId: string): Promise<MeterInfo> {
|
|
97
|
-
return getMeterInfo({
|
|
98
|
-
meterId,
|
|
99
|
-
includePaymentCurrency: true,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* 根据事件名称获取meter信息
|
|
105
|
-
* @param eventName 事件名称
|
|
106
|
-
* @param includePaymentCurrency 是否包含支付货币信息
|
|
107
|
-
* @returns Promise<MeterInfo>
|
|
108
|
-
*/
|
|
109
|
-
export function getMeterByEventName(eventName: string, includePaymentCurrency = true): Promise<MeterInfo> {
|
|
110
|
-
return getMeterInfo({
|
|
111
|
-
eventName,
|
|
112
|
-
includePaymentCurrency,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* 获取meter事件的完整上下文信息
|
|
118
|
-
* @param options 配置选项
|
|
119
|
-
* @returns Promise<MeterInfo>
|
|
120
|
-
*/
|
|
121
|
-
export function getMeterEventContext(options: {
|
|
122
|
-
meterId?: string;
|
|
123
|
-
eventName?: string;
|
|
124
|
-
customerId?: string;
|
|
125
|
-
subscriptionId?: string;
|
|
126
|
-
}): Promise<MeterInfo> {
|
|
127
|
-
const { meterId, eventName, customerId, subscriptionId } = options;
|
|
128
|
-
|
|
129
|
-
return getMeterInfo({
|
|
130
|
-
meterId,
|
|
131
|
-
eventName,
|
|
132
|
-
customerId,
|
|
133
|
-
subscriptionId,
|
|
134
|
-
includeCustomer: !!customerId,
|
|
135
|
-
includePaymentCurrency: true,
|
|
136
|
-
includeSubscription: !!subscriptionId,
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* 批量获取多个meter的信息
|
|
142
|
-
* @param meterIds meter ID数组
|
|
143
|
-
* @param includePaymentCurrency 是否包含支付货币信息
|
|
144
|
-
* @returns Promise<MeterInfo[]>
|
|
145
|
-
*/
|
|
146
|
-
export function getBatchMeterInfo(meterIds: string[], includePaymentCurrency = true): Promise<MeterInfo[]> {
|
|
147
|
-
const promises = meterIds.map((meterId) =>
|
|
148
|
-
getMeterInfo({
|
|
149
|
-
meterId,
|
|
150
|
-
includePaymentCurrency,
|
|
151
|
-
}).catch((error) => {
|
|
152
|
-
console.warn(`Failed to fetch meter ${meterId}:`, error);
|
|
153
|
-
return { meter: undefined };
|
|
154
|
-
})
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
return Promise.all(promises);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* 获取客户的meter使用情况
|
|
162
|
-
* @param customerId 客户ID
|
|
163
|
-
* @param meterId 可选的meter ID,如果不提供则获取所有meter
|
|
164
|
-
* @returns Promise<MeterInfo[]>
|
|
165
|
-
*/
|
|
166
|
-
export async function getCustomerMeterUsage(customerId: string, meterId?: string): Promise<MeterInfo[]> {
|
|
167
|
-
try {
|
|
168
|
-
// 获取客户的credit transactions来找到相关的meters
|
|
169
|
-
const params = new URLSearchParams();
|
|
170
|
-
params.append('customer_id', customerId);
|
|
171
|
-
if (meterId) {
|
|
172
|
-
params.append('meter_id', meterId);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const { data: transactions } = await api.get(`/api/credit-transactions?${params.toString()}`);
|
|
176
|
-
|
|
177
|
-
// 提取唯一的meter IDs
|
|
178
|
-
const uniqueMeterIds = [
|
|
179
|
-
...new Set(
|
|
180
|
-
transactions.list
|
|
181
|
-
?.map((t: any) => t.meter?.id)
|
|
182
|
-
.filter((id: any): id is string => Boolean(id) && typeof id === 'string')
|
|
183
|
-
),
|
|
184
|
-
] as string[];
|
|
185
|
-
|
|
186
|
-
if (uniqueMeterIds.length === 0) {
|
|
187
|
-
return [];
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// 批量获取meter信息
|
|
191
|
-
return await getBatchMeterInfo(uniqueMeterIds);
|
|
192
|
-
} catch (error) {
|
|
193
|
-
console.error('Error fetching customer meter usage:', error);
|
|
194
|
-
return [];
|
|
195
|
-
}
|
|
196
|
-
}
|
/package/screenshots/{7ea8ef758865ecf6edb712d3534d2974.png → admin-payment-integrations.png}
RENAMED
|
File without changes
|
|
File without changes
|
/package/screenshots/{77ac49b79ae920f0f253ce8c694ffd65.png → customer-billing-dashboard.png}
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|