payment-kit 1.13.25 → 1.13.27

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 (73) hide show
  1. package/api/src/index.ts +8 -1
  2. package/api/src/integrations/blockchain/nft.ts +125 -0
  3. package/api/src/integrations/blockchain/stake.ts +55 -0
  4. package/api/src/integrations/blocklet/notification.ts +101 -0
  5. package/api/src/integrations/blocklet/passport.ts +139 -0
  6. package/api/src/integrations/stripe/handlers/invoice.ts +1 -1
  7. package/api/src/integrations/stripe/resource.ts +7 -7
  8. package/api/src/integrations/stripe/setup.ts +1 -1
  9. package/api/src/jobs/checkout-session.ts +23 -0
  10. package/api/src/jobs/payment.ts +1 -2
  11. package/api/src/libs/audit.ts +44 -2
  12. package/api/src/libs/payment.ts +3 -4
  13. package/api/src/locales/en.ts +9 -1
  14. package/api/src/locales/zh.ts +9 -1
  15. package/api/src/routes/checkout-sessions.ts +44 -14
  16. package/api/src/routes/connect/collect.ts +1 -2
  17. package/api/src/routes/connect/pay.ts +1 -2
  18. package/api/src/routes/connect/setup.ts +1 -2
  19. package/api/src/routes/connect/shared.ts +7 -3
  20. package/api/src/routes/connect/subscribe.ts +2 -3
  21. package/api/src/routes/index.ts +4 -0
  22. package/api/src/routes/integrations/stripe.ts +1 -1
  23. package/api/src/routes/passports.ts +74 -0
  24. package/api/src/routes/payment-links.ts +12 -2
  25. package/api/src/routes/pricing-table.ts +17 -3
  26. package/api/src/routes/products.ts +3 -3
  27. package/api/src/routes/redirect.ts +18 -0
  28. package/api/src/routes/subscriptions.ts +2 -5
  29. package/api/src/store/migrations/20231021-nft.ts +22 -0
  30. package/api/src/store/models/checkout-session.ts +76 -20
  31. package/api/src/store/models/invoice.ts +2 -0
  32. package/api/src/store/models/payment-intent.ts +2 -0
  33. package/api/src/store/models/payment-link.ts +26 -15
  34. package/api/src/store/models/payment-method.ts +22 -1
  35. package/api/src/store/models/price.ts +2 -0
  36. package/api/src/store/models/subscription.ts +26 -4
  37. package/api/src/store/models/types.ts +32 -1
  38. package/api/third.d.ts +2 -0
  39. package/blocklet.yml +1 -1
  40. package/package.json +7 -5
  41. package/src/components/customer/actions.tsx +15 -17
  42. package/src/components/customer/form.tsx +1 -1
  43. package/src/components/invoice/list.tsx +2 -1
  44. package/src/components/passport/actions.tsx +62 -0
  45. package/src/components/passport/assign.tsx +82 -0
  46. package/src/components/payment-intent/list.tsx +5 -1
  47. package/src/components/payment-link/actions.tsx +14 -1
  48. package/src/components/payment-link/after-pay.tsx +33 -1
  49. package/src/components/payment-link/preview.tsx +3 -6
  50. package/src/components/price/form.tsx +22 -23
  51. package/src/components/pricing-table/actions.tsx +14 -1
  52. package/src/components/pricing-table/payment-settings.tsx +33 -1
  53. package/src/components/pricing-table/preview.tsx +3 -7
  54. package/src/components/pricing-table/product-settings.tsx +4 -0
  55. package/src/components/pricing-table/product-skeleton.tsx +39 -0
  56. package/src/components/product/actions.tsx +14 -1
  57. package/src/components/status.tsx +1 -1
  58. package/src/components/subscription/status.tsx +3 -3
  59. package/src/components/table.tsx +14 -4
  60. package/src/global.css +7 -5
  61. package/src/libs/util.ts +6 -0
  62. package/src/locales/en.tsx +53 -2
  63. package/src/locales/zh.tsx +272 -116
  64. package/src/pages/admin/payments/links/create.tsx +4 -0
  65. package/src/pages/admin/payments/links/detail.tsx +9 -4
  66. package/src/pages/admin/products/index.tsx +2 -0
  67. package/src/pages/admin/products/passports/index.tsx +154 -0
  68. package/src/pages/admin/products/pricing-tables/create.tsx +1 -1
  69. package/src/pages/admin/settings/index.tsx +1 -1
  70. package/src/pages/admin/settings/payment-methods/index.tsx +17 -7
  71. package/src/pages/checkout/pay.tsx +15 -13
  72. package/src/pages/checkout/pricing-table.tsx +127 -91
  73. package/api/src/libs/chain/arcblock.ts +0 -13
