payment-kit 1.18.34 → 1.18.36
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/crons/subscription-trial-will-end.ts +1 -1
- package/api/src/index.ts +2 -0
- package/api/src/libs/did-space.ts +235 -0
- package/api/src/libs/util.ts +33 -1
- package/api/src/queues/payment.ts +7 -4
- package/api/src/queues/space.ts +661 -0
- package/api/src/queues/subscription.ts +1 -1
- package/api/src/routes/customers.ts +47 -0
- package/api/src/store/models/invoice.ts +12 -3
- package/api/tests/libs/util.spec.ts +215 -1
- package/blocklet.yml +2 -1
- package/package.json +10 -9
- package/scripts/sdk.js +58 -2
- package/src/components/customer/link.tsx +32 -57
- package/src/components/invoice/list.tsx +1 -1
- package/src/components/payouts/list.tsx +8 -2
- package/src/libs/util.ts +0 -13
- package/src/pages/admin/customers/customers/detail.tsx +2 -2
- package/src/pages/admin/customers/customers/index.tsx +1 -2
- package/src/pages/admin/payments/payouts/detail.tsx +1 -1
- package/src/pages/customer/recharge/account.tsx +2 -1
- package/src/pages/customer/recharge/subscription.tsx +2 -1
- package/src/pages/customer/subscription/embed.tsx +1 -0
|
@@ -25,6 +25,8 @@ import {
|
|
|
25
25
|
import { getSubscriptionPaymentAddress, calculateRecommendedRechargeAmount } from '../libs/subscription';
|
|
26
26
|
import { expandLineItems } from '../libs/session';
|
|
27
27
|
import { handleNotificationPreferenceChange } from '../queues/notification';
|
|
28
|
+
import { getEndpointAndSpaceDid } from '../libs/did-space';
|
|
29
|
+
import { spaceQueue } from '../queues/space';
|
|
28
30
|
|
|
29
31
|
const router = Router();
|
|
30
32
|
const auth = authenticate<Customer>({ component: true, roles: ['owner', 'admin'] });
|
|
@@ -172,6 +174,51 @@ router.get('/me', sessionMiddleware(), async (req, res) => {
|
|
|
172
174
|
}
|
|
173
175
|
});
|
|
174
176
|
|
|
177
|
+
router.post('/sync-to-space', sessionMiddleware(), async (req, res) => {
|
|
178
|
+
if (!req.user) {
|
|
179
|
+
return res.status(403).json({ error: 'Unauthorized' });
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const userDid = req.user.did;
|
|
183
|
+
const jobId = `space-${userDid}`;
|
|
184
|
+
const { endpoint } = await getEndpointAndSpaceDid(userDid);
|
|
185
|
+
if (endpoint) {
|
|
186
|
+
const mainTask = await spaceQueue.get(jobId);
|
|
187
|
+
if (mainTask) {
|
|
188
|
+
return res.json({
|
|
189
|
+
success: true,
|
|
190
|
+
message: 'Billing data sync already in progress',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
spaceQueue.push({
|
|
194
|
+
id: jobId,
|
|
195
|
+
job: {
|
|
196
|
+
type: 'customer',
|
|
197
|
+
data: {
|
|
198
|
+
id: userDid,
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
delay: 60, // delay 1min
|
|
202
|
+
});
|
|
203
|
+
logger.info('Queued billing sync to DID Space for user:', { did: req.user.did });
|
|
204
|
+
|
|
205
|
+
return res.json({
|
|
206
|
+
success: true,
|
|
207
|
+
message: 'Billing data sync will start soon',
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return res.json({
|
|
211
|
+
success: false,
|
|
212
|
+
message: 'No endpoint found for the user',
|
|
213
|
+
});
|
|
214
|
+
} catch (error) {
|
|
215
|
+
return res.json({
|
|
216
|
+
success: false,
|
|
217
|
+
message: error.message,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
175
222
|
// get overdue invoices
|
|
176
223
|
router.get('/:id/overdue/invoices', sessionMiddleware(), async (req, res) => {
|
|
177
224
|
if (!req.user) {
|
|
@@ -298,7 +298,12 @@ export class Invoice extends Model<InferAttributes<Invoice>, InferCreationAttrib
|
|
|
298
298
|
'subscription_cancel',
|
|
299
299
|
'subscription',
|
|
300
300
|
'manual',
|
|
301
|
-
'upcoming'
|
|
301
|
+
'upcoming',
|
|
302
|
+
'slash_stake',
|
|
303
|
+
'stake',
|
|
304
|
+
'overdraft_protection',
|
|
305
|
+
'stake_overdraft_protection',
|
|
306
|
+
'recharge'
|
|
302
307
|
),
|
|
303
308
|
},
|
|
304
309
|
custom_fields: {
|
|
@@ -469,8 +474,12 @@ export class Invoice extends Model<InferAttributes<Invoice>, InferCreationAttrib
|
|
|
469
474
|
createdAt: 'created_at',
|
|
470
475
|
updatedAt: 'updated_at',
|
|
471
476
|
hooks: {
|
|
472
|
-
afterCreate: (model: Invoice, options) =>
|
|
473
|
-
createEvent('Invoice', 'invoice.created', model, options).catch(console.error)
|
|
477
|
+
afterCreate: (model: Invoice, options) => {
|
|
478
|
+
createEvent('Invoice', 'invoice.created', model, options).catch(console.error);
|
|
479
|
+
if (model.status === 'paid') {
|
|
480
|
+
createEvent('Invoice', 'invoice.paid', model, options).catch(console.error);
|
|
481
|
+
}
|
|
482
|
+
},
|
|
474
483
|
afterUpdate: (model: Invoice, options) => {
|
|
475
484
|
createEvent('Invoice', 'invoice.updated', model, options).catch(console.error);
|
|
476
485
|
createStatusEvent(
|
|
@@ -13,8 +13,14 @@ import {
|
|
|
13
13
|
getSubscriptionNotificationCustomActions,
|
|
14
14
|
isUserInBlocklist,
|
|
15
15
|
api,
|
|
16
|
+
md5,
|
|
17
|
+
safeJsonParse,
|
|
18
|
+
getExplorerLink,
|
|
19
|
+
resolveAddressChainTypes,
|
|
20
|
+
formatCurrencyInfo,
|
|
21
|
+
getExplorerTxUrl,
|
|
16
22
|
} from '../../src/libs/util';
|
|
17
|
-
import type { Subscription, PaymentMethod } from '../../src/store/models';
|
|
23
|
+
import type { Subscription, PaymentMethod, PaymentCurrency } from '../../src/store/models';
|
|
18
24
|
|
|
19
25
|
import { blocklet } from '../../src/libs/auth';
|
|
20
26
|
import logger from '../../src/libs/logger';
|
|
@@ -653,3 +659,211 @@ describe('isUserInBlocklist', () => {
|
|
|
653
659
|
expect(result).toBe(true);
|
|
654
660
|
});
|
|
655
661
|
});
|
|
662
|
+
|
|
663
|
+
describe('md5', () => {
|
|
664
|
+
it('should generate a consistent hash for the same input', () => {
|
|
665
|
+
const input = 'test-string';
|
|
666
|
+
const result1 = md5(input);
|
|
667
|
+
const result2 = md5(input);
|
|
668
|
+
expect(result1).toBe(result2);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it('should generate different hashes for different inputs', () => {
|
|
672
|
+
const result1 = md5('test-string-1');
|
|
673
|
+
const result2 = md5('test-string-2');
|
|
674
|
+
expect(result1).not.toBe(result2);
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it('should return a 32 character hexadecimal string', () => {
|
|
678
|
+
const result = md5('test-string');
|
|
679
|
+
expect(result).toMatch(/^[0-9a-f]{32}$/);
|
|
680
|
+
});
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
describe('safeJsonParse', () => {
|
|
684
|
+
it('should parse valid JSON strings', () => {
|
|
685
|
+
const input = '{"key":"value","number":123}';
|
|
686
|
+
const result = safeJsonParse(input, null);
|
|
687
|
+
expect(result).toEqual({ key: 'value', number: 123 });
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
it('should return the default value when parsing invalid JSON', () => {
|
|
691
|
+
const input = '{invalid json}';
|
|
692
|
+
const defaultValue = { default: true };
|
|
693
|
+
const result = safeJsonParse(input, defaultValue);
|
|
694
|
+
expect(result).toEqual(defaultValue);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it('should return the input when parsing fails and no default value is provided', () => {
|
|
698
|
+
const input = '{invalid json}';
|
|
699
|
+
const result = safeJsonParse(input, undefined);
|
|
700
|
+
expect(result).toBe(input);
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
describe('getExplorerLink', () => {
|
|
705
|
+
it('should return undefined when chainHost is not provided', () => {
|
|
706
|
+
const result = getExplorerLink({
|
|
707
|
+
type: 'asset',
|
|
708
|
+
did: 'did:example:123',
|
|
709
|
+
chainHost: undefined,
|
|
710
|
+
});
|
|
711
|
+
expect(result).toBeUndefined();
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it('should construct a proper asset URL with the given parameters', () => {
|
|
715
|
+
const result = getExplorerLink({
|
|
716
|
+
type: 'asset',
|
|
717
|
+
did: 'did:example:123',
|
|
718
|
+
chainHost: 'https://chain.example.com',
|
|
719
|
+
});
|
|
720
|
+
expect(result).toBe('https://chain.example.com/explorer/assets/did:example:123');
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it('should construct a proper account URL with the given parameters', () => {
|
|
724
|
+
const result = getExplorerLink({
|
|
725
|
+
type: 'account',
|
|
726
|
+
did: 'did:example:123',
|
|
727
|
+
chainHost: 'https://chain.example.com',
|
|
728
|
+
});
|
|
729
|
+
expect(result).toBe('https://chain.example.com/explorer/accounts/did:example:123');
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
it('should append query parameters when provided', () => {
|
|
733
|
+
const result = getExplorerLink({
|
|
734
|
+
type: 'tx',
|
|
735
|
+
did: 'txhash123',
|
|
736
|
+
chainHost: 'https://chain.example.com',
|
|
737
|
+
queryParams: { foo: 'bar', baz: 'qux' },
|
|
738
|
+
});
|
|
739
|
+
expect(result).toBe('https://chain.example.com/explorer/txs/txhash123?foo=bar&baz=qux');
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
it('should construct a default URL when type is not recognized', () => {
|
|
743
|
+
const result = getExplorerLink({
|
|
744
|
+
type: 'unknown',
|
|
745
|
+
did: 'did:example:123',
|
|
746
|
+
chainHost: 'https://chain.example.com',
|
|
747
|
+
});
|
|
748
|
+
expect(result).toBe('https://chain.example.com/');
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
it('should handle invalid chain host URLs', () => {
|
|
752
|
+
const result = getExplorerLink({
|
|
753
|
+
type: 'asset',
|
|
754
|
+
did: 'did:example:123',
|
|
755
|
+
chainHost: 'invalid-url',
|
|
756
|
+
});
|
|
757
|
+
expect(result).toBeUndefined();
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
describe('resolveAddressChainTypes', () => {
|
|
762
|
+
it('should return ethereum, base, and arcblock for Ethereum addresses', () => {
|
|
763
|
+
const result = resolveAddressChainTypes('0x71C7656EC7ab88b098defB751B7401B5f6d8976F');
|
|
764
|
+
expect(result).toEqual(['ethereum', 'base', 'arcblock']);
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
it('should return arcblock for non-Ethereum addresses', () => {
|
|
768
|
+
const result = resolveAddressChainTypes('did:abt:z1muQ3xqHQK2uiACHyChikobsiY5kLqtShA');
|
|
769
|
+
expect(result).toEqual(['arcblock']);
|
|
770
|
+
});
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
describe('formatCurrencyInfo', () => {
|
|
774
|
+
it('should format amount with currency symbol when isToken is true', () => {
|
|
775
|
+
const paymentCurrency = { symbol: 'ETH', decimal: 18 } as PaymentCurrency;
|
|
776
|
+
const result = formatCurrencyInfo('10', paymentCurrency, null, true);
|
|
777
|
+
expect(result).toBe('10 ETH');
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
it('should use fromUnitToToken to format amount when isToken is false', () => {
|
|
781
|
+
const paymentCurrency = { symbol: 'ETH', decimal: 18 } as PaymentCurrency;
|
|
782
|
+
// Mock the fromUnitToToken function to return a predictable value
|
|
783
|
+
// In a real test, you might use jest.mock to mock this dependency
|
|
784
|
+
// For this example, we'll assume fromUnitToToken converts correctly
|
|
785
|
+
const result = formatCurrencyInfo('1000000000000000000', paymentCurrency, null);
|
|
786
|
+
// In reality, this would be '1 ETH'
|
|
787
|
+
expect(result).toContain('ETH');
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
it('should include payment method name for non-arcblock payment methods', () => {
|
|
791
|
+
const paymentCurrency = { symbol: 'USD', decimal: 2 } as PaymentCurrency;
|
|
792
|
+
const paymentMethod = { type: 'stripe', name: 'Credit Card' } as PaymentMethod;
|
|
793
|
+
const result = formatCurrencyInfo('1000', paymentCurrency, paymentMethod, true);
|
|
794
|
+
expect(result).toBe('1000 USD (Credit Card)');
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it('should not include payment method name for arcblock payment methods', () => {
|
|
798
|
+
const paymentCurrency = { symbol: 'ABT', decimal: 18 } as PaymentCurrency;
|
|
799
|
+
const paymentMethod = { type: 'arcblock', name: 'ArcBlock' } as PaymentMethod;
|
|
800
|
+
const result = formatCurrencyInfo('5', paymentCurrency, paymentMethod, true);
|
|
801
|
+
expect(result).toBe('5 ABT');
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
it('should handle undefined or empty amount', () => {
|
|
805
|
+
const paymentCurrency = { symbol: 'ETH', decimal: 18 } as PaymentCurrency;
|
|
806
|
+
const result = formatCurrencyInfo('', paymentCurrency, null, true);
|
|
807
|
+
expect(result).toBe('0 ETH');
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
it('should use default values when currency information is missing', () => {
|
|
811
|
+
const paymentCurrency = {} as PaymentCurrency;
|
|
812
|
+
const result = formatCurrencyInfo('10', paymentCurrency, null, true);
|
|
813
|
+
expect(result).toBe('10 ');
|
|
814
|
+
});
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
describe('getExplorerTxUrl', () => {
|
|
818
|
+
it('should return empty string when explorerHost or txHash is not provided', () => {
|
|
819
|
+
const result1 = getExplorerTxUrl({
|
|
820
|
+
explorerHost: '',
|
|
821
|
+
txHash: 'hash123',
|
|
822
|
+
type: 'ethereum',
|
|
823
|
+
});
|
|
824
|
+
expect(result1).toBe('');
|
|
825
|
+
|
|
826
|
+
const result2 = getExplorerTxUrl({
|
|
827
|
+
explorerHost: 'https://explorer.example.com',
|
|
828
|
+
txHash: '',
|
|
829
|
+
type: 'ethereum',
|
|
830
|
+
});
|
|
831
|
+
expect(result2).toBe('');
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
it('should construct a proper URL for arcblock type', () => {
|
|
835
|
+
const result = getExplorerTxUrl({
|
|
836
|
+
explorerHost: 'https://explorer.example.com',
|
|
837
|
+
txHash: 'hash123',
|
|
838
|
+
type: 'arcblock',
|
|
839
|
+
});
|
|
840
|
+
expect(result).toBe('https://explorer.example.com/txs/hash123');
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
it('should construct a proper URL for non-arcblock types', () => {
|
|
844
|
+
const result = getExplorerTxUrl({
|
|
845
|
+
explorerHost: 'https://explorer.example.com',
|
|
846
|
+
txHash: 'hash123',
|
|
847
|
+
type: 'ethereum',
|
|
848
|
+
});
|
|
849
|
+
expect(result).toBe('https://explorer.example.com/tx/hash123');
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
it('should handle different blockchain types correctly', () => {
|
|
853
|
+
const testCases = [
|
|
854
|
+
{ type: 'ethereum', expected: '/tx/' },
|
|
855
|
+
{ type: 'base', expected: '/tx/' },
|
|
856
|
+
{ type: 'bitcoin', expected: '/tx/' },
|
|
857
|
+
{ type: 'arcblock', expected: '/txs/' },
|
|
858
|
+
];
|
|
859
|
+
|
|
860
|
+
testCases.forEach(({ type, expected }) => {
|
|
861
|
+
const result = getExplorerTxUrl({
|
|
862
|
+
explorerHost: 'https://explorer.example.com',
|
|
863
|
+
txHash: 'hash123',
|
|
864
|
+
type: type as any,
|
|
865
|
+
});
|
|
866
|
+
expect(result).toContain(expected);
|
|
867
|
+
});
|
|
868
|
+
});
|
|
869
|
+
});
|
package/blocklet.yml
CHANGED
|
@@ -14,7 +14,7 @@ repository:
|
|
|
14
14
|
type: git
|
|
15
15
|
url: git+https://github.com/blocklet/payment-kit.git
|
|
16
16
|
specVersion: 1.2.8
|
|
17
|
-
version: 1.18.
|
|
17
|
+
version: 1.18.36
|
|
18
18
|
logo: logo.png
|
|
19
19
|
files:
|
|
20
20
|
- dist
|
|
@@ -71,6 +71,7 @@ capabilities:
|
|
|
71
71
|
navigation: true
|
|
72
72
|
clusterMode: false
|
|
73
73
|
component: true
|
|
74
|
+
didSpace: requiredOnConnect
|
|
74
75
|
screenshots:
|
|
75
76
|
- setting.png
|
|
76
77
|
- payment.png
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.36",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -47,18 +47,19 @@
|
|
|
47
47
|
"@abtnode/cron": "^1.16.42",
|
|
48
48
|
"@arcblock/did": "^1.20.2",
|
|
49
49
|
"@arcblock/did-auth-storage-nedb": "^1.7.1",
|
|
50
|
-
"@arcblock/did-connect": "^2.13.
|
|
50
|
+
"@arcblock/did-connect": "^2.13.13",
|
|
51
51
|
"@arcblock/did-util": "^1.20.2",
|
|
52
52
|
"@arcblock/jwt": "^1.20.2",
|
|
53
|
-
"@arcblock/ux": "^2.13.
|
|
53
|
+
"@arcblock/ux": "^2.13.13",
|
|
54
54
|
"@arcblock/validator": "^1.20.2",
|
|
55
|
+
"@blocklet/did-space-js": "^1.0.48",
|
|
55
56
|
"@blocklet/js-sdk": "^1.16.42",
|
|
56
57
|
"@blocklet/logger": "^1.16.42",
|
|
57
|
-
"@blocklet/payment-react": "1.18.
|
|
58
|
+
"@blocklet/payment-react": "1.18.36",
|
|
58
59
|
"@blocklet/sdk": "^1.16.42",
|
|
59
|
-
"@blocklet/ui-react": "^2.13.
|
|
60
|
-
"@blocklet/uploader": "^0.1.
|
|
61
|
-
"@blocklet/xss": "^0.1.
|
|
60
|
+
"@blocklet/ui-react": "^2.13.13",
|
|
61
|
+
"@blocklet/uploader": "^0.1.84",
|
|
62
|
+
"@blocklet/xss": "^0.1.33",
|
|
62
63
|
"@mui/icons-material": "^5.16.6",
|
|
63
64
|
"@mui/lab": "^5.0.0-alpha.173",
|
|
64
65
|
"@mui/material": "^5.16.6",
|
|
@@ -122,7 +123,7 @@
|
|
|
122
123
|
"devDependencies": {
|
|
123
124
|
"@abtnode/types": "^1.16.42",
|
|
124
125
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
125
|
-
"@blocklet/payment-types": "1.18.
|
|
126
|
+
"@blocklet/payment-types": "1.18.36",
|
|
126
127
|
"@types/cookie-parser": "^1.4.7",
|
|
127
128
|
"@types/cors": "^2.8.17",
|
|
128
129
|
"@types/debug": "^4.1.12",
|
|
@@ -168,5 +169,5 @@
|
|
|
168
169
|
"parser": "typescript"
|
|
169
170
|
}
|
|
170
171
|
},
|
|
171
|
-
"gitHead": "
|
|
172
|
+
"gitHead": "98872e06bac0c437b53a6648ce4487e9f6d2336b"
|
|
172
173
|
}
|
package/scripts/sdk.js
CHANGED
|
@@ -69,16 +69,72 @@ const checkoutModule = {
|
|
|
69
69
|
subscription_data: {
|
|
70
70
|
no_stake: true,
|
|
71
71
|
},
|
|
72
|
+
});
|
|
73
|
+
console.log('createBatchSubscription', checkoutSession);
|
|
74
|
+
return checkoutSession;
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
// 批量订阅 + 免质押 + 自定义表单规则
|
|
78
|
+
async createBatchSubscriptionWithCustomField() {
|
|
79
|
+
const checkoutSession = await payment.checkout.sessions.create({
|
|
80
|
+
mode: 'subscription',
|
|
81
|
+
line_items: [
|
|
82
|
+
{
|
|
83
|
+
price_id: 'price_fQFIS12yi0JR3KePLmitjrhA',
|
|
84
|
+
quantity: 1,
|
|
85
|
+
subscription_data: {
|
|
86
|
+
metadata: { test: 'test price_fQFIS12yi0JR3KePLmitjrhA' },
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
price_id: 'price_PXyI9Duz99eqty1AqbaEc73u',
|
|
91
|
+
quantity: 1,
|
|
92
|
+
subscription_data: {
|
|
93
|
+
metadata: { test: 'test price_PXyI9Duz99eqty1AqbaEc73u' },
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
enable_subscription_grouping: true,
|
|
98
|
+
subscription_data: {
|
|
99
|
+
no_stake: true,
|
|
100
|
+
},
|
|
101
|
+
phone_number_collection: {
|
|
102
|
+
enabled: true,
|
|
103
|
+
},
|
|
104
|
+
billing_address_collection: 'required',
|
|
72
105
|
metadata: {
|
|
73
106
|
page_info: {
|
|
74
107
|
form_purpose_description: {
|
|
75
108
|
en: 'Information collected helps us process your payment and deliver our services.',
|
|
76
109
|
zh: '收集的信息帮助我们处理您的付款并提供服务。',
|
|
77
110
|
},
|
|
111
|
+
field_validation: {
|
|
112
|
+
customer_name: {
|
|
113
|
+
pattern: '^[a-zA-Z\\s]{2,50}$',
|
|
114
|
+
pattern_message: {
|
|
115
|
+
en: 'Name should only contain 2-50 letters and spaces',
|
|
116
|
+
zh: '姓名应只包含2-50个字母和空格',
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
customer_email: {
|
|
120
|
+
pattern: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$',
|
|
121
|
+
pattern_message: {
|
|
122
|
+
en: 'Please enter a valid email address',
|
|
123
|
+
zh: '请输入有效的电子邮件地址',
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
'billing_address.line1': {
|
|
127
|
+
pattern: '^.{5,100}$',
|
|
128
|
+
pattern_message: {
|
|
129
|
+
en: 'Address should be 5-100 characters',
|
|
130
|
+
zh: '地址应为5-100个字符',
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
78
134
|
},
|
|
79
135
|
},
|
|
80
136
|
});
|
|
81
|
-
console.log('
|
|
137
|
+
console.log('createBatchSubscriptionWithCustomField', checkoutSession);
|
|
82
138
|
return checkoutSession;
|
|
83
139
|
},
|
|
84
140
|
|
|
@@ -469,7 +525,7 @@ const testModules = {
|
|
|
469
525
|
|
|
470
526
|
async function runTest() {
|
|
471
527
|
payment.environments.setTestMode(true);
|
|
472
|
-
await testModules.checkout.
|
|
528
|
+
await testModules.checkout.createBatchSubscriptionWithCustomField();
|
|
473
529
|
}
|
|
474
530
|
|
|
475
531
|
async function main() {
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
import type { TCustomer } from '@blocklet/payment-types';
|
|
2
2
|
import { Link } from 'react-router-dom';
|
|
3
|
-
|
|
3
|
+
import UserCard from '@arcblock/ux/lib/UserCard';
|
|
4
4
|
import { getCustomerAvatar } from '@blocklet/payment-react';
|
|
5
|
-
import DID from '@arcblock/ux/lib/DID';
|
|
6
|
-
import { Box, Typography } from '@mui/material';
|
|
7
|
-
import InfoCard from '../info-card';
|
|
8
5
|
|
|
9
6
|
export default function CustomerLink({
|
|
10
7
|
customer,
|
|
11
8
|
linked,
|
|
12
9
|
linkTo,
|
|
13
10
|
size,
|
|
14
|
-
tooltip,
|
|
15
11
|
}: {
|
|
16
12
|
customer: TCustomer;
|
|
17
13
|
linked?: boolean;
|
|
@@ -22,61 +18,40 @@ export default function CustomerLink({
|
|
|
22
18
|
if (!customer) {
|
|
23
19
|
return null;
|
|
24
20
|
}
|
|
21
|
+
const CustomerCard = (
|
|
22
|
+
// @ts-ignore
|
|
23
|
+
<UserCard
|
|
24
|
+
did={customer?.did}
|
|
25
|
+
showHoverCard
|
|
26
|
+
sx={{
|
|
27
|
+
border: 'none',
|
|
28
|
+
p: 0,
|
|
29
|
+
minWidth: 0,
|
|
30
|
+
}}
|
|
31
|
+
avatarProps={{
|
|
32
|
+
size: size === 'small' ? 24 : 40,
|
|
33
|
+
}}
|
|
34
|
+
showDid={size !== 'small'}
|
|
35
|
+
{...(customer.metadata.anonymous === true
|
|
36
|
+
? {
|
|
37
|
+
user: {
|
|
38
|
+
fullName: customer.name || customer.email,
|
|
39
|
+
did: customer.did,
|
|
40
|
+
email: customer.email,
|
|
41
|
+
avatar: getCustomerAvatar(
|
|
42
|
+
customer?.did,
|
|
43
|
+
customer?.updated_at ? new Date(customer.updated_at).toISOString() : ''
|
|
44
|
+
),
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
: {})}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
25
50
|
if (linked) {
|
|
26
|
-
return
|
|
27
|
-
<Link to={linkTo || `/admin/customers/${customer.id}`}>
|
|
28
|
-
<Box sx={{ '.info-card-wrapper': { cursor: 'pointer' }, '.info-card': { minWidth: 0 } }}>
|
|
29
|
-
{/* @ts-ignore */}
|
|
30
|
-
<InfoCard
|
|
31
|
-
logo={getCustomerAvatar(
|
|
32
|
-
customer?.did,
|
|
33
|
-
customer?.updated_at ? new Date(customer.updated_at).toISOString() : '',
|
|
34
|
-
size === 'small' ? 24 : 48
|
|
35
|
-
)}
|
|
36
|
-
name={
|
|
37
|
-
<Typography
|
|
38
|
-
sx={{ maxWidth: 208, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
|
|
39
|
-
className="customer-link-name">
|
|
40
|
-
{customer.name || customer.email}
|
|
41
|
-
</Typography>
|
|
42
|
-
}
|
|
43
|
-
{...(size === 'small'
|
|
44
|
-
? { tooltip: tooltip ? <DID did={customer?.did} /> : false, size: 24 }
|
|
45
|
-
: {
|
|
46
|
-
description: <DID did={customer?.did} compact responsive={false} sx={{ whiteSpace: 'nowrap' }} />,
|
|
47
|
-
size: 48,
|
|
48
|
-
})}
|
|
49
|
-
/>
|
|
50
|
-
</Box>
|
|
51
|
-
</Link>
|
|
52
|
-
);
|
|
51
|
+
return <Link to={linkTo || `/admin/customers/${customer.id}`}>{CustomerCard}</Link>;
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
return
|
|
56
|
-
<Box sx={{ '.info-card': { minWidth: 0 } }}>
|
|
57
|
-
{/* @ts-ignore */}
|
|
58
|
-
<InfoCard
|
|
59
|
-
logo={getCustomerAvatar(
|
|
60
|
-
customer.did,
|
|
61
|
-
customer.updated_at ? new Date(customer.updated_at).toISOString() : '',
|
|
62
|
-
size === 'small' ? 24 : 48
|
|
63
|
-
)}
|
|
64
|
-
name={
|
|
65
|
-
<Typography
|
|
66
|
-
sx={{ maxWidth: 320, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
|
|
67
|
-
className="customer-link-name">
|
|
68
|
-
{customer.name || customer.email}
|
|
69
|
-
</Typography>
|
|
70
|
-
}
|
|
71
|
-
{...(size === 'small'
|
|
72
|
-
? { tooltip: tooltip ? <DID did={customer?.did} /> : false, size: 24 }
|
|
73
|
-
: {
|
|
74
|
-
description: <DID did={customer?.did} compact responsive={false} sx={{ whiteSpace: 'nowrap' }} />,
|
|
75
|
-
size: 48,
|
|
76
|
-
})}
|
|
77
|
-
/>
|
|
78
|
-
</Box>
|
|
79
|
-
);
|
|
54
|
+
return CustomerCard;
|
|
80
55
|
}
|
|
81
56
|
|
|
82
57
|
CustomerLink.defaultProps = {
|
|
@@ -282,7 +282,7 @@ export default function InvoiceList({
|
|
|
282
282
|
options: {
|
|
283
283
|
customBodyRenderLite: (_: string, index: number) => {
|
|
284
284
|
const item = data.list[index] as TInvoiceExpanded;
|
|
285
|
-
return <CustomerLink customer={item.customer} />;
|
|
285
|
+
return <CustomerLink customer={item.customer} size="small" />;
|
|
286
286
|
},
|
|
287
287
|
},
|
|
288
288
|
});
|
|
@@ -15,6 +15,8 @@ import { useLocalStorageState } from 'ahooks';
|
|
|
15
15
|
import { useEffect, useState } from 'react';
|
|
16
16
|
import { Link } from 'react-router-dom';
|
|
17
17
|
|
|
18
|
+
import DID from '@arcblock/ux/lib/DID';
|
|
19
|
+
import ShortenLabel from '@arcblock/ux/lib/UserCard/Content/shorten-label';
|
|
18
20
|
import { debounce, getAppInfo } from '../../libs/util';
|
|
19
21
|
import CustomerLink from '../customer/link';
|
|
20
22
|
import FilterToolbar from '../filter-toolbar';
|
|
@@ -202,8 +204,12 @@ export default function PayoutList({ customer_id, payment_intent_id, status, fea
|
|
|
202
204
|
if (appInfo) {
|
|
203
205
|
return (
|
|
204
206
|
<InfoCard
|
|
205
|
-
name={
|
|
206
|
-
|
|
207
|
+
name={
|
|
208
|
+
<ShortenLabel sx={{ fontWeight: 500 }} maxLength={30}>
|
|
209
|
+
{appInfo.name}
|
|
210
|
+
</ShortenLabel>
|
|
211
|
+
}
|
|
212
|
+
description={<DID did={item.destination} compact responsive={false} sx={{ whiteSpace: 'nowrap' }} />}
|
|
207
213
|
logo={appInfo.avatar}
|
|
208
214
|
size={40}
|
|
209
215
|
/>
|
package/src/libs/util.ts
CHANGED
|
@@ -3,14 +3,12 @@
|
|
|
3
3
|
/* eslint-disable @typescript-eslint/indent */
|
|
4
4
|
import { formatCheckoutHeadlines, formatPrice, getPrefix, getPriceCurrencyOptions } from '@blocklet/payment-react';
|
|
5
5
|
import type {
|
|
6
|
-
ChainType,
|
|
7
6
|
LineItem,
|
|
8
7
|
PriceRecurring,
|
|
9
8
|
TInvoiceExpanded,
|
|
10
9
|
TLineItemExpanded,
|
|
11
10
|
TPaymentCurrency,
|
|
12
11
|
TPaymentLinkExpanded,
|
|
13
|
-
TPaymentMethod,
|
|
14
12
|
TPaymentMethodExpanded,
|
|
15
13
|
TPrice,
|
|
16
14
|
TProductExpanded,
|
|
@@ -350,17 +348,6 @@ export function getAppInfo(address: string): { name: string; avatar: string; typ
|
|
|
350
348
|
return null;
|
|
351
349
|
}
|
|
352
350
|
|
|
353
|
-
export function getTokenBalanceLink(method: TPaymentMethod, address: string) {
|
|
354
|
-
const explorerHost = (method?.settings?.[method?.type as ChainType] as any)?.explorer_host || '';
|
|
355
|
-
if (method.type === 'arcblock' && address) {
|
|
356
|
-
return joinURL(explorerHost, 'accounts', address, 'tokens');
|
|
357
|
-
}
|
|
358
|
-
if (['ethereum', 'base'].includes(method.type) && address) {
|
|
359
|
-
return joinURL(explorerHost, 'address', address);
|
|
360
|
-
}
|
|
361
|
-
return '';
|
|
362
|
-
}
|
|
363
|
-
|
|
364
351
|
export function isWillCanceled(subscription: TSubscriptionExpanded) {
|
|
365
352
|
const now = Date.now() / 1000;
|
|
366
353
|
if (
|
|
@@ -211,8 +211,8 @@ export default function CustomerDetail(props: { id: string }) {
|
|
|
211
211
|
52
|
|
212
212
|
)}
|
|
213
213
|
alt={data.customer.name}
|
|
214
|
-
variant="
|
|
215
|
-
sx={{ width: 52, height: 52
|
|
214
|
+
variant="circular"
|
|
215
|
+
sx={{ width: 52, height: 52 }}
|
|
216
216
|
/>
|
|
217
217
|
<Typography variant="h2" sx={{ fontWeight: 600 }}>
|
|
218
218
|
{data.customer.name}
|
|
@@ -75,9 +75,8 @@ export default function CustomersList() {
|
|
|
75
75
|
item?.updated_at ? new Date(item.updated_at).toISOString() : '',
|
|
76
76
|
48
|
|
77
77
|
)}
|
|
78
|
-
variant="
|
|
78
|
+
variant="circular"
|
|
79
79
|
alt={item?.name}
|
|
80
|
-
sx={{ borderRadius: 'var(--radius-m, 8px)' }}
|
|
81
80
|
/>
|
|
82
81
|
<Typography sx={{ wordBreak: 'break-all' }}>{item.name}</Typography>
|
|
83
82
|
</Stack>
|