payment-kit 1.14.4 → 1.14.6
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/README.md +10 -5
- package/api/src/libs/refund.ts +3 -0
- package/api/src/libs/session.ts +16 -0
- package/api/src/libs/util.ts +12 -0
- package/api/src/routes/checkout-sessions.ts +18 -5
- package/api/src/routes/connect/setup.ts +2 -1
- package/api/src/routes/connect/shared.ts +4 -2
- package/api/src/routes/connect/subscribe.ts +2 -1
- package/api/src/routes/payment-intents.ts +2 -1
- package/api/src/routes/payment-methods.ts +2 -2
- package/api/src/routes/pricing-table.ts +2 -1
- package/api/src/routes/settings.ts +2 -2
- package/api/src/store/migrations/20240729-payment-currency.ts +22 -0
- package/api/src/store/models/payment-currency.ts +6 -0
- package/api/src/store/models/subscription.ts +1 -0
- package/api/src/store/models/types.ts +1 -0
- package/api/tests/libs/util.spec.ts +38 -0
- package/blocklet.yml +1 -1
- package/package.json +4 -4
- package/src/components/payment-intent/actions.tsx +32 -16
- package/src/components/price/form.tsx +20 -11
- package/src/components/product/add-price.tsx +9 -1
- package/src/components/product/create.tsx +10 -1
- package/src/components/product/edit-price.tsx +9 -1
- package/src/pages/admin/payments/refunds/detail.tsx +16 -1
- package/src/pages/admin/products/prices/actions.tsx +2 -2
- package/src/pages/admin/products/prices/list.tsx +1 -1
- package/src/pages/admin/products/products/create.tsx +3 -1
package/README.md
CHANGED
|
@@ -11,15 +11,19 @@ The decentralized stripe for blocklet platform.
|
|
|
11
11
|
3. run `cd blocklets/core && blocklet dev`
|
|
12
12
|
|
|
13
13
|
##### when error
|
|
14
|
+
|
|
14
15
|
1. pre-start error component xxx is not running or unreachable
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
|
|
16
|
+
|
|
17
|
+
- create .env.local file in this root
|
|
18
|
+
- add BLOCKLET_DEV_APP_DID="did:abt:your payment kit server did"
|
|
19
|
+
- add BLOCKLET_DEV_MOUNT_POINT="/example"
|
|
20
|
+
- copy .env.local to be under the /core
|
|
21
|
+
- edit BLOCKLET_DEV_MOUNT_POINT="/"
|
|
22
|
+
|
|
20
23
|
2. Insufficient fund to pay for tx cost from xxx, expected 1.0020909, got 0
|
|
21
24
|
- copy BLOCKLET_DEV_APP_DID
|
|
22
25
|
- transfer 2 TBA in your DID Wallet to your copied address
|
|
26
|
+
|
|
23
27
|
### Debug Stripe
|
|
24
28
|
|
|
25
29
|
1. Install and login with instructions from: https://stripe.com/docs/stripe-cli
|
|
@@ -37,5 +41,6 @@ None public environment variables used in the code, can change the behavior of t
|
|
|
37
41
|
- PAYMENT_CHANGE_LOCKED_PRICE: allow change locked price, must be set to "1" to work
|
|
38
42
|
- PAYMENT_RELOAD_SUBSCRIPTION_JOBS: reload subscription jobs on start, must be set to "1" to work
|
|
39
43
|
- PAYMENT_BILLING_THRESHOLD: global default billing threshold, must be a number
|
|
44
|
+
- PAYMENT_MIN_STAKE_AMOUNT: global min stake amount limit, must be a number
|
|
40
45
|
- PAYMENT_DAYS_UNTIL_DUE: global default days until due, must be a number
|
|
41
46
|
- PAYMENT_DAYS_UNTIL_CANCEL: global default days until cancel, must be a number
|
package/api/src/libs/refund.ts
CHANGED
package/api/src/libs/session.ts
CHANGED
|
@@ -299,6 +299,22 @@ export function getBillingThreshold(config: Record<string, any> = {}) {
|
|
|
299
299
|
return 0;
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
+
export function getMinStakeAmount(config: Record<string, any> = {}) {
|
|
303
|
+
if (config?.min_stake_amount) {
|
|
304
|
+
const threshold = +(config.min_stake_amount as string);
|
|
305
|
+
if (threshold > 0) {
|
|
306
|
+
return threshold;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const threshold = +(process.env.PAYMENT_MIN_STAKE_AMOUNT as string);
|
|
311
|
+
if (threshold > 0) {
|
|
312
|
+
return threshold;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return 0;
|
|
316
|
+
}
|
|
317
|
+
|
|
302
318
|
export function canPayWithDelegation(beneficiaries: PaymentBeneficiary[]) {
|
|
303
319
|
return beneficiaries.length === 0 || beneficiaries.every((x) => x.address === wallet.address);
|
|
304
320
|
}
|
package/api/src/libs/util.ts
CHANGED
|
@@ -248,3 +248,15 @@ export function getConnectQueryParam({ userDid }: { userDid: string }): {
|
|
|
248
248
|
'__did-connect__': Buffer.from(JSON.stringify(data), 'utf8').toString('base64'),
|
|
249
249
|
};
|
|
250
250
|
}
|
|
251
|
+
|
|
252
|
+
export function formatAmountPrecisionLimit(
|
|
253
|
+
amount: string,
|
|
254
|
+
precision: number = 6,
|
|
255
|
+
amountLabel: LiteralUnion<'Amount' | 'Price', string> = 'Amount'
|
|
256
|
+
) {
|
|
257
|
+
const [, decimal] = amount.split('.');
|
|
258
|
+
if (decimal && decimal.length > precision) {
|
|
259
|
+
return `${amountLabel} decimal places must be less than or equal to ${precision}`;
|
|
260
|
+
}
|
|
261
|
+
return '';
|
|
262
|
+
}
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
getCheckoutAmount,
|
|
31
31
|
getCheckoutMode,
|
|
32
32
|
getFastCheckoutAmount,
|
|
33
|
+
getMinStakeAmount,
|
|
33
34
|
getStatementDescriptor,
|
|
34
35
|
getSupportedPaymentCurrencies,
|
|
35
36
|
getSupportedPaymentMethods,
|
|
@@ -41,7 +42,7 @@ import {
|
|
|
41
42
|
getDaysUntilDue,
|
|
42
43
|
getSubscriptionCreateSetup,
|
|
43
44
|
} from '../libs/subscription';
|
|
44
|
-
import { CHECKOUT_SESSION_TTL, formatMetadata, getDataObjectFromQuery } from '../libs/util';
|
|
45
|
+
import { CHECKOUT_SESSION_TTL, formatAmountPrecisionLimit, formatMetadata, getDataObjectFromQuery } from '../libs/util';
|
|
45
46
|
import { invoiceQueue } from '../queues/invoice';
|
|
46
47
|
import { paymentQueue } from '../queues/payment';
|
|
47
48
|
import type { LineItem, TPriceExpanded, TProductExpanded } from '../store/models';
|
|
@@ -146,6 +147,7 @@ export const formatCheckoutSession = async (payload: any, throwOnEmptyItems = tr
|
|
|
146
147
|
description: '',
|
|
147
148
|
trial_period_days: 0,
|
|
148
149
|
billing_threshold_amount: 0,
|
|
150
|
+
min_stake_amount: 0,
|
|
149
151
|
trial_end: 0,
|
|
150
152
|
},
|
|
151
153
|
payment_intent_data: {},
|
|
@@ -373,6 +375,7 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
|
|
|
373
375
|
raw.payment_link_id = link.id;
|
|
374
376
|
raw.subscription_data = merge(link.subscription_data, getDataObjectFromQuery(req.query, 'subscription_data'), {
|
|
375
377
|
billing_threshold_amount: getBillingThreshold(link.subscription_data),
|
|
378
|
+
min_stake_amount: getMinStakeAmount(link.subscription_data),
|
|
376
379
|
});
|
|
377
380
|
|
|
378
381
|
if (link.after_completion?.hosted_confirmation?.custom_message) {
|
|
@@ -559,6 +562,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
559
562
|
const trialInDays = Number(checkoutSession.subscription_data?.trial_period_days || 0);
|
|
560
563
|
const trialEnds = Number(checkoutSession.subscription_data?.trial_end || 0);
|
|
561
564
|
const billingThreshold = Number(checkoutSession.subscription_data?.billing_threshold_amount || 0);
|
|
565
|
+
const minStakeAmount = Number(checkoutSession.subscription_data?.min_stake_amount || 0);
|
|
562
566
|
const amount = getCheckoutAmount(lineItems, paymentCurrency.id, trialInDays > 0 || trialEnds > now);
|
|
563
567
|
await checkoutSession.update({
|
|
564
568
|
amount_subtotal: amount.subtotal,
|
|
@@ -809,10 +813,11 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
809
813
|
missing_payment_method: 'create_invoice',
|
|
810
814
|
},
|
|
811
815
|
},
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
:
|
|
816
|
+
billing_thresholds: {
|
|
817
|
+
amount_gte: billingThreshold,
|
|
818
|
+
stake_gte: minStakeAmount,
|
|
819
|
+
reset_billing_cycle_anchor: false,
|
|
820
|
+
},
|
|
816
821
|
pending_invoice_item_interval: setup.recurring,
|
|
817
822
|
pending_setup_intent: setupIntent?.id,
|
|
818
823
|
default_payment_method_id: paymentMethod.id,
|
|
@@ -1184,6 +1189,14 @@ router.put('/:id/amount', ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
1184
1189
|
if (input > Number(maximum)) {
|
|
1185
1190
|
return res.status(400).json({ error: 'Custom amount should not be smaller than maximum' });
|
|
1186
1191
|
}
|
|
1192
|
+
const precisionError = formatAmountPrecisionLimit(
|
|
1193
|
+
input.toString(),
|
|
1194
|
+
currency.maximum_precision || 6,
|
|
1195
|
+
'Custom amount'
|
|
1196
|
+
);
|
|
1197
|
+
if (precisionError) {
|
|
1198
|
+
return res.status(400).json({ error: precisionError });
|
|
1199
|
+
}
|
|
1187
1200
|
} else if (presets?.some((x) => Number(x) === input) === false) {
|
|
1188
1201
|
return res.status(400).json({ error: 'Custom amount must be one of the presets' });
|
|
1189
1202
|
}
|
|
@@ -43,6 +43,7 @@ export default {
|
|
|
43
43
|
const trialEnds = Number(checkoutSession.subscription_data?.trial_end || 0);
|
|
44
44
|
const trialing = trialInDays > 0 || trialEnds > now;
|
|
45
45
|
const billingThreshold = Number(checkoutSession.subscription_data?.billing_threshold_amount || 0);
|
|
46
|
+
const minStakeAmount = Number(checkoutSession.subscription_data?.min_stake_amount || 0);
|
|
46
47
|
const fastCheckoutAmount = getFastCheckoutAmount(items, checkoutSession.mode, paymentCurrency.id, trialing);
|
|
47
48
|
|
|
48
49
|
if (paymentMethod.type === 'arcblock') {
|
|
@@ -65,7 +66,7 @@ export default {
|
|
|
65
66
|
paymentCurrency,
|
|
66
67
|
paymentMethod,
|
|
67
68
|
trialing,
|
|
68
|
-
billingThreshold,
|
|
69
|
+
billingThreshold: Math.max(minStakeAmount, billingThreshold),
|
|
69
70
|
items,
|
|
70
71
|
}),
|
|
71
72
|
];
|
|
@@ -732,8 +732,10 @@ export async function getStakeTxClaim({
|
|
|
732
732
|
paymentMethod: PaymentMethod;
|
|
733
733
|
}) {
|
|
734
734
|
// create staking amount
|
|
735
|
-
const billingThreshold =
|
|
736
|
-
const
|
|
735
|
+
const billingThreshold = Number(subscription.billing_thresholds?.amount_gte || 0);
|
|
736
|
+
const minStakeAmount = Number(subscription.billing_thresholds?.stake_gte || 0);
|
|
737
|
+
const threshold = fromTokenToUnit(Math.max(billingThreshold, minStakeAmount), paymentCurrency.decimal);
|
|
738
|
+
const staking = getSubscriptionStakeSetup(items, paymentCurrency.id, threshold.toString());
|
|
737
739
|
const amount = staking.licensed.add(staking.metered).toString();
|
|
738
740
|
|
|
739
741
|
if (paymentMethod.type === 'arcblock') {
|
|
@@ -44,6 +44,7 @@ export default {
|
|
|
44
44
|
const trialEnds = Number(checkoutSession.subscription_data?.trial_end || 0);
|
|
45
45
|
const trialing = trialInDays > 0 || trialEnds > now;
|
|
46
46
|
const billingThreshold = Number(checkoutSession.subscription_data?.billing_threshold_amount || 0);
|
|
47
|
+
const minStakeAmount = Number(checkoutSession.subscription_data?.min_stake_amount || 0);
|
|
47
48
|
const fastCheckoutAmount = getFastCheckoutAmount(items, checkoutSession.mode, paymentCurrency.id, trialing);
|
|
48
49
|
|
|
49
50
|
if (paymentMethod.type === 'arcblock') {
|
|
@@ -67,7 +68,7 @@ export default {
|
|
|
67
68
|
paymentCurrency,
|
|
68
69
|
paymentMethod,
|
|
69
70
|
trialing,
|
|
70
|
-
billingThreshold,
|
|
71
|
+
billingThreshold: Math.max(minStakeAmount, billingThreshold),
|
|
71
72
|
items,
|
|
72
73
|
}),
|
|
73
74
|
];
|
|
@@ -246,11 +246,12 @@ const refundRequestSchema = Joi.object({
|
|
|
246
246
|
metadata: Joi.object().optional(),
|
|
247
247
|
});
|
|
248
248
|
|
|
249
|
+
// eslint-disable-next-line consistent-return
|
|
249
250
|
router.put('/:id/refund', authAdmin, async (req, res) => {
|
|
250
251
|
try {
|
|
251
252
|
const { error } = refundRequestSchema.validate(req.body);
|
|
252
253
|
if (error) {
|
|
253
|
-
res.status(400).json({ error: `payment intent refund request invalid: ${error.message}` });
|
|
254
|
+
return res.status(400).json({ error: `payment intent refund request invalid: ${error.message}` });
|
|
254
255
|
}
|
|
255
256
|
const doc = await PaymentIntent.findOne({
|
|
256
257
|
where: { id: req.params.id },
|
|
@@ -106,8 +106,8 @@ router.post('/', auth, async (req, res) => {
|
|
|
106
106
|
raw.settings.ethereum.chain_id = network.chainId.toString();
|
|
107
107
|
logger.info('ethereum api endpoint verified', { settings: raw.settings.ethereum, network, blockNumber });
|
|
108
108
|
} catch (err) {
|
|
109
|
-
|
|
110
|
-
return res.status(400).json({ error:
|
|
109
|
+
logger.error('verify ethereum api endpoint failed', err);
|
|
110
|
+
return res.status(400).json({ error: err.message });
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
const exist = await PaymentMethod.findOne({
|
|
@@ -11,7 +11,7 @@ import { checkPassportForPricingTable } from '../integrations/blocklet/passport'
|
|
|
11
11
|
import { createListParamSchema } from '../libs/api';
|
|
12
12
|
import logger from '../libs/logger';
|
|
13
13
|
import { authenticate } from '../libs/security';
|
|
14
|
-
import { getBillingThreshold, isLineItemCurrencyAligned } from '../libs/session';
|
|
14
|
+
import { getBillingThreshold, getMinStakeAmount, isLineItemCurrencyAligned } from '../libs/session';
|
|
15
15
|
import { getDaysUntilCancel, getDaysUntilDue } from '../libs/subscription';
|
|
16
16
|
import { getDataObjectFromQuery } from '../libs/util';
|
|
17
17
|
import { CheckoutSession } from '../store/models/checkout-session';
|
|
@@ -224,6 +224,7 @@ router.post('/:id/checkout/:priceId', async (req, res) => {
|
|
|
224
224
|
]),
|
|
225
225
|
subscription_data: merge(price.subscription_data || {}, getDataObjectFromQuery(req.query, 'subscription_data'), {
|
|
226
226
|
billing_threshold_amount: getBillingThreshold(price.subscription_data),
|
|
227
|
+
min_stake_amount: getMinStakeAmount(price.subscription_data),
|
|
227
228
|
}),
|
|
228
229
|
metadata: {
|
|
229
230
|
...doc.metadata,
|
|
@@ -8,7 +8,7 @@ import { PaymentMethod } from '../store/models/payment-method';
|
|
|
8
8
|
const router = Router();
|
|
9
9
|
|
|
10
10
|
router.get('/', async (req, res) => {
|
|
11
|
-
const attributes = ['id', 'name', 'symbol', 'decimal', 'logo', 'payment_method_id'];
|
|
11
|
+
const attributes = ['id', 'name', 'symbol', 'decimal', 'logo', 'payment_method_id', 'maximum_precision'];
|
|
12
12
|
const where: WhereOptions<PaymentMethod> = { livemode: req.livemode, active: true };
|
|
13
13
|
|
|
14
14
|
const methods = await PaymentMethod.findAll({
|
|
@@ -26,7 +26,7 @@ router.get('/', async (req, res) => {
|
|
|
26
26
|
|
|
27
27
|
res.json({
|
|
28
28
|
paymentMethods: methods.map((x) =>
|
|
29
|
-
pick(x, ['id', 'name', 'type', 'logo', 'payment_currencies', 'default_currency_id'])
|
|
29
|
+
pick(x, ['id', 'name', 'type', 'logo', 'payment_currencies', 'default_currency_id', 'maximum_precision'])
|
|
30
30
|
),
|
|
31
31
|
baseCurrency: await PaymentCurrency.findOne({
|
|
32
32
|
where: { is_base_currency: true, livemode: req.livemode },
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { DataTypes } from 'sequelize';
|
|
2
|
+
|
|
3
|
+
import { Migration, safeApplyColumnChanges } from '../migrate';
|
|
4
|
+
|
|
5
|
+
export const up: Migration = async ({ context }) => {
|
|
6
|
+
await safeApplyColumnChanges(context, {
|
|
7
|
+
payment_links: [
|
|
8
|
+
{
|
|
9
|
+
name: 'maximum_precision',
|
|
10
|
+
field: {
|
|
11
|
+
type: DataTypes.NUMBER,
|
|
12
|
+
allowNull: true,
|
|
13
|
+
defaultValue: 6,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const down: Migration = async ({ context }) => {
|
|
21
|
+
await context.removeColumn('payment_currencies', 'maximum_precision');
|
|
22
|
+
};
|
|
@@ -29,6 +29,7 @@ export class PaymentCurrency extends Model<InferAttributes<PaymentCurrency>, Inf
|
|
|
29
29
|
declare logo: string;
|
|
30
30
|
declare symbol: string;
|
|
31
31
|
declare decimal: number;
|
|
32
|
+
declare maximum_precision?: number;
|
|
32
33
|
|
|
33
34
|
declare minimum_payment_amount: string;
|
|
34
35
|
declare maximum_payment_amount: string;
|
|
@@ -87,6 +88,11 @@ export class PaymentCurrency extends Model<InferAttributes<PaymentCurrency>, Inf
|
|
|
87
88
|
type: DataTypes.NUMBER,
|
|
88
89
|
defaultValue: 2,
|
|
89
90
|
},
|
|
91
|
+
maximum_precision: {
|
|
92
|
+
type: DataTypes.NUMBER,
|
|
93
|
+
allowNull: true,
|
|
94
|
+
defaultValue: 6,
|
|
95
|
+
},
|
|
90
96
|
minimum_payment_amount: {
|
|
91
97
|
type: DataTypes.STRING(32),
|
|
92
98
|
defaultValue: '0',
|
|
@@ -5,6 +5,7 @@ import dayjs from '../../src/libs/dayjs';
|
|
|
5
5
|
import {
|
|
6
6
|
createCodeGenerator,
|
|
7
7
|
createIdGenerator,
|
|
8
|
+
formatAmountPrecisionLimit,
|
|
8
9
|
formatMetadata,
|
|
9
10
|
getDataObjectFromQuery,
|
|
10
11
|
getNextRetry,
|
|
@@ -202,3 +203,40 @@ describe('getWhereFromKvQuery', () => {
|
|
|
202
203
|
});
|
|
203
204
|
});
|
|
204
205
|
});
|
|
206
|
+
|
|
207
|
+
describe('formatAmountPrecisionLimit', () => {
|
|
208
|
+
it('should return an empty string if the decimal places are within the precision limit', () => {
|
|
209
|
+
const result = formatAmountPrecisionLimit('123.456', 6);
|
|
210
|
+
expect(result).toBe('');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should return an error message if the decimal places exceed the precision limit', () => {
|
|
214
|
+
const result = formatAmountPrecisionLimit('123.456789', 5);
|
|
215
|
+
expect(result).toBe('Amount decimal places must be less than or equal to 5');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should use the default precision of 6 if not provided', () => {
|
|
219
|
+
const result = formatAmountPrecisionLimit('123.456789');
|
|
220
|
+
expect(result).toBe('');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should use the provided amountLabel in the error message', () => {
|
|
224
|
+
const result = formatAmountPrecisionLimit('123.456789', 5, 'Price');
|
|
225
|
+
expect(result).toBe('Price decimal places must be less than or equal to 5');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should return an empty string if there are no decimal places', () => {
|
|
229
|
+
const result = formatAmountPrecisionLimit('123');
|
|
230
|
+
expect(result).toBe('');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should handle an empty string as amount', () => {
|
|
234
|
+
const result = formatAmountPrecisionLimit('');
|
|
235
|
+
expect(result).toBe('');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should handle an amount with no decimal part', () => {
|
|
239
|
+
const result = formatAmountPrecisionLimit('123.');
|
|
240
|
+
expect(result).toBe('');
|
|
241
|
+
});
|
|
242
|
+
});
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.6",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"@arcblock/validator": "^1.18.124",
|
|
53
53
|
"@blocklet/js-sdk": "1.16.28",
|
|
54
54
|
"@blocklet/logger": "1.16.28",
|
|
55
|
-
"@blocklet/payment-react": "1.14.
|
|
55
|
+
"@blocklet/payment-react": "1.14.6",
|
|
56
56
|
"@blocklet/sdk": "1.16.28",
|
|
57
57
|
"@blocklet/ui-react": "^2.10.3",
|
|
58
58
|
"@blocklet/uploader": "^0.1.20",
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
"devDependencies": {
|
|
119
119
|
"@abtnode/types": "1.16.28",
|
|
120
120
|
"@arcblock/eslint-config-ts": "^0.3.2",
|
|
121
|
-
"@blocklet/payment-types": "1.14.
|
|
121
|
+
"@blocklet/payment-types": "1.14.6",
|
|
122
122
|
"@types/cookie-parser": "^1.4.7",
|
|
123
123
|
"@types/cors": "^2.8.17",
|
|
124
124
|
"@types/debug": "^4.1.12",
|
|
@@ -160,5 +160,5 @@
|
|
|
160
160
|
"parser": "typescript"
|
|
161
161
|
}
|
|
162
162
|
},
|
|
163
|
-
"gitHead": "
|
|
163
|
+
"gitHead": "6c2ea2db76a8bb8413d0b802f35b50b650c7f6d2"
|
|
164
164
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
2
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
3
|
-
import { ConfirmDialog, api, formatBNStr, formatError } from '@blocklet/payment-react';
|
|
3
|
+
import { ConfirmDialog, api, formatAmountPrecisionLimit, formatBNStr, formatError } from '@blocklet/payment-react';
|
|
4
4
|
import type { TPaymentIntentExpanded } from '@blocklet/payment-types';
|
|
5
5
|
import { useRequest, useSetState } from 'ahooks';
|
|
6
6
|
import { useNavigate } from 'react-router-dom';
|
|
@@ -39,11 +39,26 @@ const fetchRefundData = (id: string) => {
|
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
function RefundForm({ data, refundMaxAmount }: { data: TPaymentIntentExpanded; refundMaxAmount: string }) {
|
|
42
|
-
const { t } = useLocaleContext();
|
|
42
|
+
const { t, locale } = useLocaleContext();
|
|
43
43
|
const { control, getFieldState, setValue } = useFormContext();
|
|
44
44
|
|
|
45
|
-
const
|
|
46
|
-
|
|
45
|
+
const validateAmount = (v: number) => {
|
|
46
|
+
if (Number(v) <= 0 || Number(v) > Number(refundMaxAmount)) {
|
|
47
|
+
return t('admin.paymentIntent.refundForm.amountRange', {
|
|
48
|
+
min: 0,
|
|
49
|
+
max: refundMaxAmount,
|
|
50
|
+
symbol: data.paymentCurrency.symbol,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const validPrecision = formatAmountPrecisionLimit(
|
|
54
|
+
v.toString(),
|
|
55
|
+
locale,
|
|
56
|
+
data.paymentCurrency.maximum_precision || 6
|
|
57
|
+
);
|
|
58
|
+
if (validPrecision) {
|
|
59
|
+
return validPrecision;
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
47
62
|
};
|
|
48
63
|
return (
|
|
49
64
|
<Stack direction="column" spacing={1} alignItems="flex-start" sx={{ width: 400 }}>
|
|
@@ -90,6 +105,9 @@ function RefundForm({ data, refundMaxAmount }: { data: TPaymentIntentExpanded; r
|
|
|
90
105
|
<Controller
|
|
91
106
|
name="refund.amount"
|
|
92
107
|
control={control}
|
|
108
|
+
rules={{
|
|
109
|
+
validate: validateAmount,
|
|
110
|
+
}}
|
|
93
111
|
render={({ field }) => (
|
|
94
112
|
<TextField
|
|
95
113
|
{...field}
|
|
@@ -98,16 +116,8 @@ function RefundForm({ data, refundMaxAmount }: { data: TPaymentIntentExpanded; r
|
|
|
98
116
|
type="number"
|
|
99
117
|
fullWidth
|
|
100
118
|
placeholder={t('admin.paymentIntent.refundForm.amount')}
|
|
101
|
-
error={!!getFieldState('refund.amount').error
|
|
102
|
-
helperText={
|
|
103
|
-
!positive(field.value)
|
|
104
|
-
? t('admin.paymentIntent.refundForm.amountRange', {
|
|
105
|
-
min: 0,
|
|
106
|
-
max: refundMaxAmount,
|
|
107
|
-
symbol: data.paymentCurrency.symbol,
|
|
108
|
-
})
|
|
109
|
-
: getFieldState('refund.amount').error?.message
|
|
110
|
-
}
|
|
119
|
+
error={!!getFieldState('refund.amount').error}
|
|
120
|
+
helperText={getFieldState('refund.amount').error?.message}
|
|
111
121
|
InputProps={{
|
|
112
122
|
endAdornment: <InputAdornment position="end">{data.paymentCurrency.symbol}</InputAdornment>,
|
|
113
123
|
}}
|
|
@@ -126,6 +136,9 @@ function RefundForm({ data, refundMaxAmount }: { data: TPaymentIntentExpanded; r
|
|
|
126
136
|
<Controller
|
|
127
137
|
name="refund.description"
|
|
128
138
|
control={control}
|
|
139
|
+
rules={{
|
|
140
|
+
required: t('common.required'),
|
|
141
|
+
}}
|
|
129
142
|
render={({ field }) => (
|
|
130
143
|
<TextField
|
|
131
144
|
{...field}
|
|
@@ -136,6 +149,8 @@ function RefundForm({ data, refundMaxAmount }: { data: TPaymentIntentExpanded; r
|
|
|
136
149
|
minRows={2}
|
|
137
150
|
maxRows={4}
|
|
138
151
|
placeholder={t('admin.paymentIntent.refundForm.description')}
|
|
152
|
+
error={!!getFieldState('refund.description').error}
|
|
153
|
+
helperText={getFieldState('refund.description').error?.message}
|
|
139
154
|
/>
|
|
140
155
|
)}
|
|
141
156
|
/>
|
|
@@ -146,7 +161,7 @@ function RefundForm({ data, refundMaxAmount }: { data: TPaymentIntentExpanded; r
|
|
|
146
161
|
export function PaymentIntentActionsInner({ data, variant, onChange }: Props) {
|
|
147
162
|
const { t } = useLocaleContext();
|
|
148
163
|
const navigate = useNavigate();
|
|
149
|
-
const { reset, getValues, setValue } = useFormContext();
|
|
164
|
+
const { reset, getValues, setValue, handleSubmit } = useFormContext();
|
|
150
165
|
const [state, setState] = useSetState({
|
|
151
166
|
action: '',
|
|
152
167
|
loading: false,
|
|
@@ -219,7 +234,7 @@ export function PaymentIntentActionsInner({ data, variant, onChange }: Props) {
|
|
|
219
234
|
<Actions variant={variant} actions={actions} />
|
|
220
235
|
{state.action === 'refund' && (
|
|
221
236
|
<ConfirmDialog
|
|
222
|
-
onConfirm={onRefund}
|
|
237
|
+
onConfirm={handleSubmit(onRefund)}
|
|
223
238
|
onCancel={() => setState({ action: '' })}
|
|
224
239
|
title={t('admin.paymentIntent.refund')}
|
|
225
240
|
message={<RefundForm data={data} refundMaxAmount={refundMaxAmount} />}
|
|
@@ -232,6 +247,7 @@ export function PaymentIntentActionsInner({ data, variant, onChange }: Props) {
|
|
|
232
247
|
|
|
233
248
|
export default function PaymentIntentActions(props: Props) {
|
|
234
249
|
const methods = useForm({
|
|
250
|
+
mode: 'onChange',
|
|
235
251
|
defaultValues: {
|
|
236
252
|
refund: {
|
|
237
253
|
reason: 'requested_by_admin',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable no-nested-ternary */
|
|
2
2
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
|
-
import { findCurrency, usePaymentContext } from '@blocklet/payment-react';
|
|
3
|
+
import { findCurrency, formatAmountPrecisionLimit, usePaymentContext } from '@blocklet/payment-react';
|
|
4
4
|
import type {
|
|
5
5
|
InferFormType,
|
|
6
6
|
PriceRecurring,
|
|
@@ -98,7 +98,7 @@ function stripeCurrencyValidate(v: number, currency: TPaymentCurrencyExpanded |
|
|
|
98
98
|
export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
99
99
|
const getFieldName = (name: string) => (prefix ? `${prefix}.${name}` : name);
|
|
100
100
|
|
|
101
|
-
const { t } = useLocaleContext();
|
|
101
|
+
const { t, locale } = useLocaleContext();
|
|
102
102
|
const { control, setValue, getFieldState } = useFormContext();
|
|
103
103
|
const { settings, livemode } = usePaymentContext();
|
|
104
104
|
|
|
@@ -109,7 +109,6 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
|
109
109
|
const isMetered = useWatch({ control, name: getFieldName('recurring.usage_type') }) === 'metered';
|
|
110
110
|
const isCustomInterval = useWatch({ control, name: getFieldName('recurring.interval_config') }) === 'month_2';
|
|
111
111
|
const model = useWatch({ control, name: getFieldName('model') });
|
|
112
|
-
const positive = (v: number) => v >= 0;
|
|
113
112
|
const quantityPositive = (v: number | undefined) => !v || v.toString().match(/^(0|[1-9]\d*)$/);
|
|
114
113
|
const intervalCountPositive = (v: number) => Number.isInteger(Number(v)) && v > 0;
|
|
115
114
|
|
|
@@ -119,6 +118,17 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
|
119
118
|
|
|
120
119
|
const isLocked = priceLocked && window.blocklet?.PAYMENT_CHANGE_LOCKED_PRICE !== '1';
|
|
121
120
|
|
|
121
|
+
const validateAmount = (v: number, currency: { maximum_precision?: number }) => {
|
|
122
|
+
if (Number(v) < 0) {
|
|
123
|
+
return t('admin.price.unit_amount.positive');
|
|
124
|
+
}
|
|
125
|
+
const validPrecision = formatAmountPrecisionLimit(v.toString(), locale, currency?.maximum_precision || 6);
|
|
126
|
+
if (validPrecision) {
|
|
127
|
+
return validPrecision;
|
|
128
|
+
}
|
|
129
|
+
return true;
|
|
130
|
+
};
|
|
131
|
+
|
|
122
132
|
return (
|
|
123
133
|
<Root direction="column" alignItems="flex-start" spacing={2}>
|
|
124
134
|
{isLocked && <Alert severity="info">{t('admin.price.locked')}</Alert>}
|
|
@@ -154,7 +164,7 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
|
154
164
|
control={control}
|
|
155
165
|
rules={{
|
|
156
166
|
required: t('admin.price.unit_amount.required'),
|
|
157
|
-
validate: (v) => (
|
|
167
|
+
validate: (v) => validateAmount(v, settings.baseCurrency),
|
|
158
168
|
}}
|
|
159
169
|
disabled={isLocked}
|
|
160
170
|
render={({ field }) => (
|
|
@@ -174,12 +184,8 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
|
174
184
|
type="number"
|
|
175
185
|
size="small"
|
|
176
186
|
sx={{ width: INPUT_WIDTH }}
|
|
177
|
-
error={!!getFieldState(getFieldName('unit_amount')).error
|
|
178
|
-
helperText={
|
|
179
|
-
!positive(field.value)
|
|
180
|
-
? t('admin.price.unit_amount.positive')
|
|
181
|
-
: getFieldState(getFieldName('unit_amount')).error?.message
|
|
182
|
-
}
|
|
187
|
+
error={!!getFieldState(getFieldName('unit_amount')).error}
|
|
188
|
+
helperText={getFieldState(getFieldName('unit_amount')).error?.message}
|
|
183
189
|
InputProps={{
|
|
184
190
|
endAdornment: (
|
|
185
191
|
<InputAdornment position="end">
|
|
@@ -227,7 +233,10 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
|
227
233
|
<Controller
|
|
228
234
|
name={fieldName}
|
|
229
235
|
control={control}
|
|
230
|
-
rules={{
|
|
236
|
+
rules={{
|
|
237
|
+
required: t('admin.price.unit_amount.required'),
|
|
238
|
+
validate: (v) => validateAmount(v, currency ?? {}),
|
|
239
|
+
}}
|
|
231
240
|
disabled={isLocked}
|
|
232
241
|
render={({ field }) => {
|
|
233
242
|
const hasStripError = !stripeCurrencyValidate(field.value, currency);
|
|
@@ -19,14 +19,22 @@ export default function AddPrice({
|
|
|
19
19
|
const { t } = useLocaleContext();
|
|
20
20
|
const { settings } = usePaymentContext();
|
|
21
21
|
const methods = useForm<Price>({
|
|
22
|
+
mode: 'onChange',
|
|
22
23
|
defaultValues: {
|
|
23
24
|
...DEFAULT_PRICE,
|
|
24
25
|
currency_id: settings.baseCurrency.id,
|
|
25
26
|
},
|
|
26
27
|
});
|
|
27
28
|
|
|
28
|
-
const {
|
|
29
|
+
const {
|
|
30
|
+
handleSubmit,
|
|
31
|
+
reset,
|
|
32
|
+
formState: { errors },
|
|
33
|
+
} = methods;
|
|
29
34
|
const onSubmit = () => {
|
|
35
|
+
if (Object.keys(errors).length > 0) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
30
38
|
handleSubmit(async (formData: any) => {
|
|
31
39
|
await onSave(formData);
|
|
32
40
|
reset();
|
|
@@ -23,6 +23,7 @@ export default function CreateProduct({
|
|
|
23
23
|
const { settings } = usePaymentContext();
|
|
24
24
|
const [state, setState] = useSetState({ loading: false });
|
|
25
25
|
const methods = useForm<Product>({
|
|
26
|
+
mode: 'onChange',
|
|
26
27
|
defaultValues: {
|
|
27
28
|
type: 'service',
|
|
28
29
|
name: '',
|
|
@@ -36,7 +37,12 @@ export default function CreateProduct({
|
|
|
36
37
|
},
|
|
37
38
|
});
|
|
38
39
|
|
|
39
|
-
const {
|
|
40
|
+
const {
|
|
41
|
+
control,
|
|
42
|
+
handleSubmit,
|
|
43
|
+
reset,
|
|
44
|
+
formState: { errors },
|
|
45
|
+
} = methods;
|
|
40
46
|
const prices = useFieldArray({ control, name: 'prices' });
|
|
41
47
|
|
|
42
48
|
const onCreate = (data: Product) => {
|
|
@@ -56,6 +62,9 @@ export default function CreateProduct({
|
|
|
56
62
|
};
|
|
57
63
|
|
|
58
64
|
const onSubmit = () => {
|
|
65
|
+
if (Object.keys(errors).length > 0) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
59
68
|
handleSubmit(async (formData: any) => {
|
|
60
69
|
await onCreate(formData);
|
|
61
70
|
await reset();
|
|
@@ -24,6 +24,7 @@ export default function EditPrice({
|
|
|
24
24
|
}) {
|
|
25
25
|
const { t } = useLocaleContext();
|
|
26
26
|
const methods = useForm<Price>({
|
|
27
|
+
mode: 'onChange',
|
|
27
28
|
defaultValues: {
|
|
28
29
|
...price,
|
|
29
30
|
unit_amount: fromUnitToToken(price.unit_amount, price.currency.decimal),
|
|
@@ -46,8 +47,15 @@ export default function EditPrice({
|
|
|
46
47
|
},
|
|
47
48
|
});
|
|
48
49
|
|
|
49
|
-
const {
|
|
50
|
+
const {
|
|
51
|
+
handleSubmit,
|
|
52
|
+
reset,
|
|
53
|
+
formState: { errors },
|
|
54
|
+
} = methods;
|
|
50
55
|
const onSubmit = () => {
|
|
56
|
+
if (Object.keys(errors).length > 0) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
51
59
|
handleSubmit(async (formData: any) => {
|
|
52
60
|
if (
|
|
53
61
|
Number(formData.quantity_available) > 0 &&
|
|
@@ -19,6 +19,7 @@ import { styled } from '@mui/system';
|
|
|
19
19
|
import { useRequest, useSetState } from 'ahooks';
|
|
20
20
|
import { Link } from 'react-router-dom';
|
|
21
21
|
|
|
22
|
+
import { startCase } from 'lodash';
|
|
22
23
|
import Copyable from '../../../../components/copyable';
|
|
23
24
|
import Currency from '../../../../components/currency';
|
|
24
25
|
import CustomerLink from '../../../../components/customer/link';
|
|
@@ -128,7 +129,21 @@ export default function RefundDetail(props: { id: string }) {
|
|
|
128
129
|
<Stack direction="row" alignItems="center" spacing={1}>
|
|
129
130
|
<Status label={data.status} color={getRefundStatusColor(data.status)} />
|
|
130
131
|
{data.last_attempt_error && (
|
|
131
|
-
<Tooltip
|
|
132
|
+
<Tooltip
|
|
133
|
+
title={
|
|
134
|
+
<pre>
|
|
135
|
+
{JSON.stringify(
|
|
136
|
+
{
|
|
137
|
+
...data.last_attempt_error,
|
|
138
|
+
next_attempt: data.next_attempt ? formatTime(data.next_attempt * 1000) : '-',
|
|
139
|
+
attempt_count: data.attempt_count,
|
|
140
|
+
type: startCase(data.last_attempt_error?.type),
|
|
141
|
+
},
|
|
142
|
+
null,
|
|
143
|
+
2
|
|
144
|
+
)}
|
|
145
|
+
</pre>
|
|
146
|
+
}>
|
|
132
147
|
<InfoOutlined fontSize="small" color="error" />
|
|
133
148
|
</Tooltip>
|
|
134
149
|
)}
|
|
@@ -131,8 +131,8 @@ export default function PriceActions({ data, onChange, variant, setAsDefault }:
|
|
|
131
131
|
color: 'text.primary',
|
|
132
132
|
disabled: true,
|
|
133
133
|
},
|
|
134
|
-
{ label: '
|
|
135
|
-
{ label: '
|
|
134
|
+
{ label: t('admin.paymentLink.add'), handler: onCreatePaymentLink, color: 'primary' },
|
|
135
|
+
{ label: t('admin.pricingTable.add'), handler: onCreatePricingTable, color: 'primary' },
|
|
136
136
|
];
|
|
137
137
|
|
|
138
138
|
if (setAsDefault) {
|
|
@@ -100,7 +100,7 @@ export default function PricesList({ product, onChange }: { product: Product; on
|
|
|
100
100
|
sort: false,
|
|
101
101
|
customBodyRenderLite: (_: any, index: number) => {
|
|
102
102
|
const price = product.prices[index] as any;
|
|
103
|
-
return <PriceActions data={price} onChange={onChange} setAsDefault={price.id
|
|
103
|
+
return <PriceActions data={price} onChange={onChange} setAsDefault={price.id === product.default_price_id} />;
|
|
104
104
|
},
|
|
105
105
|
},
|
|
106
106
|
},
|
|
@@ -20,6 +20,7 @@ export default function ProductsCreate() {
|
|
|
20
20
|
const { settings } = usePaymentContext();
|
|
21
21
|
|
|
22
22
|
const methods = useForm<Product>({
|
|
23
|
+
mode: 'onChange',
|
|
23
24
|
defaultValues: {
|
|
24
25
|
type: 'service',
|
|
25
26
|
name: '',
|
|
@@ -32,7 +33,7 @@ export default function ProductsCreate() {
|
|
|
32
33
|
metadata: [],
|
|
33
34
|
},
|
|
34
35
|
});
|
|
35
|
-
const { control, handleSubmit, getValues } = methods;
|
|
36
|
+
const { control, handleSubmit, getValues, clearErrors } = methods;
|
|
36
37
|
|
|
37
38
|
const prices = useFieldArray({ control, name: 'prices' });
|
|
38
39
|
const getPrice = (index: number) => methods.getValues().prices[index];
|
|
@@ -56,6 +57,7 @@ export default function ProductsCreate() {
|
|
|
56
57
|
<DrawerForm
|
|
57
58
|
icon={<AddOutlined />}
|
|
58
59
|
text={t('admin.product.add')}
|
|
60
|
+
onClose={() => clearErrors()}
|
|
59
61
|
width={640}
|
|
60
62
|
addons={
|
|
61
63
|
<Button variant="contained" size="small" onClick={handleSubmit(onSubmit)}>
|