@@ -1,3 +1,11 @@
1
1
  import flat from 'flat';
2
2
 
3
- export default flat({});
3
+ export default flat({
4
+ notification: {
5
+ sendTo: '发送给',
6
+ mintNFT: {
7
+ title: '{collection} NFT 铸造成功',
8
+ message: '{collection} NFT 已经铸造完成并发送到你的钱包,请查收',
9
+ },
10
+ },
11
+ });
@@ -1,12 +1,13 @@
1
1
  /* eslint-disable consistent-return */
2
2
  import { getUrl } from '@blocklet/sdk/lib/component';
3
3
  import userMiddleware from '@blocklet/sdk/lib/middlewares/user';
4
- import { Router } from 'express';
4
+ import { Request, Response, Router } from 'express';
5
5
  import omit from 'lodash/omit';
6
6
  import pick from 'lodash/pick';
7
7
  import sortBy from 'lodash/sortBy';
8
8
  import uniq from 'lodash/uniq';
9
9
 
10
+ import { checkPassportForPaymentLink } from '../integrations/blocklet/passport';
10
11
  import { handleStripePaymentSucceed } from '../integrations/stripe/handlers/payment-intent';
11
12
  import { handleStripeSubscriptionSucceed } from '../integrations/stripe/handlers/subscription';
12
13
  import { ensureStripePaymentIntent, ensureStripeSubscription } from '../integrations/stripe/resource';
@@ -76,11 +77,15 @@ export const formatCheckoutSession = async (payload: any, throwOnEmptyItems = tr
76
77
  phone_number_collection: {
77
78
  enabled: false,
78
79
  },
80
+ nft_mint_settings: {
81
+ enabled: false,
82
+ },
79
83
  billing_address_collection: 'auto',
80
84
  subscription_data: {
81
85
  description: '',
82
86
  trial_period_days: 0,
83
87
  },
88
+ payment_intent_data: {},
84
89
  submit_type: 'pay',
85
90
  },
