payment-kit 1.13.17 → 1.13.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/api/src/index.ts +17 -6
- package/api/src/integrations/stripe/handlers/index.ts +53 -0
- package/api/src/integrations/stripe/handlers/invoice.ts +252 -0
- package/api/src/integrations/stripe/handlers/payment-intent.ts +172 -0
- package/api/src/integrations/stripe/handlers/setup-intent.ts +42 -0
- package/api/src/integrations/stripe/handlers/subscription.ts +61 -0
- package/api/src/integrations/stripe/resource.ts +317 -0
- package/api/src/integrations/stripe/setup.ts +50 -0
- package/api/src/jobs/invoice.ts +11 -0
- package/api/src/jobs/payment.ts +15 -7
- package/api/src/jobs/subscription.ts +18 -2
- package/api/src/libs/session.ts +104 -8
- package/api/src/libs/util.ts +47 -1
- package/api/src/routes/checkout-sessions.ts +134 -27
- package/api/src/routes/connect/collect.ts +12 -4
- package/api/src/routes/connect/pay.ts +30 -20
- package/api/src/routes/connect/setup.ts +12 -4
- package/api/src/routes/connect/shared.ts +28 -4
- package/api/src/routes/connect/subscribe.ts +12 -5
- package/api/src/routes/customers.ts +5 -5
- package/api/src/routes/events.ts +9 -6
- package/api/src/routes/index.ts +2 -0
- package/api/src/routes/integrations/stripe.ts +64 -0
- package/api/src/routes/invoices.ts +19 -9
- package/api/src/routes/payment-intents.ts +19 -9
- package/api/src/routes/payment-links.ts +57 -15
- package/api/src/routes/payment-methods.ts +98 -1
- package/api/src/routes/prices.ts +71 -14
- package/api/src/routes/products.ts +79 -22
- package/api/src/routes/settings.ts +10 -11
- package/api/src/routes/subscription-items.ts +5 -5
- package/api/src/routes/subscriptions.ts +61 -10
- package/api/src/routes/usage-records.ts +52 -18
- package/api/src/routes/webhook-attempts.ts +5 -5
- package/api/src/routes/webhook-endpoints.ts +5 -5
- package/api/src/store/migrations/20230905-genesis.ts +2 -2
- package/api/src/store/migrations/20230911-seeding.ts +4 -3
- package/api/src/store/models/checkout-session.ts +15 -7
- package/api/src/store/models/index.ts +31 -7
- package/api/src/store/models/invoice.ts +1 -1
- package/api/src/store/models/payment-intent.ts +2 -5
- package/api/src/store/models/payment-link.ts +1 -1
- package/api/src/store/models/payment-method.ts +54 -33
- package/api/src/store/models/price.ts +52 -17
- package/api/src/store/models/product.ts +0 -3
- package/api/src/store/models/subscription.ts +3 -5
- package/api/src/store/models/types.ts +56 -2
- package/api/third.d.ts +2 -0
- package/blocklet.yml +1 -1
- package/package.json +36 -29
- package/public/currencies/dai.png +0 -0
- package/public/currencies/dollar.png +0 -0
- package/public/currencies/usdc.png +0 -0
- package/public/currencies/usdt.png +0 -0
- package/public/methods/arcblock.png +0 -0
- package/public/methods/binance.png +0 -0
- package/public/methods/coinbase.png +0 -0
- package/public/methods/ethereum.jpg +0 -0
- package/public/methods/stripe.png +0 -0
- package/src/components/checkout/form/address.tsx +86 -10
- package/src/components/checkout/form/index.tsx +169 -83
- package/src/components/checkout/form/phone.tsx +96 -0
- package/src/components/checkout/form/stripe.tsx +195 -0
- package/src/components/checkout/pay.tsx +115 -34
- package/src/components/checkout/product-item.tsx +4 -3
- package/src/components/checkout/summary.tsx +5 -4
- package/src/components/drawer-form.tsx +4 -4
- package/src/components/input.tsx +22 -4
- package/src/components/invoice/table.tsx +8 -3
- package/src/components/payment-link/before-pay.tsx +11 -6
- package/src/components/payment-link/chrome.tsx +13 -0
- package/src/components/payment-link/preview.tsx +31 -0
- package/src/components/payment-link/product-select.tsx +8 -3
- package/src/components/payment-method/arcblock.tsx +53 -0
- package/src/components/payment-method/bitcoin.tsx +53 -0
- package/src/components/payment-method/ethereum.tsx +53 -0
- package/src/components/payment-method/form.tsx +54 -0
- package/src/components/payment-method/stripe.tsx +45 -0
- package/src/components/portal/invoice/list.tsx +1 -1
- package/src/components/portal/subscription/list.tsx +1 -1
- package/src/components/price/currency-select.tsx +53 -0
- package/src/components/price/form.tsx +118 -24
- package/src/components/product/add-price.tsx +1 -1
- package/src/components/product/edit-price.tsx +6 -2
- package/src/components/subscription/items/index.tsx +7 -6
- package/src/components/subscription/items/usage-records.tsx +98 -0
- package/src/components/subscription/list.tsx +3 -2
- package/src/components/subscription/status.tsx +68 -0
- package/src/contexts/settings.tsx +2 -2
- package/src/env.d.ts +2 -0
- package/src/libs/util.ts +116 -21
- package/src/locales/en.tsx +71 -3
- package/src/pages/admin/billing/invoices/detail.tsx +5 -2
- package/src/pages/admin/billing/subscriptions/detail.tsx +6 -6
- package/src/pages/admin/customers/customers/detail.tsx +13 -1
- package/src/pages/admin/payments/intents/detail.tsx +8 -3
- package/src/pages/admin/payments/links/create.tsx +23 -3
- package/src/pages/admin/payments/links/detail.tsx +13 -26
- package/src/pages/admin/products/prices/detail.tsx +55 -11
- package/src/pages/admin/products/prices/list.tsx +7 -1
- package/src/pages/admin/products/products/create.tsx +1 -1
- package/src/pages/admin/products/products/detail.tsx +14 -7
- package/src/pages/admin/settings/index.tsx +16 -6
- package/src/pages/admin/settings/payment-methods/create.tsx +81 -0
- package/src/pages/admin/settings/{payment-methods.tsx → payment-methods/index.tsx} +9 -6
- package/src/pages/checkout/pay.tsx +3 -1
- package/src/pages/customer/index.tsx +12 -1
- package/public/.gitkeep +0 -0
package/api/src/libs/util.ts
CHANGED
|
@@ -1,10 +1,56 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
2
|
|
|
3
|
+
import { getUrl } from '@blocklet/sdk/lib/component';
|
|
3
4
|
import { customAlphabet } from 'nanoid';
|
|
4
5
|
|
|
5
6
|
import dayjs from './dayjs';
|
|
6
7
|
|
|
7
8
|
export const MAX_RETRY_COUNT = 18; // 2^18 seconds ~~ 3 days, total retry time: 6 days
|
|
9
|
+
export const STRIPE_API_VERSION = '2023-08-16';
|
|
10
|
+
export const STRIPE_ENDPOINT: string = getUrl('/api/integrations/stripe/webhook');
|
|
11
|
+
export const STRIPE_EVENTS: any[] = [
|
|
12
|
+
'checkout.session.async_payment_failed',
|
|
13
|
+
'checkout.session.async_payment_succeeded',
|
|
14
|
+
'checkout.session.completed',
|
|
15
|
+
'checkout.session.expired',
|
|
16
|
+
|
|
17
|
+
// 'customer.subscription.created',
|
|
18
|
+
'customer.subscription.deleted',
|
|
19
|
+
'customer.subscription.paused',
|
|
20
|
+
'customer.subscription.pending_update_applied',
|
|
21
|
+
'customer.subscription.pending_update_expired',
|
|
22
|
+
'customer.subscription.resumed',
|
|
23
|
+
'customer.subscription.trial_will_end',
|
|
24
|
+
'customer.subscription.updated',
|
|
25
|
+
|
|
26
|
+
'invoice.created',
|
|
27
|
+
'invoice.deleted',
|
|
28
|
+
'invoice.finalization_failed',
|
|
29
|
+
'invoice.finalized',
|
|
30
|
+
'invoice.marked_uncollectible',
|
|
31
|
+
'invoice.paid',
|
|
32
|
+
'invoice.payment_action_required',
|
|
33
|
+
'invoice.payment_failed',
|
|
34
|
+
'invoice.payment_succeeded',
|
|
35
|
+
'invoice.sent',
|
|
36
|
+
'invoice.upcoming',
|
|
37
|
+
'invoice.updated',
|
|
38
|
+
'invoice.voided',
|
|
39
|
+
|
|
40
|
+
'payment_intent.canceled',
|
|
41
|
+
'payment_intent.created',
|
|
42
|
+
'payment_intent.partially_funded',
|
|
43
|
+
'payment_intent.payment_failed',
|
|
44
|
+
'payment_intent.processing',
|
|
45
|
+
'payment_intent.requires_action',
|
|
46
|
+
'payment_intent.succeeded',
|
|
47
|
+
|
|
48
|
+
'setup_intent.canceled',
|
|
49
|
+
// 'setup_intent.created',
|
|
50
|
+
'setup_intent.requires_action',
|
|
51
|
+
'setup_intent.setup_failed',
|
|
52
|
+
'setup_intent.succeeded',
|
|
53
|
+
];
|
|
8
54
|
|
|
9
55
|
export function md5(input: string) {
|
|
10
56
|
return crypto.createHash('md5').update(input).digest('hex');
|
|
@@ -40,7 +86,7 @@ export function formatMetadata(metadata?: Record<string, any>): Record<string, a
|
|
|
40
86
|
acc[key] = metadata[key];
|
|
41
87
|
}
|
|
42
88
|
return acc;
|
|
43
|
-
},
|
|
89
|
+
}, {});
|
|
44
90
|
}
|
|
45
91
|
|
|
46
92
|
export function sleep(timeout = 0) {
|
|
@@ -4,7 +4,12 @@ import userMiddleware from '@blocklet/sdk/lib/middlewares/user';
|
|
|
4
4
|
import { Router } from 'express';
|
|
5
5
|
import omit from 'lodash/omit';
|
|
6
6
|
import pick from 'lodash/pick';
|
|
7
|
+
import sortBy from 'lodash/sortBy';
|
|
8
|
+
import uniq from 'lodash/uniq';
|
|
7
9
|
|
|
10
|
+
import { handleStripePaymentSucceed } from '../integrations/stripe/handlers/payment-intent';
|
|
11
|
+
import { handleStripeSubscriptionSucceed } from '../integrations/stripe/handlers/subscription';
|
|
12
|
+
import { ensureStripePaymentIntent, ensureStripeSubscription } from '../integrations/stripe/resource';
|
|
8
13
|
import { invoiceQueue } from '../jobs/invoice';
|
|
9
14
|
import { paymentQueue } from '../jobs/payment';
|
|
10
15
|
import { subscriptionQueue } from '../jobs/subscription';
|
|
@@ -17,9 +22,12 @@ import {
|
|
|
17
22
|
getCheckoutMode,
|
|
18
23
|
getStatementDescriptor,
|
|
19
24
|
getSubscriptionCreateSetup,
|
|
25
|
+
getSupportedPaymentCurrencies,
|
|
26
|
+
getSupportedPaymentMethods,
|
|
27
|
+
isLineItemAligned,
|
|
20
28
|
} from '../libs/session';
|
|
21
29
|
import { createCodeGenerator, formatMetadata } from '../libs/util';
|
|
22
|
-
import type { LineItem } from '../store/models';
|
|
30
|
+
import type { LineItem, TPaymentCurrency } from '../store/models';
|
|
23
31
|
import { CheckoutSession } from '../store/models/checkout-session';
|
|
24
32
|
import { Customer } from '../store/models/customer';
|
|
25
33
|
import { PaymentCurrency } from '../store/models/payment-currency';
|
|
@@ -39,7 +47,21 @@ const router = Router();
|
|
|
39
47
|
const user = userMiddleware();
|
|
40
48
|
const auth = authenticate<CheckoutSession>({ component: true, roles: ['owner', 'admin'] });
|
|
41
49
|
|
|
42
|
-
const
|
|
50
|
+
const getPaymentMethods = async (doc: CheckoutSession) => {
|
|
51
|
+
const paymentMethods = await PaymentMethod.expand(doc.livemode, { type: doc.payment_method_types });
|
|
52
|
+
const supportedCurrencies = getSupportedPaymentCurrencies(doc.line_items as any[]);
|
|
53
|
+
const methods = getSupportedPaymentMethods(paymentMethods as any[], (x) => supportedCurrencies.includes(x.id));
|
|
54
|
+
return sortBy(methods, (m) => (m.payment_currencies.some((c) => c.is_base_currency) ? 0 : 1));
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const getPaymentTypes = async (items: any[]) => {
|
|
58
|
+
const supportedCurrencies = getSupportedPaymentCurrencies(items);
|
|
59
|
+
const currencies = await PaymentCurrency.findAll({ where: { id: supportedCurrencies } });
|
|
60
|
+
const methods = await PaymentMethod.findAll({ where: { id: uniq(currencies.map((x) => x.payment_method_id)) } });
|
|
61
|
+
return methods.map((x) => x.type);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const formatBeforeSave = async (payload: any, throwOnEmptyItems = true) => {
|
|
43
65
|
const raw: Partial<CheckoutSession> = Object.assign(
|
|
44
66
|
{
|
|
45
67
|
allow_promotion_codes: false,
|
|
@@ -62,6 +84,7 @@ const formatBeforeSave = async (payload: any) => {
|
|
|
62
84
|
submit_type: 'pay',
|
|
63
85
|
},
|
|
64
86
|
pick(payload, [
|
|
87
|
+
'currency_id',
|
|
65
88
|
'expires_at',
|
|
66
89
|
'line_items',
|
|
67
90
|
'allow_promotion_codes',
|
|
@@ -97,6 +120,10 @@ const formatBeforeSave = async (payload: any) => {
|
|
|
97
120
|
|
|
98
121
|
raw.metadata = formatMetadata(raw.metadata);
|
|
99
122
|
|
|
123
|
+
if (raw.line_items?.length === 0 && throwOnEmptyItems) {
|
|
124
|
+
throw new Error('line items should not be empty for checkout session');
|
|
125
|
+
}
|
|
126
|
+
|
|
100
127
|
const items = await Price.expand(raw.line_items as LineItem[]);
|
|
101
128
|
if (items.some((x) => !x.price)) {
|
|
102
129
|
throw new Error('Invalid line items for checkout session, some price may have been deleted');
|
|
@@ -104,8 +131,23 @@ const formatBeforeSave = async (payload: any) => {
|
|
|
104
131
|
if (items.some((x) => !x.price.active)) {
|
|
105
132
|
throw new Error('Invalid line items for checkout session, some price may have been archived');
|
|
106
133
|
}
|
|
134
|
+
for (let i = 0; i < items.length; i++) {
|
|
135
|
+
const result = isLineItemAligned(items, i);
|
|
136
|
+
if (result.currency === false) {
|
|
137
|
+
throw new Error('line_items should have same currency');
|
|
138
|
+
}
|
|
139
|
+
if (result.recurring === false) {
|
|
140
|
+
throw new Error('line_items should have same recurring');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
107
143
|
|
|
108
|
-
|
|
144
|
+
// use first price currency as default
|
|
145
|
+
if (!raw.currency_id) {
|
|
146
|
+
raw.currency_id = items[0]?.price.currency_id;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const currency = await PaymentCurrency.findByPk(raw.currency_id);
|
|
150
|
+
const amount = getCheckoutAmount(items, currency as TPaymentCurrency, !!raw.subscription_data?.trial_period_days);
|
|
109
151
|
const mode = getCheckoutMode(items);
|
|
110
152
|
|
|
111
153
|
return Object.assign(raw, {
|
|
@@ -121,6 +163,8 @@ const formatBeforeSave = async (payload: any) => {
|
|
|
121
163
|
amount_tax: amount.tax,
|
|
122
164
|
},
|
|
123
165
|
|
|
166
|
+
payment_method_types: await getPaymentTypes(items),
|
|
167
|
+
|
|
124
168
|
// always create invoice for subscriptions
|
|
125
169
|
invoice_creation: mode === 'subscription' ? { enabled: true } : raw.invoice_creation,
|
|
126
170
|
});
|
|
@@ -131,7 +175,6 @@ router.post('/', auth, async (req, res) => {
|
|
|
131
175
|
const raw: Partial<CheckoutSession> = await formatBeforeSave(req.body);
|
|
132
176
|
raw.livemode = !!req.livemode;
|
|
133
177
|
raw.created_via = req.user?.via as string;
|
|
134
|
-
raw.currency_id = raw.currency_id || req.currency.id;
|
|
135
178
|
|
|
136
179
|
const doc = await CheckoutSession.create(raw as any);
|
|
137
180
|
|
|
@@ -154,7 +197,7 @@ router.post('/start/:id', user, async (req, res) => {
|
|
|
154
197
|
|
|
155
198
|
const items = await Price.expand(link.line_items);
|
|
156
199
|
|
|
157
|
-
const raw: Partial<CheckoutSession> = await formatBeforeSave(link);
|
|
200
|
+
const raw: Partial<CheckoutSession> = await formatBeforeSave(link, false);
|
|
158
201
|
raw.livemode = link.livemode;
|
|
159
202
|
raw.created_via = 'portal';
|
|
160
203
|
raw.currency_id = link.currency_id || req.currency.id;
|
|
@@ -177,10 +220,11 @@ router.post('/start/:id', user, async (req, res) => {
|
|
|
177
220
|
|
|
178
221
|
doc.line_items = items;
|
|
179
222
|
res.json({
|
|
180
|
-
checkoutSession:
|
|
181
|
-
paymentMethods: await
|
|
223
|
+
checkoutSession: doc.toJSON(),
|
|
224
|
+
paymentMethods: await getPaymentMethods(doc),
|
|
182
225
|
paymentLink: link,
|
|
183
226
|
paymentIntent: null,
|
|
227
|
+
customer: req.user ? await Customer.findOne({ where: { did: req.user.did } }) : null,
|
|
184
228
|
});
|
|
185
229
|
} catch (err) {
|
|
186
230
|
console.error(err);
|
|
@@ -188,17 +232,20 @@ router.post('/start/:id', user, async (req, res) => {
|
|
|
188
232
|
}
|
|
189
233
|
});
|
|
190
234
|
|
|
235
|
+
// for Node.js SDK
|
|
191
236
|
router.get('/:id', auth, async (req, res) => {
|
|
192
237
|
const doc = await CheckoutSession.findByPk(req.params.id);
|
|
193
238
|
|
|
194
239
|
if (doc) {
|
|
195
240
|
// @ts-ignore
|
|
196
241
|
doc.line_items = await Price.expand(doc.line_items);
|
|
242
|
+
doc.url = getUrl(`/checkout/${doc.submit_type}/${doc.id}`);
|
|
197
243
|
}
|
|
198
244
|
|
|
199
245
|
res.json(doc?.toJSON());
|
|
200
246
|
});
|
|
201
247
|
|
|
248
|
+
// for checkout page
|
|
202
249
|
router.get('/retrieve/:id', user, async (req, res) => {
|
|
203
250
|
const doc = await CheckoutSession.findByPk(req.params.id);
|
|
204
251
|
|
|
@@ -210,12 +257,15 @@ router.get('/retrieve/:id', user, async (req, res) => {
|
|
|
210
257
|
// @ts-ignore
|
|
211
258
|
doc.line_items = await Price.expand(doc.line_items);
|
|
212
259
|
|
|
260
|
+
// check payment intent
|
|
261
|
+
const paymentIntent = doc.payment_intent_id ? await PaymentIntent.findByPk(doc.payment_intent_id) : null;
|
|
262
|
+
|
|
213
263
|
// FIXME: possible sensitive data leak
|
|
214
264
|
res.json({
|
|
215
|
-
checkoutSession:
|
|
216
|
-
paymentMethods: await
|
|
265
|
+
checkoutSession: doc.toJSON(),
|
|
266
|
+
paymentMethods: await getPaymentMethods(doc),
|
|
217
267
|
paymentLink: doc.payment_link_id ? await PaymentLink.findByPk(doc.payment_link_id) : null,
|
|
218
|
-
paymentIntent
|
|
268
|
+
paymentIntent,
|
|
219
269
|
customer: req.user ? await Customer.findOne({ where: { did: req.user.did } }) : null,
|
|
220
270
|
});
|
|
221
271
|
});
|
|
@@ -253,6 +303,20 @@ router.put('/:id/submit', user, async (req, res) => {
|
|
|
253
303
|
}
|
|
254
304
|
await checkoutSession.update({ currency_id: paymentCurrency.id });
|
|
255
305
|
|
|
306
|
+
// always update payment amount in case currency has changed
|
|
307
|
+
const lineItems = await Price.expand(checkoutSession.line_items, true);
|
|
308
|
+
const trialInDays = checkoutSession.subscription_data?.trial_period_days || 0;
|
|
309
|
+
const amount = getCheckoutAmount(lineItems, paymentCurrency, !!trialInDays);
|
|
310
|
+
await checkoutSession.update({
|
|
311
|
+
amount_subtotal: amount.subtotal,
|
|
312
|
+
amount_total: amount.total,
|
|
313
|
+
total_details: {
|
|
314
|
+
amount_discount: amount.discount,
|
|
315
|
+
amount_shipping: amount.shipping,
|
|
316
|
+
amount_tax: amount.tax,
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
|
|
256
320
|
// ensure customer created or updated
|
|
257
321
|
let customer = await Customer.findOne({ where: { did: req.user.did } });
|
|
258
322
|
if (!customer) {
|
|
@@ -286,8 +350,7 @@ router.put('/:id/submit', user, async (req, res) => {
|
|
|
286
350
|
|
|
287
351
|
await customer.update(updates);
|
|
288
352
|
}
|
|
289
|
-
|
|
290
|
-
const lineItems = await Price.expand(checkoutSession.line_items, true);
|
|
353
|
+
await checkoutSession.update({ customer_id: customer.id, customer_did: req.user.did });
|
|
291
354
|
|
|
292
355
|
// payment intent is only created when checkout session is in payment mode
|
|
293
356
|
let paymentIntent: PaymentIntent | null = null;
|
|
@@ -316,8 +379,6 @@ router.put('/:id/submit', user, async (req, res) => {
|
|
|
316
379
|
receipt_email: customer.email,
|
|
317
380
|
});
|
|
318
381
|
} else {
|
|
319
|
-
// ensure payment intent
|
|
320
|
-
// FIXME: support and validate currency converting here
|
|
321
382
|
paymentIntent = await PaymentIntent.create({
|
|
322
383
|
livemode: !!checkoutSession.livemode,
|
|
323
384
|
amount: checkoutSession.amount_total,
|
|
@@ -330,7 +391,7 @@ router.put('/:id/submit', user, async (req, res) => {
|
|
|
330
391
|
status: 'requires_payment_method',
|
|
331
392
|
capture_method: 'automatic',
|
|
332
393
|
confirmation_method: 'automatic',
|
|
333
|
-
payment_method_types:
|
|
394
|
+
payment_method_types: checkoutSession.payment_method_types,
|
|
334
395
|
receipt_email: customer.email,
|
|
335
396
|
statement_descriptor: getStatementDescriptor(lineItems),
|
|
336
397
|
statement_descriptor_suffix: '',
|
|
@@ -338,21 +399,21 @@ router.put('/:id/submit', user, async (req, res) => {
|
|
|
338
399
|
metadata: {},
|
|
339
400
|
});
|
|
340
401
|
|
|
402
|
+
// lock prices used by this payment
|
|
403
|
+
await Price.update({ locked: true }, { where: { id: lineItems.map((x) => x.price_id) } });
|
|
404
|
+
|
|
341
405
|
// persist payment intent id
|
|
342
406
|
await checkoutSession.update({ payment_intent_id: paymentIntent.id });
|
|
343
407
|
}
|
|
344
408
|
}
|
|
345
409
|
|
|
346
|
-
// payment intent is only created when checkout session is in payment mode
|
|
347
410
|
let setupIntent: SetupIntent | null = null;
|
|
348
|
-
if (checkoutSession.mode === 'setup') {
|
|
411
|
+
if (checkoutSession.mode === 'setup' || paymentMethod.type !== 'stripe') {
|
|
349
412
|
if (checkoutSession.setup_intent_id) {
|
|
350
413
|
setupIntent = await SetupIntent.findByPk(checkoutSession.setup_intent_id);
|
|
351
414
|
}
|
|
352
415
|
|
|
353
|
-
// check existing payment intent
|
|
354
416
|
if (setupIntent) {
|
|
355
|
-
// Check payment intent, if we have a payment intent, we should not create a new one
|
|
356
417
|
if (setupIntent.status === 'succeeded') {
|
|
357
418
|
return res.status(403).json({ error: 'Checkout session setup completed' });
|
|
358
419
|
}
|
|
@@ -376,7 +437,7 @@ router.put('/:id/submit', user, async (req, res) => {
|
|
|
376
437
|
currency_id: paymentCurrency.id,
|
|
377
438
|
payment_method_id: paymentMethod.id,
|
|
378
439
|
status: 'requires_payment_method',
|
|
379
|
-
payment_method_types:
|
|
440
|
+
payment_method_types: checkoutSession.payment_method_types,
|
|
380
441
|
flow_directions: ['inbound', 'outbound'],
|
|
381
442
|
usage: 'off_session',
|
|
382
443
|
metadata: {},
|
|
@@ -397,16 +458,16 @@ router.put('/:id/submit', user, async (req, res) => {
|
|
|
397
458
|
return res.status(403).json({ error: 'Checkout session subscription status unexpected' });
|
|
398
459
|
}
|
|
399
460
|
subscription = await subscription.update({
|
|
400
|
-
currency_id:
|
|
461
|
+
currency_id: paymentCurrency.id,
|
|
401
462
|
customer_id: customer.id,
|
|
402
|
-
default_payment_method_id:
|
|
463
|
+
default_payment_method_id: paymentMethod.id,
|
|
403
464
|
pending_setup_intent: setupIntent?.id,
|
|
404
465
|
});
|
|
405
466
|
} else {
|
|
406
|
-
const setup = getSubscriptionCreateSetup(lineItems,
|
|
467
|
+
const setup = getSubscriptionCreateSetup(lineItems, paymentCurrency, trialInDays);
|
|
407
468
|
subscription = await Subscription.create({
|
|
408
469
|
livemode: !!checkoutSession.livemode,
|
|
409
|
-
currency_id:
|
|
470
|
+
currency_id: paymentCurrency.id,
|
|
410
471
|
customer_id: customer.id,
|
|
411
472
|
status: 'incomplete',
|
|
412
473
|
current_period_start: setup.period.start,
|
|
@@ -422,7 +483,7 @@ router.put('/:id/submit', user, async (req, res) => {
|
|
|
422
483
|
},
|
|
423
484
|
pending_invoice_item_interval: setup.recurring,
|
|
424
485
|
pending_setup_intent: setupIntent?.id,
|
|
425
|
-
default_payment_method_id:
|
|
486
|
+
default_payment_method_id: paymentMethod.id,
|
|
426
487
|
cancel_at_period_end: false,
|
|
427
488
|
collection_method: 'charge_automatically',
|
|
428
489
|
// FIXME: support discount
|
|
@@ -445,6 +506,9 @@ router.put('/:id/submit', user, async (req, res) => {
|
|
|
445
506
|
)
|
|
446
507
|
);
|
|
447
508
|
|
|
509
|
+
// lock prices used by this subscription
|
|
510
|
+
await Price.update({ locked: true }, { where: { id: lineItems.map((x) => x.price_id) } });
|
|
511
|
+
|
|
448
512
|
// persist subscription id
|
|
449
513
|
await checkoutSession.update({ subscription_id: subscription.id });
|
|
450
514
|
}
|
|
@@ -459,7 +523,7 @@ router.put('/:id/submit', user, async (req, res) => {
|
|
|
459
523
|
});
|
|
460
524
|
if (delegation.sufficient) {
|
|
461
525
|
const paymentSettings = {
|
|
462
|
-
payment_method_types:
|
|
526
|
+
payment_method_types: checkoutSession.payment_method_types,
|
|
463
527
|
payment_method_options: {
|
|
464
528
|
arcblock: { payer: delegation.delegator as string },
|
|
465
529
|
},
|
|
@@ -507,7 +571,50 @@ router.put('/:id/submit', user, async (req, res) => {
|
|
|
507
571
|
}
|
|
508
572
|
}
|
|
509
573
|
|
|
510
|
-
|
|
574
|
+
let stripeContext: any = null;
|
|
575
|
+
if (paymentMethod.type === 'stripe') {
|
|
576
|
+
if (paymentIntent) {
|
|
577
|
+
const stripeIntent = await ensureStripePaymentIntent(paymentIntent, paymentMethod, paymentCurrency);
|
|
578
|
+
if (stripeIntent && paymentIntent?.payment_details?.stripe?.payment_intent_id === stripeIntent.id) {
|
|
579
|
+
if (stripeIntent.status === 'succeeded' && paymentIntent.status !== 'succeeded') {
|
|
580
|
+
await handleStripePaymentSucceed(paymentIntent);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
stripeContext = {
|
|
584
|
+
type: 'payment_intent',
|
|
585
|
+
id: stripeIntent.id,
|
|
586
|
+
client_secret: stripeIntent.client_secret,
|
|
587
|
+
status: stripeIntent.status,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (subscription) {
|
|
592
|
+
const stripeSubscription = await ensureStripeSubscription(
|
|
593
|
+
subscription,
|
|
594
|
+
paymentMethod,
|
|
595
|
+
paymentCurrency,
|
|
596
|
+
lineItems as any[],
|
|
597
|
+
trialInDays
|
|
598
|
+
);
|
|
599
|
+
if (stripeSubscription && subscription?.payment_details?.stripe?.subscription_id === stripeSubscription.id) {
|
|
600
|
+
if (stripeSubscription.status === 'active' && subscription.status === 'incomplete') {
|
|
601
|
+
await handleStripeSubscriptionSucceed(subscription);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
stripeContext = {
|
|
605
|
+
type: 'subscription',
|
|
606
|
+
id: stripeSubscription.id,
|
|
607
|
+
// @ts-ignore
|
|
608
|
+
client_secret:
|
|
609
|
+
stripeSubscription.latest_invoice?.payment_intent?.client_secret ||
|
|
610
|
+
stripeSubscription.pending_setup_intent?.client_secret,
|
|
611
|
+
intent_type: stripeSubscription.latest_invoice?.payment_intent ? 'payment_intent' : 'setup_intent',
|
|
612
|
+
status: stripeSubscription.status,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return res.json({ paymentIntent, setupIntent, stripeContext, subscription, checkoutSession, customer, delegation });
|
|
511
618
|
} catch (err) {
|
|
512
619
|
console.error(err);
|
|
513
620
|
res.status(500).json({ error: err.message });
|
|
@@ -7,12 +7,18 @@ import type { CallbackArgs } from '../../libs/auth';
|
|
|
7
7
|
import { wallet } from '../../libs/auth';
|
|
8
8
|
import { getClient } from '../../libs/chain/arcblock';
|
|
9
9
|
import dayjs from '../../libs/dayjs';
|
|
10
|
-
import { ensureInvoiceForCollect } from './shared';
|
|
10
|
+
import { ensureInvoiceForCollect, getAuthPrincipalClaim } from './shared';
|
|
11
11
|
|
|
12
12
|
// Used to collect an open invoice failed to collect automatically
|
|
13
|
-
// TODO: support multiple chain and multiple currency
|
|
14
13
|
export default {
|
|
15
14
|
action: 'collect',
|
|
15
|
+
authPrincipal: false,
|
|
16
|
+
claims: {
|
|
17
|
+
authPrincipal: async ({ extraParams }: CallbackArgs) => {
|
|
18
|
+
const { paymentMethod } = await ensureInvoiceForCollect(extraParams.invoiceId);
|
|
19
|
+
return getAuthPrincipalClaim(paymentMethod, 'pay');
|
|
20
|
+
},
|
|
21
|
+
},
|
|
16
22
|
onConnect: async ({ userDid, userPk, extraParams }: CallbackArgs) => {
|
|
17
23
|
const { invoiceId } = extraParams;
|
|
18
24
|
const { invoice, paymentIntent, paymentCurrency, paymentMethod } = await ensureInvoiceForCollect(invoiceId);
|
|
@@ -75,8 +81,10 @@ export default {
|
|
|
75
81
|
amount_received: invoice.amount_due,
|
|
76
82
|
capture_method: 'manual',
|
|
77
83
|
payment_details: {
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
arcblock: {
|
|
85
|
+
tx_hash: txHash,
|
|
86
|
+
payer: userDid,
|
|
87
|
+
},
|
|
80
88
|
},
|
|
81
89
|
});
|
|
82
90
|
|
|
@@ -5,10 +5,17 @@ import type { CallbackArgs } from '../../libs/auth';
|
|
|
5
5
|
import { wallet } from '../../libs/auth';
|
|
6
6
|
import { getClient } from '../../libs/chain/arcblock';
|
|
7
7
|
import dayjs from '../../libs/dayjs';
|
|
8
|
-
import { ensureInvoiceForCheckout, ensurePaymentIntent } from './shared';
|
|
8
|
+
import { ensureInvoiceForCheckout, ensurePaymentIntent, getAuthPrincipalClaim } from './shared';
|
|
9
9
|
|
|
10
10
|
export default {
|
|
11
11
|
action: 'pay',
|
|
12
|
+
authPrincipal: false,
|
|
13
|
+
claims: {
|
|
14
|
+
authPrincipal: async ({ extraParams }: CallbackArgs) => {
|
|
15
|
+
const { paymentMethod } = await ensurePaymentIntent(extraParams.checkoutSessionId);
|
|
16
|
+
return getAuthPrincipalClaim(paymentMethod, 'pay');
|
|
17
|
+
},
|
|
18
|
+
},
|
|
12
19
|
onConnect: async ({ userDid, userPk, extraParams }: CallbackArgs) => {
|
|
13
20
|
const { checkoutSessionId } = extraParams;
|
|
14
21
|
const { paymentIntent, paymentCurrency, paymentMethod } = await ensurePaymentIntent(checkoutSessionId, userDid);
|
|
@@ -16,23 +23,22 @@ export default {
|
|
|
16
23
|
throw new Error('Payment intent not found');
|
|
17
24
|
}
|
|
18
25
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
if (paymentMethod.type === 'arcblock') {
|
|
27
|
+
const tokens = [{ address: paymentCurrency.contract as string, value: paymentIntent.amount }];
|
|
28
|
+
// @ts-ignore
|
|
29
|
+
const itx: TransferV3Tx = {
|
|
30
|
+
outputs: [{ owner: wallet.address, tokens, assets: [] }],
|
|
31
|
+
data: {
|
|
32
|
+
type: 'json',
|
|
33
|
+
// @ts-ignore
|
|
34
|
+
value: {
|
|
35
|
+
appId: wallet.address,
|
|
36
|
+
paymentIntentId: paymentIntent.id,
|
|
37
|
+
checkoutSessionId,
|
|
38
|
+
},
|
|
30
39
|
},
|
|
31
|
-
}
|
|
32
|
-
};
|
|
40
|
+
};
|
|
33
41
|
|
|
34
|
-
// TODO: support multiple chain and multiple currency
|
|
35
|
-
if (paymentMethod.type === 'arcblock') {
|
|
36
42
|
return {
|
|
37
43
|
prepareTx: {
|
|
38
44
|
type: 'TransferV3Tx',
|
|
@@ -82,16 +88,20 @@ export default {
|
|
|
82
88
|
status: 'complete',
|
|
83
89
|
payment_status: 'paid',
|
|
84
90
|
payment_details: {
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
arcblock: {
|
|
92
|
+
tx_hash: txHash,
|
|
93
|
+
payer: userDid,
|
|
94
|
+
},
|
|
87
95
|
},
|
|
88
96
|
});
|
|
89
97
|
await paymentIntent.update({
|
|
90
98
|
status: 'succeeded',
|
|
91
99
|
amount_received: paymentIntent.amount,
|
|
92
100
|
payment_details: {
|
|
93
|
-
|
|
94
|
-
|
|
101
|
+
arcblock: {
|
|
102
|
+
tx_hash: txHash,
|
|
103
|
+
payer: userDid,
|
|
104
|
+
},
|
|
95
105
|
},
|
|
96
106
|
});
|
|
97
107
|
|
|
@@ -7,10 +7,17 @@ import { subscriptionQueue } from '../../jobs/subscription';
|
|
|
7
7
|
import type { CallbackArgs } from '../../libs/auth';
|
|
8
8
|
import { wallet } from '../../libs/auth';
|
|
9
9
|
import { getClient } from '../../libs/chain/arcblock';
|
|
10
|
-
import { ensureSetupIntent } from './shared';
|
|
10
|
+
import { ensureSetupIntent, getAuthPrincipalClaim } from './shared';
|
|
11
11
|
|
|
12
12
|
export default {
|
|
13
13
|
action: 'setup',
|
|
14
|
+
authPrincipal: false,
|
|
15
|
+
claims: {
|
|
16
|
+
authPrincipal: async ({ extraParams }: CallbackArgs) => {
|
|
17
|
+
const { paymentMethod } = await ensureSetupIntent(extraParams.checkoutSessionId);
|
|
18
|
+
return getAuthPrincipalClaim(paymentMethod, 'subscribe');
|
|
19
|
+
},
|
|
20
|
+
},
|
|
14
21
|
onConnect: async ({ userDid, userPk, extraParams }: CallbackArgs) => {
|
|
15
22
|
const { checkoutSessionId } = extraParams;
|
|
16
23
|
const { paymentMethod, subscription } = await ensureSetupIntent(checkoutSessionId, userDid);
|
|
@@ -18,7 +25,6 @@ export default {
|
|
|
18
25
|
throw new Error('Subscription for checkoutSession not found');
|
|
19
26
|
}
|
|
20
27
|
|
|
21
|
-
// TODO: support multiple chain and multiple currency
|
|
22
28
|
if (paymentMethod.type === 'arcblock') {
|
|
23
29
|
return {
|
|
24
30
|
signature: {
|
|
@@ -98,8 +104,10 @@ export default {
|
|
|
98
104
|
await subscription.update({
|
|
99
105
|
status: subscription.trail_end ? 'trialing' : 'active',
|
|
100
106
|
payment_details: {
|
|
101
|
-
|
|
102
|
-
|
|
107
|
+
arcblock: {
|
|
108
|
+
tx_hash: txHash,
|
|
109
|
+
payer: userDid,
|
|
110
|
+
},
|
|
103
111
|
},
|
|
104
112
|
});
|
|
105
113
|
|
|
@@ -40,7 +40,7 @@ export async function ensureCheckoutSession(checkoutSessionId: string) {
|
|
|
40
40
|
return checkoutSession;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export async function ensurePaymentIntent(checkoutSessionId: string, userDid
|
|
43
|
+
export async function ensurePaymentIntent(checkoutSessionId: string, userDid?: string): Promise<Result> {
|
|
44
44
|
const checkoutSession = await ensureCheckoutSession(checkoutSessionId);
|
|
45
45
|
|
|
46
46
|
let customerId;
|
|
@@ -84,7 +84,7 @@ export async function ensurePaymentIntent(checkoutSessionId: string, userDid: st
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
let customer;
|
|
87
|
-
if (customerId) {
|
|
87
|
+
if (customerId && userDid) {
|
|
88
88
|
customer = await Customer.findByPk(customerId);
|
|
89
89
|
if (!customer) {
|
|
90
90
|
throw new Error('Customer not found');
|
|
@@ -122,7 +122,7 @@ export async function ensurePaymentIntent(checkoutSessionId: string, userDid: st
|
|
|
122
122
|
};
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
export async function ensureSetupIntent(checkoutSessionId: string, userDid
|
|
125
|
+
export async function ensureSetupIntent(checkoutSessionId: string, userDid?: string) {
|
|
126
126
|
const checkoutSession = await ensureCheckoutSession(checkoutSessionId);
|
|
127
127
|
|
|
128
128
|
let customerId;
|
|
@@ -166,7 +166,7 @@ export async function ensureSetupIntent(checkoutSessionId: string, userDid: stri
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
let customer;
|
|
169
|
-
if (customerId) {
|
|
169
|
+
if (customerId && userDid) {
|
|
170
170
|
customer = await Customer.findByPk(customerId);
|
|
171
171
|
if (!customer) {
|
|
172
172
|
throw new Error('Customer not found');
|
|
@@ -408,3 +408,27 @@ export async function ensureInvoiceForCollect(invoiceId: string) {
|
|
|
408
408
|
paymentMethod: paymentMethod as PaymentMethod,
|
|
409
409
|
};
|
|
410
410
|
}
|
|
411
|
+
|
|
412
|
+
export function getAuthPrincipalClaim(method: PaymentMethod, action: string) {
|
|
413
|
+
let chainInfo = { type: 'arcblock', id: 'none', host: 'none' };
|
|
414
|
+
if (method.type === 'arcblock') {
|
|
415
|
+
chainInfo = {
|
|
416
|
+
type: 'arcblock',
|
|
417
|
+
id: method.settings?.arcblock?.chain_id as string,
|
|
418
|
+
host: method.settings?.arcblock?.api_host as string,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
if (method.type === 'ethereum') {
|
|
422
|
+
chainInfo = {
|
|
423
|
+
type: 'ethereum',
|
|
424
|
+
// @ts-ignore
|
|
425
|
+
id: method.settings?.ethereum?.chain_id as number,
|
|
426
|
+
host: method.settings?.ethereum?.api_host as string,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
description: `Select account to ${action}`,
|
|
432
|
+
chainInfo,
|
|
433
|
+
};
|
|
434
|
+
}
|