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.
Files changed (109) hide show
  1. package/README.md +14 -0
  2. package/api/src/index.ts +17 -6
  3. package/api/src/integrations/stripe/handlers/index.ts +53 -0
  4. package/api/src/integrations/stripe/handlers/invoice.ts +252 -0
  5. package/api/src/integrations/stripe/handlers/payment-intent.ts +172 -0
  6. package/api/src/integrations/stripe/handlers/setup-intent.ts +42 -0
  7. package/api/src/integrations/stripe/handlers/subscription.ts +61 -0
  8. package/api/src/integrations/stripe/resource.ts +317 -0
  9. package/api/src/integrations/stripe/setup.ts +50 -0
  10. package/api/src/jobs/invoice.ts +11 -0
  11. package/api/src/jobs/payment.ts +15 -7
  12. package/api/src/jobs/subscription.ts +18 -2
  13. package/api/src/libs/session.ts +104 -8
  14. package/api/src/libs/util.ts +47 -1
  15. package/api/src/routes/checkout-sessions.ts +134 -27
  16. package/api/src/routes/connect/collect.ts +12 -4
  17. package/api/src/routes/connect/pay.ts +30 -20
  18. package/api/src/routes/connect/setup.ts +12 -4
  19. package/api/src/routes/connect/shared.ts +28 -4
  20. package/api/src/routes/connect/subscribe.ts +12 -5
  21. package/api/src/routes/customers.ts +5 -5
  22. package/api/src/routes/events.ts +9 -6
  23. package/api/src/routes/index.ts +2 -0
  24. package/api/src/routes/integrations/stripe.ts +64 -0
  25. package/api/src/routes/invoices.ts +19 -9
  26. package/api/src/routes/payment-intents.ts +19 -9
  27. package/api/src/routes/payment-links.ts +57 -15
  28. package/api/src/routes/payment-methods.ts +98 -1
  29. package/api/src/routes/prices.ts +71 -14
  30. package/api/src/routes/products.ts +79 -22
  31. package/api/src/routes/settings.ts +10 -11
  32. package/api/src/routes/subscription-items.ts +5 -5
  33. package/api/src/routes/subscriptions.ts +61 -10
  34. package/api/src/routes/usage-records.ts +52 -18
  35. package/api/src/routes/webhook-attempts.ts +5 -5
  36. package/api/src/routes/webhook-endpoints.ts +5 -5
  37. package/api/src/store/migrations/20230905-genesis.ts +2 -2
  38. package/api/src/store/migrations/20230911-seeding.ts +4 -3
  39. package/api/src/store/models/checkout-session.ts +15 -7
  40. package/api/src/store/models/index.ts +31 -7
  41. package/api/src/store/models/invoice.ts +1 -1
  42. package/api/src/store/models/payment-intent.ts +2 -5
  43. package/api/src/store/models/payment-link.ts +1 -1
  44. package/api/src/store/models/payment-method.ts +54 -33
  45. package/api/src/store/models/price.ts +52 -17
  46. package/api/src/store/models/product.ts +0 -3
  47. package/api/src/store/models/subscription.ts +3 -5
  48. package/api/src/store/models/types.ts +56 -2
  49. package/api/third.d.ts +2 -0
  50. package/blocklet.yml +1 -1
  51. package/package.json +36 -29
  52. package/public/currencies/dai.png +0 -0
  53. package/public/currencies/dollar.png +0 -0
  54. package/public/currencies/usdc.png +0 -0
  55. package/public/currencies/usdt.png +0 -0
  56. package/public/methods/arcblock.png +0 -0
  57. package/public/methods/binance.png +0 -0
  58. package/public/methods/coinbase.png +0 -0
  59. package/public/methods/ethereum.jpg +0 -0
  60. package/public/methods/stripe.png +0 -0
  61. package/src/components/checkout/form/address.tsx +86 -10
  62. package/src/components/checkout/form/index.tsx +169 -83
  63. package/src/components/checkout/form/phone.tsx +96 -0
  64. package/src/components/checkout/form/stripe.tsx +195 -0
  65. package/src/components/checkout/pay.tsx +115 -34
  66. package/src/components/checkout/product-item.tsx +4 -3
  67. package/src/components/checkout/summary.tsx +5 -4
  68. package/src/components/drawer-form.tsx +4 -4
  69. package/src/components/input.tsx +22 -4
  70. package/src/components/invoice/table.tsx +8 -3
  71. package/src/components/payment-link/before-pay.tsx +11 -6
  72. package/src/components/payment-link/chrome.tsx +13 -0
  73. package/src/components/payment-link/preview.tsx +31 -0
  74. package/src/components/payment-link/product-select.tsx +8 -3
  75. package/src/components/payment-method/arcblock.tsx +53 -0
  76. package/src/components/payment-method/bitcoin.tsx +53 -0
  77. package/src/components/payment-method/ethereum.tsx +53 -0
  78. package/src/components/payment-method/form.tsx +54 -0
  79. package/src/components/payment-method/stripe.tsx +45 -0
  80. package/src/components/portal/invoice/list.tsx +1 -1
  81. package/src/components/portal/subscription/list.tsx +1 -1
  82. package/src/components/price/currency-select.tsx +53 -0
  83. package/src/components/price/form.tsx +118 -24
  84. package/src/components/product/add-price.tsx +1 -1
  85. package/src/components/product/edit-price.tsx +6 -2
  86. package/src/components/subscription/items/index.tsx +7 -6
  87. package/src/components/subscription/items/usage-records.tsx +98 -0
  88. package/src/components/subscription/list.tsx +3 -2
  89. package/src/components/subscription/status.tsx +68 -0
  90. package/src/contexts/settings.tsx +2 -2
  91. package/src/env.d.ts +2 -0
  92. package/src/libs/util.ts +116 -21
  93. package/src/locales/en.tsx +71 -3
  94. package/src/pages/admin/billing/invoices/detail.tsx +5 -2
  95. package/src/pages/admin/billing/subscriptions/detail.tsx +6 -6
  96. package/src/pages/admin/customers/customers/detail.tsx +13 -1
  97. package/src/pages/admin/payments/intents/detail.tsx +8 -3
  98. package/src/pages/admin/payments/links/create.tsx +23 -3
  99. package/src/pages/admin/payments/links/detail.tsx +13 -26
  100. package/src/pages/admin/products/prices/detail.tsx +55 -11
  101. package/src/pages/admin/products/prices/list.tsx +7 -1
  102. package/src/pages/admin/products/products/create.tsx +1 -1
  103. package/src/pages/admin/products/products/detail.tsx +14 -7
  104. package/src/pages/admin/settings/index.tsx +16 -6
  105. package/src/pages/admin/settings/payment-methods/create.tsx +81 -0
  106. package/src/pages/admin/settings/{payment-methods.tsx → payment-methods/index.tsx} +9 -6
  107. package/src/pages/checkout/pay.tsx +3 -1
  108. package/src/pages/customer/index.tsx +12 -1
  109. package/public/.gitkeep +0 -0