86
91
  pick(payload, [
@@ -94,6 +99,8 @@ export const formatCheckoutSession = async (payload: any, throwOnEmptyItems = tr
94
99
  'invoice_creation',
95
100
  'phone_number_collection',
96
101
  'billing_address_collection',
102
+ 'nft_mint_settings',
103
+ 'payment_intent_data',
97
104
  'submit_type',
98
105
  'subscription_data',
99
106
  'metadata',
@@ -111,6 +118,12 @@ export const formatCheckoutSession = async (payload: any, throwOnEmptyItems = tr
111
118
  raw.expires_at = dayjs().unix() + 60 * 60 * 24; // 24 hours after creation
112
119
  }
113
120
 
121
+ if (raw.nft_mint_settings?.enabled) {
122
+ if (!raw.nft_mint_settings?.factory) {
123
+ throw new Error('factory is required when nft mint is enabled');
124
+ }
125
+ }
126
+
114
127
  raw.line_items?.forEach((x) => {
115
128
  if (x.adjustable_quantity?.enabled) {
116
129
  x.adjustable_quantity.minimum = Number(x.adjustable_quantity?.minimum);
@@ -154,6 +167,7 @@ export const formatCheckoutSession = async (payload: any, throwOnEmptyItems = tr
154
167
  mode,
155
168
  status: 'open',
156
169
  payment_status: 'unpaid',
170
+ nft_mint_status: raw.nft_mint_settings?.enabled ? 'pending' : 'disabled',
157
171
 
158
172
  amount_subtotal: amount.subtotal,
159
173
  amount_total: amount.total,
@@ -178,14 +192,11 @@ router.post('/', auth, async (req, res) => {
178
192
 
179
193
  const doc = await CheckoutSession.create(raw as any);
180
194
 
181
- // FIXME: lock price and product
182
-
183
195
  res.json({ ...doc.toJSON(), url: getUrl(`/checkout/${doc.submit_type}/${doc.id}`) });
184
196
  });
185
197
 
186
- // start checkout session from payment link
187
- router.post('/start/:id', user, async (req, res) => {
188
- const link = await PaymentLink.findByPk(req.params.id);
198
+ export async function startCheckoutSessionFromPaymentLink(id: string, req: Request, res: Response) {
199
+ const link = await PaymentLink.findByPk(id);
189
200
  if (!link) {
190
201
  res.status(400).json({ error: 'Payment link not found, please contact the source of the payment link.' });
191
202
  return;
@@ -203,6 +214,11 @@ router.post('/start/:id', user, async (req, res) => {
203
214
  raw.currency_id = link.currency_id || req.currency.id;
204
215
  raw.payment_link_id = link.id;
205
216
 
217
+ if (req.query.redirect) {
218
+ raw.success_url = req.query.redirect as string;
219
+ raw.cancel_url = req.query.redirect as string;
220
+ }
221
+
206
222
  try {
207
223
  let doc;
208
224
  if (req.query.preview === '1') {
@@ -210,8 +226,17 @@ router.post('/start/:id', user, async (req, res) => {
210
226
  if (doc) {
211
227
  await doc.update(omit(raw, ['metadata']));
212
228
  } else {
213
- raw.metadata = { preview: '1' };
229
+ raw.metadata = {
230
+ ...link.metadata,
231
+ passport: await checkPassportForPaymentLink(link),
232
+ preview: '1',
233
+ };
214
234
  }
235
+ } else {
236
+ raw.metadata = {
237
+ ...link.metadata,
238
+ passport: await checkPassportForPaymentLink(link),
239
+ };
215
240
  }
216
241
 
217
242
  if (!doc) {
@@ -230,6 +255,11 @@ router.post('/start/:id', user, async (req, res) => {
230
255
  console.error(err);
231
256
  res.status(500).json({ error: err.message });
232
257
  }
258
+ }
259
+
260
+ // start checkout session from payment link
261
+ router.post('/start/:id', user, async (req, res) => {
262
+ await startCheckoutSessionFromPaymentLink(req.params.id as string, req, res);
233
263
  });
234
264
 
235
265
  // for Node.js SDK
@@ -385,7 +415,7 @@ router.put('/:id/submit', user, async (req, res) => {
385
415
  amount_received: '0',
386
416
  amount_capturable: checkoutSession.amount_total,
387
417
  customer_id: customer.id,
388
- description: '',
418
+ description: checkoutSession.payment_intent_data?.description || '',
389
419
  currency_id: paymentCurrency.id,
390
420
  payment_method_id: paymentMethod.id,
391
421
  status: 'requires_payment_method',
@@ -393,10 +423,11 @@ router.put('/:id/submit', user, async (req, res) => {
393
423
  confirmation_method: 'automatic',
394
424
  payment_method_types: checkoutSession.payment_method_types,
395
425
  receipt_email: customer.email,
396
- statement_descriptor: getStatementDescriptor(lineItems),
426
+ statement_descriptor:
427
+ checkoutSession.payment_intent_data?.statement_descriptor || getStatementDescriptor(lineItems),
397
428
  statement_descriptor_suffix: '',
398
429
  setup_future_usage: 'on_session',
399
- metadata: {},
430
+ metadata: checkoutSession.payment_intent_data?.metadata || checkoutSession.metadata,
400
431
  });
401
432
 
402
433
  // lock prices used by this payment
@@ -440,7 +471,7 @@ router.put('/:id/submit', user, async (req, res) => {
440
471
  payment_method_types: checkoutSession.payment_method_types,
441
472
  flow_directions: ['inbound', 'outbound'],
442
473
  usage: 'off_session',
443
- metadata: {},
474
+ metadata: checkoutSession.metadata,
444
475
  });
445
476
 
446
477
  // persist setup intent id
@@ -486,8 +517,7 @@ router.put('/:id/submit', user, async (req, res) => {
486
517
  default_payment_method_id: paymentMethod.id,
487
518
  cancel_at_period_end: false,
488
519
  collection_method: 'charge_automatically',
489
- // FIXME: support discount
490
- metadata: {},
520
+ metadata: checkoutSession.metadata as any,
491
521
  });
492
522
 
493
523
  // create subscription items
@@ -501,7 +531,7 @@ router.put('/:id/submit', user, async (req, res) => {
501
531
  subscription_id: subscription.id,
502
532
  price_id: x.price_id,
503
533
  quantity: x.quantity,
504
- metadata: {},
534
+ metadata: checkoutSession.metadata as any,
505
535
  })
506
536
  )
507
537
  );
@@ -5,7 +5,6 @@ import { invoiceQueue } from '../../jobs/invoice';
5
5
  import { paymentQueue } from '../../jobs/payment';
6
6
  import type { CallbackArgs } from '../../libs/auth';
7
7
  import { wallet } from '../../libs/auth';
8
- import { getClient } from '../../libs/chain/arcblock';
9
8
  import dayjs from '../../libs/dayjs';
10
9
  import { ensureInvoiceForCollect, getAuthPrincipalClaim } from './shared';
11
10
 
@@ -61,7 +60,7 @@ export default {
61
60
 
62
61
  if (paymentMethod.type === 'arcblock') {
63
62
  await paymentIntent.update({ status: 'processing' });
64
- const client = getClient(paymentMethod.settings.arcblock?.api_host as string);
63
+ const client = paymentMethod.getOcapClient();
65
64
  const claim = claims.find((x) => x.type === 'prepareTx');
66
65
 
67
66
  const tx: Partial<Transaction> = client.decodeTx(claim.finalTx);
@@ -3,7 +3,6 @@ import { fromAddress } from '@ocap/wallet';
3
3
 
4
4
  import type { CallbackArgs } from '../../libs/auth';
5
5
  import { wallet } from '../../libs/auth';
6
- import { getClient } from '../../libs/chain/arcblock';
7
6
  import dayjs from '../../libs/dayjs';
8
7
  import { ensureInvoiceForCheckout, ensurePaymentIntent, getAuthPrincipalClaim } from './shared';
9
8
 
@@ -69,7 +68,7 @@ export default {
69
68
 
70
69
  if (paymentMethod.type === 'arcblock') {
71
70
  await paymentIntent.update({ status: 'processing' });
72
- const client = getClient(paymentMethod.settings.arcblock?.api_host as string);
71
+ const client = paymentMethod.getOcapClient();
73
72
  const claim = claims.find((x) => x.type === 'prepareTx');
74
73
 
75
74
  const tx: Partial<Transaction> = client.decodeTx(claim.finalTx);
@@ -6,7 +6,6 @@ import { fromPublicKey } from '@ocap/wallet';
6
6
  import { subscriptionQueue } from '../../jobs/subscription';
7
7
  import type { CallbackArgs } from '../../libs/auth';
8
8
  import { wallet } from '../../libs/auth';
9
- import { getClient } from '../../libs/chain/arcblock';
10
9
  import { ensureSetupIntent, getAuthPrincipalClaim } from './shared';
11
10
 
12
11
  export default {
@@ -78,7 +77,7 @@ export default {
78
77
  },
79
78
  });
80
79
 
81
- const client = getClient(paymentMethod.settings.arcblock?.api_host as string);
80
+ const client = paymentMethod.getOcapClient();
82
81
  const claim = claims.find((x) => x.type === 'signature');
83
82
 
84
83
  // execute the delegate tx
@@ -235,7 +235,10 @@ export async function ensureInvoiceForCheckout({
235
235
  const invoice = await Invoice.create({
236
236
  livemode: checkoutSession.livemode,
237
237
  number: await customer.getInvoiceNumber(),
238
- description: paymentIntent?.description || 'Subscription creation',
238
+ description:
239
+ checkoutSession.invoice_creation?.invoice_data?.description ||
240
+ paymentIntent?.description ||
241
+ 'Subscription creation',
239
242
  statement_descriptor: paymentIntent?.statement_descriptor || getStatementDescriptor(checkoutSession.line_items),
240
243
  period_start: subscription?.current_period_start ?? 0,
241
244
  period_end: subscription?.current_period_end ?? 0,
@@ -270,7 +273,7 @@ export async function ensureInvoiceForCheckout({
270
273
  attempted: false,
271
274
  // next_payment_attempt: undefined,
272
275
 
273
- custom_fields: [],
276
+ custom_fields: checkoutSession.invoice_creation?.invoice_data?.custom_fields || [],
274
277
  customer_address: customer.address,
275
278
  customer_email: customer.email,
276
279
  customer_name: customer.name,
@@ -290,7 +293,8 @@ export async function ensureInvoiceForCheckout({
290
293
 
291
294
  account_country: '',
292
295
  account_name: '',
293
- metadata: {},
296
+ footer: checkoutSession.invoice_creation?.invoice_data?.footer || '',
297
+ metadata: checkoutSession.invoice_creation?.invoice_data?.metadata || {},
294
298
  });
295
299
  logger.info(`Invoice created for checkoutSession ${checkoutSession.id}: ${invoice.id}`);
296
300
 
@@ -8,7 +8,6 @@ import { invoiceQueue } from '../../jobs/invoice';
8
8
  import { subscriptionQueue } from '../../jobs/subscription';
9
9
  import type { CallbackArgs } from '../../libs/auth';
10
10
  import { wallet } from '../../libs/auth';
11
- import { getClient } from '../../libs/chain/arcblock';
12
11
  import { ensureInvoiceForCheckout, ensurePaymentIntent, getAuthPrincipalClaim } from './shared';
13
12
 
14
13
  export default {
@@ -32,7 +31,7 @@ export default {
32
31
 
33
32
  if (paymentMethod.type === 'arcblock') {
34
33
  if (checkoutSession.amount_total > '0') {
35
- const client = getClient(paymentMethod.settings.arcblock?.api_host as string);
34
+ const client = paymentMethod.getOcapClient();
36
35
  const result = await client.getAccountTokens({ address: userDid, token: paymentCurrency.contract });
37
36
  const balance = result.tokens[0]?.balance || '0';
38
37
  if (new BN(balance).lt(new BN(checkoutSession.amount_total))) {
@@ -96,7 +95,7 @@ export default {
96
95
 
97
96
  const { invoice } = await ensureInvoiceForCheckout({ checkoutSession, customer, subscription });
98
97
 
99
- const client = getClient(paymentMethod.settings.arcblock?.api_host as string);
98
+ const client = paymentMethod.getOcapClient();
100
99
  const claim = claims.find((x) => x.type === 'signature');
101
100
 
102
101
  // execute the delegate tx
@@ -6,6 +6,7 @@ import customers from './customers';
6
6
  import events from './events';
7
7
  import stripe from './integrations/stripe';
8
8
  import invoices from './invoices';
9
+ import passports from './passports';
9
10
  import paymentCurrencies from './payment-currencies';
10
11
  import paymentIntents from './payment-intents';
11
12
  import paymentLinks from './payment-links';
@@ -13,6 +14,7 @@ import paymentMethods from './payment-methods';
13
14
  import prices from './prices';
14
15
  import pricingTables from './pricing-table';
15
16
  import products from './products';
17
+ import redirect from './redirect';
16
18
  import settings from './settings';
17
19
  import subscriptionItems from './subscription-items';
18
20
  import subscriptions from './subscriptions';
@@ -46,6 +48,7 @@ router.use('/customers', customers);
46
48
  router.use('/events', events);
47
49
  router.use('/invoices', invoices);
48
50
  router.use('/integrations/stripe', stripe);
51
+ router.use('/passports', passports);
49
52
  router.use('/payment-intents', paymentIntents);
50
53
  router.use('/payment-links', paymentLinks);
51
54
  router.use('/payment-methods', paymentMethods);
@@ -53,6 +56,7 @@ router.use('/payment-currencies', paymentCurrencies);
53
56
  router.use('/prices', prices);
54
57
  router.use('/pricing-tables', pricingTables);
55
58
  router.use('/products', products);
59
+ router.use('/redirect', redirect);
56
60
  router.use('/settings', settings);
57
61
  router.use('/subscription-items', subscriptionItems);
58
62
  router.use('/subscriptions', subscriptions);
@@ -22,7 +22,7 @@ const verifyWebhookSig = async (req: Request, res: Response, next: NextFunction)
22
22
  return res.status(400).json({ error: 'No stripe payment method found' });
23
23
  }
24
24
 
25
- const stripe = method.getStripe();
25
+ const stripe = method.getStripeClient();
26
26
  const settings = PaymentMethod.decryptSettings(method.settings);
27
27
  const secret =
28
28
  process.env.BLOCKLET_MODE === 'development' && process.env.STRIPE_WEBHOOK_SECRET
@@ -0,0 +1,74 @@
1
+ import { Router } from 'express';
2
+ import merge from 'lodash/merge';
3
+
4
+ import { blocklet } from '../libs/auth';
5
+ import { authenticate } from '../libs/security';
6
+ import { PaymentLink, PricingTable, Product } from '../store/models';
7
+
8
+ const router = Router();
9
+ const auth = authenticate<any>({ component: false, roles: ['owner', 'admin'] });
10
+
11
+ router.get('/', auth, async (_, res) => {
12
+ const result = await blocklet.getRoles();
13
+ res.json(result.roles);
14
+ });
15
+
16
+ const updateRoleExtra = async (name: string, updates: any) => {
17
+ const { role } = await blocklet.getRole(name);
18
+ if (!role) {
19
+ throw new Error(`passport ${name} not found`);
20
+ }
21
+ const result = await blocklet.updateRole(name, { extra: JSON.stringify(merge(role.extra, updates)) });
22
+ return result.role;
23
+ };
24
+
25
+ router.put('/assign', auth, async (req, res) => {
26
+ const { name, id } = req.body;
27
+
28
+ if (!id) {
29
+ return res.status(400).json({ message: 'payment entry or product id is required' });
30
+ }
31
+ if (!name) {
32
+ return res.status(400).json({ message: 'passport name is required' });
33
+ }
34
+
35
+ if (id.startsWith('plink_')) {
36
+ const doc = await PaymentLink.findByPk(id);
37
+ if (!doc?.active) {
38
+ return res.status(400).json({ message: 'payment link is not active' });
39
+ }
40
+
41
+ const result = await updateRoleExtra(name, { acquire: { pay: id } });
42
+ return res.json(result);
43
+ }
44
+
45
+ if (id.startsWith('prctbl_')) {
46
+ const doc = await PricingTable.findByPk(id);
47
+ if (!doc?.active) {
48
+ return res.status(400).json({ message: 'pricing table is not active' });
49
+ }
50
+
51
+ const result = await updateRoleExtra(name, { acquire: { pay: id } });
52
+ return res.json(result);
53
+ }
54
+
55
+ if (id.startsWith('prod_')) {
56
+ const doc = await Product.findByPk(id);
57
+ if (!doc?.active) {
58
+ return res.status(400).json({ message: 'product is not active' });
59
+ }
60
+
61
+ await doc.update({ metadata: { ...doc.metadata, passport: name } });
62
+ const result = await updateRoleExtra(name, { payment: { product: id } });
63
+ return res.json(result);
64
+ }
65
+
66
+ return res.status(400).json({ message: 'pay link is not support' });
67
+ });
68
+
69
+ router.delete('/assign/:name', auth, async (req, res) => {
70
+ const result = await updateRoleExtra(req.params.name as string, { payment: { product: '' }, acquire: { pay: '' } });
71
+ return res.json(result);
72
+ });
73
+
74
+ export default router;
@@ -1,6 +1,5 @@
1
1
  import { Router } from 'express';
2
2
  import Joi from 'joi';
3
- // import isEmpty from 'lodash/isEmpty';
4
3
  import pick from 'lodash/pick';
5
4
  import { Op, WhereOptions } from 'sequelize';
6
5
 
@@ -40,6 +39,10 @@ const formatBeforeSave = (payload: any) => {
40
39
  description: '',
41
40
  trial_period_days: 0,
42
41
  },
42
+ nft_mint_settings: {
43
+ enabled: false,
44
+ factory: '',
45
+ },
43
46
  submit_type: 'pay',
44
47
  },
45
48
  pick(payload, [
@@ -56,6 +59,7 @@ const formatBeforeSave = (payload: any) => {
56
59
  'billing_address_collection',
57
60
  'submit_type',
58
61
  'subscription_data',
62
+ 'nft_mint_settings',
59
63
  'metadata',
60
64
  ])
61
65
  );
@@ -72,6 +76,12 @@ const formatBeforeSave = (payload: any) => {
72
76
  raw.subscription_data = null;
73
77
  }
74
78
 
79
+ if (raw.nft_mint_settings?.enabled) {
80
+ if (!raw.nft_mint_settings?.factory) {
81
+ throw new Error('factory is required when nft mint is enabled');
82
+ }
83
+ }
84
+
75
85
  raw.line_items?.forEach((x) => {
76
86
  if (x.adjustable_quantity?.enabled) {
77
87
  x.adjustable_quantity.minimum = Number(x.adjustable_quantity?.minimum);
@@ -192,7 +202,7 @@ router.put('/:id', auth, async (req, res) => {
192
202
  // return res.status(403).json({ error: 'payment link locked' });
193
203
  // }
194
204
 
195
- await doc.update(formatBeforeSave(req.body));
205
+ await doc.update(formatBeforeSave(Object.assign({}, doc.dataValues, req.body)));
196
206
 
197
207
  res.json(doc);
198
208
  });
@@ -5,6 +5,7 @@ import pick from 'lodash/pick';
5
5
  import uniq from 'lodash/uniq';
6
6
  import { Op, WhereOptions } from 'sequelize';
7
7
 
8
+ import { checkPassportForPricingTable } from '../integrations/blocklet/passport';
8
9
  import { authenticate } from '../libs/security';
9
10
  import { isLineItemCurrencyAligned } from '../libs/session';
10
11
  import { formatMetadata } from '../libs/util';
@@ -62,6 +63,10 @@ const formatPricingTable = (payload: any) => {
62
63
  description: '',
63
64
  trial_period_days: 0,
64
65
  },
66
+ nft_mint_settings: {
67
+ enabled: false,
68
+ factory: '',
69
+ },
65
70
  submit_type: 'auto',
66
71
  },
67
72
  pick(x, [
@@ -77,6 +82,7 @@ const formatPricingTable = (payload: any) => {
77
82
  'phone_number_collection',
78
83
  'billing_address_collection',
79
84
  'submit_type',
85
+ 'nft_mint_settings',
80
86
  'subscription_data',
81
87
  ])
82
88
  );
@@ -230,8 +236,7 @@ router.put('/:id', auth, async (req, res) => {
230
236
  // return res.status(403).json({ error: 'pricing table locked' });
231
237
  // }
232
238
 
233
- // FIXME: should only allow update some fields
234
- await doc.update(formatPricingTable(req.body));
239
+ await doc.update(formatPricingTable(Object.assign({}, doc.dataValues, req.body)));
235
240
 
236
241
  res.json(doc);
237
242
  });
@@ -294,6 +299,7 @@ router.post('/stash', auth, async (req, res) => {
294
299
  }
295
300
  });
296
301
 
302
+ // start checkout session for pricing table
297
303
  // eslint-disable-next-line consistent-return
298
304
  router.post('/:id/checkout/:priceId', async (req, res) => {
299
305
  const doc = await PricingTable.findByPk(req.params.id);
@@ -324,10 +330,13 @@ router.post('/:id/checkout/:priceId', async (req, res) => {
324
330
  'phone_number_collection',
325
331
  'billing_address_collection',
326
332
  'submit_type',
333
+ 'nft_mint_settings',
327
334
  'subscription_data',
328
335
  ]),
329
336
  metadata: {
330
- pricing_table: doc.id,
337
+ ...doc.metadata,
338
+ passport: await checkPassportForPricingTable(doc),
339
+ pricing_table_id: doc.id,
331
340
  },
332
341
  });
333
342
 
@@ -335,6 +344,11 @@ router.post('/:id/checkout/:priceId', async (req, res) => {
335
344
  raw.created_via = 'portal';
336
345
  raw.currency_id = req.currency.id;
337
346
 
347
+ if (req.query.redirect) {
348
+ raw.success_url = req.query.redirect as string;
349
+ raw.cancel_url = req.query.redirect as string;
350
+ }
351
+
338
352
  const session = await CheckoutSession.create(raw as any);
339
353
  res.json({ ...session.toJSON(), url: getUrl(`/checkout/pay/${session.id}`) });
340
354
  });
@@ -2,7 +2,7 @@ import { fromTokenToUnit } from '@ocap/util';
2
2
  import { Router } from 'express';
3
3
  import Joi from 'joi';
4
4
  import pick from 'lodash/pick';
5
- import { Op, WhereOptions } from 'sequelize';
5
+ import type { WhereOptions } from 'sequelize';
6
6
 
7
7
  import { authenticate } from '../libs/security';
8
8
  import { formatMetadata } from '../libs/util';
@@ -114,10 +114,10 @@ router.get('/', auth, async (req, res) => {
114
114
  where.livemode = livemode;
115
115
  }
116
116
  if (name) {
117
- where.name = { [Op.like]: `%${name}%` };
117
+ where.name = name;
118
118
  }
119
119
  if (description) {
120
- where.description = { [Op.like]: `%${description}%` };
120
+ where.description = description;
121
121
  }
122
122
 
123
123
  Object.keys(query)
@@ -0,0 +1,18 @@
1
+ import { getUrl } from '@blocklet/sdk/lib/component';
2
+ import { Router } from 'express';
3
+
4
+ const router = Router();
5
+
6
+ router.get('/checkout/:entryId', (req, res) => {
7
+ const { entryId } = req.params;
8
+ if (entryId.startsWith('plink_')) {
9
+ return res.redirect(getUrl(`/checkout/pay/${entryId}?redirect=${req.query.redirect || ''}`));
10
+ }
11
+ if (entryId.startsWith('prctbl_')) {
12
+ return res.redirect(getUrl(`/checkout/pricing-table/${entryId}?redirect=${req.query.redirect || ''}`));
13
+ }
14
+
15
+ return res.redirect(getUrl('/'));
16
+ });
17
+
18
+ export default router;
@@ -34,7 +34,7 @@ const updateStripSubscription = async (doc: Subscription, updates: any) => {
34
34
  if (doc.payment_details?.stripe?.subscription_id) {
35
35
  const method = await PaymentMethod.findByPk(doc.default_payment_method_id);
36
36
  if (method && method.type === 'stripe') {
37
- const client = method.getStripe();
37
+ const client = method.getStripeClient();
38
38
  await client.subscriptions.update(doc.payment_details.stripe.subscription_id, updates);
39
39
  }
40
40
  }
@@ -153,9 +153,6 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
153
153
  if (doc.status === 'canceled') {
154
154
  return res.status(400).json({ error: 'Subscription already canceled' });
155
155
  }
156
- if (doc.cancel_at) {
157
- return res.status(400).json({ error: 'subscription scheduled to canceled' });
158
- }
159
156
 
160
157
  const { at = 'current_period_end', time, feedback = 'other', comment = '' } = req.body;
161
158
  if (at === 'custom' && dayjs(time).unix() < dayjs().unix()) {
@@ -190,7 +187,7 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
190
187
  if (doc.payment_details?.stripe?.subscription_id) {
191
188
  const method = await PaymentMethod.findByPk(doc.default_payment_method_id);
192
189
  if (method && method.type === 'stripe') {
193
- const client = method.getStripe();
190
+ const client = method.getStripeClient();
194
191
  if (updates.cancel_at_period_end) {
195
192
  await client.subscriptions.update(doc.payment_details.stripe.subscription_id, {
196
193
  cancel_at_period_end: updates.cancel_at_period_end,
@@ -0,0 +1,22 @@
1
+ import { DataTypes } from 'sequelize';
2
+
3
+ import type { Migration } from '../migrate';
4
+
5
+ export const up: Migration = async ({ context }) => {
6
+ await context.addColumn('payment_links', 'nft_mint_settings', { type: DataTypes.JSON, allowNull: true });
7
+ await context.addColumn('checkout_sessions', 'payment_intent_data', { type: DataTypes.JSON, allowNull: true });
8
+ await context.addColumn('checkout_sessions', 'nft_mint_settings', { type: DataTypes.JSON, allowNull: true });
9
+ await context.addColumn('checkout_sessions', 'nft_mint_details', { type: DataTypes.JSON, allowNull: true });
10
+ await context.addColumn('checkout_sessions', 'nft_mint_status', {
11
+ type: DataTypes.ENUM('disabled', 'pending', 'minted', 'error'),
12
+ allowNull: false,
13
+ });
14
+ };
15
+
16
+ export const down: Migration = async ({ context }) => {
17
+ await context.removeColumn('payment_links', 'nft_mint_settings');
18
+ await context.removeColumn('checkout_sessions', 'payment_intent_data');
19
+ await context.removeColumn('checkout_sessions', 'nft_mint_settings');
20
+ await context.removeColumn('checkout_sessions', 'nft_mint_details');
21
+ await context.removeColumn('checkout_sessions', 'nft_mint_status');
22
+ };