payment-kit 1.19.18 → 1.19.19
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/index.ts +3 -1
- package/api/src/integrations/ethereum/tx.ts +11 -0
- package/api/src/integrations/stripe/handlers/invoice.ts +26 -6
- package/api/src/integrations/stripe/handlers/setup-intent.ts +34 -2
- package/api/src/integrations/stripe/resource.ts +185 -1
- package/api/src/libs/invoice.ts +2 -1
- package/api/src/libs/session.ts +6 -1
- package/api/src/queues/auto-recharge.ts +343 -0
- package/api/src/queues/credit-consume.ts +15 -1
- package/api/src/queues/credit-grant.ts +15 -0
- package/api/src/queues/payment.ts +14 -1
- package/api/src/queues/space.ts +1 -0
- package/api/src/routes/auto-recharge-configs.ts +454 -0
- package/api/src/routes/connect/auto-recharge-auth.ts +182 -0
- package/api/src/routes/connect/recharge-account.ts +72 -10
- package/api/src/routes/connect/setup.ts +5 -3
- package/api/src/routes/connect/shared.ts +45 -4
- package/api/src/routes/customers.ts +10 -6
- package/api/src/routes/index.ts +2 -0
- package/api/src/routes/invoices.ts +10 -1
- package/api/src/routes/meters.ts +1 -1
- package/api/src/routes/payment-currencies.ts +129 -0
- package/api/src/store/migrate.ts +20 -0
- package/api/src/store/migrations/20250821-auto-recharge-config.ts +38 -0
- package/api/src/store/models/auto-recharge-config.ts +225 -0
- package/api/src/store/models/credit-grant.ts +1 -1
- package/api/src/store/models/customer.ts +1 -0
- package/api/src/store/models/index.ts +3 -0
- package/api/src/store/models/invoice.ts +2 -1
- package/api/src/store/models/payment-currency.ts +10 -2
- package/api/src/store/models/types.ts +11 -0
- package/blocklet.yml +1 -1
- package/package.json +17 -17
- package/src/components/customer/credit-overview.tsx +103 -18
- package/src/components/customer/overdraft-protection.tsx +5 -5
- package/src/components/info-metric.tsx +11 -2
- package/src/components/invoice/recharge.tsx +8 -2
- package/src/components/metadata/form.tsx +29 -27
- package/src/components/meter/form.tsx +1 -2
- package/src/components/price/form.tsx +39 -26
- package/src/components/product/form.tsx +1 -2
- package/src/locales/en.tsx +15 -0
- package/src/locales/zh.tsx +14 -0
- package/src/pages/admin/billing/meters/detail.tsx +18 -0
- package/src/pages/admin/customers/customers/credit-grant/detail.tsx +10 -0
- package/src/pages/admin/products/prices/actions.tsx +42 -2
- package/src/pages/admin/products/products/create.tsx +1 -2
- package/src/pages/admin/settings/vault-config/edit-form.tsx +8 -8
- package/src/pages/customer/credit-grant/detail.tsx +9 -1
- package/src/pages/customer/recharge/account.tsx +14 -7
- package/src/pages/customer/recharge/subscription.tsx +4 -4
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/lines-between-class-members */
|
|
2
|
+
import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
|
|
3
|
+
|
|
4
|
+
import { BN } from '@ocap/util';
|
|
5
|
+
import { createIdGenerator } from '../../libs/util';
|
|
6
|
+
import { PaymentDetails, PaymentSettings } from './types';
|
|
7
|
+
|
|
8
|
+
const nextId = createIdGenerator('carc', 24);
|
|
9
|
+
|
|
10
|
+
// eslint-disable-next-line prettier/prettier
|
|
11
|
+
export class AutoRechargeConfig extends Model<InferAttributes<AutoRechargeConfig>, InferCreationAttributes<AutoRechargeConfig>> {
|
|
12
|
+
declare id: CreationOptional<string>;
|
|
13
|
+
declare customer_id: string;
|
|
14
|
+
declare livemode: boolean;
|
|
15
|
+
|
|
16
|
+
declare enabled: boolean;
|
|
17
|
+
declare threshold: string;
|
|
18
|
+
declare currency_id: string; // default credit currency id
|
|
19
|
+
declare recharge_currency_id?: string; // payment currency id
|
|
20
|
+
declare price_id: string;
|
|
21
|
+
declare quantity: number;
|
|
22
|
+
declare payment_method_id?: string;
|
|
23
|
+
|
|
24
|
+
declare payment_settings?: PaymentSettings;
|
|
25
|
+
|
|
26
|
+
declare payment_details?: PaymentDetails;
|
|
27
|
+
|
|
28
|
+
declare daily_limits?: {
|
|
29
|
+
max_attempts: number;
|
|
30
|
+
max_amount: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
declare last_recharge_date?: string;
|
|
34
|
+
declare daily_stats?: {
|
|
35
|
+
attempt_count: number;
|
|
36
|
+
total_amount: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
declare metadata?: Record<string, any>;
|
|
40
|
+
declare created_at: CreationOptional<Date>;
|
|
41
|
+
declare updated_at: CreationOptional<Date>;
|
|
42
|
+
|
|
43
|
+
public static readonly GENESIS_ATTRIBUTES = {
|
|
44
|
+
id: {
|
|
45
|
+
type: DataTypes.STRING(30),
|
|
46
|
+
primaryKey: true,
|
|
47
|
+
allowNull: false,
|
|
48
|
+
defaultValue: nextId,
|
|
49
|
+
},
|
|
50
|
+
customer_id: {
|
|
51
|
+
type: DataTypes.STRING(18),
|
|
52
|
+
allowNull: false,
|
|
53
|
+
references: {
|
|
54
|
+
model: 'customers',
|
|
55
|
+
key: 'id',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
livemode: {
|
|
59
|
+
type: DataTypes.BOOLEAN,
|
|
60
|
+
allowNull: false,
|
|
61
|
+
},
|
|
62
|
+
enabled: {
|
|
63
|
+
type: DataTypes.BOOLEAN,
|
|
64
|
+
allowNull: false,
|
|
65
|
+
defaultValue: false,
|
|
66
|
+
},
|
|
67
|
+
threshold: {
|
|
68
|
+
type: DataTypes.STRING(32),
|
|
69
|
+
allowNull: false,
|
|
70
|
+
},
|
|
71
|
+
payment_method_id: {
|
|
72
|
+
type: DataTypes.STRING(15),
|
|
73
|
+
allowNull: true,
|
|
74
|
+
},
|
|
75
|
+
currency_id: {
|
|
76
|
+
type: DataTypes.STRING(15),
|
|
77
|
+
allowNull: false,
|
|
78
|
+
},
|
|
79
|
+
recharge_currency_id: {
|
|
80
|
+
type: DataTypes.STRING(15),
|
|
81
|
+
allowNull: true,
|
|
82
|
+
},
|
|
83
|
+
price_id: {
|
|
84
|
+
type: DataTypes.STRING(32),
|
|
85
|
+
allowNull: false,
|
|
86
|
+
},
|
|
87
|
+
quantity: {
|
|
88
|
+
type: DataTypes.INTEGER,
|
|
89
|
+
defaultValue: 1,
|
|
90
|
+
allowNull: false,
|
|
91
|
+
},
|
|
92
|
+
payment_settings: {
|
|
93
|
+
type: DataTypes.JSON,
|
|
94
|
+
allowNull: true,
|
|
95
|
+
defaultValue: {
|
|
96
|
+
payment_method_types: [],
|
|
97
|
+
payment_method_options: {},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
payment_details: {
|
|
101
|
+
type: DataTypes.JSON,
|
|
102
|
+
allowNull: true,
|
|
103
|
+
},
|
|
104
|
+
daily_limits: {
|
|
105
|
+
type: DataTypes.JSON,
|
|
106
|
+
allowNull: true,
|
|
107
|
+
defaultValue: {
|
|
108
|
+
max_attempts: 0,
|
|
109
|
+
max_amount: '0',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
last_recharge_date: {
|
|
113
|
+
type: DataTypes.STRING(10),
|
|
114
|
+
allowNull: true,
|
|
115
|
+
},
|
|
116
|
+
daily_stats: {
|
|
117
|
+
type: DataTypes.JSON,
|
|
118
|
+
allowNull: false,
|
|
119
|
+
defaultValue: {
|
|
120
|
+
attempt_count: 0,
|
|
121
|
+
total_amount: '0',
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
metadata: {
|
|
125
|
+
type: DataTypes.JSON,
|
|
126
|
+
allowNull: true,
|
|
127
|
+
},
|
|
128
|
+
created_at: {
|
|
129
|
+
type: DataTypes.DATE,
|
|
130
|
+
defaultValue: DataTypes.NOW,
|
|
131
|
+
allowNull: false,
|
|
132
|
+
},
|
|
133
|
+
updated_at: {
|
|
134
|
+
type: DataTypes.DATE,
|
|
135
|
+
defaultValue: DataTypes.NOW,
|
|
136
|
+
allowNull: false,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
public static initialize(sequelize: any) {
|
|
141
|
+
this.init(AutoRechargeConfig.GENESIS_ATTRIBUTES, {
|
|
142
|
+
sequelize,
|
|
143
|
+
modelName: 'AutoRechargeConfig',
|
|
144
|
+
tableName: 'auto_recharge_configs',
|
|
145
|
+
createdAt: 'created_at',
|
|
146
|
+
updatedAt: 'updated_at',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public static associate(models: any) {
|
|
151
|
+
this.belongsTo(models.Customer, {
|
|
152
|
+
foreignKey: 'customer_id',
|
|
153
|
+
as: 'customer',
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
this.belongsTo(models.PaymentCurrency, {
|
|
157
|
+
foreignKey: 'currency_id',
|
|
158
|
+
as: 'currency',
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
this.belongsTo(models.PaymentCurrency, {
|
|
162
|
+
foreignKey: 'recharge_currency_id',
|
|
163
|
+
as: 'rechargeCurrency',
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
this.belongsTo(models.Price, {
|
|
167
|
+
foreignKey: 'price_id',
|
|
168
|
+
as: 'price',
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
this.belongsTo(models.PaymentMethod, {
|
|
172
|
+
foreignKey: 'payment_method_id',
|
|
173
|
+
as: 'paymentMethod',
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
public canRechargeToday(nextAmount: string): boolean {
|
|
178
|
+
const today = new Date().toISOString().split('T')[0];
|
|
179
|
+
|
|
180
|
+
if (this.last_recharge_date !== today) {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!this.daily_stats || !this.daily_limits) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const attemptValid =
|
|
189
|
+
Number(this.daily_limits.max_attempts) === 0 ||
|
|
190
|
+
Number(this.daily_stats.attempt_count) < Number(this.daily_limits.max_attempts);
|
|
191
|
+
const amountValid =
|
|
192
|
+
new BN(this.daily_limits?.max_amount || '0').lte(new BN(0)) ||
|
|
193
|
+
new BN(this.daily_stats.total_amount || '0')
|
|
194
|
+
.add(new BN(nextAmount))
|
|
195
|
+
.lte(new BN(this.daily_limits.max_amount || '0'));
|
|
196
|
+
|
|
197
|
+
if (attemptValid && amountValid) {
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
public async updateDailyStats(amount: string): Promise<void> {
|
|
204
|
+
const today = new Date().toISOString().split('T')[0];
|
|
205
|
+
|
|
206
|
+
if (this.last_recharge_date !== today) {
|
|
207
|
+
await this.update({
|
|
208
|
+
last_recharge_date: today,
|
|
209
|
+
daily_stats: {
|
|
210
|
+
attempt_count: 1,
|
|
211
|
+
total_amount: amount,
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
} else {
|
|
215
|
+
await this.update({
|
|
216
|
+
daily_stats: {
|
|
217
|
+
attempt_count: (this.daily_stats?.attempt_count || 0) + 1,
|
|
218
|
+
total_amount: new BN(this.daily_stats?.total_amount || '0').add(new BN(amount)).toString(),
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export type TAutoRechargeConfig = InferAttributes<AutoRechargeConfig>;
|
|
@@ -429,7 +429,7 @@ export class CreditGrant extends Model<InferAttributes<CreditGrant>, InferCreati
|
|
|
429
429
|
|
|
430
430
|
await Promise.all(
|
|
431
431
|
targetCurrencyIds.map(async (currencyId: string) => {
|
|
432
|
-
const paymentCurrency = await PaymentCurrency.findByPk(currencyId);
|
|
432
|
+
const paymentCurrency = await PaymentCurrency.scope('withRechargeConfig').findByPk(currencyId);
|
|
433
433
|
if (!paymentCurrency) {
|
|
434
434
|
return null;
|
|
435
435
|
}
|
|
@@ -157,6 +157,7 @@ export class Customer extends Model<InferAttributes<Customer>, InferCreationAttr
|
|
|
157
157
|
public async getInvoiceNumber() {
|
|
158
158
|
const lock = getLock(`${this.id}-invoice-number`);
|
|
159
159
|
await lock.acquire();
|
|
160
|
+
await this.reload();
|
|
160
161
|
const sequence = this.next_invoice_sequence || 1;
|
|
161
162
|
await this.increment('next_invoice_sequence', { by: 1 });
|
|
162
163
|
lock.release();
|
|
@@ -31,6 +31,7 @@ import { CreditGrant, TCreditGrant } from './credit-grant';
|
|
|
31
31
|
import { CreditTransaction, TCreditTransaction } from './credit-transaction';
|
|
32
32
|
import { Meter, TMeter } from './meter';
|
|
33
33
|
import { MeterEvent, TMeterEvent } from './meter-event';
|
|
34
|
+
import { AutoRechargeConfig } from './auto-recharge-config';
|
|
34
35
|
|
|
35
36
|
const models = {
|
|
36
37
|
CheckoutSession,
|
|
@@ -65,6 +66,7 @@ const models = {
|
|
|
65
66
|
CreditTransaction,
|
|
66
67
|
Meter,
|
|
67
68
|
MeterEvent,
|
|
69
|
+
AutoRechargeConfig,
|
|
68
70
|
};
|
|
69
71
|
|
|
70
72
|
export function initialize(sequelize: any) {
|
|
@@ -113,6 +115,7 @@ export * from './credit-grant';
|
|
|
113
115
|
export * from './credit-transaction';
|
|
114
116
|
export * from './meter';
|
|
115
117
|
export * from './meter-event';
|
|
118
|
+
export * from './auto-recharge-config';
|
|
116
119
|
|
|
117
120
|
export type TPriceExpanded = TPrice & {
|
|
118
121
|
object: 'price';
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
import { getUrl } from '@blocklet/sdk';
|
|
13
13
|
import type { LiteralUnion } from 'type-fest';
|
|
14
14
|
import { createIdGenerator } from '../../libs/util';
|
|
15
|
-
import { VaultConfig } from './types';
|
|
15
|
+
import { RechargeConfig, VaultConfig } from './types';
|
|
16
16
|
|
|
17
17
|
const nextId = createIdGenerator('pc', 12);
|
|
18
18
|
|
|
@@ -45,6 +45,7 @@ export class PaymentCurrency extends Model<InferAttributes<PaymentCurrency>, Inf
|
|
|
45
45
|
declare updated_at: CreationOptional<Date>;
|
|
46
46
|
declare vault_config?: VaultConfig;
|
|
47
47
|
declare type: LiteralUnion<'standard' | 'credit', string>;
|
|
48
|
+
declare recharge_config?: RechargeConfig;
|
|
48
49
|
|
|
49
50
|
public static readonly GENESIS_ATTRIBUTES = {
|
|
50
51
|
id: {
|
|
@@ -139,6 +140,10 @@ export class PaymentCurrency extends Model<InferAttributes<PaymentCurrency>, Inf
|
|
|
139
140
|
defaultValue: 'standard',
|
|
140
141
|
allowNull: false,
|
|
141
142
|
},
|
|
143
|
+
recharge_config: {
|
|
144
|
+
type: DataTypes.JSON,
|
|
145
|
+
allowNull: true,
|
|
146
|
+
},
|
|
142
147
|
},
|
|
143
148
|
{
|
|
144
149
|
sequelize,
|
|
@@ -147,12 +152,15 @@ export class PaymentCurrency extends Model<InferAttributes<PaymentCurrency>, Inf
|
|
|
147
152
|
createdAt: 'created_at',
|
|
148
153
|
updatedAt: 'updated_at',
|
|
149
154
|
defaultScope: {
|
|
150
|
-
attributes: { exclude: ['vault_config'] },
|
|
155
|
+
attributes: { exclude: ['vault_config', 'recharge_config'] },
|
|
151
156
|
},
|
|
152
157
|
scopes: {
|
|
153
158
|
withVaultConfig: {
|
|
154
159
|
attributes: { include: ['vault_config'] },
|
|
155
160
|
},
|
|
161
|
+
withRechargeConfig: {
|
|
162
|
+
attributes: { include: ['recharge_config'] },
|
|
163
|
+
},
|
|
156
164
|
},
|
|
157
165
|
}
|
|
158
166
|
);
|
|
@@ -339,6 +339,7 @@ export type PaymentDetails = {
|
|
|
339
339
|
subscription_id?: string;
|
|
340
340
|
customer_id?: string;
|
|
341
341
|
refund_id?: string;
|
|
342
|
+
payment_method_id?: string;
|
|
342
343
|
};
|
|
343
344
|
ethereum?: {
|
|
344
345
|
tx_hash: string;
|
|
@@ -777,3 +778,13 @@ export type MeterEventPayload = {
|
|
|
777
778
|
value: string;
|
|
778
779
|
subscription_id?: string;
|
|
779
780
|
};
|
|
781
|
+
|
|
782
|
+
export type RechargeConfig = {
|
|
783
|
+
base_price_id: string;
|
|
784
|
+
payment_link_id?: string;
|
|
785
|
+
checkout_url?: string;
|
|
786
|
+
settings?: {
|
|
787
|
+
min_recharge_amount?: number;
|
|
788
|
+
max_recharge_amount?: number;
|
|
789
|
+
};
|
|
790
|
+
};
|
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.19",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
|
|
@@ -44,31 +44,31 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@abtnode/cron": "^1.16.48",
|
|
47
|
-
"@arcblock/did": "^1.
|
|
48
|
-
"@arcblock/did-connect-react": "^3.1.
|
|
47
|
+
"@arcblock/did": "^1.23.1",
|
|
48
|
+
"@arcblock/did-connect-react": "^3.1.31",
|
|
49
49
|
"@arcblock/did-connect-storage-nedb": "^1.8.0",
|
|
50
|
-
"@arcblock/did-util": "^1.
|
|
51
|
-
"@arcblock/jwt": "^1.
|
|
52
|
-
"@arcblock/ux": "^3.1.
|
|
53
|
-
"@arcblock/validator": "^1.
|
|
54
|
-
"@blocklet/did-space-js": "^1.1.
|
|
50
|
+
"@arcblock/did-util": "^1.23.1",
|
|
51
|
+
"@arcblock/jwt": "^1.23.1",
|
|
52
|
+
"@arcblock/ux": "^3.1.31",
|
|
53
|
+
"@arcblock/validator": "^1.23.1",
|
|
54
|
+
"@blocklet/did-space-js": "^1.1.18",
|
|
55
55
|
"@blocklet/error": "^0.2.5",
|
|
56
56
|
"@blocklet/js-sdk": "^1.16.48",
|
|
57
57
|
"@blocklet/logger": "^1.16.48",
|
|
58
|
-
"@blocklet/payment-react": "1.19.
|
|
58
|
+
"@blocklet/payment-react": "1.19.19",
|
|
59
59
|
"@blocklet/sdk": "^1.16.48",
|
|
60
|
-
"@blocklet/ui-react": "^3.1.
|
|
60
|
+
"@blocklet/ui-react": "^3.1.31",
|
|
61
61
|
"@blocklet/uploader": "^0.2.7",
|
|
62
62
|
"@blocklet/xss": "^0.2.5",
|
|
63
63
|
"@mui/icons-material": "^7.1.2",
|
|
64
64
|
"@mui/lab": "7.0.0-beta.14",
|
|
65
65
|
"@mui/material": "^7.1.2",
|
|
66
66
|
"@mui/system": "^7.1.1",
|
|
67
|
-
"@ocap/asset": "^1.
|
|
68
|
-
"@ocap/client": "^1.
|
|
69
|
-
"@ocap/mcrypto": "^1.
|
|
70
|
-
"@ocap/util": "^1.
|
|
71
|
-
"@ocap/wallet": "^1.
|
|
67
|
+
"@ocap/asset": "^1.23.1",
|
|
68
|
+
"@ocap/client": "^1.23.1",
|
|
69
|
+
"@ocap/mcrypto": "^1.23.1",
|
|
70
|
+
"@ocap/util": "^1.23.1",
|
|
71
|
+
"@ocap/wallet": "^1.23.1",
|
|
72
72
|
"@stripe/react-stripe-js": "^2.9.0",
|
|
73
73
|
"@stripe/stripe-js": "^2.4.0",
|
|
74
74
|
"ahooks": "^3.8.5",
|
|
@@ -124,7 +124,7 @@
|
|
|
124
124
|
"devDependencies": {
|
|
125
125
|
"@abtnode/types": "^1.16.48",
|
|
126
126
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
127
|
-
"@blocklet/payment-types": "1.19.
|
|
127
|
+
"@blocklet/payment-types": "1.19.19",
|
|
128
128
|
"@types/cookie-parser": "^1.4.9",
|
|
129
129
|
"@types/cors": "^2.8.19",
|
|
130
130
|
"@types/debug": "^4.1.12",
|
|
@@ -170,5 +170,5 @@
|
|
|
170
170
|
"parser": "typescript"
|
|
171
171
|
}
|
|
172
172
|
},
|
|
173
|
-
"gitHead": "
|
|
173
|
+
"gitHead": "76facca620eda1132f8c6d5b8f42d8fd9ef78b05"
|
|
174
174
|
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { formatBNStr, CreditGrantsList, CreditTransactionsList, api } from '@blocklet/payment-react';
|
|
1
|
+
import { formatBNStr, CreditGrantsList, CreditTransactionsList, api, AutoTopupModal } from '@blocklet/payment-react';
|
|
2
2
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
|
-
import { Box, Card, CardContent, Stack, Typography, Tabs, Tab } from '@mui/material';
|
|
4
|
-
import { useMemo, useState } from 'react';
|
|
3
|
+
import { Box, Card, CardContent, Stack, Typography, Tabs, Tab, Button } from '@mui/material';
|
|
4
|
+
import { useMemo, useState, useEffect } from 'react';
|
|
5
|
+
import { useSearchParams } from 'react-router-dom';
|
|
5
6
|
import type { TPaymentCurrency } from '@blocklet/payment-types';
|
|
6
7
|
import { useRequest } from 'ahooks';
|
|
8
|
+
import { AddOutlined, AutoModeOutlined } from '@mui/icons-material';
|
|
9
|
+
import { Toast } from '@arcblock/ux';
|
|
10
|
+
import { formatError } from '@blocklet/error';
|
|
11
|
+
import SplitButton from '@arcblock/ux/lib/SplitButton';
|
|
7
12
|
import { useConditionalSection } from '../conditional-section';
|
|
8
13
|
|
|
9
14
|
enum CreditTab {
|
|
@@ -43,8 +48,29 @@ const fetchCreditSummary = async (customerId: string) => {
|
|
|
43
48
|
|
|
44
49
|
export default function CreditOverview({ customerId, settings, mode = 'portal' }: CreditOverviewProps) {
|
|
45
50
|
const { t } = useLocaleContext();
|
|
46
|
-
const [
|
|
51
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
52
|
+
|
|
53
|
+
const getInitialTab = (): CreditTab => {
|
|
54
|
+
const tabParam = searchParams.get('creditTab');
|
|
55
|
+
if (tabParam && Object.values(CreditTab).includes(tabParam as CreditTab)) {
|
|
56
|
+
return tabParam as CreditTab;
|
|
57
|
+
}
|
|
58
|
+
return CreditTab.OVERVIEW;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const [creditTab, setCreditTab] = useState<CreditTab>(getInitialTab);
|
|
47
62
|
const conditionalSection = useConditionalSection();
|
|
63
|
+
const [autoRecharge, setAutoRecharge] = useState({
|
|
64
|
+
currencyId: '',
|
|
65
|
+
open: false,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
const tabParam = searchParams.get('creditTab');
|
|
70
|
+
if (tabParam && Object.values(CreditTab).includes(tabParam as CreditTab)) {
|
|
71
|
+
setCreditTab(tabParam as CreditTab);
|
|
72
|
+
}
|
|
73
|
+
}, [searchParams]);
|
|
48
74
|
|
|
49
75
|
const creditCurrencies = useMemo(() => {
|
|
50
76
|
return (
|
|
@@ -61,15 +87,42 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
|
|
|
61
87
|
defaultParams: [customerId],
|
|
62
88
|
refreshDeps: [creditTab === CreditTab.OVERVIEW],
|
|
63
89
|
onSuccess: (data) => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
conditionalSection?.hideRender(filteredCurrencies.length === 0);
|
|
69
|
-
}
|
|
90
|
+
const filteredCurrencies = creditCurrencies.filter((currency: TPaymentCurrency) => {
|
|
91
|
+
return data.grants?.[currency.id];
|
|
92
|
+
});
|
|
93
|
+
conditionalSection?.hideRender(filteredCurrencies.length === 0);
|
|
70
94
|
},
|
|
71
95
|
});
|
|
72
96
|
|
|
97
|
+
const handleRecharge = async (currency: TPaymentCurrency) => {
|
|
98
|
+
try {
|
|
99
|
+
const response = await api.get(`/api/payment-currencies/${currency.id}/recharge-config`).then((res) => res.data);
|
|
100
|
+
if (response.recharge_config && response.recharge_config.payment_url) {
|
|
101
|
+
window.open(response.recharge_config.payment_url, '_blank');
|
|
102
|
+
} else {
|
|
103
|
+
Toast.error(t('customer.recharge.unsupported'));
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error('Failed to fetch recharge config:', error);
|
|
107
|
+
Toast.error(formatError(error));
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const handleAutoRecharge = (currency: TPaymentCurrency) => {
|
|
112
|
+
setAutoRecharge({ currencyId: currency.id, open: true });
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const handleTabChange = (newValue: CreditTab) => {
|
|
116
|
+
setCreditTab(newValue);
|
|
117
|
+
const newParams = new URLSearchParams(searchParams);
|
|
118
|
+
if (newValue === CreditTab.OVERVIEW) {
|
|
119
|
+
newParams.delete('creditTab');
|
|
120
|
+
} else {
|
|
121
|
+
newParams.set('creditTab', newValue);
|
|
122
|
+
}
|
|
123
|
+
setSearchParams(newParams);
|
|
124
|
+
};
|
|
125
|
+
|
|
73
126
|
// 渲染信用概览卡片
|
|
74
127
|
const renderCreditOverviewCard = (currency: any) => {
|
|
75
128
|
const method = settings?.paymentMethods?.find((m: any) => m.id === currency.payment_method_id);
|
|
@@ -85,6 +138,8 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
|
|
|
85
138
|
return null;
|
|
86
139
|
}
|
|
87
140
|
|
|
141
|
+
const showRecharge = grantData.paymentCurrency.recharge_config?.base_price_id;
|
|
142
|
+
|
|
88
143
|
const totalAmount = grantData.totalAmount || '0';
|
|
89
144
|
const remainingAmount = grantData.remainingAmount || '0';
|
|
90
145
|
|
|
@@ -106,10 +161,11 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
|
|
|
106
161
|
{/* 货币信息 */}
|
|
107
162
|
|
|
108
163
|
<Stack
|
|
109
|
-
direction="
|
|
164
|
+
direction="row"
|
|
110
165
|
spacing={0.5}
|
|
111
166
|
sx={{
|
|
112
|
-
alignItems: '
|
|
167
|
+
alignItems: 'center',
|
|
168
|
+
justifyContent: 'space-between',
|
|
113
169
|
borderBottom: '1px solid',
|
|
114
170
|
borderColor: 'divider',
|
|
115
171
|
pb: 2,
|
|
@@ -117,6 +173,32 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
|
|
|
117
173
|
<Typography variant="h6" component="div">
|
|
118
174
|
{currency.name}
|
|
119
175
|
</Typography>
|
|
176
|
+
{showRecharge && (
|
|
177
|
+
<SplitButton
|
|
178
|
+
variant="outlined"
|
|
179
|
+
size="small"
|
|
180
|
+
color="primary"
|
|
181
|
+
menu={[
|
|
182
|
+
<SplitButton.Item
|
|
183
|
+
key="autoRecharge"
|
|
184
|
+
onClick={() => handleAutoRecharge(currency)}
|
|
185
|
+
sx={{
|
|
186
|
+
color: 'primary.main',
|
|
187
|
+
}}>
|
|
188
|
+
<AutoModeOutlined fontSize="small" sx={{ mr: 0.5 }} />
|
|
189
|
+
<Typography variant="body2" className="recharge-title">
|
|
190
|
+
{t('customer.credit.autoRecharge')}
|
|
191
|
+
</Typography>
|
|
192
|
+
</SplitButton.Item>,
|
|
193
|
+
]}>
|
|
194
|
+
<Button variant="text" color="primary" size="small" onClick={() => handleRecharge(currency)}>
|
|
195
|
+
<AddOutlined fontSize="small" />
|
|
196
|
+
<Typography variant="body2" className="recharge-title">
|
|
197
|
+
{t('customer.credit.recharge')}
|
|
198
|
+
</Typography>
|
|
199
|
+
</Button>
|
|
200
|
+
</SplitButton>
|
|
201
|
+
)}
|
|
120
202
|
</Stack>
|
|
121
203
|
|
|
122
204
|
{/* 可用额度 / 总额度 */}
|
|
@@ -133,10 +215,7 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
|
|
|
133
215
|
{totalAmount === '0' && remainingAmount === '0' ? (
|
|
134
216
|
<>0 </>
|
|
135
217
|
) : (
|
|
136
|
-
<>
|
|
137
|
-
{formatBNStr(remainingAmount, currency.decimal, 6, true)} /{' '}
|
|
138
|
-
{formatBNStr(totalAmount, currency.decimal, 6, true)}
|
|
139
|
-
</>
|
|
218
|
+
<>{formatBNStr(remainingAmount, currency.decimal, 6, true)}</>
|
|
140
219
|
)}
|
|
141
220
|
</Typography>
|
|
142
221
|
</Box>
|
|
@@ -177,8 +256,7 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
|
|
|
177
256
|
<Stack sx={{ width: '100%' }}>
|
|
178
257
|
<Tabs
|
|
179
258
|
value={creditTab}
|
|
180
|
-
onChange={(_, newValue) =>
|
|
181
|
-
// sx={{ borderBottom: 1, borderColor: 'divider', }}
|
|
259
|
+
onChange={(_, newValue) => handleTabChange(newValue as CreditTab)}
|
|
182
260
|
sx={{
|
|
183
261
|
flex: '1 0 auto',
|
|
184
262
|
maxWidth: '100%',
|
|
@@ -247,6 +325,13 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
|
|
|
247
325
|
<CreditTransactionsList customer_id={customerId} mode={mode} key={creditTab} />
|
|
248
326
|
)}
|
|
249
327
|
</Box>
|
|
328
|
+
{autoRecharge.open && (
|
|
329
|
+
<AutoTopupModal
|
|
330
|
+
open
|
|
331
|
+
onClose={() => setAutoRecharge({ currencyId: '', open: false })}
|
|
332
|
+
currencyId={autoRecharge.currencyId}
|
|
333
|
+
/>
|
|
334
|
+
)}
|
|
250
335
|
</Stack>
|
|
251
336
|
);
|
|
252
337
|
}
|
|
@@ -598,11 +598,11 @@ export default function OverdraftProtectionDialog({
|
|
|
598
598
|
<Currency logo={currency.logo} name={currency.symbol} />
|
|
599
599
|
</Box>
|
|
600
600
|
),
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
601
|
+
},
|
|
602
|
+
htmlInput: {
|
|
603
|
+
min: 0,
|
|
604
|
+
max: MAX_SAFE_AMOUNT,
|
|
605
|
+
step: Number(estimateAmount),
|
|
606
606
|
},
|
|
607
607
|
}}
|
|
608
608
|
/>
|
|
@@ -31,7 +31,16 @@ export default function InfoMetric(rawProps: Props) {
|
|
|
31
31
|
return (
|
|
32
32
|
<>
|
|
33
33
|
<Stack sx={stackSx}>
|
|
34
|
-
<Typography
|
|
34
|
+
<Typography
|
|
35
|
+
className="info-metric-label"
|
|
36
|
+
component="div"
|
|
37
|
+
variant="subtitle2"
|
|
38
|
+
mb={1}
|
|
39
|
+
color="text.primary"
|
|
40
|
+
sx={{
|
|
41
|
+
display: 'flex',
|
|
42
|
+
alignItems: 'center',
|
|
43
|
+
}}>
|
|
35
44
|
{/* eslint-disable-next-line react/prop-types */}
|
|
36
45
|
{props.label}
|
|
37
46
|
{/* eslint-disable-next-line react/prop-types */}
|
|
@@ -39,7 +48,7 @@ export default function InfoMetric(rawProps: Props) {
|
|
|
39
48
|
{!!props.tip && (
|
|
40
49
|
// eslint-disable-next-line react/prop-types
|
|
41
50
|
<Tooltip title={props.tip}>
|
|
42
|
-
<InfoOutlined
|
|
51
|
+
<InfoOutlined sx={{ color: 'text.secondary', cursor: 'pointer', fontSize: 'medium', ml: 0.5 }} />
|
|
43
52
|
</Tooltip>
|
|
44
53
|
)}
|
|
45
54
|
</Typography>
|