@@ -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 formatBeforeSave = async (payload: any) => {
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
- const amount = getCheckoutAmount(items, !!raw.subscription_data?.trial_period_days);
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: { ...doc.toJSON(), currency: await PaymentCurrency.findByPk(doc.currency_id) },
181
- paymentMethods: await PaymentMethod.expand(req.livemode),
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: { ...doc.toJSON(), currency: await PaymentCurrency.findByPk(doc.currency_id) },
216
- paymentMethods: await PaymentMethod.expand(req.livemode),
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: doc.payment_intent_id ? await PaymentIntent.findByPk(doc.payment_intent_id) : null,
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: req.body.payment_currency,
461
+ currency_id: paymentCurrency.id,
401
462
  customer_id: customer.id,
402
- default_payment_method_id: req.body.payment_method,
463
+ default_payment_method_id: paymentMethod.id,
403
464
  pending_setup_intent: setupIntent?.id,
404
465
  });
405
466
  } else {
406
- const setup = getSubscriptionCreateSetup(lineItems, checkoutSession.subscription_data?.trial_period_days);
467
+ const setup = getSubscriptionCreateSetup(lineItems, paymentCurrency, trialInDays);
407
468
  subscription = await Subscription.create({
408
469
  livemode: !!checkoutSession.livemode,
409
- currency_id: req.body.payment_currency,
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: req.body.payment_method,
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: ['arcblock'],
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
- return res.json({ paymentIntent, setupIntent, subscription, checkoutSession, delegation });
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
- tx_hash: txHash,
79
- payer: userDid,
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
- const tokens = [{ address: paymentCurrency.contract as string, value: paymentIntent.amount }];
20
- // @ts-ignore
21
- const itx: TransferV3Tx = {
22
- outputs: [{ owner: wallet.address, tokens, assets: [] }],
23
- data: {
24
- type: 'json',
25
- // @ts-ignore
26
- value: {
27
- appId: wallet.address,
28
- paymentIntentId: paymentIntent.id,
29
- checkoutSessionId,
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
- tx_hash: txHash,
86
- payer: userDid,
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
- tx_hash: txHash,
94
- payer: userDid,
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
- tx_hash: txHash,
102
- payer: userDid,
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: string): Promise<Result> {
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: string) {
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
+ }