payment-kit 1.14.37 → 1.14.39
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/integrations/arcblock/stake.ts +1 -0
- package/api/src/integrations/stripe/resource.ts +2 -2
- package/api/src/libs/api.ts +2 -2
- package/api/src/libs/subscription.ts +23 -5
- package/api/src/libs/util.ts +12 -0
- package/api/src/queues/payment.ts +3 -3
- package/api/src/queues/refund.ts +5 -4
- package/api/src/queues/subscription.ts +4 -4
- package/api/src/routes/connect/shared.ts +4 -4
- package/api/src/routes/products.ts +11 -3
- package/api/tests/libs/api.spec.ts +2 -4
- package/blocklet.yml +1 -1
- package/package.json +19 -19
- package/src/components/customer/link.tsx +7 -2
- package/src/components/filter-toolbar.tsx +2 -2
- package/src/components/metadata/form.tsx +2 -2
- package/src/components/price/form.tsx +28 -2
- package/src/components/product/form.tsx +13 -7
- package/src/locales/en.tsx +4 -0
- package/src/locales/zh.tsx +3 -0
- package/src/pages/admin/customers/customers/detail.tsx +14 -2
- package/src/pages/admin/customers/customers/index.tsx +6 -2
- package/src/pages/customer/index.tsx +2 -1
|
@@ -189,6 +189,7 @@ export async function getStakeSummaryByDid(did: string, livemode: boolean = true
|
|
|
189
189
|
return {};
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
// FIXME: should use listStakes to find all stakes and summarize here
|
|
192
193
|
const address = toStakeAddress(did, wallet.address);
|
|
193
194
|
const results: GroupedBN = {};
|
|
194
195
|
await Promise.all(
|
|
@@ -44,7 +44,7 @@ export async function ensureStripeProduct(internal: Product, method: PaymentMeth
|
|
|
44
44
|
attrs.unit_label = internal.unit_label;
|
|
45
45
|
}
|
|
46
46
|
if (internal.statement_descriptor) {
|
|
47
|
-
attrs.statement_descriptor_suffix =
|
|
47
|
+
attrs.statement_descriptor_suffix = '';
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
const product = await client.products.create(attrs);
|
|
@@ -176,7 +176,7 @@ export async function ensureStripePaymentIntent(
|
|
|
176
176
|
enabled: true,
|
|
177
177
|
allow_redirects: 'never',
|
|
178
178
|
},
|
|
179
|
-
statement_descriptor_suffix:
|
|
179
|
+
statement_descriptor_suffix: '',
|
|
180
180
|
metadata: {
|
|
181
181
|
appPid: env.appPid,
|
|
182
182
|
id: internal.id,
|
package/api/src/libs/api.ts
CHANGED
|
@@ -163,13 +163,13 @@ function validateMetadataValue(value: any, helpers: any) {
|
|
|
163
163
|
export const MetadataSchema = Joi.alternatives()
|
|
164
164
|
.try(
|
|
165
165
|
Joi.object()
|
|
166
|
-
.pattern(Joi.string().max(
|
|
166
|
+
.pattern(Joi.string().max(40), Joi.any().custom(validateMetadataValue, 'Custom Validation'))
|
|
167
167
|
.min(0)
|
|
168
168
|
.allow(null),
|
|
169
169
|
Joi.array()
|
|
170
170
|
.items(
|
|
171
171
|
Joi.object({
|
|
172
|
-
key: Joi.string().max(
|
|
172
|
+
key: Joi.string().max(40).required(),
|
|
173
173
|
value: Joi.any().custom(validateMetadataValue, 'Custom Validation').required(),
|
|
174
174
|
})
|
|
175
175
|
)
|
|
@@ -30,7 +30,7 @@ import dayjs from './dayjs';
|
|
|
30
30
|
import env from './env';
|
|
31
31
|
import logger from './logger';
|
|
32
32
|
import { getPriceCurrencyOptions, getPriceUintAmountByCurrency, getRecurringPeriod } from './session';
|
|
33
|
-
import { getConnectQueryParam } from './util';
|
|
33
|
+
import { getConnectQueryParam, getCustomerStakeAddress } from './util';
|
|
34
34
|
|
|
35
35
|
export function getCustomerSubscriptionPageUrl({
|
|
36
36
|
subscriptionId,
|
|
@@ -700,16 +700,25 @@ export async function getSubscriptionRemainingStakeSetup(
|
|
|
700
700
|
paymentMethod: PaymentMethod,
|
|
701
701
|
action: 'return' | 'slash' = 'return'
|
|
702
702
|
) {
|
|
703
|
+
const currency = await PaymentCurrency.findByPk(subscription.currency_id);
|
|
704
|
+
if (!currency) {
|
|
705
|
+
return {
|
|
706
|
+
total: '0',
|
|
707
|
+
return_amount: '0',
|
|
708
|
+
sender: '',
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
703
712
|
const client = paymentMethod.getOcapClient();
|
|
704
713
|
const { state } = await client.getStakeState({ address });
|
|
705
|
-
|
|
706
|
-
if (!state.tokens || !currency) {
|
|
714
|
+
if (!state) {
|
|
707
715
|
return {
|
|
708
716
|
total: '0',
|
|
709
717
|
return_amount: '0',
|
|
710
718
|
sender: '',
|
|
711
719
|
};
|
|
712
720
|
}
|
|
721
|
+
|
|
713
722
|
let total = new BN(state.tokens.find((x: any) => x.address === currency.contract)?.value || '0');
|
|
714
723
|
if (action === 'slash') {
|
|
715
724
|
// add revoked tokens to total
|
|
@@ -763,17 +772,18 @@ export async function checkRemainingStake(
|
|
|
763
772
|
revoked: '0',
|
|
764
773
|
};
|
|
765
774
|
}
|
|
775
|
+
|
|
766
776
|
const client = paymentMethod.getOcapClient();
|
|
767
777
|
const { state } = await client.getStakeState({ address });
|
|
768
|
-
|
|
769
778
|
if (!state) {
|
|
770
|
-
logger.warn('getStakeState failed in checkRemainingStake', { address
|
|
779
|
+
logger.warn('getStakeState failed in checkRemainingStake', { address });
|
|
771
780
|
return {
|
|
772
781
|
enough: false,
|
|
773
782
|
staked: '0',
|
|
774
783
|
revoked: '0',
|
|
775
784
|
};
|
|
776
785
|
}
|
|
786
|
+
|
|
777
787
|
const staked = state.tokens?.find((x: any) => x.address === paymentCurrency.contract);
|
|
778
788
|
const revoked = state.revokedTokens?.find((x: any) => x.address === paymentCurrency.contract);
|
|
779
789
|
let total = new BN(0);
|
|
@@ -783,6 +793,7 @@ export async function checkRemainingStake(
|
|
|
783
793
|
if (revoked) {
|
|
784
794
|
total = total.add(new BN(revoked?.value || '0'));
|
|
785
795
|
}
|
|
796
|
+
|
|
786
797
|
return {
|
|
787
798
|
enough: total.gte(new BN(amount)),
|
|
788
799
|
staked,
|
|
@@ -805,3 +816,10 @@ export function getSubscriptionTrialSetup(data: Partial<SubscriptionData>, curre
|
|
|
805
816
|
trialEnd,
|
|
806
817
|
};
|
|
807
818
|
}
|
|
819
|
+
|
|
820
|
+
export async function getSubscriptionStakeAddress(subscription: Subscription, customerDid: string) {
|
|
821
|
+
return (
|
|
822
|
+
subscription.payment_details?.arcblock?.staking?.address ||
|
|
823
|
+
(await getCustomerStakeAddress(customerDid, subscription.id))
|
|
824
|
+
);
|
|
825
|
+
}
|
package/api/src/libs/util.ts
CHANGED
|
@@ -2,11 +2,14 @@ import crypto from 'crypto';
|
|
|
2
2
|
|
|
3
3
|
import { getUrl } from '@blocklet/sdk/lib/component';
|
|
4
4
|
import env from '@blocklet/sdk/lib/env';
|
|
5
|
+
import { getWalletDid } from '@blocklet/sdk/lib/did';
|
|
6
|
+
import { toStakeAddress } from '@arcblock/did-util';
|
|
5
7
|
import { customAlphabet } from 'nanoid';
|
|
6
8
|
import type { LiteralUnion } from 'type-fest';
|
|
7
9
|
import { withQuery } from 'ufo';
|
|
8
10
|
|
|
9
11
|
import dayjs from './dayjs';
|
|
12
|
+
import { blocklet, wallet } from './auth';
|
|
10
13
|
|
|
11
14
|
export const OCAP_PAYMENT_TX_TYPE = 'fg:t:transfer_v2';
|
|
12
15
|
|
|
@@ -260,3 +263,12 @@ export function formatAmountPrecisionLimit(
|
|
|
260
263
|
}
|
|
261
264
|
return '';
|
|
262
265
|
}
|
|
266
|
+
|
|
267
|
+
export async function getCustomerStakeAddress(customerDid: string, nonce?: string) {
|
|
268
|
+
const { user } = await blocklet.getUser(customerDid, { enableConnectedAccount: true });
|
|
269
|
+
if (user) {
|
|
270
|
+
return toStakeAddress(getWalletDid(user), wallet.address, nonce);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return toStakeAddress(customerDid, wallet.address, nonce);
|
|
274
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import isEmpty from 'lodash/isEmpty';
|
|
2
2
|
|
|
3
|
-
import { toStakeAddress } from '@arcblock/did-util';
|
|
4
3
|
import { ensureStakedForGas } from '../integrations/arcblock/stake';
|
|
5
4
|
import { transferErc20FromUser } from '../integrations/ethereum/token';
|
|
6
5
|
import { createEvent } from '../libs/audit';
|
|
@@ -19,6 +18,7 @@ import {
|
|
|
19
18
|
getMaxRetryCount,
|
|
20
19
|
getMinRetryMail,
|
|
21
20
|
getSubscriptionCreateSetup,
|
|
21
|
+
getSubscriptionStakeAddress,
|
|
22
22
|
shouldCancelSubscription,
|
|
23
23
|
} from '../libs/subscription';
|
|
24
24
|
import { MAX_RETRY_COUNT, MIN_RETRY_MAIL, getNextRetry } from '../libs/util';
|
|
@@ -402,7 +402,7 @@ const handleStakeSlash = async (
|
|
|
402
402
|
return;
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
-
const address =
|
|
405
|
+
const address = await getSubscriptionStakeAddress(subscription, customer.did);
|
|
406
406
|
const slashAmount = paymentIntent.amount;
|
|
407
407
|
const stakeEnough = await checkRemainingStake(paymentMethod, paymentCurrency, address, slashAmount);
|
|
408
408
|
if (!stakeEnough.enough) {
|
|
@@ -432,7 +432,7 @@ const handleStakeSlash = async (
|
|
|
432
432
|
const signed = await client.signSlashStakeTx({
|
|
433
433
|
tx: {
|
|
434
434
|
itx: {
|
|
435
|
-
address
|
|
435
|
+
address,
|
|
436
436
|
outputs: [{ owner: wallet.address, tokens: [{ address: paymentCurrency.contract, value: slashAmount }] }],
|
|
437
437
|
message: 'stake_slash_on_subscription_cancel',
|
|
438
438
|
data: {
|
package/api/src/queues/refund.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { checkRemainingStake } from '@api/libs/subscription';
|
|
1
|
+
import { isRefundReasonSupportedByStripe } from '../libs/refund';
|
|
2
|
+
import { checkRemainingStake, getSubscriptionStakeAddress } from '../libs/subscription';
|
|
4
3
|
import { sendErc20ToUser } from '../integrations/ethereum/token';
|
|
5
4
|
import { wallet } from '../libs/auth';
|
|
6
5
|
import CustomError from '../libs/error';
|
|
@@ -14,6 +13,7 @@ import { PaymentCurrency } from '../store/models/payment-currency';
|
|
|
14
13
|
import { PaymentIntent } from '../store/models/payment-intent';
|
|
15
14
|
import { PaymentMethod } from '../store/models/payment-method';
|
|
16
15
|
import { Refund } from '../store/models/refund';
|
|
16
|
+
import { Subscription } from '../store/models/subscription';
|
|
17
17
|
import type { PaymentError } from '../store/models/types';
|
|
18
18
|
|
|
19
19
|
type RefundJob = {
|
|
@@ -313,7 +313,8 @@ const handleStakeReturnJob = async (
|
|
|
313
313
|
return;
|
|
314
314
|
}
|
|
315
315
|
const client = paymentMethod.getOcapClient();
|
|
316
|
-
const
|
|
316
|
+
const subscription = await Subscription.findByPk(refund.subscription_id);
|
|
317
|
+
const address = await getSubscriptionStakeAddress(subscription!, customer.did);
|
|
317
318
|
const stakeEnough = await checkRemainingStake(paymentMethod, paymentCurrency, address, refund.amount);
|
|
318
319
|
if (!stakeEnough.enough) {
|
|
319
320
|
logger.warn('Stake return aborted because stake is not enough ', {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { toStakeAddress } from '@arcblock/did-util';
|
|
2
1
|
import type { LiteralUnion } from 'type-fest';
|
|
3
2
|
|
|
4
3
|
import { ensurePassportRevoked } from '../integrations/blocklet/passport';
|
|
@@ -15,6 +14,7 @@ import {
|
|
|
15
14
|
checkRemainingStake,
|
|
16
15
|
getSubscriptionCycleAmount,
|
|
17
16
|
getSubscriptionCycleSetup,
|
|
17
|
+
getSubscriptionStakeAddress,
|
|
18
18
|
getSubscriptionStakeReturnSetup,
|
|
19
19
|
getSubscriptionStakeSlashSetup,
|
|
20
20
|
shouldCancelSubscription,
|
|
@@ -329,7 +329,7 @@ const handleStakeSlashAfterCancel = async (subscription: Subscription) => {
|
|
|
329
329
|
|
|
330
330
|
// check the staking
|
|
331
331
|
const client = method.getOcapClient();
|
|
332
|
-
const address =
|
|
332
|
+
const address = await getSubscriptionStakeAddress(subscription, customer.did);
|
|
333
333
|
const { state } = await client.getStakeState({ address });
|
|
334
334
|
if (!state || !state.data?.value) {
|
|
335
335
|
logger.warn('Stake slashing aborted because no staking state', {
|
|
@@ -374,7 +374,7 @@ const handleStakeSlashAfterCancel = async (subscription: Subscription) => {
|
|
|
374
374
|
const signed = await client.signSlashStakeTx({
|
|
375
375
|
tx: {
|
|
376
376
|
itx: {
|
|
377
|
-
address
|
|
377
|
+
address,
|
|
378
378
|
outputs: [{ owner: wallet.address, tokens: [{ address: currency.contract, value: invoice.amount_remaining }] }],
|
|
379
379
|
message: 'uncollectible_past_due_invoice',
|
|
380
380
|
data: {
|
|
@@ -530,7 +530,6 @@ const slashStakeOnCancel = async (subscription: Subscription) => {
|
|
|
530
530
|
});
|
|
531
531
|
return;
|
|
532
532
|
}
|
|
533
|
-
const address = toStakeAddress(customer.did, wallet.address, subscription.id);
|
|
534
533
|
const currency = await PaymentCurrency.findByPk(subscription.currency_id);
|
|
535
534
|
if (!currency) {
|
|
536
535
|
logger.warn('Stake slashing skipped because currency not found', {
|
|
@@ -539,6 +538,7 @@ const slashStakeOnCancel = async (subscription: Subscription) => {
|
|
|
539
538
|
});
|
|
540
539
|
return;
|
|
541
540
|
}
|
|
541
|
+
const address = await getSubscriptionStakeAddress(subscription, customer.did);
|
|
542
542
|
const result = await getSubscriptionStakeSlashSetup(subscription, address, paymentMethod);
|
|
543
543
|
const stakeEnough = await checkRemainingStake(paymentMethod, currency, address, result.return_amount);
|
|
544
544
|
if (!stakeEnough.enough) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/indent */
|
|
2
2
|
/* eslint-disable prettier/prettier */
|
|
3
3
|
import { toTypeInfo } from '@arcblock/did';
|
|
4
|
-
import { toDelegateAddress
|
|
4
|
+
import { toDelegateAddress } from '@arcblock/did-util';
|
|
5
5
|
import type { Transaction } from '@ocap/client';
|
|
6
6
|
import { BN, fromTokenToUnit, toBase58 } from '@ocap/util';
|
|
7
7
|
import { fromPublicKey } from '@ocap/wallet';
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
getSubscriptionItemPrice,
|
|
22
22
|
getSubscriptionStakeSetup,
|
|
23
23
|
} from '../../libs/subscription';
|
|
24
|
-
import { OCAP_PAYMENT_TX_TYPE } from '../../libs/util';
|
|
24
|
+
import { getCustomerStakeAddress, OCAP_PAYMENT_TX_TYPE } from '../../libs/util';
|
|
25
25
|
import { invoiceQueue } from '../../queues/invoice';
|
|
26
26
|
import type { TLineItemExpanded } from '../../store/models';
|
|
27
27
|
import { CheckoutSession } from '../../store/models/checkout-session';
|
|
@@ -745,7 +745,7 @@ export async function getStakeTxClaim({
|
|
|
745
745
|
if (paymentMethod.type === 'arcblock') {
|
|
746
746
|
// create staking data
|
|
747
747
|
const client = paymentMethod.getOcapClient();
|
|
748
|
-
const address =
|
|
748
|
+
const address = await getCustomerStakeAddress(userDid, subscription.id);
|
|
749
749
|
const { state } = await client.getStakeState({ address });
|
|
750
750
|
const data = {
|
|
751
751
|
type: 'json',
|
|
@@ -1026,7 +1026,7 @@ export async function executeOcapTransactions(
|
|
|
1026
1026
|
type: 'delegate',
|
|
1027
1027
|
staking: {
|
|
1028
1028
|
tx_hash: stakingTxHash,
|
|
1029
|
-
address:
|
|
1029
|
+
address: await getCustomerStakeAddress(userDid, nonce),
|
|
1030
1030
|
},
|
|
1031
1031
|
};
|
|
1032
1032
|
}
|
|
@@ -21,11 +21,19 @@ const auth = authenticate<Product>({ component: true, roles: ['owner', 'admin']
|
|
|
21
21
|
const ProductAndPriceSchema = Joi.object({
|
|
22
22
|
name: Joi.string().max(64).empty('').optional(),
|
|
23
23
|
type: Joi.string().valid('service', 'good').empty('').optional(),
|
|
24
|
-
description: Joi.string().max(
|
|
24
|
+
description: Joi.string().max(250).empty('').optional(),
|
|
25
25
|
images: Joi.any().optional(),
|
|
26
26
|
metadata: MetadataSchema,
|
|
27
|
-
statement_descriptor: Joi.string()
|
|
28
|
-
|
|
27
|
+
statement_descriptor: Joi.string()
|
|
28
|
+
.max(22)
|
|
29
|
+
.pattern(/^(?=.*[A-Za-z])[^\u4e00-\u9fa5<>"’\\]*$/)
|
|
30
|
+
.messages({
|
|
31
|
+
'string.pattern.base':
|
|
32
|
+
'statement_descriptor should be at least one letter and cannot include Chinese characters and special characters such as <, >、"、’ or \\',
|
|
33
|
+
})
|
|
34
|
+
.empty('')
|
|
35
|
+
.optional(),
|
|
36
|
+
unit_label: Joi.string().max(12).empty('').optional(),
|
|
29
37
|
nft_factory: Joi.string().max(40).allow(null).empty('').optional(),
|
|
30
38
|
features: Joi.array()
|
|
31
39
|
.items(Joi.object({ name: Joi.string().max(64).empty('').optional() }).unknown(true))
|
|
@@ -177,13 +177,11 @@ describe('MetadataSchema', () => {
|
|
|
177
177
|
|
|
178
178
|
it('should invalidate an object with a key longer than 64 characters', () => {
|
|
179
179
|
const data = {
|
|
180
|
-
['a'.repeat(
|
|
180
|
+
['a'.repeat(41)]: 'value1',
|
|
181
181
|
};
|
|
182
182
|
const { error } = MetadataSchema.validate(data);
|
|
183
183
|
expect(error).toBeDefined();
|
|
184
|
-
expect(error?.details?.[0]?.message).toMatch(
|
|
185
|
-
/"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" is not allowed/
|
|
186
|
-
);
|
|
184
|
+
expect(error?.details?.[0]?.message).toMatch(/"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" is not allowed/);
|
|
187
185
|
});
|
|
188
186
|
|
|
189
187
|
it('should invalidate an array with an object missing the key field', () => {
|
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.39",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -43,34 +43,34 @@
|
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@abtnode/cron": "1.16.30",
|
|
46
|
-
"@arcblock/did": "^1.18.
|
|
46
|
+
"@arcblock/did": "^1.18.135",
|
|
47
47
|
"@arcblock/did-auth-storage-nedb": "^1.7.1",
|
|
48
|
-
"@arcblock/did-connect": "^2.10.
|
|
49
|
-
"@arcblock/did-util": "^1.18.
|
|
50
|
-
"@arcblock/jwt": "^1.18.
|
|
51
|
-
"@arcblock/ux": "
|
|
52
|
-
"@arcblock/validator": "^1.18.
|
|
48
|
+
"@arcblock/did-connect": "^2.10.25",
|
|
49
|
+
"@arcblock/did-util": "^1.18.135",
|
|
50
|
+
"@arcblock/jwt": "^1.18.135",
|
|
51
|
+
"@arcblock/ux": "2.10.24",
|
|
52
|
+
"@arcblock/validator": "^1.18.135",
|
|
53
53
|
"@blocklet/js-sdk": "1.16.30",
|
|
54
54
|
"@blocklet/logger": "1.16.30",
|
|
55
|
-
"@blocklet/payment-react": "1.14.
|
|
55
|
+
"@blocklet/payment-react": "1.14.39",
|
|
56
56
|
"@blocklet/sdk": "1.16.30",
|
|
57
|
-
"@blocklet/ui-react": "^2.10.
|
|
57
|
+
"@blocklet/ui-react": "^2.10.25",
|
|
58
58
|
"@blocklet/uploader": "^0.1.27",
|
|
59
59
|
"@mui/icons-material": "^5.16.6",
|
|
60
60
|
"@mui/lab": "^5.0.0-alpha.173",
|
|
61
61
|
"@mui/material": "^5.16.6",
|
|
62
62
|
"@mui/styles": "^5.16.6",
|
|
63
63
|
"@mui/system": "^5.16.6",
|
|
64
|
-
"@ocap/asset": "^1.18.
|
|
65
|
-
"@ocap/client": "^1.18.
|
|
66
|
-
"@ocap/mcrypto": "^1.18.
|
|
67
|
-
"@ocap/util": "^1.18.
|
|
68
|
-
"@ocap/wallet": "^1.18.
|
|
64
|
+
"@ocap/asset": "^1.18.135",
|
|
65
|
+
"@ocap/client": "^1.18.135",
|
|
66
|
+
"@ocap/mcrypto": "^1.18.135",
|
|
67
|
+
"@ocap/util": "^1.18.135",
|
|
68
|
+
"@ocap/wallet": "^1.18.135",
|
|
69
69
|
"@react-pdf/renderer": "^3.4.4",
|
|
70
70
|
"@stripe/react-stripe-js": "^2.7.3",
|
|
71
71
|
"@stripe/stripe-js": "^2.4.0",
|
|
72
72
|
"ahooks": "^3.8.0",
|
|
73
|
-
"axios": "^1.7.
|
|
73
|
+
"axios": "^1.7.5",
|
|
74
74
|
"body-parser": "^1.20.2",
|
|
75
75
|
"cls-hooked": "^4.2.2",
|
|
76
76
|
"cookie-parser": "^1.4.6",
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"devDependencies": {
|
|
120
120
|
"@abtnode/types": "1.16.30",
|
|
121
121
|
"@arcblock/eslint-config-ts": "^0.3.2",
|
|
122
|
-
"@blocklet/payment-types": "1.14.
|
|
122
|
+
"@blocklet/payment-types": "1.14.39",
|
|
123
123
|
"@types/cookie-parser": "^1.4.7",
|
|
124
124
|
"@types/cors": "^2.8.17",
|
|
125
125
|
"@types/debug": "^4.1.12",
|
|
@@ -139,13 +139,13 @@
|
|
|
139
139
|
"npm-run-all": "^4.1.5",
|
|
140
140
|
"prettier": "^3.3.3",
|
|
141
141
|
"prettier-plugin-import-sort": "^0.0.7",
|
|
142
|
-
"ts-jest": "^29.2.
|
|
142
|
+
"ts-jest": "^29.2.5",
|
|
143
143
|
"ts-node": "^10.9.2",
|
|
144
144
|
"type-fest": "^4.23.0",
|
|
145
145
|
"typescript": "^4.9.5",
|
|
146
146
|
"vite": "^5.3.5",
|
|
147
147
|
"vite-node": "^2.0.4",
|
|
148
|
-
"vite-plugin-blocklet": "^0.9.
|
|
148
|
+
"vite-plugin-blocklet": "^0.9.3",
|
|
149
149
|
"vite-plugin-node-polyfills": "^0.21.0",
|
|
150
150
|
"vite-plugin-svgr": "^4.2.0",
|
|
151
151
|
"vite-tsconfig-paths": "^4.3.2",
|
|
@@ -161,5 +161,5 @@
|
|
|
161
161
|
"parser": "typescript"
|
|
162
162
|
}
|
|
163
163
|
},
|
|
164
|
-
"gitHead": "
|
|
164
|
+
"gitHead": "a0f45877c7288ccd7a7f73b3fedc464c19cc0163"
|
|
165
165
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { TCustomer } from '@blocklet/payment-types';
|
|
2
2
|
import { Link } from 'react-router-dom';
|
|
3
3
|
|
|
4
|
+
import { getCustomerAvatar } from '@blocklet/payment-react';
|
|
4
5
|
import InfoCard from '../info-card';
|
|
5
6
|
|
|
6
7
|
export default function CustomerLink({ customer, linked }: { customer: TCustomer; linked?: boolean }) {
|
|
@@ -11,7 +12,11 @@ export default function CustomerLink({ customer, linked }: { customer: TCustomer
|
|
|
11
12
|
return (
|
|
12
13
|
<Link to={`/admin/customers/${customer.id}`}>
|
|
13
14
|
<InfoCard
|
|
14
|
-
logo={
|
|
15
|
+
logo={getCustomerAvatar(
|
|
16
|
+
customer?.did,
|
|
17
|
+
customer?.updated_at ? new Date(customer.updated_at).toISOString() : '',
|
|
18
|
+
48
|
|
19
|
+
)}
|
|
15
20
|
name={customer.email}
|
|
16
21
|
description={`${customer.did.slice(0, 6)}...${customer.did.slice(-6)}`}
|
|
17
22
|
/>
|
|
@@ -21,7 +26,7 @@ export default function CustomerLink({ customer, linked }: { customer: TCustomer
|
|
|
21
26
|
|
|
22
27
|
return (
|
|
23
28
|
<InfoCard
|
|
24
|
-
logo={
|
|
29
|
+
logo={getCustomerAvatar(customer.did, customer.updated_at ? new Date(customer.updated_at).toISOString() : '', 48)}
|
|
25
30
|
name={customer.email}
|
|
26
31
|
description={<span style={{ wordBreak: 'break-all' }}>{customer?.did}</span>}
|
|
27
32
|
/>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
-
import { api, useMobile, usePaymentContext } from '@blocklet/payment-react';
|
|
2
|
+
import { api, getCustomerAvatar, useMobile, usePaymentContext } from '@blocklet/payment-react';
|
|
3
3
|
import type { TCustomer } from '@blocklet/payment-types';
|
|
4
4
|
import { Add, Close } from '@mui/icons-material';
|
|
5
5
|
import { Button, Menu, MenuItem } from '@mui/material';
|
|
@@ -300,7 +300,7 @@ function SearchCustomers({ setSearch, search }: Pick<Props, 'setSearch' | 'searc
|
|
|
300
300
|
setShow(null);
|
|
301
301
|
}}>
|
|
302
302
|
<InfoCard
|
|
303
|
-
logo={
|
|
303
|
+
logo={getCustomerAvatar(x?.did, x?.updated_at, 48)}
|
|
304
304
|
name={x.email}
|
|
305
305
|
key={x.id}
|
|
306
306
|
description={`${x.did.slice(0, 6)}...${x.did.slice(-6)}`}
|
|
@@ -75,7 +75,7 @@ export default function MetadataForm({
|
|
|
75
75
|
required: t('payment.checkout.required'),
|
|
76
76
|
maxLength: {
|
|
77
77
|
value: 64,
|
|
78
|
-
message: t('common.maxLength', { len:
|
|
78
|
+
message: t('common.maxLength', { len: 40 }),
|
|
79
79
|
},
|
|
80
80
|
}}
|
|
81
81
|
placeholder="Key"
|
|
@@ -83,7 +83,7 @@ export default function MetadataForm({
|
|
|
83
83
|
// @ts-ignore
|
|
84
84
|
ref={errors?.metadata?.[index]?.key ? errorRef : null}
|
|
85
85
|
inputProps={{
|
|
86
|
-
maxLength:
|
|
86
|
+
maxLength: 40,
|
|
87
87
|
}}
|
|
88
88
|
/>
|
|
89
89
|
<FormInput
|
|
@@ -104,6 +104,7 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
|
104
104
|
control,
|
|
105
105
|
setValue,
|
|
106
106
|
formState: { errors },
|
|
107
|
+
trigger,
|
|
107
108
|
} = useFormContext();
|
|
108
109
|
const getFieldError = (name: string) => {
|
|
109
110
|
const names = name?.split('.');
|
|
@@ -111,7 +112,6 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
|
111
112
|
};
|
|
112
113
|
const { settings, livemode } = usePaymentContext();
|
|
113
114
|
const currencies = useFieldArray({ control, name: getFieldName('currency_options') });
|
|
114
|
-
|
|
115
115
|
const priceLocked = useWatch({ control, name: getFieldName('locked') });
|
|
116
116
|
const isRecurring = useWatch({ control, name: getFieldName('type') }) === 'recurring';
|
|
117
117
|
const isMetered = useWatch({ control, name: getFieldName('recurring.usage_type') }) === 'metered';
|
|
@@ -138,6 +138,11 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
|
138
138
|
return true;
|
|
139
139
|
};
|
|
140
140
|
|
|
141
|
+
const handleRemoveCurrency = async (index: number) => {
|
|
142
|
+
await currencies.remove(index);
|
|
143
|
+
trigger(getFieldName('recurring.interval_config'));
|
|
144
|
+
};
|
|
145
|
+
|
|
141
146
|
return (
|
|
142
147
|
<Root direction="column" alignItems="flex-start" spacing={2}>
|
|
143
148
|
{isLocked && <Alert severity="info">{t('admin.price.locked')}</Alert>}
|
|
@@ -280,7 +285,7 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
|
280
285
|
);
|
|
281
286
|
}}
|
|
282
287
|
/>
|
|
283
|
-
<IconButton size="small" disabled={isLocked} onClick={() =>
|
|
288
|
+
<IconButton size="small" disabled={isLocked} onClick={() => handleRemoveCurrency(index)}>
|
|
284
289
|
<DeleteOutlineOutlined color="error" sx={{ opacity: 0.75 }} />
|
|
285
290
|
</IconButton>
|
|
286
291
|
</Stack>
|
|
@@ -322,6 +327,19 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
|
322
327
|
name={getFieldName('recurring.interval_config')}
|
|
323
328
|
control={control}
|
|
324
329
|
disabled={isLocked}
|
|
330
|
+
rules={{
|
|
331
|
+
validate: (val) => {
|
|
332
|
+
const hasStripe = currencies.fields?.some((x: any) => {
|
|
333
|
+
return !!settings.paymentMethods.find(
|
|
334
|
+
(y) => y?.type === 'stripe' && x?.currency_id === y?.default_currency_id
|
|
335
|
+
);
|
|
336
|
+
});
|
|
337
|
+
if (val === 'hour_1' && hasStripe) {
|
|
338
|
+
return t('admin.price.recurring.stripeTip');
|
|
339
|
+
}
|
|
340
|
+
return true;
|
|
341
|
+
},
|
|
342
|
+
}}
|
|
325
343
|
render={({ field }) => (
|
|
326
344
|
<Box>
|
|
327
345
|
<FormLabel sx={{ color: 'text.primary' }}>{t('admin.price.recurring.interval')}</FormLabel>
|
|
@@ -333,8 +351,10 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
|
333
351
|
setValue(getFieldName('recurring.interval'), interval);
|
|
334
352
|
setValue(getFieldName('recurring.interval_count'), +count);
|
|
335
353
|
setValue(getFieldName('recurring.interval_config'), e.target.value);
|
|
354
|
+
trigger(getFieldName('recurring.interval_config'));
|
|
336
355
|
}}
|
|
337
356
|
sx={{ width: INPUT_WIDTH }}
|
|
357
|
+
error={!!get(errors, getFieldName('recurring.interval_config'))}
|
|
338
358
|
size="small">
|
|
339
359
|
{!livemode && <MenuItem value="hour_1">{t('common.hourly')}</MenuItem>}
|
|
340
360
|
<MenuItem value="day_1">{t('common.daily')}</MenuItem>
|
|
@@ -345,6 +365,12 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
|
345
365
|
<MenuItem value="year_1">{t('common.yearly')}</MenuItem>
|
|
346
366
|
<MenuItem value="month_2">{t('common.custom')}</MenuItem>
|
|
347
367
|
</Select>
|
|
368
|
+
{get(errors, getFieldName('recurring.interval_config'))?.message && (
|
|
369
|
+
<Typography color="error" sx={{ fontSize: '0.75rem', mt: 0.5, ml: 1.75 }}>
|
|
370
|
+
{/* @ts-ignore */}
|
|
371
|
+
{get(errors, getFieldName('recurring.interval_config')).message}
|
|
372
|
+
</Typography>
|
|
373
|
+
)}
|
|
348
374
|
</Box>
|
|
349
375
|
)}
|
|
350
376
|
/>
|
|
@@ -57,8 +57,8 @@ export default function ProductForm(props: Props) {
|
|
|
57
57
|
rules={{
|
|
58
58
|
required: t('admin.product.description.required'),
|
|
59
59
|
maxLength: {
|
|
60
|
-
value:
|
|
61
|
-
message: t('common.maxLength', { len:
|
|
60
|
+
value: 250,
|
|
61
|
+
message: t('common.maxLength', { len: 250 }),
|
|
62
62
|
},
|
|
63
63
|
}}
|
|
64
64
|
label={t('admin.product.description.label')}
|
|
@@ -68,21 +68,27 @@ export default function ProductForm(props: Props) {
|
|
|
68
68
|
multiline
|
|
69
69
|
minRows={2}
|
|
70
70
|
maxRows={4}
|
|
71
|
-
inputProps={{ maxLength:
|
|
71
|
+
inputProps={{ maxLength: 250 }}
|
|
72
72
|
/>
|
|
73
73
|
<Collapse trigger={t('admin.product.additional')}>
|
|
74
74
|
<Stack spacing={2} alignItems="flex-start">
|
|
75
75
|
<FormInput
|
|
76
76
|
name="statement_descriptor"
|
|
77
77
|
label={t('admin.product.statement_descriptor.label')}
|
|
78
|
-
rules={{
|
|
79
|
-
|
|
78
|
+
rules={{
|
|
79
|
+
maxLength: { value: 22, message: t('common.maxLength', { len: 22 }) },
|
|
80
|
+
pattern: {
|
|
81
|
+
value: /^(?=.*[A-Za-z])[^\u4e00-\u9fa5<>"’\\]*$/,
|
|
82
|
+
message: t('common.latinOnly'),
|
|
83
|
+
},
|
|
84
|
+
}}
|
|
80
85
|
/>
|
|
81
86
|
<FormInput
|
|
82
87
|
name="unit_label"
|
|
83
88
|
label={t('admin.product.unit_label.label')}
|
|
84
|
-
rules={{
|
|
85
|
-
|
|
89
|
+
rules={{
|
|
90
|
+
maxLength: { value: 12, message: t('common.maxLength', { len: 12 }) },
|
|
91
|
+
}}
|
|
86
92
|
/>
|
|
87
93
|
{!props.simple && <ProductFeatures />}
|
|
88
94
|
{!props.simple && <MetadataForm title={t('common.metadata.label')} />}
|
package/src/locales/en.tsx
CHANGED
|
@@ -18,6 +18,9 @@ export default flat({
|
|
|
18
18
|
exit: 'Exit',
|
|
19
19
|
maxLength: 'Max {len} characters',
|
|
20
20
|
minLength: 'Min {len} characters',
|
|
21
|
+
invalidCharacters: 'Invalid characters',
|
|
22
|
+
latinOnly:
|
|
23
|
+
'At least one letter and cannot include Chinese characters and special characters such as <, >、"、’ or \\',
|
|
21
24
|
loading: 'Loading...',
|
|
22
25
|
},
|
|
23
26
|
admin: {
|
|
@@ -148,6 +151,7 @@ export default flat({
|
|
|
148
151
|
'Metered billing lets you charge customers based on reported usage at the end of each billing period.',
|
|
149
152
|
aggregate: 'Charge for metered usage by',
|
|
150
153
|
intervalCountTip: 'Billing interval must be a positive integer',
|
|
154
|
+
stripeTip: 'Stripe requires the billing period to be greater than or equal to 1 day',
|
|
151
155
|
},
|
|
152
156
|
currency: {
|
|
153
157
|
add: 'Add more currencies',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -18,6 +18,8 @@ export default flat({
|
|
|
18
18
|
exit: '退出',
|
|
19
19
|
maxLength: '最多输入{len}个字符',
|
|
20
20
|
minLength: '最少输入{len}个字符',
|
|
21
|
+
invalidCharacters: '无效字符',
|
|
22
|
+
latinOnly: '至少包含一个字母,并且不能包含中文字符和特殊字符如 <, >、"、’ 或 \\',
|
|
21
23
|
loading: '加载中...',
|
|
22
24
|
},
|
|
23
25
|
admin: {
|
|
@@ -144,6 +146,7 @@ export default flat({
|
|
|
144
146
|
meteredTip: '计量计费允许您根据每个计费周期结束时的报告的使用情况向客户收费。',
|
|
145
147
|
aggregate: '按何种方式收费计量使用',
|
|
146
148
|
intervalCountTip: '计费周期必须是正整数',
|
|
149
|
+
stripeTip: 'Stripe要求计费周期不能为小时',
|
|
147
150
|
},
|
|
148
151
|
currency: {
|
|
149
152
|
add: '添加更多货币',
|
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
import DidAddress from '@arcblock/ux/lib/DID';
|
|
3
3
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
4
4
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
api,
|
|
7
|
+
formatBNStr,
|
|
8
|
+
formatError,
|
|
9
|
+
formatTime,
|
|
10
|
+
getCustomerAvatar,
|
|
11
|
+
useMobile,
|
|
12
|
+
usePaymentContext,
|
|
13
|
+
} from '@blocklet/payment-react';
|
|
6
14
|
import type { GroupedBN, TCustomerExpanded, TPaymentMethodExpanded } from '@blocklet/payment-types';
|
|
7
15
|
import { ArrowBackOutlined } from '@mui/icons-material';
|
|
8
16
|
import { Alert, Avatar, Box, Button, CircularProgress, Divider, Stack, Typography } from '@mui/material';
|
|
@@ -196,7 +204,11 @@ export default function CustomerDetail(props: { id: string }) {
|
|
|
196
204
|
<Stack direction="row" alignItems="center" spacing={1}>
|
|
197
205
|
<Avatar
|
|
198
206
|
title={data.customer.name}
|
|
199
|
-
src={
|
|
207
|
+
src={getCustomerAvatar(
|
|
208
|
+
data.customer?.did,
|
|
209
|
+
data.customer?.updated_at ? new Date(data.customer.updated_at).toISOString() : '',
|
|
210
|
+
52
|
|
211
|
+
)}
|
|
200
212
|
variant="square"
|
|
201
213
|
sx={{ width: 52, height: 52, borderRadius: 'var(--radius-s, 4px)' }}
|
|
202
214
|
/>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { getDurableData } from '@arcblock/ux/lib/Datatable';
|
|
3
3
|
import DidAddress from '@arcblock/ux/lib/DID';
|
|
4
4
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
5
|
-
import { api, formatTime, Table } from '@blocklet/payment-react';
|
|
5
|
+
import { api, formatTime, getCustomerAvatar, Table } from '@blocklet/payment-react';
|
|
6
6
|
import type { TCustomer } from '@blocklet/payment-types';
|
|
7
7
|
import { Avatar, CircularProgress, Stack, Typography } from '@mui/material';
|
|
8
8
|
import { useEffect, useState } from 'react';
|
|
@@ -60,7 +60,11 @@ export default function CustomersList() {
|
|
|
60
60
|
<Link to={`/admin/customers/${item.id}`}>
|
|
61
61
|
<Stack direction="row" alignItems="center" spacing={1}>
|
|
62
62
|
<Avatar
|
|
63
|
-
src={
|
|
63
|
+
src={getCustomerAvatar(
|
|
64
|
+
item?.did,
|
|
65
|
+
item?.updated_at ? new Date(item.updated_at).toISOString() : '',
|
|
66
|
+
48
|
|
67
|
+
)}
|
|
64
68
|
variant="square"
|
|
65
69
|
sx={{ borderRadius: 'var(--radius-m, 8px)' }}
|
|
66
70
|
/>
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
CustomerInvoiceList,
|
|
6
6
|
formatBNStr,
|
|
7
7
|
formatError,
|
|
8
|
+
getCustomerAvatar,
|
|
8
9
|
getPrefix,
|
|
9
10
|
TruncatedText,
|
|
10
11
|
useMobile,
|
|
@@ -337,7 +338,7 @@ export default function CustomerHome() {
|
|
|
337
338
|
<Box display="flex" alignItems="center" gap={1} flexWrap="wrap" sx={{ mb: 3 }}>
|
|
338
339
|
<Avatar
|
|
339
340
|
title={data?.name}
|
|
340
|
-
src={
|
|
341
|
+
src={getCustomerAvatar(data?.did, data?.updated_at ? new Date(data.updated_at).toISOString() : '', 48)}
|
|
341
342
|
variant="circular"
|
|
342
343
|
sx={{ width: 48, height: 48 }}
|
|
343
344
|
/>
|