payment-kit 1.13.26 → 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
package/api/src/index.ts CHANGED
@@ -9,7 +9,9 @@ import dotenv from 'dotenv-flow';
9
9
  import express, { ErrorRequestHandler, Request, Response } from 'express';
10
10
  import morgan from 'morgan';
11
11
 
12
+ import { ensureStakedForGas } from './integrations/blockchain/stake';
12
13
  import { ensureWebhookRegistered } from './integrations/stripe/setup';
14
+ import { startCheckoutSessionQueue } from './jobs/checkout-session';
13
15
  import { startEventQueue } from './jobs/event';
14
16
  import { startInvoiceQueue } from './jobs/invoice';
15
17
  import { startPaymentQueue } from './jobs/payment';
@@ -98,6 +100,11 @@ export const server = app.listen(port, (err?: any) => {
98
100
  startInvoiceQueue().then(() => logger.info('invoice queue started'));
99
101
  startSubscriptionQueue().then(() => logger.info('subscription queue started'));
100
102
  startEventQueue().then(() => logger.info('event queue started'));
103
+ startCheckoutSessionQueue().then(() => logger.info('checkoutSession queue started'));
101
104
 
102
- ensureWebhookRegistered().catch(console.error);
105
+ if (process.env.BLOCKLET_MODE === 'production') {
106
+ ensureWebhookRegistered().catch(console.error);
107
+ }
108
+
109
+ ensureStakedForGas().catch(console.error);
103
110
  });
