payment-kit 1.17.4 → 1.17.5
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/currency.ts +1 -1
- package/api/src/integrations/arcblock/stake.ts +4 -3
- package/api/src/libs/constants.ts +3 -0
- package/api/src/libs/invoice.ts +6 -5
- package/api/src/libs/notification/template/subscription-renew-failed.ts +4 -3
- package/api/src/libs/notification/template/subscription-renewed.ts +4 -3
- package/api/src/libs/notification/template/subscription-succeeded.ts +4 -3
- package/api/src/libs/notification/template/subscription-trial-start.ts +11 -3
- package/api/src/libs/notification/template/subscription-upgraded.ts +2 -1
- package/api/src/libs/payment.ts +5 -4
- package/api/src/libs/product.ts +24 -1
- package/api/src/queues/payment.ts +7 -5
- package/api/src/queues/refund.ts +8 -6
- package/api/src/routes/connect/change-payment.ts +3 -2
- package/api/src/routes/connect/change-plan.ts +3 -2
- package/api/src/routes/connect/collect-batch.ts +5 -4
- package/api/src/routes/connect/collect.ts +6 -5
- package/api/src/routes/connect/pay.ts +9 -4
- package/api/src/routes/connect/recharge.ts +9 -4
- package/api/src/routes/connect/setup.ts +3 -2
- package/api/src/routes/connect/shared.ts +25 -7
- package/api/src/routes/connect/subscribe.ts +3 -2
- package/api/src/routes/payment-currencies.ts +11 -10
- package/api/src/routes/payment-methods.ts +35 -19
- package/api/src/routes/payment-stats.ts +9 -3
- package/api/src/routes/prices.ts +19 -1
- package/api/src/routes/products.ts +60 -28
- package/api/src/routes/subscriptions.ts +4 -3
- package/api/src/store/models/payment-method.ts +11 -8
- package/api/src/store/models/types.ts +27 -1
- package/blocklet.yml +1 -1
- package/package.json +19 -19
- package/public/methods/base.png +0 -0
- package/src/components/payment-method/base.tsx +79 -0
- package/src/components/payment-method/form.tsx +3 -0
- package/src/components/price/upsell-select.tsx +1 -0
- package/src/components/subscription/metrics.tsx +1 -1
- package/src/components/subscription/portal/actions.tsx +1 -1
- package/src/libs/util.ts +1 -1
- package/src/locales/en.tsx +25 -0
- package/src/locales/zh.tsx +24 -0
- package/src/pages/admin/billing/invoices/detail.tsx +1 -1
- package/src/pages/admin/customers/customers/detail.tsx +2 -2
- package/src/pages/admin/overview.tsx +15 -2
- package/src/pages/admin/payments/intents/detail.tsx +1 -1
- package/src/pages/admin/payments/payouts/detail.tsx +1 -1
- package/src/pages/admin/payments/refunds/detail.tsx +1 -1
- package/src/pages/admin/products/links/detail.tsx +1 -0
- package/src/pages/admin/products/prices/actions.tsx +2 -1
- package/src/pages/admin/products/prices/detail.tsx +1 -0
- package/src/pages/admin/products/products/detail.tsx +1 -0
- package/src/pages/admin/settings/payment-methods/create.tsx +7 -0
- package/src/pages/admin/settings/payment-methods/index.tsx +99 -11
- package/src/pages/customer/index.tsx +1 -1
- package/src/pages/customer/invoice/detail.tsx +1 -1
- package/src/pages/customer/recharge.tsx +1 -1
- package/src/pages/customer/refund/list.tsx +7 -3
- package/src/pages/customer/subscription/change-payment.tsx +1 -1
|
@@ -9,6 +9,8 @@ import { getTxMetadata } from '../../libs/util';
|
|
|
9
9
|
import { ensureSubscriptionRecharge, getAuthPrincipalClaim } from './shared';
|
|
10
10
|
import logger from '../../libs/logger';
|
|
11
11
|
import { ensureRechargeInvoice } from '../../libs/invoice';
|
|
12
|
+
import { EVMChainType } from '../../store/models';
|
|
13
|
+
import { EVM_CHAIN_TYPES } from '../../libs/constants';
|
|
12
14
|
|
|
13
15
|
export default {
|
|
14
16
|
action: 'recharge',
|
|
@@ -54,14 +56,14 @@ export default {
|
|
|
54
56
|
return claims;
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
if (paymentMethod.type
|
|
59
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
58
60
|
return {
|
|
59
61
|
signature: {
|
|
60
62
|
type: 'eth:transaction',
|
|
61
63
|
data: toBase58(
|
|
62
64
|
Buffer.from(
|
|
63
65
|
JSON.stringify({
|
|
64
|
-
network: paymentMethod.settings?.
|
|
66
|
+
network: paymentMethod.settings[paymentMethod.type as EVMChainType]?.chain_id,
|
|
65
67
|
tx: encodeTransferItx(receiverAddress, amount, paymentCurrency.contract as string),
|
|
66
68
|
}),
|
|
67
69
|
'utf-8'
|
|
@@ -137,7 +139,7 @@ export default {
|
|
|
137
139
|
}
|
|
138
140
|
}
|
|
139
141
|
|
|
140
|
-
if (paymentMethod.type
|
|
142
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
141
143
|
try {
|
|
142
144
|
const paymentDetails = await executeEvmTransaction('transfer', userDid, claims, paymentMethod);
|
|
143
145
|
waitForEvmTxConfirm(
|
|
@@ -159,7 +161,10 @@ export default {
|
|
|
159
161
|
return { hash: paymentDetails.tx_hash };
|
|
160
162
|
} catch (err) {
|
|
161
163
|
console.error(err);
|
|
162
|
-
logger.error(
|
|
164
|
+
logger.error(`Failed to finalize recharge on ${paymentMethod.type}`, {
|
|
165
|
+
receiverAddress,
|
|
166
|
+
error: err,
|
|
167
|
+
});
|
|
163
168
|
throw err;
|
|
164
169
|
}
|
|
165
170
|
}
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
getStakeTxClaim,
|
|
18
18
|
} from './shared';
|
|
19
19
|
import { ensureStakeInvoice } from '../../libs/invoice';
|
|
20
|
+
import { EVM_CHAIN_TYPES } from '../../libs/constants';
|
|
20
21
|
|
|
21
22
|
export default {
|
|
22
23
|
action: 'setup',
|
|
@@ -94,7 +95,7 @@ export default {
|
|
|
94
95
|
return claims;
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
if (paymentMethod.type
|
|
98
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
98
99
|
if (!paymentCurrency.contract) {
|
|
99
100
|
throw new Error(`Payment currency ${paymentMethod.type}:${paymentCurrency.id} does not support setup`);
|
|
100
101
|
}
|
|
@@ -213,7 +214,7 @@ export default {
|
|
|
213
214
|
}
|
|
214
215
|
}
|
|
215
216
|
|
|
216
|
-
if (paymentMethod.type
|
|
217
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
217
218
|
await prepareTxExecution();
|
|
218
219
|
broadcastEvmTransaction(checkoutSessionId, 'pending', claims);
|
|
219
220
|
const paymentDetails = await executeEvmTransaction('approve', userDid, claims, paymentMethod);
|
|
@@ -35,6 +35,7 @@ import { Price } from '../../store/models/price';
|
|
|
35
35
|
import { SetupIntent } from '../../store/models/setup-intent';
|
|
36
36
|
import { Subscription } from '../../store/models/subscription';
|
|
37
37
|
import { ensureInvoiceAndItems } from '../../libs/invoice';
|
|
38
|
+
import { CHARGE_SUPPORTED_CHAIN_TYPES, EVM_CHAIN_TYPES } from '../../libs/constants';
|
|
38
39
|
|
|
39
40
|
type Result = {
|
|
40
41
|
checkoutSession: CheckoutSession;
|
|
@@ -124,7 +125,7 @@ export async function ensurePaymentIntent(checkoutSessionId: string, userDid?: s
|
|
|
124
125
|
throw new Error('Payment currency not found');
|
|
125
126
|
}
|
|
126
127
|
|
|
127
|
-
if (
|
|
128
|
+
if (CHARGE_SUPPORTED_CHAIN_TYPES.includes(paymentMethod.type) === false) {
|
|
128
129
|
throw new Error(`Payment method ${paymentMethod.type} should not be here`);
|
|
129
130
|
}
|
|
130
131
|
|
|
@@ -195,7 +196,7 @@ export async function ensureSetupIntent(checkoutSessionId: string, userDid?: str
|
|
|
195
196
|
throw new Error('Payment currency not found for checkoutSession');
|
|
196
197
|
}
|
|
197
198
|
|
|
198
|
-
if (
|
|
199
|
+
if (CHARGE_SUPPORTED_CHAIN_TYPES.includes(paymentMethod.type) === false) {
|
|
199
200
|
throw new Error(`Payment method ${paymentMethod.type} should not be here`);
|
|
200
201
|
}
|
|
201
202
|
|
|
@@ -266,7 +267,7 @@ export async function ensureSubscriptionDelegation(subscriptionId: string) {
|
|
|
266
267
|
if (!subscription) {
|
|
267
268
|
throw new Error('Subscription not found');
|
|
268
269
|
}
|
|
269
|
-
if (!
|
|
270
|
+
if (!CHARGE_SUPPORTED_CHAIN_TYPES.includes(subscription.paymentMethod?.type)) {
|
|
270
271
|
throw new Error(`Payment method ${subscription.paymentMethod?.type} not supported for delegation`);
|
|
271
272
|
}
|
|
272
273
|
subscription.items = await expandSubscriptionItems(subscription.id);
|
|
@@ -497,6 +498,14 @@ export function getAuthPrincipalClaim(method: PaymentMethod, action: string) {
|
|
|
497
498
|
host: method.settings?.ethereum?.api_host as string,
|
|
498
499
|
};
|
|
499
500
|
}
|
|
501
|
+
if (method.type === 'base') {
|
|
502
|
+
chainInfo = {
|
|
503
|
+
type: 'ethereum',
|
|
504
|
+
// @ts-ignore
|
|
505
|
+
id: method.settings?.base?.chain_id as number,
|
|
506
|
+
host: method.settings?.base?.api_host as string,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
500
509
|
|
|
501
510
|
return {
|
|
502
511
|
description: `Select account to ${action}`,
|
|
@@ -570,16 +579,25 @@ export async function getDelegationTxClaim({
|
|
|
570
579
|
};
|
|
571
580
|
}
|
|
572
581
|
|
|
573
|
-
if (paymentMethod.type
|
|
582
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type) && paymentCurrency.contract) {
|
|
574
583
|
const requirement = tokenRequirements.find((x) => x.address === paymentCurrency.contract);
|
|
575
584
|
const limit = tokenLimits.find((x) => x.address === paymentCurrency.contract);
|
|
585
|
+
// logger.info('getDelegationTxClaim base', {
|
|
586
|
+
// address: ethWallet.address,
|
|
587
|
+
// amount: mode === 'setup' ? requirement!.value : limit!.totalAllowance,
|
|
588
|
+
// contract: paymentCurrency.contract,
|
|
589
|
+
// mode,
|
|
590
|
+
// requirement,
|
|
591
|
+
// limit,
|
|
592
|
+
// });
|
|
576
593
|
return {
|
|
577
594
|
type: 'eth:transaction',
|
|
578
595
|
description: 'Sign the approval to continue',
|
|
579
596
|
data: toBase58(
|
|
580
597
|
Buffer.from(
|
|
581
598
|
JSON.stringify({
|
|
582
|
-
|
|
599
|
+
// @ts-ignore
|
|
600
|
+
network: paymentMethod.settings?.[paymentMethod.type]?.chain_id,
|
|
583
601
|
tx: await encodeApproveItx(
|
|
584
602
|
paymentMethod.getEvmClient(),
|
|
585
603
|
ethWallet.address,
|
|
@@ -847,7 +865,7 @@ export async function ensureSubscription(subscriptionId: string): Promise<Result
|
|
|
847
865
|
throw new Error(`Payment currency not found for subscription ${subscriptionId}`);
|
|
848
866
|
}
|
|
849
867
|
|
|
850
|
-
if (
|
|
868
|
+
if (CHARGE_SUPPORTED_CHAIN_TYPES.includes(paymentMethod.type) === false) {
|
|
851
869
|
throw new Error(`Payment method ${paymentMethod.type} should not be here`);
|
|
852
870
|
}
|
|
853
871
|
|
|
@@ -900,7 +918,7 @@ export async function ensureChangePaymentContext(subscriptionId: string) {
|
|
|
900
918
|
throw new Error(`Payment currency not found for SetupIntent ${setupIntent.id}`);
|
|
901
919
|
}
|
|
902
920
|
|
|
903
|
-
if (
|
|
921
|
+
if (CHARGE_SUPPORTED_CHAIN_TYPES.includes(paymentMethod.type) === false) {
|
|
904
922
|
throw new Error(`Payment method ${paymentMethod.type} should not be here`);
|
|
905
923
|
}
|
|
906
924
|
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
getStakeTxClaim,
|
|
19
19
|
} from './shared';
|
|
20
20
|
import { ensureStakeInvoice } from '../../libs/invoice';
|
|
21
|
+
import { EVM_CHAIN_TYPES } from '../../libs/constants';
|
|
21
22
|
|
|
22
23
|
export default {
|
|
23
24
|
action: 'subscription',
|
|
@@ -94,7 +95,7 @@ export default {
|
|
|
94
95
|
return claims;
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
if (paymentMethod.type
|
|
98
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
98
99
|
if (!paymentCurrency.contract) {
|
|
99
100
|
throw new Error(`Payment currency ${paymentMethod.type}:${paymentCurrency.id} does not support subscription`);
|
|
100
101
|
}
|
|
@@ -194,7 +195,7 @@ export default {
|
|
|
194
195
|
return { hash: paymentDetails.tx_hash };
|
|
195
196
|
}
|
|
196
197
|
|
|
197
|
-
if (paymentMethod.type
|
|
198
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
198
199
|
await prepareTxExecution();
|
|
199
200
|
const { invoice } = await ensureInvoiceForCheckout({ checkoutSession, customer, subscription });
|
|
200
201
|
broadcastEvmTransaction(checkoutSessionId, 'pending', claims);
|
|
@@ -7,6 +7,7 @@ import logger from '../libs/logger';
|
|
|
7
7
|
import { authenticate } from '../libs/security';
|
|
8
8
|
import { PaymentCurrency, TPaymentCurrency } from '../store/models/payment-currency';
|
|
9
9
|
import { PaymentMethod } from '../store/models/payment-method';
|
|
10
|
+
import { EVM_CHAIN_TYPES } from '../libs/constants';
|
|
10
11
|
|
|
11
12
|
const router = Router();
|
|
12
13
|
|
|
@@ -24,6 +25,11 @@ router.post('/', auth, async (req, res) => {
|
|
|
24
25
|
if (!raw.description) {
|
|
25
26
|
return res.status(400).json({ error: 'payment currency description is required' });
|
|
26
27
|
}
|
|
28
|
+
const method = await PaymentMethod.findByPk(raw.payment_method_id);
|
|
29
|
+
if (!method) {
|
|
30
|
+
return res.status(400).json({ error: 'payment method not found' });
|
|
31
|
+
}
|
|
32
|
+
raw.logo = raw.logo || method.logo;
|
|
27
33
|
if (!raw.logo) {
|
|
28
34
|
return res.status(400).json({ error: 'payment currency logo is required' });
|
|
29
35
|
}
|
|
@@ -35,22 +41,17 @@ router.post('/', auth, async (req, res) => {
|
|
|
35
41
|
return res.status(400).json({ error: 'payment currency with same contract already exist' });
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
const method = await PaymentMethod.findByPk(raw.payment_method_id);
|
|
39
|
-
if (!method) {
|
|
40
|
-
return res.status(400).json({ error: 'payment method not found' });
|
|
41
|
-
}
|
|
42
|
-
|
|
43
44
|
if (method.type === 'stripe') {
|
|
44
45
|
return res.status(400).json({ error: 'Adding method for stripe not supported' });
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
if (method.type
|
|
48
|
+
if (EVM_CHAIN_TYPES.includes(method.type)) {
|
|
48
49
|
try {
|
|
49
50
|
const client = method.getEvmClient();
|
|
50
51
|
const info = await fetchErc20Meta(client, raw.contract);
|
|
51
|
-
logger.info(
|
|
52
|
+
logger.info(`${method.type} erc20 info fetched`, { raw, info });
|
|
52
53
|
if (!info.symbol || !info.decimal) {
|
|
53
|
-
return res.status(400).json({ error:
|
|
54
|
+
return res.status(400).json({ error: `${method.type} token not found` });
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
const currency = await PaymentCurrency.create({
|
|
@@ -76,7 +77,7 @@ router.post('/', auth, async (req, res) => {
|
|
|
76
77
|
return res.json(currency);
|
|
77
78
|
} catch (err) {
|
|
78
79
|
console.error(err);
|
|
79
|
-
return res.status(400).json({ error:
|
|
80
|
+
return res.status(400).json({ error: `${method.type} currency contract verify failed` });
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
|
|
@@ -112,7 +113,7 @@ router.post('/', auth, async (req, res) => {
|
|
|
112
113
|
return res.json(currency);
|
|
113
114
|
} catch (err) {
|
|
114
115
|
console.error(err);
|
|
115
|
-
return res.status(400).json({ error:
|
|
116
|
+
return res.status(400).json({ error: `${method.type} currency contract verify failed` });
|
|
116
117
|
}
|
|
117
118
|
}
|
|
118
119
|
|
|
@@ -11,7 +11,9 @@ import logger from '../libs/logger';
|
|
|
11
11
|
import { authenticate } from '../libs/security';
|
|
12
12
|
import { PaymentCurrency } from '../store/models/payment-currency';
|
|
13
13
|
import { PaymentMethod, TPaymentMethod } from '../store/models/payment-method';
|
|
14
|
-
import type { PaymentMethodSettings } from '../store/models/types';
|
|
14
|
+
import type { EVMChainType, PaymentMethodSettings } from '../store/models/types';
|
|
15
|
+
import { ethWallet, wallet } from '../libs/auth';
|
|
16
|
+
import { EVM_CHAIN_TYPES } from '../libs/constants';
|
|
15
17
|
|
|
16
18
|
const router = Router();
|
|
17
19
|
|
|
@@ -90,36 +92,41 @@ router.post('/', auth, async (req, res) => {
|
|
|
90
92
|
return res.json({ ...method.toJSON(), payment_currencies: [currency.toJSON()] });
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
if (raw.type
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
if (EVM_CHAIN_TYPES.includes(raw.type as string)) {
|
|
96
|
+
const paymentType = raw.type as EVMChainType;
|
|
97
|
+
if (!raw.settings[paymentType]?.api_host) {
|
|
98
|
+
return res.status(400).json({ error: `${paymentType} api_host is required` });
|
|
96
99
|
}
|
|
97
|
-
if (!raw.settings
|
|
98
|
-
return res.status(400).json({ error:
|
|
100
|
+
if (!raw.settings[paymentType]?.explorer_host) {
|
|
101
|
+
return res.status(400).json({ error: `${paymentType} explorer_host is required` });
|
|
99
102
|
}
|
|
100
|
-
if (!raw.settings
|
|
101
|
-
return res.status(400).json({ error:
|
|
103
|
+
if (!raw.settings[paymentType]?.native_symbol) {
|
|
104
|
+
return res.status(400).json({ error: `${paymentType} native_symbol is required` });
|
|
102
105
|
}
|
|
103
106
|
|
|
104
107
|
try {
|
|
105
|
-
const provider = new ethers.JsonRpcProvider(raw.settings
|
|
108
|
+
const provider = new ethers.JsonRpcProvider(raw.settings[paymentType]?.api_host);
|
|
106
109
|
const [network, blockNumber] = await Promise.all([provider.getNetwork(), provider.getBlockNumber()]);
|
|
107
|
-
raw.settings
|
|
108
|
-
logger.info(
|
|
110
|
+
raw.settings[paymentType]!.chain_id = network.chainId.toString();
|
|
111
|
+
logger.info(`${paymentType} api endpoint verified`, {
|
|
112
|
+
settings: raw.settings[paymentType],
|
|
113
|
+
network,
|
|
114
|
+
blockNumber,
|
|
115
|
+
});
|
|
109
116
|
} catch (err) {
|
|
110
117
|
logger.error('verify ethereum api endpoint failed', err);
|
|
111
118
|
return res.status(400).json({ error: err.message });
|
|
112
119
|
}
|
|
113
120
|
|
|
114
121
|
const exist = await PaymentMethod.findOne({
|
|
115
|
-
where: { type:
|
|
122
|
+
where: { type: paymentType, [`settings.${paymentType}.chain_id`]: raw.settings[paymentType]?.chain_id },
|
|
116
123
|
});
|
|
117
124
|
if (exist) {
|
|
118
|
-
return res.status(400).json({ error:
|
|
125
|
+
return res.status(400).json({ error: `${paymentType} payment method already exist` });
|
|
119
126
|
}
|
|
120
127
|
|
|
121
|
-
raw.settings = pick(PaymentMethod.encryptSettings(raw.settings), [
|
|
122
|
-
raw.logo = raw.logo || getUrl(
|
|
128
|
+
raw.settings = pick(PaymentMethod.encryptSettings(raw.settings), [paymentType]) as PaymentMethodSettings;
|
|
129
|
+
raw.logo = raw.logo || getUrl(`/methods/${paymentType}.png`);
|
|
123
130
|
raw.features = {
|
|
124
131
|
recurring: true,
|
|
125
132
|
refund: true,
|
|
@@ -127,13 +134,13 @@ router.post('/', auth, async (req, res) => {
|
|
|
127
134
|
};
|
|
128
135
|
raw.confirmation = {
|
|
129
136
|
type: 'block',
|
|
130
|
-
block: raw.settings
|
|
137
|
+
block: raw.settings[paymentType]?.confirmation || 1,
|
|
131
138
|
};
|
|
132
139
|
|
|
133
140
|
const method = await PaymentMethod.create(raw as TPaymentMethod);
|
|
134
141
|
|
|
135
142
|
// create currency for native currency
|
|
136
|
-
const symbol = raw.settings
|
|
143
|
+
const symbol = raw.settings[paymentType]?.native_symbol as string;
|
|
137
144
|
const currency = await PaymentCurrency.create({
|
|
138
145
|
livemode: method.livemode,
|
|
139
146
|
active: method.active,
|
|
@@ -177,8 +184,11 @@ router.get('/', auth, async (req, res) => {
|
|
|
177
184
|
order: [['created_at', 'ASC']],
|
|
178
185
|
include: [{ model: PaymentCurrency, as: 'payment_currencies', order: [['created_at', 'ASC']] }],
|
|
179
186
|
});
|
|
180
|
-
|
|
181
|
-
|
|
187
|
+
if (query.addresses === 'true') {
|
|
188
|
+
res.json({ list, addresses: { arcblock: wallet.address, ethereum: ethWallet.address } });
|
|
189
|
+
} else {
|
|
190
|
+
res.json(list);
|
|
191
|
+
}
|
|
182
192
|
});
|
|
183
193
|
|
|
184
194
|
router.get('/types', auth, (_, res) => {
|
|
@@ -201,6 +211,12 @@ router.get('/types', auth, (_, res) => {
|
|
|
201
211
|
description: 'Pay with ethereum compatible chains',
|
|
202
212
|
logo: getUrl('/methods/ethereum.png'),
|
|
203
213
|
},
|
|
214
|
+
{
|
|
215
|
+
type: 'base',
|
|
216
|
+
name: 'Base',
|
|
217
|
+
description: 'Pay with base compatible chains',
|
|
218
|
+
logo: getUrl('/methods/base.png'),
|
|
219
|
+
},
|
|
204
220
|
]);
|
|
205
221
|
});
|
|
206
222
|
|
|
@@ -10,6 +10,7 @@ import { ethWallet, wallet } from '../libs/auth';
|
|
|
10
10
|
import dayjs from '../libs/dayjs';
|
|
11
11
|
import { authenticate } from '../libs/security';
|
|
12
12
|
import {
|
|
13
|
+
EVMChainType,
|
|
13
14
|
Invoice,
|
|
14
15
|
PaymentCurrency,
|
|
15
16
|
PaymentIntent,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
Refund,
|
|
20
21
|
Subscription,
|
|
21
22
|
} from '../store/models';
|
|
23
|
+
import { EVM_CHAIN_TYPES } from '../libs/constants';
|
|
22
24
|
|
|
23
25
|
const router = Router();
|
|
24
26
|
const auth = authenticate<PaymentStat>({ component: true, roles: ['owner', 'admin'] });
|
|
@@ -98,8 +100,12 @@ async function getCurrencyLinks(livemode: boolean) {
|
|
|
98
100
|
'tokens'
|
|
99
101
|
);
|
|
100
102
|
}
|
|
101
|
-
if (item.payment_method?.type
|
|
102
|
-
acc[item.id] = joinURL(
|
|
103
|
+
if (EVM_CHAIN_TYPES.includes(item.payment_method?.type as string)) {
|
|
104
|
+
acc[item.id] = joinURL(
|
|
105
|
+
item.payment_method.settings[item.payment_method.type as EVMChainType]?.explorer_host,
|
|
106
|
+
'address',
|
|
107
|
+
ethWallet.address
|
|
108
|
+
);
|
|
103
109
|
}
|
|
104
110
|
return acc;
|
|
105
111
|
}, {} as any);
|
|
@@ -110,7 +116,7 @@ router.get('/summary', auth, async (req, res) => {
|
|
|
110
116
|
try {
|
|
111
117
|
const [arcblock, ethereum, links] = await Promise.all([
|
|
112
118
|
getTokenSummaryByDid(wallet.address, !!req.livemode, 'arcblock'),
|
|
113
|
-
getTokenSummaryByDid(ethWallet.address, !!req.livemode,
|
|
119
|
+
getTokenSummaryByDid(ethWallet.address, !!req.livemode, EVM_CHAIN_TYPES),
|
|
114
120
|
getCurrencyLinks(!!req.livemode),
|
|
115
121
|
]);
|
|
116
122
|
res.json({
|
package/api/src/routes/prices.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { canUpsell } from '../libs/session';
|
|
|
11
11
|
import { PaymentCurrency } from '../store/models/payment-currency';
|
|
12
12
|
import { Price } from '../store/models/price';
|
|
13
13
|
import { Product } from '../store/models/product';
|
|
14
|
+
import { checkCurrencySupportRecurring } from '../libs/product';
|
|
14
15
|
|
|
15
16
|
const router = Router();
|
|
16
17
|
|
|
@@ -162,7 +163,14 @@ export async function createPrice(payload: any) {
|
|
|
162
163
|
|
|
163
164
|
raw.currency_options = Price.formatCurrencies(raw.currency_options, currencies);
|
|
164
165
|
raw.unit_amount = fromTokenToUnit(raw.unit_amount, currency.decimal).toString();
|
|
165
|
-
|
|
166
|
+
const isRecurring = payload.type === 'recurring';
|
|
167
|
+
const { notSupportCurrencies, validate } = await checkCurrencySupportRecurring(
|
|
168
|
+
(raw.currency_options || [])?.map((x) => x.currency_id).filter(Boolean),
|
|
169
|
+
isRecurring
|
|
170
|
+
);
|
|
171
|
+
if (!validate) {
|
|
172
|
+
throw new Error(`currency ${notSupportCurrencies.map((x) => x.name).join(', ')} does not support recurring`);
|
|
173
|
+
}
|
|
166
174
|
const price = await Price.insert(raw);
|
|
167
175
|
return getExpandedPrice(price.id as string);
|
|
168
176
|
}
|
|
@@ -349,6 +357,16 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
349
357
|
});
|
|
350
358
|
}
|
|
351
359
|
}
|
|
360
|
+
const isRecurring = updates.type === 'recurring';
|
|
361
|
+
const { notSupportCurrencies, validate } = await checkCurrencySupportRecurring(
|
|
362
|
+
(updates.currency_options || [])?.map((x) => x.currency_id).filter(Boolean),
|
|
363
|
+
isRecurring
|
|
364
|
+
);
|
|
365
|
+
if (!validate) {
|
|
366
|
+
return res
|
|
367
|
+
.status(400)
|
|
368
|
+
.json({ error: `currency ${notSupportCurrencies.map((x) => x.name).join(', ')} does not support recurring` });
|
|
369
|
+
}
|
|
352
370
|
|
|
353
371
|
try {
|
|
354
372
|
await doc.update(Price.formatBeforeSave(updates));
|
|
@@ -13,6 +13,7 @@ import { PaymentCurrency } from '../store/models/payment-currency';
|
|
|
13
13
|
import { Price } from '../store/models/price';
|
|
14
14
|
import { Product } from '../store/models/product';
|
|
15
15
|
import type { CustomUnitAmount } from '../store/models/types';
|
|
16
|
+
import { checkCurrencySupportRecurring } from '../libs/product';
|
|
16
17
|
|
|
17
18
|
const router = Router();
|
|
18
19
|
|
|
@@ -57,6 +58,7 @@ const ProductAndPriceSchema = Joi.object({
|
|
|
57
58
|
}).unknown(true);
|
|
58
59
|
|
|
59
60
|
export async function createProductAndPrices(payload: any) {
|
|
61
|
+
// 1. 准备 product 数据
|
|
60
62
|
const raw: Partial<Product> = pick(payload, [
|
|
61
63
|
'name',
|
|
62
64
|
'type',
|
|
@@ -75,66 +77,96 @@ export async function createProductAndPrices(payload: any) {
|
|
|
75
77
|
raw.created_via = payload.via || 'api';
|
|
76
78
|
raw.metadata = formatMetadata(raw.metadata);
|
|
77
79
|
|
|
78
|
-
|
|
80
|
+
// 2. 验证并准备 prices 数据
|
|
81
|
+
let pricesRaw: (Price & { model: 'string' })[] = [];
|
|
79
82
|
if (Array.isArray(payload.prices) && payload.prices.length) {
|
|
80
83
|
if (payload.prices.some((x: any) => !x.unit_amount && !x.custom_unit_amount)) {
|
|
81
84
|
throw new Error('unit_amount or custom_unit_amount is required for price');
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
const currencies = await PaymentCurrency.findAll({ where: { active: true } });
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
pricesRaw = payload.prices.map((price: Price & { model: 'string' }) => {
|
|
89
|
+
const newPrice = { ...price };
|
|
90
|
+
newPrice.active = !!raw.active;
|
|
91
|
+
newPrice.livemode = !!raw.livemode;
|
|
92
|
+
newPrice.currency_id = price.currency_id || payload.currency_id;
|
|
90
93
|
|
|
91
|
-
const currency = currencies.find((x) => x.id ===
|
|
94
|
+
const currency = currencies.find((x) => x.id === newPrice.currency_id);
|
|
92
95
|
if (!currency) {
|
|
93
|
-
throw new Error(`currency ${
|
|
96
|
+
throw new Error(`currency ${newPrice.currency_id} used in price not found or inactive`);
|
|
94
97
|
}
|
|
95
|
-
|
|
98
|
+
|
|
99
|
+
if (newPrice.custom_unit_amount) {
|
|
96
100
|
// @ts-ignore
|
|
97
101
|
['preset', 'maximum', 'minimum'].forEach((key: keyof CustomUnitAmount) => {
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
if (newPrice.custom_unit_amount?.[key]) {
|
|
103
|
+
newPrice.custom_unit_amount[key] = fromTokenToUnit(
|
|
104
|
+
newPrice.custom_unit_amount[key] as string,
|
|
101
105
|
currency.decimal
|
|
102
106
|
).toString();
|
|
103
107
|
}
|
|
104
108
|
});
|
|
105
|
-
if (Array.isArray(
|
|
106
|
-
|
|
109
|
+
if (Array.isArray(newPrice.custom_unit_amount.presets)) {
|
|
110
|
+
newPrice.custom_unit_amount.presets = newPrice.custom_unit_amount.presets.map((x) =>
|
|
107
111
|
fromTokenToUnit(x, currency.decimal).toString()
|
|
108
112
|
);
|
|
109
113
|
}
|
|
110
114
|
} else {
|
|
111
|
-
if (!
|
|
115
|
+
if (!newPrice.unit_amount) {
|
|
112
116
|
throw new Error('price.unit_amount is required');
|
|
113
117
|
}
|
|
114
|
-
|
|
118
|
+
newPrice.unit_amount = fromTokenToUnit(newPrice.unit_amount, currency.decimal).toString();
|
|
115
119
|
}
|
|
116
120
|
|
|
117
|
-
if (Array.isArray(
|
|
118
|
-
|
|
121
|
+
if (Array.isArray(newPrice.currency_options)) {
|
|
122
|
+
newPrice.currency_options = Price.formatCurrencies(newPrice.currency_options, currencies);
|
|
119
123
|
} else {
|
|
120
|
-
|
|
124
|
+
newPrice.currency_options = [];
|
|
121
125
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
+
|
|
127
|
+
if (newPrice.currency_options.some((x) => x.currency_id === newPrice.currency_id) === false) {
|
|
128
|
+
newPrice.currency_options.unshift({
|
|
129
|
+
currency_id: newPrice.currency_id,
|
|
130
|
+
unit_amount: newPrice.unit_amount,
|
|
126
131
|
tiers: null,
|
|
127
|
-
custom_unit_amount:
|
|
132
|
+
custom_unit_amount: newPrice.custom_unit_amount,
|
|
128
133
|
});
|
|
129
134
|
}
|
|
130
135
|
|
|
131
|
-
return
|
|
136
|
+
return newPrice;
|
|
132
137
|
});
|
|
133
138
|
|
|
134
|
-
|
|
139
|
+
// 3. 验证货币是否支持 recurring
|
|
140
|
+
const validationResults = await Promise.all(
|
|
141
|
+
pricesRaw.map((x) => {
|
|
142
|
+
const isRecurring = x.type === 'recurring';
|
|
143
|
+
const currencyIds = (x.currency_options || []).map((c) => c.currency_id).filter(Boolean);
|
|
144
|
+
return checkCurrencySupportRecurring(currencyIds, isRecurring);
|
|
145
|
+
})
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const invalidCurrencies = validationResults.filter((result) => !result.validate);
|
|
149
|
+
if (invalidCurrencies.length > 0) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`Currency ${invalidCurrencies.flatMap((result) => result.notSupportCurrencies.map((c) => c?.name)).join(', ')} does not support recurring`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 4. 所有验证通过后,创建 product
|
|
157
|
+
const product = await Product.create(raw as Product);
|
|
158
|
+
|
|
159
|
+
// 5. 创建 prices
|
|
160
|
+
if (pricesRaw.length > 0) {
|
|
161
|
+
const prices = await Promise.all(
|
|
162
|
+
pricesRaw.map((price) => {
|
|
163
|
+
price.product_id = product.id;
|
|
164
|
+
return Price.insert(price);
|
|
165
|
+
})
|
|
166
|
+
);
|
|
135
167
|
|
|
136
|
-
//
|
|
137
|
-
product.default_price_id = prices[0]
|
|
168
|
+
// 6. 更新 default price
|
|
169
|
+
product.default_price_id = prices?.[0]?.id;
|
|
138
170
|
await product.save();
|
|
139
171
|
|
|
140
172
|
return { ...product.toJSON(), prices: prices.map((x) => x.toJSON()) };
|
|
@@ -64,6 +64,7 @@ import { createUsageRecordQueryFn } from './usage-records';
|
|
|
64
64
|
import { SubscriptionWillCanceledSchedule } from '../crons/subscription-will-canceled';
|
|
65
65
|
import { getTokenByAddress } from '../integrations/arcblock/stake';
|
|
66
66
|
import { ensureOverdraftProtectionPrice } from '../libs/overdraft-protection';
|
|
67
|
+
import { CHARGE_SUPPORTED_CHAIN_TYPES } from '../libs/constants';
|
|
67
68
|
|
|
68
69
|
const router = Router();
|
|
69
70
|
const auth = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'] });
|
|
@@ -1766,7 +1767,7 @@ router.get('/:id/cycle-amount', authPortal, async (req, res) => {
|
|
|
1766
1767
|
|
|
1767
1768
|
if (req.query?.overdraftProtection) {
|
|
1768
1769
|
const { price } = await ensureOverdraftProtectionPrice(subscription.livemode);
|
|
1769
|
-
const invoicePrice = price
|
|
1770
|
+
const invoicePrice = (price?.currency_options || []).find((x: any) => x.currency_id === subscription?.currency_id);
|
|
1770
1771
|
const gas = invoicePrice?.unit_amount;
|
|
1771
1772
|
return res.json({
|
|
1772
1773
|
amount: new BN(maxAmount).add(new BN(gas)).toString(),
|
|
@@ -1856,7 +1857,7 @@ router.get('/:id/payer-token', authMine, async (req, res) => {
|
|
|
1856
1857
|
|
|
1857
1858
|
// @ts-ignore
|
|
1858
1859
|
const paymentAddress = getSubscriptionPaymentAddress(subscription, paymentMethod.type);
|
|
1859
|
-
if (!paymentAddress &&
|
|
1860
|
+
if (!paymentAddress && CHARGE_SUPPORTED_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
1860
1861
|
return res.status(400).json({ error: `Payer not found on subscription payment detail: ${subscription.id}` });
|
|
1861
1862
|
}
|
|
1862
1863
|
|
|
@@ -2015,7 +2016,7 @@ router.get('/:id/overdraft-protection', authPortal, async (req, res) => {
|
|
|
2015
2016
|
await isSubscriptionOverdraftProtectionEnabled(subscription);
|
|
2016
2017
|
const upcoming = await getUpcomingInvoiceAmount(req.params.id as string);
|
|
2017
2018
|
const { price } = await ensureOverdraftProtectionPrice(subscription.livemode);
|
|
2018
|
-
const invoicePrice = price
|
|
2019
|
+
const invoicePrice = (price?.currency_options || []).find((x: any) => x.currency_id === subscription?.currency_id);
|
|
2019
2020
|
const gas = invoicePrice?.unit_amount;
|
|
2020
2021
|
return res.json({
|
|
2021
2022
|
enabled,
|