payment-kit 1.13.92 → 1.13.93
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/src/index.ts +2 -0
- package/api/src/libs/audit.ts +28 -34
- package/api/src/libs/payment.ts +2 -11
- package/api/src/libs/session.ts +1 -1
- package/api/src/libs/util.ts +8 -5
- package/api/src/routes/checkout-sessions.ts +41 -39
- package/api/src/routes/connect/collect.ts +12 -12
- package/api/src/routes/connect/setup.ts +8 -11
- package/api/src/routes/connect/shared.ts +81 -20
- package/api/src/routes/connect/subscribe.ts +8 -11
- package/api/src/routes/connect/update.ts +134 -0
- package/api/src/routes/pricing-table.ts +9 -121
- package/api/src/routes/subscriptions.ts +416 -141
- package/api/src/store/models/index.ts +3 -0
- package/api/src/store/models/pricing-table.ts +125 -1
- package/api/src/store/models/subscription.ts +4 -0
- package/api/src/store/models/types.ts +8 -0
- package/api/tests/libs/util.spec.ts +6 -6
- package/blocklet.yml +1 -1
- package/package.json +6 -6
- package/src/app.tsx +12 -4
- package/src/components/checkout/form/address.tsx +41 -34
- package/src/components/checkout/form/index.tsx +1 -1
- package/src/components/checkout/pricing-table.tsx +205 -0
- package/src/components/payment-link/product-select.tsx +13 -3
- package/src/components/portal/invoice/list.tsx +1 -1
- package/src/components/portal/subscription/actions.tsx +153 -0
- package/src/components/portal/subscription/list.tsx +21 -150
- package/src/components/subscription/metrics.tsx +46 -0
- package/src/contexts/products.tsx +2 -1
- package/src/libs/util.ts +43 -0
- package/src/locales/en.tsx +15 -1
- package/src/locales/zh.tsx +16 -2
- package/src/pages/admin/billing/subscriptions/detail.tsx +2 -34
- package/src/pages/checkout/pricing-table.tsx +9 -158
- package/src/pages/customer/subscription/{index.tsx → detail.tsx} +6 -36
- package/src/pages/customer/subscription/update.tsx +281 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { toTypeInfo } from '@arcblock/did';
|
|
2
|
+
import { toDelegateAddress } from '@arcblock/did-util';
|
|
3
|
+
import type { Transaction } from '@ocap/client';
|
|
4
|
+
import { fromPublicKey } from '@ocap/wallet';
|
|
5
|
+
|
|
6
|
+
import type { CallbackArgs } from '../../libs/auth';
|
|
7
|
+
import { wallet } from '../../libs/auth';
|
|
8
|
+
import { getGasPayerExtra, getTokenLimitsForDelegation } from '../../libs/payment';
|
|
9
|
+
import { getFastCheckoutAmount } from '../../libs/session';
|
|
10
|
+
import { OCAP_PAYMENT_TX_TYPE, getTxMetadata } from '../../libs/util';
|
|
11
|
+
import { invoiceQueue } from '../../queues/invoice';
|
|
12
|
+
import { subscriptionQueue } from '../../queues/subscription';
|
|
13
|
+
import type { TLineItemExpanded } from '../../store/models';
|
|
14
|
+
import { ensureSubscription, getAuthPrincipalClaim, getTokenRequirements } from './shared';
|
|
15
|
+
|
|
16
|
+
export default {
|
|
17
|
+
action: 'update',
|
|
18
|
+
authPrincipal: false,
|
|
19
|
+
claims: {
|
|
20
|
+
authPrincipal: async ({ extraParams }: CallbackArgs) => {
|
|
21
|
+
const { paymentMethod } = await ensureSubscription(extraParams.subscriptionId);
|
|
22
|
+
return getAuthPrincipalClaim(paymentMethod, 'pay');
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
onConnect: async ({ userDid, userPk, extraParams }: CallbackArgs) => {
|
|
26
|
+
const { subscriptionId } = extraParams;
|
|
27
|
+
const { paymentMethod, paymentCurrency, subscription } = await ensureSubscription(subscriptionId);
|
|
28
|
+
|
|
29
|
+
if (paymentMethod.type === 'arcblock') {
|
|
30
|
+
// @ts-ignore
|
|
31
|
+
const items = subscription!.items as TLineItemExpanded[];
|
|
32
|
+
const address = toDelegateAddress(userDid, wallet.address);
|
|
33
|
+
const amount = getFastCheckoutAmount(items, 'subscription', paymentCurrency.id);
|
|
34
|
+
|
|
35
|
+
const tokenLimits = await getTokenLimitsForDelegation(items, paymentMethod, paymentCurrency, address, amount);
|
|
36
|
+
const tokenRequirements = await getTokenRequirements({
|
|
37
|
+
items,
|
|
38
|
+
mode: 'subscription',
|
|
39
|
+
includeFreeTrial: false,
|
|
40
|
+
paymentMethod,
|
|
41
|
+
paymentCurrency,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
signature: {
|
|
46
|
+
type: 'DelegateTx',
|
|
47
|
+
description: `Sign the delegation to update subscription ${subscription?.id}`,
|
|
48
|
+
wallet: fromPublicKey(userPk, toTypeInfo(userDid)),
|
|
49
|
+
data: {
|
|
50
|
+
itx: {
|
|
51
|
+
address,
|
|
52
|
+
to: wallet.address,
|
|
53
|
+
ops: [{ typeUrl: OCAP_PAYMENT_TX_TYPE, limit: { tokens: tokenLimits, assets: [] } }],
|
|
54
|
+
data: getTxMetadata({ subscriptionId }),
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
nonce: subscriptionId,
|
|
58
|
+
requirement: {
|
|
59
|
+
tokens: tokenRequirements,
|
|
60
|
+
},
|
|
61
|
+
chainInfo: {
|
|
62
|
+
host: paymentMethod.settings?.arcblock?.api_host as string,
|
|
63
|
+
id: paymentMethod.settings?.arcblock?.chain_id as string,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
throw new Error(`Payment method ${paymentMethod.type} not supported`);
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
onAuth: async ({ userDid, userPk, claims, extraParams }: CallbackArgs) => {
|
|
73
|
+
const { subscriptionId } = extraParams;
|
|
74
|
+
const { invoice, paymentMethod, subscription } = await ensureSubscription(subscriptionId);
|
|
75
|
+
|
|
76
|
+
if (paymentMethod.type === 'arcblock') {
|
|
77
|
+
await subscription?.update({
|
|
78
|
+
payment_settings: {
|
|
79
|
+
payment_method_types: ['arcblock'],
|
|
80
|
+
payment_method_options: {
|
|
81
|
+
arcblock: { payer: userDid },
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const client = paymentMethod.getOcapClient();
|
|
87
|
+
const claim = claims.find((x) => x.type === 'signature');
|
|
88
|
+
|
|
89
|
+
// execute the delegate tx
|
|
90
|
+
const tx: Partial<Transaction> = client.decodeTx(claim.origin);
|
|
91
|
+
tx.signature = claim.sig;
|
|
92
|
+
|
|
93
|
+
// @ts-ignore
|
|
94
|
+
const { buffer } = await client.encodeDelegateTx({ tx });
|
|
95
|
+
const txHash = await client.sendDelegateTx(
|
|
96
|
+
// @ts-ignore
|
|
97
|
+
{ tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid)) },
|
|
98
|
+
getGasPayerExtra(buffer)
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
await subscription?.update({
|
|
102
|
+
payment_details: {
|
|
103
|
+
arcblock: {
|
|
104
|
+
tx_hash: txHash,
|
|
105
|
+
payer: userDid,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (invoice) {
|
|
111
|
+
if (invoice.status === 'uncollectible') {
|
|
112
|
+
await invoice.update({ status: 'open' });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
await invoiceQueue.pushAndWait({
|
|
116
|
+
id: invoice.id,
|
|
117
|
+
job: { invoiceId: invoice.id, retryOnError: false, waitForPayment: true },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (subscription) {
|
|
121
|
+
subscriptionQueue.push({
|
|
122
|
+
id: subscription.id,
|
|
123
|
+
job: { subscriptionId: subscription.id, action: 'cycle' },
|
|
124
|
+
// our next invoice should be generated at the end of current period, either trailing or normal
|
|
125
|
+
runAt: subscription.trail_end || subscription.current_period_end,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { hash: txHash };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
throw new Error(`Payment method ${paymentMethod.type} not supported`);
|
|
133
|
+
},
|
|
134
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getUrl } from '@blocklet/sdk/lib/component';
|
|
2
2
|
import { Router } from 'express';
|
|
3
3
|
import Joi from 'joi';
|
|
4
|
+
import merge from 'lodash/merge';
|
|
4
5
|
import pick from 'lodash/pick';
|
|
5
6
|
import uniq from 'lodash/uniq';
|
|
6
7
|
import type { WhereOptions } from 'sequelize';
|
|
@@ -10,7 +11,7 @@ import logger from '../libs/logger';
|
|
|
10
11
|
import { authenticate } from '../libs/security';
|
|
11
12
|
import { isLineItemCurrencyAligned } from '../libs/session';
|
|
12
13
|
import { getDaysUntilDue } from '../libs/subscription';
|
|
13
|
-
import {
|
|
14
|
+
import { getDataObjectFromQuery } from '../libs/util';
|
|
14
15
|
import { CheckoutSession } from '../store/models/checkout-session';
|
|
15
16
|
import { PaymentCurrency } from '../store/models/payment-currency';
|
|
16
17
|
import { Price } from '../store/models/price';
|
|
@@ -21,113 +22,10 @@ import { formatCheckoutSession } from './checkout-sessions';
|
|
|
21
22
|
const router = Router();
|
|
22
23
|
const auth = authenticate<PricingTable>({ component: true, roles: ['owner', 'admin'] });
|
|
23
24
|
|
|
24
|
-
const formatPricingTable = (payload: any) => {
|
|
25
|
-
const raw: Partial<PricingTable> = Object.assign(
|
|
26
|
-
{
|
|
27
|
-
branding_settings: {
|
|
28
|
-
background_color: '#ffffff',
|
|
29
|
-
border_style: 'default',
|
|
30
|
-
button_color: '#0074d4',
|
|
31
|
-
font_family: 'default',
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
pick(payload, ['name', 'items', 'metadata', 'brand_settings'])
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
raw.items = raw.items?.map((x) => {
|
|
38
|
-
const item = Object.assign(
|
|
39
|
-
{
|
|
40
|
-
adjustable_quantity: {
|
|
41
|
-
enabled: false,
|
|
42
|
-
maximum: 1,
|
|
43
|
-
minimum: 0,
|
|
44
|
-
},
|
|
45
|
-
after_completion: {
|
|
46
|
-
type: 'hosted_confirmation',
|
|
47
|
-
hosted_confirmation: {
|
|
48
|
-
custom_message: '',
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
allow_promotion_codes: false,
|
|
52
|
-
customer_creation: 'always',
|
|
53
|
-
consent_collection: {
|
|
54
|
-
promotions: 'none',
|
|
55
|
-
terms_of_service: 'none',
|
|
56
|
-
},
|
|
57
|
-
invoice_creation: {
|
|
58
|
-
enabled: true,
|
|
59
|
-
},
|
|
60
|
-
phone_number_collection: {
|
|
61
|
-
enabled: false,
|
|
62
|
-
},
|
|
63
|
-
billing_address_collection: 'auto',
|
|
64
|
-
subscription_data: {
|
|
65
|
-
description: '',
|
|
66
|
-
trial_period_days: 0,
|
|
67
|
-
},
|
|
68
|
-
nft_mint_settings: {
|
|
69
|
-
enabled: false,
|
|
70
|
-
factory: '',
|
|
71
|
-
},
|
|
72
|
-
submit_type: 'auto',
|
|
73
|
-
cross_sell_behavior: 'auto',
|
|
74
|
-
},
|
|
75
|
-
pick(x, [
|
|
76
|
-
'adjustable_quantity',
|
|
77
|
-
'after_completion',
|
|
78
|
-
'allow_promotion_codes',
|
|
79
|
-
'billing_address_collection',
|
|
80
|
-
'consent_collection',
|
|
81
|
-
'cross_sell_behavior',
|
|
82
|
-
'custom_fields',
|
|
83
|
-
'highlight_text',
|
|
84
|
-
'is_highlight',
|
|
85
|
-
'nft_mint_settings',
|
|
86
|
-
'phone_number_collection',
|
|
87
|
-
'price_id',
|
|
88
|
-
'product_id',
|
|
89
|
-
'submit_type',
|
|
90
|
-
'subscription_data',
|
|
91
|
-
])
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
if (item.adjustable_quantity?.enabled) {
|
|
95
|
-
item.adjustable_quantity.minimum = Number(item.adjustable_quantity?.minimum);
|
|
96
|
-
item.adjustable_quantity.maximum = Number(item.adjustable_quantity?.maximum);
|
|
97
|
-
}
|
|
98
|
-
if (item.after_completion?.type === 'hosted_confirmation') {
|
|
99
|
-
// @ts-ignore
|
|
100
|
-
item.after_completion.redirect = null;
|
|
101
|
-
}
|
|
102
|
-
if (item.after_completion?.type === 'redirect') {
|
|
103
|
-
// @ts-ignore
|
|
104
|
-
item.after_completion.hosted_confirmation = null;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return item;
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
if (payload.highlight && payload.highlight_product_id) {
|
|
111
|
-
raw.items?.forEach((x) => {
|
|
112
|
-
if (x.product_id === payload.highlight_product_id) {
|
|
113
|
-
x.is_highlight = x.product_id === payload.highlight_product_id;
|
|
114
|
-
x.highlight_text = payload.highlight_text || 'popular';
|
|
115
|
-
} else {
|
|
116
|
-
x.is_highlight = false;
|
|
117
|
-
x.highlight_text = 'popular';
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
raw.metadata = formatMetadata(raw.metadata);
|
|
123
|
-
|
|
124
|
-
return raw;
|
|
125
|
-
};
|
|
126
|
-
|
|
127
25
|
// FIXME: @wangshijun use schema validation
|
|
128
26
|
// eslint-disable-next-line consistent-return
|
|
129
27
|
router.post('/', auth, async (req, res) => {
|
|
130
|
-
const raw: Partial<PricingTable> =
|
|
28
|
+
const raw: Partial<PricingTable> = PricingTable.format(req.body);
|
|
131
29
|
raw.active = true;
|
|
132
30
|
raw.locked = false;
|
|
133
31
|
raw.livemode = !!req.livemode;
|
|
@@ -210,19 +108,9 @@ router.get('/:id', async (req, res) => {
|
|
|
210
108
|
return res.status(404).json({ error: 'pricing table not found' });
|
|
211
109
|
}
|
|
212
110
|
|
|
213
|
-
const prices = await Price.findAll({ where: { id: uniq(doc.items.map((x) => x.price_id)) } });
|
|
214
|
-
const products = await Product.findAll({ where: { id: uniq(doc.items.map((x) => x.product_id)) } });
|
|
215
|
-
|
|
216
|
-
doc.items.forEach((i) => {
|
|
217
|
-
// @ts-ignore
|
|
218
|
-
i.price = prices.find((p) => p.id === i.price_id);
|
|
219
|
-
// @ts-ignore
|
|
220
|
-
i.product = products.find((p) => p.id === i.product_id);
|
|
221
|
-
});
|
|
222
|
-
|
|
223
111
|
const currency = await PaymentCurrency.findOne({ where: { livemode: doc.livemode, is_base_currency: true } });
|
|
224
|
-
|
|
225
|
-
res.json({ ...
|
|
112
|
+
const expanded = await doc.expand();
|
|
113
|
+
res.json({ ...expanded, currency });
|
|
226
114
|
});
|
|
227
115
|
|
|
228
116
|
// update
|
|
@@ -240,7 +128,7 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
240
128
|
// return res.status(403).json({ error: 'pricing table locked' });
|
|
241
129
|
// }
|
|
242
130
|
|
|
243
|
-
await doc.update(
|
|
131
|
+
await doc.update(PricingTable.format(Object.assign({}, doc.dataValues, req.body)));
|
|
244
132
|
|
|
245
133
|
res.json(doc);
|
|
246
134
|
});
|
|
@@ -293,7 +181,7 @@ router.post('/stash', auth, async (req, res) => {
|
|
|
293
181
|
|
|
294
182
|
let doc = await PricingTable.findByPk(raw.id);
|
|
295
183
|
if (doc) {
|
|
296
|
-
await doc.update({ ...
|
|
184
|
+
await doc.update({ ...PricingTable.format(req.body), metadata: raw.metadata, livemode: raw.livemode });
|
|
297
185
|
} else {
|
|
298
186
|
doc = await PricingTable.create(raw as PricingTable);
|
|
299
187
|
}
|
|
@@ -337,11 +225,11 @@ router.post('/:id/checkout/:priceId', async (req, res) => {
|
|
|
337
225
|
'submit_type',
|
|
338
226
|
'cross_sell_behavior',
|
|
339
227
|
'nft_mint_settings',
|
|
340
|
-
'subscription_data',
|
|
341
228
|
]),
|
|
229
|
+
subscription_data: merge(price.subscription_data || {}, getDataObjectFromQuery(req.query, 'subscription_data')),
|
|
342
230
|
metadata: {
|
|
343
231
|
...doc.metadata,
|
|
344
|
-
...
|
|
232
|
+
...getDataObjectFromQuery(req.query),
|
|
345
233
|
days_until_due: getDaysUntilDue(req.query),
|
|
346
234
|
passport: await checkPassportForPricingTable(doc),
|
|
347
235
|
pricing_table_id: doc.id,
|