payment-kit 1.13.49 → 1.13.51

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.
@@ -1,10 +1,11 @@
1
+ import { isEthereumDid, isValid } from '@arcblock/did';
1
2
  // import pick from 'lodash/pick';
2
3
  import { formatFactoryState, preMintFromFactory } from '@ocap/asset';
3
4
  import merge from 'lodash/merge';
4
5
 
5
6
  import { wallet } from '../../libs/auth';
6
7
  import logger from '../../libs/logger';
7
- import { CheckoutSession, PaymentCurrency, PaymentIntent, PaymentMethod, Subscription } from '../../store/models';
8
+ import { CheckoutSession, PaymentIntent, PaymentMethod, Subscription } from '../../store/models';
8
9
  import { sendNftNotification } from '../blocklet/notification';
9
10
 
10
11
  export async function mintNftForCheckoutSession(id: string) {
@@ -34,13 +35,23 @@ export async function mintNftForCheckoutSession(id: string) {
34
35
  return;
35
36
  }
36
37
 
38
+ const { factory, inputs } = checkoutSession.nft_mint_settings as any;
39
+ if (isValid(factory) === false) {
40
+ logger.warn('checkoutSession nft mint settings invalid', { id, factory });
41
+ return;
42
+ }
43
+
44
+ // TODO: we only support minting from arcblock chain now
45
+ if (isEthereumDid(factory)) {
46
+ logger.warn('nft mint not supported for factory', { id, factory });
47
+ return;
48
+ }
49
+
37
50
  // TODO: we may need retry here when the chain is temporarily inaccessible
38
- const currency = await PaymentCurrency.findByPk(checkoutSession.currency_id);
39
- const method = await PaymentMethod.findByPk(currency?.payment_method_id);
40
- if (method?.type === 'arcblock') {
51
+ const method = await PaymentMethod.findOne({ where: { livemode: checkoutSession.livemode, type: 'arcblock' } });
52
+ if (method) {
41
53
  const client = method.getOcapClient();
42
54
  const nftOwner = checkoutSession.customer_did;
43
- const { factory, inputs } = checkoutSession.nft_mint_settings as any;
44
55
 
45
56
  const [{ state: factoryState }, { state: appState }] = await Promise.all([
46
57
  client.getFactoryState({ address: factory }),
@@ -118,9 +129,5 @@ export async function mintNftForCheckoutSession(id: string) {
118
129
  factoryState.name
119
130
  );
120
131
  logger.info('nft sent for checkoutSession', { id, nftOwner });
121
-
122
- return;
123
132
  }
124
-
125
- logger.warn('nft mint not supported for payment method', { id, type: method?.type });
126
133
  }
@@ -13,6 +13,7 @@ import { handleStripeInvoiceCreated } from './invoice';
13
13
  export async function handleStripePaymentSucceed(paymentIntent: PaymentIntent, event?: TEventExpanded) {
14
14
  await paymentIntent.update({
15
15
  status: 'succeeded',
16
+ last_payment_error: null,
16
17
  amount_received: paymentIntent.amount,
17
18
  payment_details: merge(
18
19
  paymentIntent.metadata,
@@ -37,8 +38,6 @@ export async function syncStripPayment(paymentIntent: PaymentIntent) {
37
38
  const client = await method.getStripeClient();
38
39
  const stripeIntent = await client.paymentIntents.retrieve(paymentIntent.metadata.stripe_id);
39
40
  if (stripeIntent) {
40
- const justSucceed = stripeIntent.status === 'succeeded' && paymentIntent.status !== 'succeeded';
41
-
42
41
  // @ts-ignore
43
42
  await paymentIntent.update({
44
43
  amount: String(stripeIntent.amount),
@@ -49,9 +48,7 @@ export async function syncStripPayment(paymentIntent: PaymentIntent) {
49
48
  });
50
49
  logger.info('stripe payment intent synced', { locale: paymentIntent.id, remote: stripeIntent.id });
51
50
 
52
- if (justSucceed) {
53
- await handlePaymentSucceed(paymentIntent);
54
- }
51
+ await handlePaymentSucceed(paymentIntent);
55
52
  }
56
53
  }
57
54
 
@@ -106,6 +103,9 @@ export async function handleStripePaymentCreated(event: TEventExpanded, client:
106
103
  }
107
104
 
108
105
  logger.info('stripe payment intent mirrored', { locale: paymentIntent.id, remote: stripeIntent.id });
106
+ if (stripeIntent.status === 'succeeded') {
107
+ await handlePaymentSucceed(paymentIntent);
108
+ }
109
109
  }
110
110
  }
111
111
 
@@ -2,7 +2,7 @@ import { wallet } from '../libs/auth';
2
2
  import dayjs from '../libs/dayjs';
3
3
  import CustomError from '../libs/error';
4
4
  import logger from '../libs/logger';
5
- import { isDelegationSufficientForPayment } from '../libs/payment';
5
+ import { getGasPayerExtra, isDelegationSufficientForPayment } from '../libs/payment';
6
6
  import createQueue from '../libs/queue';
7
7
  import { MAX_RETRY_COUNT, getNextRetry } from '../libs/util';
8
8
  import { CheckoutSession } from '../store/models/checkout-session';
@@ -37,17 +37,19 @@ export const handlePaymentSucceed = async (paymentIntent: PaymentIntent) => {
37
37
  return;
38
38
  }
39
39
 
40
- await invoice.update({
41
- paid: true,
42
- status: 'paid',
43
- amount_due: '0',
44
- amount_paid: paymentIntent.amount,
45
- amount_remaining: '0',
46
- attempt_count: invoice.attempt_count + 1,
47
- attempted: true,
48
- status_transitions: { ...invoice.status_transitions, paid_at: dayjs().unix() },
49
- });
50
- logger.info(`Invoice ${invoice.id} updated on payment done: ${paymentIntent.id}`);
40
+ if (invoice.status !== 'paid') {
41
+ await invoice.update({
42
+ paid: true,
43
+ status: 'paid',
44
+ amount_due: '0',
45
+ amount_paid: paymentIntent.amount,
46
+ amount_remaining: '0',
47
+ attempt_count: invoice.attempt_count + 1,
48
+ attempted: true,
49
+ status_transitions: { ...invoice.status_transitions, paid_at: dayjs().unix() },
50
+ });
51
+ logger.info(`Invoice ${invoice.id} updated on payment done: ${paymentIntent.id}`);
52
+ }
51
53
 
52
54
  if (invoice.subscription_id) {
53
55
  const subscription = await Subscription.findByPk(invoice.subscription_id);
@@ -56,6 +58,7 @@ export const handlePaymentSucceed = async (paymentIntent: PaymentIntent) => {
56
58
  await subscription.update({ status: subscription.trail_end ? 'trialing' : 'active' });
57
59
  logger.info(`Subscription ${subscription.id} updated on payment done ${invoice.id}`);
58
60
  } else {
61
+ // FIXME: possible error here
59
62
  await subscription.update({ status: 'active' });
60
63
  logger.info(`Subscription ${subscription.id} moved to active after payment done ${paymentIntent.id}`);
61
64
  }
@@ -128,12 +131,14 @@ export const handlePayment = async (job: PaymentJob) => {
128
131
  amount: paymentIntent.amount,
129
132
  });
130
133
  if (result.sufficient === false) {
134
+ logger.error('PaymentIntent capture aborted on preCheck', { id: paymentIntent.id, result });
135
+ // FIXME: send email to customer, pause subscription
131
136
  throw new CustomError(result.reason, 'payer balance or delegation not sufficient for this payment');
132
137
  }
133
138
 
134
139
  // do the capture
135
140
  await paymentIntent.update({ status: 'processing' });
136
- const txHash = await client.sendTransferV2Tx({
141
+ const signed = await client.signTransferV2Tx({
137
142
  tx: {
138
143
  itx: {
139
144
  to: wallet.address,
@@ -150,14 +155,20 @@ export const handlePayment = async (job: PaymentJob) => {
150
155
  },
151
156
  },
152
157
  },
153
- delegator: payer,
154
158
  wallet,
159
+ delegator: payer,
155
160
  });
161
+ // @ts-ignore
162
+ const { buffer } = await client.encodeTransferV2Tx({ tx: signed });
163
+ // @ts-ignore
164
+ const txHash = await client.sendTransferV2Tx({ tx: signed, wallet, delegator: payer }, getGasPayerExtra(buffer));
165
+
156
166
  logger.info(`PaymentIntent capture done: ${paymentIntent.id} with tx ${txHash}`);
157
167
 
158
168
  await paymentIntent.update({
159
169
  status: 'succeeded',
160
170
  amount_received: paymentIntent.amount,
171
+ last_payment_error: null,
161
172
  payment_details: {
162
173
  arcblock: {
163
174
  tx_hash: txHash,
@@ -1,6 +1,8 @@
1
1
  import { toDelegateAddress } from '@arcblock/did-util';
2
+ import { sign } from '@arcblock/jwt';
2
3
  import { getWalletDid } from '@blocklet/sdk/lib/did';
3
4
  import type { DelegateState } from '@ocap/client';
5
+ import { toTxHash } from '@ocap/mcrypto';
4
6
  import { BN } from '@ocap/util';
5
7
 
6
8
  import type { TPaymentCurrency } from '../store/models/payment-currency';
@@ -42,7 +44,7 @@ export async function isDelegationSufficientForPayment(args: {
42
44
  if (!token) {
43
45
  return { sufficient: false, reason: 'NO_TOKEN' };
44
46
  }
45
- if (new BN(token.balance).lte(new BN(amount))) {
47
+ if (new BN(token.balance).lt(new BN(amount))) {
46
48
  return { sufficient: false, reason: 'NO_ENOUGH_TOKEN' };
47
49
  }
48
50
 
@@ -55,3 +57,13 @@ export async function isDelegationSufficientForPayment(args: {
55
57
 
56
58
  throw new Error(`Payment method ${paymentMethod.type} not supported`);
57
59
  }
60
+
61
+ export function getGasPayerExtra(txBuffer: Buffer) {
62
+ const txHash = toTxHash(txBuffer);
63
+ return {
64
+ headers: {
65
+ 'x-gas-payer-sig': sign(wallet.address, wallet.secretKey, { txHash }),
66
+ 'x-gas-payer-pk': wallet.publicKey,
67
+ },
68
+ };
69
+ }
@@ -169,7 +169,9 @@ export function getSubscriptionCycleAmount(items: TLineItemExpanded[], currency:
169
169
  export function expandLineItems(items: any[], products: Product[], prices: Price[]) {
170
170
  items.forEach((item) => {
171
171
  item.price = prices.find((x) => x.id === item.price_id);
172
- item.price.product = products.find((x) => x.id === item.price.product_id);
172
+ if (item.price) {
173
+ item.price.product = products.find((x) => x.id === item.price.product_id);
174
+ }
173
175
  });
174
176
 
175
177
  return items;
@@ -1,13 +1,16 @@
1
1
  /* eslint-disable consistent-return */
2
+ import { isValid } from '@arcblock/did';
2
3
  import { getUrl } from '@blocklet/sdk/lib/component';
3
4
  import userMiddleware from '@blocklet/sdk/lib/middlewares/user';
4
5
  import { NextFunction, Request, Response, Router } from 'express';
6
+ import Joi from 'joi';
5
7
  import cloneDeep from 'lodash/cloneDeep';
6
8
  import merge from 'lodash/merge';
7
9
  import omit from 'lodash/omit';
8
10
  import pick from 'lodash/pick';
9
11
  import sortBy from 'lodash/sortBy';
10
12
  import uniq from 'lodash/uniq';
13
+ import type { WhereOptions } from 'sequelize';
11
14
 
12
15
  import { checkPassportForPaymentLink } from '../integrations/blocklet/passport';
13
16
  import { handleStripePaymentSucceed } from '../integrations/stripe/handlers/payment-intent';
@@ -22,6 +25,7 @@ import { isDelegationSufficientForPayment } from '../libs/payment';
22
25
  import { authenticate } from '../libs/security';
23
26
  import {
24
27
  canUpsell,
28
+ expandLineItems,
25
29
  getCheckoutAmount,
26
30
  getCheckoutMode,
27
31
  getFastCheckoutAmount,
@@ -506,11 +510,17 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
506
510
  return res.status(403).json({ code: 'PAYMENT_PROCESSING', error: 'Checkout session payment processing' });
507
511
  }
508
512
  paymentIntent = await paymentIntent.update({
513
+ status: 'requires_capture',
509
514
  amount: checkoutSession.amount_total,
510
515
  customer_id: customer.id,
511
516
  currency_id: paymentCurrency.id,
512
517
  payment_method_id: paymentMethod.id,
513
518
  receipt_email: customer.email,
519
+ last_payment_error: null,
520
+ });
521
+ logger.info('payment intent for checkout session reset', {
522
+ session: checkoutSession.id,
523
+ intent: paymentIntent.id,
514
524
  });
515
525
  } else {
516
526
  paymentIntent = await PaymentIntent.create({
@@ -547,7 +557,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
547
557
  }
548
558
 
549
559
  let setupIntent: SetupIntent | null = null;
550
- if (checkoutSession.mode === 'setup' || paymentMethod.type !== 'stripe') {
560
+ if (checkoutSession.mode === 'setup' && paymentMethod.type !== 'stripe') {
551
561
  if (checkoutSession.setup_intent_id) {
552
562
  setupIntent = await SetupIntent.findByPk(checkoutSession.setup_intent_id);
553
563
  }
@@ -563,16 +573,22 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
563
573
  return res.status(403).json({ code: 'SETUP_PROCESSING', error: 'Checkout session setup processing' });
564
574
  }
565
575
  await setupIntent.update({
576
+ status: 'requires_capture',
566
577
  customer_id: customer.id,
567
578
  currency_id: paymentCurrency.id,
568
579
  payment_method_id: paymentMethod.id,
580
+ last_setup_error: null,
581
+ });
582
+ logger.info('setup intent for checkout session reset', {
583
+ session: checkoutSession.id,
584
+ intent: setupIntent.id,
569
585
  });
570
586
  } else {
571
587
  // ensure payment intent
572
588
  setupIntent = await SetupIntent.create({
573
589
  livemode: !!checkoutSession.livemode,
574
590
  customer_id: customer.id,
575
- description: '',
591
+ description: checkoutSession.payment_intent_data?.description || '',
576
592
  currency_id: paymentCurrency.id,
577
593
  payment_method_id: paymentMethod.id,
578
594
  status: 'requires_payment_method',
@@ -698,20 +714,15 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
698
714
  });
699
715
  }
700
716
  if (checkoutSession.mode === 'payment' && paymentIntent) {
701
- await paymentIntent.update({ status: 'requires_capture' });
702
717
  const { invoice } = await ensureInvoiceForCheckout({ checkoutSession, customer, paymentIntent });
703
718
  if (invoice) {
704
719
  await invoice.update({ auto_advance: true, payment_settings: paymentSettings });
705
720
  invoiceQueue.push({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: false } });
706
721
  } else {
707
- const job = paymentQueue.push({
722
+ paymentQueue.push({
708
723
  id: paymentIntent.id,
709
724
  job: { paymentIntentId: paymentIntent.id, paymentSettings, retryOnError: false },
710
725
  });
711
- job.on('finished', async () => {
712
- await checkoutSession.update({ status: 'complete', payment_status: 'paid' });
713
- logger.info(`CheckoutSession ${checkoutSession.id} updated on payment done ${paymentIntent?.id}`);
714
- });
715
726
  }
716
727
  }
717
728
  if (checkoutSession.mode === 'setup' && setupIntent && subscription) {
@@ -950,4 +961,94 @@ router.delete('/:id/cross-sell', user, ensureCheckoutSessionOpen, async (req, re
950
961
  }
951
962
  });
952
963
 
964
+ const schema = Joi.object<{
965
+ page: number;
966
+ pageSize: number;
967
+ status?: string;
968
+ payment_status?: string;
969
+ nft_mint_status?: string;
970
+ customer_id?: string;
971
+ customer_did?: string;
972
+ payment_intent_id?: string;
973
+ subscription_id?: string;
974
+ livemode?: boolean;
975
+ }>({
976
+ page: Joi.number().integer().min(1).default(1),
977
+ pageSize: Joi.number().integer().min(1).max(100).default(20),
978
+ status: Joi.string().empty(''),
979
+ payment_status: Joi.string().empty(''),
980
+ nft_mint_status: Joi.string().empty(''),
981
+ customer_id: Joi.string().empty(''),
982
+ customer_did: Joi.string().empty(''),
983
+ payment_intent_id: Joi.string().empty(''),
984
+ subscription_id: Joi.string().empty(''),
985
+ livemode: Joi.boolean().empty(''),
986
+ });
987
+ router.get('/', auth, async (req, res) => {
988
+ const { page, pageSize, livemode, ...query } = await schema.validateAsync(req.query, {
989
+ stripUnknown: false,
990
+ allowUnknown: true,
991
+ });
992
+ const where: WhereOptions<CheckoutSession> = {};
993
+
994
+ ['status', 'payment_status', 'nft_mint_status'].forEach((key) => {
995
+ // @ts-ignore
996
+ if (query[key]) {
997
+ // @ts-ignore
998
+ where[key] = query[key].split(',').map((x: string) => x.trim()).filter(Boolean); // prettier-ignore
999
+ }
1000
+ });
1001
+ if (query.customer_id) {
1002
+ where.customer_id = query.customer_id;
1003
+ }
1004
+ if (query.payment_intent_id) {
1005
+ where.payment_intent_id = query.payment_intent_id;
1006
+ }
1007
+ if (query.subscription_id) {
1008
+ where.subscription_id = query.subscription_id;
1009
+ }
1010
+ if (query.customer_did && isValid(query.customer_did)) {
1011
+ const customer = await Customer.findOne({ where: { did: query.customer_did } });
1012
+ if (customer) {
1013
+ where.customer_id = customer.id;
1014
+ } else {
1015
+ res.json({ count: 0, list: [] });
1016
+ return;
1017
+ }
1018
+ }
1019
+ if (typeof livemode === 'boolean') {
1020
+ where.livemode = livemode;
1021
+ }
1022
+
1023
+ Object.keys(query)
1024
+ .filter((x) => x.startsWith('metadata.'))
1025
+ .forEach((key: string) => {
1026
+ // @ts-ignore
1027
+ where[key] = query[key];
1028
+ });
1029
+
1030
+ try {
1031
+ const { rows: list, count } = await CheckoutSession.findAndCountAll({
1032
+ where,
1033
+ order: [['created_at', 'DESC']],
1034
+ offset: (page - 1) * pageSize,
1035
+ limit: pageSize,
1036
+ include: [],
1037
+ });
1038
+
1039
+ const condition = { where: { livemode: !!req.livemode } };
1040
+ const products = (await Product.findAll(condition)).map((x) => x.toJSON());
1041
+ const prices = (await Price.findAll(condition)).map((x) => x.toJSON());
1042
+ const docs = list.map((x) => x.toJSON());
1043
+
1044
+ // @ts-ignore
1045
+ docs.forEach((x) => expandLineItems(x.line_items, products, prices));
1046
+
1047
+ res.json({ count, list: docs });
1048
+ } catch (err) {
1049
+ console.error(err);
1050
+ res.json({ count: 0, list: [] });
1051
+ }
1052
+ });
1053
+
953
1054
  export default router;
@@ -5,6 +5,7 @@ import { invoiceQueue } from '../../jobs/invoice';
5
5
  import { handlePaymentSucceed, paymentQueue } from '../../jobs/payment';
6
6
  import type { CallbackArgs } from '../../libs/auth';
7
7
  import { wallet } from '../../libs/auth';
8
+ import { getGasPayerExtra } from '../../libs/payment';
8
9
  import { getTxMetadata } from '../../libs/util';
9
10
  import { ensureInvoiceForCollect, getAuthPrincipalClaim } from './shared';
10
11
 
@@ -49,7 +50,7 @@ export default {
49
50
 
50
51
  throw new Error(`Payment method ${paymentMethod.type} not supported`);
51
52
  },
52
- onAuth: async ({ userDid, claims, request, extraParams }: CallbackArgs) => {
53
+ onAuth: async ({ userDid, claims, extraParams }: CallbackArgs) => {
53
54
  const { invoiceId } = extraParams;
54
55
  const { invoice, paymentIntent, paymentMethod } = await ensureInvoiceForCollect(invoiceId);
55
56
 
@@ -64,16 +65,19 @@ export default {
64
65
  tx.from = claim.from;
65
66
  }
66
67
 
68
+ // @ts-ignore
69
+ const { buffer } = await client.encodeTransferV3Tx({ tx });
67
70
  const txHash = await client.sendTransferV3Tx(
68
71
  // @ts-ignore
69
72
  { tx, wallet: fromAddress(userDid) },
70
- { headers: client.pickGasPayerHeaders(request) }
73
+ getGasPayerExtra(buffer)
71
74
  );
72
75
 
73
76
  await paymentIntent.update({
74
77
  status: 'succeeded',
75
78
  amount_received: invoice.amount_due,
76
79
  capture_method: 'manual',
80
+ last_payment_error: null,
77
81
  payment_details: {
78
82
  arcblock: {
79
83
  tx_hash: txHash,
@@ -4,6 +4,7 @@ import { fromAddress } from '@ocap/wallet';
4
4
  import { handlePaymentSucceed } from '../../jobs/payment';
5
5
  import type { CallbackArgs } from '../../libs/auth';
6
6
  import { wallet } from '../../libs/auth';
7
+ import { getGasPayerExtra } from '../../libs/payment';
7
8
  import { getTxMetadata } from '../../libs/util';
8
9
  import { ensureInvoiceForCheckout, ensurePaymentIntent, getAuthPrincipalClaim } from './shared';
9
10
 
@@ -50,7 +51,7 @@ export default {
50
51
 
51
52
  throw new Error(`Payment method ${paymentMethod.type} not supported`);
52
53
  },
53
- onAuth: async ({ userDid, claims, request, extraParams }: CallbackArgs) => {
54
+ onAuth: async ({ userDid, claims, extraParams }: CallbackArgs) => {
54
55
  const { checkoutSessionId } = extraParams;
55
56
  const { checkoutSession, customer, paymentIntent, paymentMethod } = await ensurePaymentIntent(
56
57
  checkoutSessionId,
@@ -73,15 +74,18 @@ export default {
73
74
  tx.from = claim.from;
74
75
  }
75
76
 
77
+ // @ts-ignore
78
+ const { buffer } = await client.encodeTransferV3Tx({ tx });
76
79
  const txHash = await client.sendTransferV3Tx(
77
80
  // @ts-ignore
78
81
  { tx, wallet: fromAddress(userDid) },
79
- { headers: client.pickGasPayerHeaders(request) }
82
+ getGasPayerExtra(buffer)
80
83
  );
81
84
 
82
85
  await paymentIntent.update({
83
86
  status: 'succeeded',
84
87
  amount_received: paymentIntent.amount,
88
+ last_payment_error: null,
85
89
  payment_details: {
86
90
  arcblock: {
87
91
  tx_hash: txHash,
@@ -6,6 +6,7 @@ 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 { getGasPayerExtra } from '../../libs/payment';
9
10
  import { getFastCheckoutAmount } from '../../libs/session';
10
11
  import { getTxMetadata } from '../../libs/util';
11
12
  import type { TLineItemExpanded } from '../../store/models';
@@ -56,7 +57,7 @@ export default {
56
57
  },
57
58
  nonce: checkoutSessionId,
58
59
  requirement: {
59
- tokens: [{ address: paymentCurrency.contract as string, value: amount }],
60
+ tokens: amount === '0' ? [] : [{ address: paymentCurrency.contract as string, value: amount }],
60
61
  },
61
62
  chainInfo: {
62
63
  host: paymentMethod.settings?.arcblock?.api_host as string,
@@ -68,7 +69,7 @@ export default {
68
69
 
69
70
  throw new Error(`Payment method ${paymentMethod.type} not supported`);
70
71
  },
71
- onAuth: async ({ userDid, userPk, claims, request, extraParams }: CallbackArgs) => {
72
+ onAuth: async ({ userDid, userPk, claims, extraParams }: CallbackArgs) => {
72
73
  const { checkoutSessionId } = extraParams;
73
74
  const { setupIntent, checkoutSession, paymentMethod, subscription } = await ensureSetupIntent(
74
75
  checkoutSessionId,
@@ -80,6 +81,7 @@ export default {
80
81
  }
81
82
 
82
83
  if (paymentMethod.type === 'arcblock') {
84
+ await setupIntent.update({ status: 'processing' });
83
85
  await subscription.update({
84
86
  payment_settings: {
85
87
  payment_method_types: ['arcblock'],
@@ -94,15 +96,20 @@ export default {
94
96
 
95
97
  // execute the delegate tx
96
98
  const tx: Partial<Transaction> = client.decodeTx(claim.origin);
99
+ tx.signature = claim.sig;
100
+
101
+ // @ts-ignore
102
+ const { buffer } = await client.encodeDelegateTx({ tx });
97
103
  const txHash = await client.sendDelegateTx(
98
104
  // @ts-ignore
99
- { tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid)), signature: claim.sig },
100
- { headers: client.pickGasPayerHeaders(request) }
105
+ { tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid)) },
106
+ getGasPayerExtra(buffer)
101
107
  );
102
108
 
103
109
  await setupIntent.update({
104
110
  status: 'succeeded',
105
111
  payment_method_types: ['arcblock'],
112
+ last_setup_error: null,
106
113
  payment_method_options: {
107
114
  arcblock: { payer: userDid },
108
115
  },
@@ -226,9 +226,20 @@ export async function ensureInvoiceForCheckout({
226
226
 
227
227
  // Do not create invoice if it's already created
228
228
  if (checkoutSession.invoice_id) {
229
- logger.warn(`Invoice already created for checkout session ${checkoutSession.id}: ${checkoutSession.invoice_id}`);
229
+ logger.info(`Invoice already created for checkout session ${checkoutSession.id}: ${checkoutSession.invoice_id}`);
230
+ const invoice = await Invoice.findByPk(checkoutSession.invoice_id);
231
+ if (invoice) {
232
+ await invoice.update({ status: 'open' });
233
+ logger.info(`Invoice status reset for checkout session ${checkoutSession.id}: ${checkoutSession.invoice_id}`);
234
+ if (invoice.payment_intent_id) {
235
+ await PaymentIntent.update({ status: 'requires_capture' }, { where: { id: invoice.payment_intent_id } });
236
+ logger.info(
237
+ `PaymentIntent status reset for checkout session ${checkoutSession.id}: ${invoice.payment_intent_id}`
238
+ );
239
+ }
240
+ }
230
241
  return {
231
- invoice: await Invoice.findByPk(checkoutSession.invoice_id),
242
+ invoice,
232
243
  items: await InvoiceItem.findAll({ where: { invoice_id: checkoutSession.invoice_id } }),
233
244
  };
234
245
  }
@@ -8,6 +8,7 @@ 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 { getGasPayerExtra } from '../../libs/payment';
11
12
  import { getFastCheckoutAmount } from '../../libs/session';
12
13
  import { getTxMetadata } from '../../libs/util';
13
14
  import type { TLineItemExpanded } from '../../store/models';
@@ -58,7 +59,7 @@ export default {
58
59
  },
59
60
  nonce: checkoutSessionId,
60
61
  requirement: {
61
- tokens: [{ address: paymentCurrency.contract as string, value: amount }],
62
+ tokens: amount === '0' ? [] : [{ address: paymentCurrency.contract as string, value: amount }],
62
63
  },
63
64
  chainInfo: {
64
65
  host: paymentMethod.settings?.arcblock?.api_host as string,
@@ -70,7 +71,7 @@ export default {
70
71
 
71
72
  throw new Error(`Payment method ${paymentMethod.type} not supported`);
72
73
  },
73
- onAuth: async ({ userDid, userPk, claims, request, extraParams }: CallbackArgs) => {
74
+ onAuth: async ({ userDid, userPk, claims, extraParams }: CallbackArgs) => {
74
75
  const { checkoutSessionId } = extraParams;
75
76
  const { checkoutSession, customer, paymentMethod, paymentCurrency, subscription } = await ensurePaymentIntent(
76
77
  checkoutSessionId,
@@ -109,10 +110,14 @@ export default {
109
110
 
110
111
  // execute the delegate tx
111
112
  const tx: Partial<Transaction> = client.decodeTx(claim.origin);
113
+ tx.signature = claim.sig;
114
+
115
+ // @ts-ignore
116
+ const { buffer } = await client.encodeDelegateTx({ tx });
112
117
  const txHash = await client.sendDelegateTx(
113
118
  // @ts-ignore
114
- { tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid)), signature: claim.sig },
115
- { headers: client.pickGasPayerHeaders(request) }
119
+ { tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid)) },
120
+ getGasPayerExtra(buffer)
116
121
  );
117
122
 
118
123
  await subscription.update({
@@ -118,7 +118,8 @@ router.get('/:id', authPortal, async (req, res) => {
118
118
  let subscription;
119
119
 
120
120
  if (doc) {
121
- if (doc.status !== 'succeeded' && doc.metadata?.stripe_id) {
121
+ const shouldSync = doc.status !== 'succeeded' || req.query.sync === '1';
122
+ if (doc.metadata?.stripe_id && shouldSync) {
122
123
  await syncStripPayment(doc);
123
124
  }
124
125
 
@@ -35,7 +35,7 @@ export class PaymentIntent extends Model<InferAttributes<PaymentIntent>, InferCr
35
35
  declare last_charge?: string;
36
36
 
37
37
  // The payment error encountered in the previous PaymentIntent confirmation
38
- declare last_payment_error?: PaymentError;
38
+ declare last_payment_error: PaymentError | null;
39
39
 
40
40
  declare metadata?: Record<string, any>;
41
41
 
@@ -22,7 +22,7 @@ export class SetupIntent extends Model<InferAttributes<SetupIntent>, InferCreati
22
22
  declare customer_id: string;
23
23
 
24
24
  // The setup error encountered in the previous SetupIntent confirmation
25
- declare last_setup_error?: PaymentError;
25
+ declare last_setup_error: PaymentError | null;
26
26
 
27
27
  declare metadata?: Record<string, any>;
28
28
 
@@ -272,7 +272,7 @@ export type PaymentDetails = {
272
272
  export type NftMintSettings = {
273
273
  enabled: boolean;
274
274
  behavior?: LiteralUnion<'per_customer' | 'per_checkout_session', string>;
275
- factory?: string;
275
+ factory?: string; // the factory address determines which chain to mint on
276
276
  inputs?: Record<string, string>;
277
277
  };
278
278
 
package/blocklet.yml CHANGED
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.13.49
17
+ version: 1.13.51
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.13.49",
3
+ "version": "1.13.51",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev",
6
6
  "eject": "vite eject",
@@ -42,13 +42,14 @@
42
42
  "dependencies": {
43
43
  "@arcblock/did": "^1.18.95",
44
44
  "@arcblock/did-auth-storage-nedb": "^1.7.1",
45
- "@arcblock/did-connect": "^2.8.7",
45
+ "@arcblock/did-connect": "^2.8.8",
46
46
  "@arcblock/did-util": "^1.18.95",
47
- "@arcblock/ux": "^2.8.7",
47
+ "@arcblock/jwt": "^1.18.95",
48
+ "@arcblock/ux": "^2.8.8",
48
49
  "@blocklet/logger": "1.16.17",
49
50
  "@blocklet/sdk": "1.16.17",
50
- "@blocklet/ui-react": "^2.8.7",
51
- "@blocklet/uploader": "^0.0.33",
51
+ "@blocklet/ui-react": "^2.8.8",
52
+ "@blocklet/uploader": "^0.0.34",
52
53
  "@mui/icons-material": "^5.14.16",
53
54
  "@mui/lab": "^5.0.0-alpha.151",
54
55
  "@mui/material": "^5.14.16",
@@ -103,7 +104,7 @@
103
104
  "@abtnode/types": "1.16.17",
104
105
  "@arcblock/eslint-config": "^0.2.4",
105
106
  "@arcblock/eslint-config-ts": "^0.2.4",
106
- "@did-pay/types": "1.13.49",
107
+ "@did-pay/types": "1.13.51",
107
108
  "@types/cookie-parser": "^1.4.5",
108
109
  "@types/cors": "^2.8.15",
109
110
  "@types/dotenv-flow": "^3.3.2",
@@ -140,5 +141,5 @@
140
141
  "parser": "typescript"
141
142
  }
142
143
  },
143
- "gitHead": "8fd11641c7ee8f092afc35eb9615c27d321d398c"
144
+ "gitHead": "cd8bc5e262b3f6e13f2d9983ade1b689ff43c725"
144
145
  }
@@ -6,7 +6,7 @@ import { joinURL } from 'ufo';
6
6
  const getTxLink = (method: TPaymentMethod, details: PaymentDetails) => {
7
7
  if (method.type === 'arcblock') {
8
8
  return {
9
- link: joinURL(method.settings.arcblock?.explorer_host as string, '/tx', details.arcblock?.tx_hash as string),
9
+ link: joinURL(method.settings.arcblock?.explorer_host as string, '/txs', details.arcblock?.tx_hash as string),
10
10
  text: details.arcblock?.tx_hash as string,
11
11
  };
12
12
  }
@@ -37,8 +37,16 @@ const getTxLink = (method: TPaymentMethod, details: PaymentDetails) => {
37
37
  return { text: 'N/A', link: '' };
38
38
  };
39
39
 
40
- export default function TxLink(props: { details: PaymentDetails; method: TPaymentMethod }) {
41
- if (!props.details) {
40
+ TxLink.defaultProps = {
41
+ mode: 'dashboard',
42
+ };
43
+
44
+ export default function TxLink(props: {
45
+ details: PaymentDetails;
46
+ method: TPaymentMethod;
47
+ mode?: 'customer' | 'dashboard';
48
+ }) {
49
+ if (!props.details || (props.mode === 'customer' && props.method.type === 'stripe')) {
42
50
  return (
43
51
  <Typography component="small" color="text.secondary">
44
52
  None
@@ -62,7 +62,7 @@ export default function CustomerInvoiceList({ customer_id }: Props) {
62
62
  const grouped = groupByDate(data.list);
63
63
 
64
64
  return (
65
- <Stack direction="column" gap={3} sx={{ mt: 1 }}>
65
+ <Stack direction="column" gap={1} sx={{ mt: 1 }}>
66
66
  {Object.entries(grouped).map(([date, invoices]) => (
67
67
  <Box key={date}>
68
68
  <Typography sx={{ fontWeight: 'bold', color: 'text.secondary', mt: 2, mb: 1 }}>{date}</Typography>
@@ -73,6 +73,7 @@ export default function CustomerInvoiceList({ customer_id }: Props) {
73
73
  xs: 'column',
74
74
  sm: 'row',
75
75
  }}
76
+ sx={{ my: 1 }}
76
77
  gap={{
77
78
  xs: 0.5,
78
79
  sm: 1.5,
@@ -111,7 +111,7 @@ export default function CustomerHome() {
111
111
  label={t('common.txHash')}
112
112
  value={
113
113
  data.paymentIntent?.payment_details ? (
114
- <TxLink details={data.paymentIntent.payment_details} method={data.paymentMethod} />
114
+ <TxLink details={data.paymentIntent.payment_details} method={data.paymentMethod} mode="customer" />
115
115
  ) : (
116
116
  ''
117
117
  )