@@ -0,0 +1,125 @@
1
+ // import pick from 'lodash/pick';
2
+ import { formatFactoryState, preMintFromFactory } from '@ocap/asset';
3
+ import merge from 'lodash/merge';
4
+
5
+ import { wallet } from '../../libs/auth';
6
+ import logger from '../../libs/logger';
7
+ import { CheckoutSession, PaymentCurrency, PaymentIntent, PaymentMethod, Subscription } from '../../store/models';
8
+ import { sendNftNotification } from '../blocklet/notification';
9
+
10
+ export async function mintNftForCheckoutSession(id: string) {
11
+ const checkoutSession = await CheckoutSession.findByPk(id);
12
+ if (!checkoutSession) {
13
+ logger.warn('checkoutSession not found when attempt to mint nft', { id });
14
+ return;
15
+ }
16
+
17
+ if (checkoutSession.status !== 'complete') {
18
+ logger.warn('checkoutSession not completed when attempt to mint nft', { id });
19
+ return;
20
+ }
21
+
22
+ if (checkoutSession.nft_mint_status === 'disabled') {
23
+ logger.warn('nft mint not enabled for checkoutSession', { id, settings: checkoutSession.nft_mint_settings });
24
+ return;
25
+ }
26
+
27
+ if (checkoutSession.nft_mint_status === 'minted') {
28
+ logger.warn('checkoutSession nft already minted', { id, details: checkoutSession.nft_mint_details });
29
+ return;
30
+ }
31
+
32
+ if (checkoutSession.nft_mint_status === 'error') {
33
+ logger.warn('checkoutSession nft already error', { id, details: checkoutSession.nft_mint_details });
34
+ return;
35
+ }
36
+
37
+ // 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') {
41
+ const client = method.getOcapClient();
42
+ const nftOwner = checkoutSession.customer_did;
43
+ const { factory, inputs } = checkoutSession.nft_mint_settings as any;
44
+
45
+ const [{ state: factoryState }, { state: appState }] = await Promise.all([
46
+ client.getFactoryState({ address: factory }),
47
+ client.getAccountState({ address: wallet.address }),
48
+ ]);
49
+
50
+ const preMint = preMintFromFactory({
51
+ factory: formatFactoryState(factoryState),
52
+ inputs: inputs || {},
53
+ owner: nftOwner,
54
+ issuer: { wallet, name: appState.moniker },
55
+ });
56
+ logger.info('nft preMint for checkoutSession', { id, inputs, nftOwner, factory, preMint });
57
+
58
+ const { state: nftState } = await client.getAccountState({ address: preMint.address });
59
+ if (nftState) {
60
+ await checkoutSession.update({
61
+ nft_mint_status: 'error',
62
+ nft_mint_details: {
63
+ arcblock: {
64
+ address: preMint.address,
65
+ owner: nftOwner as string,
66
+ error: 'NFT with same address already exist',
67
+ },
68
+ },
69
+ });
70
+ logger.warn('nft mint duplicate for checkoutSession', { id, details: checkoutSession.nft_mint_details });
71
+ return;
72
+ }
73
+
74
+ const hash = await client.sendMintAssetTx({
75
+ tx: {
76
+ itx: {
77
+ factory,
78
+ address: preMint.address,
79
+ assets: [],
80
+ variables: Object.entries(preMint.variables).map(([key, value]) => ({ name: key, value })),
81
+ owner: nftOwner,
82
+ },
83
+ },
84
+ wallet,
85
+ });
86
+
87
+ logger.info('nft minted for checkoutSession', { id, inputs, nftOwner, factory, hash });
88
+ await checkoutSession.update({
89
+ nft_mint_status: 'minted',
90
+ nft_mint_details: {
91
+ arcblock: {
92
+ tx_hash: hash,
93
+ address: preMint.address,
94
+ owner: nftOwner as string,
95
+ },
96
+ },
97
+ });
98
+
99
+ if (checkoutSession.subscription_id) {
100
+ const subscription = await Subscription.findByPk(checkoutSession.subscription_id);
101
+ if (subscription && subscription.metadata?.nft_address) {
102
+ await subscription.update({ metadata: merge(subscription.metadata || {}, { nft_address: preMint.address }) });
103
+ }
104
+ }
105
+ if (checkoutSession.payment_intent_id) {
106
+ const paymentIntent = await PaymentIntent.findByPk(checkoutSession.payment_intent_id);
107
+ if (paymentIntent && paymentIntent.metadata?.nft_address) {
108
+ await paymentIntent.update({ metadata: merge(paymentIntent.metadata || {}, { nft_address: preMint.address }) });
109
+ }
110
+ }
111
+
112
+ await sendNftNotification(
113
+ preMint.address,
114
+ nftOwner as string,
115
+ method.settings.arcblock?.api_host as string,
116
+ factoryState.name
117
+ );
118
+ logger.info('nft sent for checkoutSession', { id, nftOwner });
119
+ await checkoutSession.update({ nft_mint_status: 'sent' });
120
+
121
+ return;
122
+ }
123
+
124
+ logger.warn('nft mint not supported for payment method', { id, type: method?.type });
125
+ }
@@ -0,0 +1,55 @@
1
+ /* eslint-disable no-continue */
2
+ /* eslint-disable no-await-in-loop */
3
+ import { toStakeAddress } from '@arcblock/did-util';
4
+ import env from '@blocklet/sdk/lib/env';
5
+ import { fromUnitToToken, toBN } from '@ocap/util';
6
+
7
+ import { wallet } from '../../libs/auth';
8
+ import logger from '../../libs/logger';
9
+ import { PaymentCurrency, PaymentMethod } from '../../store/models';
10
+
11
+ export async function ensureStakedForGas() {
12
+ const currencies = await PaymentCurrency.findAll({ where: { active: true, is_base_currency: true } });
13
+
14
+ for (const currency of currencies) {
15
+ const method = await PaymentMethod.findByPk(currency.payment_method_id);
16
+ if (method && method.type === 'arcblock') {
17
+ const client = method.getOcapClient();
18
+ const host = method.settings.arcblock?.api_host;
19
+
20
+ try {
21
+ const { state: account } = await client.getAccountState({ address: wallet.address });
22
+ if (!account) {
23
+ const hash = await client.declare({ moniker: env.appNameSlug, wallet });
24
+ logger.info(`declared app on chain ${host}`, { hash });
25
+ }
26
+
27
+ const address = toStakeAddress(wallet.address, wallet.address);
28
+ const { state: stake } = await client.getStakeState({ address });
29
+ if (stake) {
30
+ logger.info(`app already staked for gas on chain ${host}`);
31
+ continue;
32
+ }
33
+
34
+ const result = await client.getForgeState({});
35
+ const { token, txConfig } = result.state;
36
+ const holding = account.tokens.find((x: any) => x.address === token.address);
37
+ if (toBN(holding?.value || '0').lte(toBN(txConfig.txGas.minStake))) {
38
+ logger.info(`not enough balance to stake for gas on chain ${host}`);
39
+ continue;
40
+ }
41
+
42
+ // @ts-ignore
43
+ const [hash] = await client.stake({
44
+ to: wallet.address,
45
+ message: 'stake-for-gas',
46
+ tokens: [{ address: token.address, value: fromUnitToToken(txConfig.txGas.minStake, token.decimal) }],
47
+ wallet,
48
+ });
49
+ logger.info(`staked for gas on chain ${host}`, { hash });
50
+ } catch (err) {
51
+ logger.error(`stake for gas on chain ${host} failed`, err);
52
+ }
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,101 @@
1
+ import Notification from '@blocklet/sdk/service/notification';
2
+ import { toDid } from '@ocap/util';
3
+
4
+ import { blocklet } from '../../libs/auth';
5
+ import logger from '../../libs/logger';
6
+ import { translate } from '../../locales';
7
+
8
+ export async function getUserLocale(userDid: string) {
9
+ const { user } = await blocklet.getUser(userDid);
10
+ return user?.locale || 'en';
11
+ }
12
+
13
+ export function createInfoRows(info: any = {}) {
14
+ const fields = Object.keys(info).reduce((list, cur) => {
15
+ const item = info[cur];
16
+ const data = {
17
+ type: 'plain',
18
+ text: item,
19
+ color: undefined,
20
+ };
21
+
22
+ if (Array.isArray(item)) {
23
+ const [text, color] = item;
24
+ data.text = text;
25
+ if (color) {
26
+ data.color = color;
27
+ }
28
+ }
29
+
30
+ if (typeof data.text !== 'string') {
31
+ data.text = String(data.text);
32
+ }
33
+
34
+ list.push(
35
+ // @ts-ignore
36
+ {
37
+ type: 'text',
38
+ data: {
39
+ type: 'plain',
40
+ color: '#9397A1',
41
+ text: cur,
42
+ },
43
+ },
44
+ {
45
+ type: 'text',
46
+ data,
47
+ }
48
+ );
49
+ return list;
50
+ }, []);
51
+
52
+ return {
53
+ type: 'section',
54
+ fields,
55
+ };
56
+ }
57
+
58
+ export async function sendNotification({
59
+ to,
60
+ title,
61
+ message,
62
+ actions,
63
+ attachments = [],
64
+ }: {
65
+ to: string;
66
+ title: string;
67
+ message: string;
68
+ actions: any[];
69
+ attachments: any[];
70
+ }) {
71
+ try {
72
+ const payload = { title, body: message, actions: actions || [], attachments: [...attachments] };
73
+ await Notification.sendToUser(to, payload);
74
+ logger.info('text message was sent', { to, payload: JSON.stringify(payload, null, 2) });
75
+ } catch (error) {
76
+ logger.error('send text message failed', { error, to, message, actions, attachments });
77
+ throw error;
78
+ }
79
+ }
80
+
81
+ export async function sendNftNotification(nftDid: string, userDid: string, chainHost: string, collection: string) {
82
+ const locale = await getUserLocale(userDid);
83
+ await sendNotification({
84
+ to: userDid,
85
+ title: translate('notification.mintNFT.title', locale, { collection }),
86
+ message: translate('notification.mintNFT.message', locale, { collection }),
87
+ attachments: [
88
+ {
89
+ type: 'asset',
90
+ data: {
91
+ chainHost,
92
+ did: nftDid,
93
+ },
94
+ },
95
+ createInfoRows({
96
+ [translate('notification.sendTo', locale)]: toDid(userDid),
97
+ }),
98
+ ],
99
+ actions: [],
100
+ });
101
+ }
@@ -0,0 +1,139 @@
1
+ import merge from 'lodash/merge';
2
+ import pick from 'lodash/pick';
3
+
4
+ import { blocklet } from '../../libs/auth';
5
+ import logger from '../../libs/logger';
6
+ import {
7
+ CheckoutSession,
8
+ PaymentLink,
9
+ Price,
10
+ PricingTable,
11
+ Product,
12
+ Subscription,
13
+ TLineItemExpanded,
14
+ } from '../../store/models';
15
+
16
+ export async function checkPassportForPaymentLink(doc: PaymentLink) {
17
+ // @ts-ignore
18
+ const items: TLineItemExpanded[] = await Price.expand(doc.line_items, true);
19
+ const item = items.find((x) => x.price.product?.metadata?.passport);
20
+ return item?.price.product?.metadata?.passport;
21
+ }
22
+
23
+ export async function checkPassportForPricingTable(doc: PricingTable) {
24
+ const products = await Product.findAll({ where: { id: doc?.items.map((x) => x.product_id) } });
25
+ const product = products.find((x) => x.metadata?.passport);
26
+ return product?.metadata?.passport;
27
+ }
28
+
29
+ export async function ensurePassportIssued(checkoutSession: CheckoutSession) {
30
+ if (checkoutSession.status !== 'complete') {
31
+ logger.warn('checkoutSession not complete', pick(checkoutSession, ['id', 'status']));
32
+ return;
33
+ }
34
+ const info = pick(checkoutSession, ['id', 'metadata']);
35
+ if (!checkoutSession.customer_did) {
36
+ logger.warn('checkoutSession have no customer did', info);
37
+ return;
38
+ }
39
+ if (!checkoutSession.metadata?.passport) {
40
+ logger.warn('checkoutSession not contain any passport issue info', info);
41
+ return;
42
+ }
43
+
44
+ // eslint-disable-next-line @typescript-eslint/naming-convention
45
+ const { passport, pricing_table_id } = checkoutSession.metadata as any;
46
+ const { role } = await blocklet.getRole(passport);
47
+ if (!role) {
48
+ logger.warn('role not found for passport issuance', info);
49
+ return;
50
+ }
51
+
52
+ if (!role.extra?.acquire?.pay || !role.extra?.payment?.product) {
53
+ logger.warn('role config not valid for passport issuance', { ...info, role });
54
+ return;
55
+ }
56
+
57
+ const productId = role.extra.payment.product;
58
+
59
+ // pre issuance check
60
+ if (pricing_table_id) {
61
+ if (role.extra?.acquire?.pay !== pricing_table_id) {
62
+ logger.warn('source pricing table not match wth role config', merge(info, { role }));
63
+ return;
64
+ }
65
+
66
+ const doc = await PricingTable.findByPk(pricing_table_id);
67
+ if (!doc) {
68
+ logger.warn('source pricing table not found for passport issuance', info);
69
+ return;
70
+ }
71
+
72
+ if (doc.items.some((x) => x.product_id === productId) === false) {
73
+ logger.warn('source pricing table not contain product for passport issuance', { ...info, productId });
74
+ return;
75
+ }
76
+ } else if (checkoutSession.payment_link_id) {
77
+ if (role.extra?.acquire?.pay !== checkoutSession.payment_link_id) {
78
+ logger.warn('source payment link not match wth role config', merge(info, { role }));
79
+ return;
80
+ }
81
+
82
+ const doc = await PaymentLink.findByPk(checkoutSession.payment_link_id);
83
+ if (!doc) {
84
+ logger.warn('source payment link not found for passport issuance', info);
85
+ return;
86
+ }
87
+
88
+ const items = await Price.expand(doc.line_items);
89
+ if (items.some((x) => x.price.product_id === productId) === false) {
90
+ logger.warn('source payment link not contain product for passport issuance', { ...info, productId });
91
+ return;
92
+ }
93
+ } else {
94
+ logger.warn('no payment link or pricing table for passport issuance', info);
95
+ }
96
+
97
+ // do the issuance
98
+ await blocklet.issuePassportToUser({ userDid: checkoutSession.customer_did, role: passport });
99
+ logger.info(
100
+ 'passport issued to user on checkout session complete',
101
+ merge(info, { role, customer: checkoutSession.customer_did })
102
+ );
103
+ }
104
+
105
+ export async function ensurePassportRevoked(subscription: Subscription) {
106
+ const info = pick(subscription, ['id', 'status']);
107
+ if (subscription.status !== 'canceled') {
108
+ logger.warn('subscription not canceled', info);
109
+ return;
110
+ }
111
+
112
+ const checkoutSession = await CheckoutSession.findOne({ where: { subscription_id: subscription.id } });
113
+ if (!checkoutSession) {
114
+ logger.warn('checkoutSession for subscription not found', info);
115
+ return;
116
+ }
117
+
118
+ if (!checkoutSession.metadata?.passport) {
119
+ logger.warn('checkoutSession not contain any passport issue info', info);
120
+ return;
121
+ }
122
+
123
+ const { user } = await blocklet.getUser(checkoutSession.customer_did as string, { enableConnectedAccount: true });
124
+ if (!user) {
125
+ logger.warn(
126
+ 'user not found when revoke on subscription cancel',
127
+ merge(info, { did: checkoutSession.customer_did })
128
+ );
129
+ return;
130
+ }
131
+
132
+ const { passport } = checkoutSession.metadata as any;
133
+ const passports = user.passports.filter((x: any) => x.role === passport && x.status === 'valid');
134
+ await Promise.all(passports.map((x: any) => blocklet.revokeUserPassport({ userDid: user.did, passportId: x.id })));
135
+ logger.warn(
136
+ 'user passports revoked on subscription cancel',
137
+ merge(info, { did: checkoutSession.customer_did, passports })
138
+ );
139
+ }
@@ -211,7 +211,7 @@ export async function handleInvoiceEvent(event: TEventExpanded, client: Stripe)
211
211
  if (subscription) {
212
212
  const method = await PaymentMethod.findByPk(subscription.default_payment_method_id);
213
213
  if (method && method.type === 'stripe') {
214
- const tmp = await ensureStripeInvoice(event.data.object, subscription, method.getStripe());
214
+ const tmp = await ensureStripeInvoice(event.data.object, subscription, method.getStripeClient());
215
215
  localInvoiceId = tmp.id;
216
216
  }
217
217
  }
@@ -16,7 +16,7 @@ import {
16
16
  } from '../../store/models';
17
17
 
18
18
  export async function ensureStripeProduct(internal: Product, method: PaymentMethod) {
19
- const client = method.getStripe();
19
+ const client = method.getStripeClient();
20
20
  const result = await client.products.search({ query: `metadata['id']:'${internal.id}'` });
21
21
  if (result.data.length > 0) {
22
22
  return result.data[0] as any;
@@ -45,7 +45,7 @@ export async function ensureStripeProduct(internal: Product, method: PaymentMeth
45
45
  }
46
46
 
47
47
  export async function ensureStripePrice(internal: Price, method: PaymentMethod, currency: PaymentCurrency) {
48
- const client = method.getStripe();
48
+ const client = method.getStripeClient();
49
49
  const result = await client.prices.search({ query: `metadata['id']:'${internal.id}'` });
50
50
  if (result.data.length > 0) {
51
51
  return result.data[0] as any;
@@ -107,7 +107,7 @@ export async function ensureStripePrice(internal: Price, method: PaymentMethod,
107
107
  }
108
108
 
109
109
  export async function ensureStripeCustomer(internal: Customer, method: PaymentMethod) {
110
- const client = method.getStripe();
110
+ const client = method.getStripeClient();
111
111
  const result = await client.customers.search({ query: `metadata['did']:'${internal.did}'` });
112
112
  if (result.data.length > 0) {
113
113
  return result.data[0] as any;
@@ -132,7 +132,7 @@ export async function ensureStripeCustomer(internal: Customer, method: PaymentMe
132
132
  }
133
133
 
134
134
  export async function ensureStripePaymentCustomer(internal: any, method: PaymentMethod) {
135
- const client = method.getStripe();
135
+ const client = method.getStripeClient();
136
136
  let customer = null;
137
137
  if (internal.payment_details?.stripe?.customer_id) {
138
138
  customer = await client.customers.retrieve(internal.payment_details.stripe.customer_id);
@@ -149,7 +149,7 @@ export async function ensureStripePaymentIntent(
149
149
  method: PaymentMethod,
150
150
  currency: PaymentCurrency
151
151
  ) {
152
- const client = method.getStripe();
152
+ const client = method.getStripeClient();
153
153
 
154
154
  let stripeIntent = null;
155
155
  if (internal.payment_details?.stripe?.payment_intent_id) {
@@ -193,7 +193,7 @@ export async function ensureStripeSubscription(
193
193
  items: TLineItemExpanded[],
194
194
  trialInDays: number = 0
195
195
  ) {
196
- const client = method.getStripe();
196
+ const client = method.getStripeClient();
197
197
 
198
198
  let stripeSubscription: any = null;
199
199
  if (internal.payment_details?.stripe?.subscription_id) {
@@ -303,7 +303,7 @@ export async function forwardUsageRecordToStripe(
303
303
  return null;
304
304
  }
305
305
 
306
- const client = method.getStripe();
306
+ const client = method.getStripeClient();
307
307
  const result = await client.subscriptionItems.createUsageRecord(subscriptionItem.metadata.stripe_id, {
308
308
  quantity: updates.quantity,
309
309
  timestamp: updates.timestamp,
@@ -16,7 +16,7 @@ export async function ensureWebhookRegistered() {
16
16
  continue;
17
17
  }
18
18
 
19
- const stripe = method.getStripe();
19
+ const stripe = method.getStripeClient();
20
20
  const { data } = await stripe.webhookEndpoints.list({ limit: 100 });
21
21
 
22
22
  const exist = data.find((webhook) => webhook.metadata?.appPid === env.appPid);
@@ -0,0 +1,23 @@
1
+ import { mintNftForCheckoutSession } from '../integrations/blockchain/nft';
2
+ import { ensurePassportIssued, ensurePassportRevoked } from '../integrations/blocklet/passport';
3
+ import { events } from '../libs/event';
4
+ import logger from '../libs/logger';
5
+ import type { CheckoutSession, Subscription } from '../store/models';
6
+
7
+ // eslint-disable-next-line require-await
8
+ export async function startCheckoutSessionQueue() {
9
+ events.on('checkout.session.completed', (checkoutSession: CheckoutSession) => {
10
+ ensurePassportIssued(checkoutSession).catch((err) => {
11
+ logger.error('ensurePassportIssued failed', { error: err, checkoutSession: checkoutSession.id });
12
+ });
13
+ mintNftForCheckoutSession(checkoutSession.id).catch((err) => {
14
+ logger.error('mintNftForCheckoutSession failed', { error: err, checkoutSession: checkoutSession.id });
15
+ });
16
+ });
17
+
18
+ events.on('customer.subscription.deleted', (subscription: Subscription) => {
19
+ ensurePassportRevoked(subscription).catch((err) => {
20
+ logger.error('ensurePassportRevoked failed', { error: err, subscription: subscription.id });
21
+ });
22
+ });
23
+ }
@@ -1,5 +1,4 @@
1
1
  import { wallet } from '../libs/auth';
2
- import { getClient } from '../libs/chain/arcblock';
3
2
  import dayjs from '../libs/dayjs';
4
3
  import logger from '../libs/logger';
5
4
  import createQueue from '../libs/queue';
@@ -61,7 +60,7 @@ export const handlePayment = async (job: PaymentJob) => {
61
60
  logger.info(`PaymentIntent capture attempt: ${paymentIntent.id}`);
62
61
  try {
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 txHash = await client.sendTransferV2Tx({
66
65
  tx: {
67
66
  itx: {
@@ -3,6 +3,8 @@ import pick from 'lodash/pick';
3
3
  import { Event } from '../store/models/event';
4
4
  import { events } from './event';
5
5
 
6
+ const API_VERSION = '2023-09-05';
7
+
6
8
  export async function createEvent(scope: string, type: string, model: any, options: any) {
7
9
  // console.log('createEvent', scope, type, model, options);
8
10
  const data: any = {
@@ -14,7 +16,7 @@ export async function createEvent(scope: string, type: string, model: any, optio
14
16
 
15
17
  const event = await Event.create({
16
18
  type,
17
- api_version: 'v1',
19
+ api_version: API_VERSION,
18
20
  livemode: !!model.livemode,
19
21
  object_id: model.id,
20
22
  object_type: scope,
@@ -29,6 +31,7 @@ export async function createEvent(scope: string, type: string, model: any, optio
29
31
  });
30
32
 
31
33
  events.emit('event.created', { id: event.id });
34
+ events.emit(event.type, data.object);
32
35
  }
33
36
 
34
37
  export async function createStatusEvent(
@@ -55,7 +58,45 @@ export async function createStatusEvent(
55
58
  const suffix = config[data.object.status];
56
59
  const event = await Event.create({
57
60
  type: [prefix, suffix].join('.'),
58
- api_version: 'v1',
61
+ api_version: API_VERSION,
62
+ livemode: !!model.livemode,
63
+ object_id: model.id,
64
+ object_type: scope,
65
+ data,
66
+ request: {
67
+ // FIXME:
68
+ id: '',
69
+ idempotency_key: '',
70
+ },
71
+ metadata: {},
72
+ pending_webhooks: 99, // force all events goto the event queue
73
+ });
74
+
75
+ events.emit('event.created', { id: event.id });
76
+ events.emit(event.type, data.object);
77
+ }
78
+
79
+ export async function createCustomEvent(
80
+ scope: string,
81
+ prefix: string,
82
+ type: (current: any, previous: any) => string,
83
+ model: any,
84
+ options: any
85
+ ) {
86
+ // console.log('createCustomEvent', scope, prefix, type, model, options);
87
+ const data: any = {
88
+ object: model.dataValues,
89
+ previous_attributes: pick(model._previousDataValues, options.fields),
90
+ };
91
+
92
+ const suffix = type(data.object, data.previous_attributes);
93
+ if (!suffix) {
94
+ return;
95
+ }
96
+
97
+ const event = await Event.create({
98
+ type: [prefix, suffix].join('.'),
99
+ api_version: API_VERSION,
59
100
  livemode: !!model.livemode,
60
101
  object_id: model.id,
61
102
  object_type: scope,
@@ -70,4 +111,5 @@ export async function createStatusEvent(
70
111
  });
71
112
 
72
113
  events.emit('event.created', { id: event.id });
114
+ events.emit(event.type, data.object);
73
115
  }
@@ -4,12 +4,11 @@ import type { DelegateState } from '@ocap/client';
4
4
  import { BN } from '@ocap/util';
5
5
 
6
6
  import type { TPaymentCurrency } from '../store/models/payment-currency';
7
- import type { TPaymentMethod } from '../store/models/payment-method';
7
+ import type { PaymentMethod } from '../store/models/payment-method';
8
8
  import { blocklet, wallet } from './auth';
9
- import { getClient } from './chain/arcblock';
10
9
 
11
10
  export async function isDelegationSufficientForPayment(args: {
12
- paymentMethod: TPaymentMethod;
11
+ paymentMethod: PaymentMethod;
13
12
  paymentCurrency: TPaymentCurrency;
14
13
  userDid: string;
15
14
  amount: string;
@@ -23,7 +22,7 @@ export async function isDelegationSufficientForPayment(args: {
23
22
  return { sufficient: false, reason: 'NO_DID_WALLET' };
24
23
  }
25
24
 
26
- const client = await getClient(paymentMethod.settings?.arcblock?.api_host as string);
25
+ const client = paymentMethod.getOcapClient();
27
26
 
28
27
  // have delegated before?
29
28
  const address = toDelegateAddress(delegator, wallet.address);
@@ -1,3 +1,11 @@
1
1
  import flat from 'flat';
2
2
 
3
- export default flat({});
3
+ export default flat({
4
+ notification: {
5
+ sendTo: 'Sent to',
6
+ mintNFT: {
7
+ title: '{collection} NFT minted',
8
+ message: 'A new {collection} NFT is minted and sent to your wallet, please check it out.',
9
+ },
10
+ },
11
+ });