payment-kit 1.13.68 → 1.13.69

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.
@@ -79,6 +79,9 @@ export const handleInvoice = async (job: InvoiceJob) => {
79
79
  if (invoice.payment_intent_id) {
80
80
  logger.warn(`PaymentIntent exist: ${invoice.payment_intent_id} for invoice ${job.invoiceId}`);
81
81
  paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
82
+ if (paymentIntent && ['succeeded', 'canceled'].includes(paymentIntent.status) === false) {
83
+ await paymentIntent.update({ status: 'requires_capture' });
84
+ }
82
85
  } else {
83
86
  const descriptionMap: any = {
84
87
  subscription_create: 'Subscription creation',
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable @typescript-eslint/brace-style */
2
2
  /* eslint-disable @typescript-eslint/indent */
3
3
  import { fromUnitToToken, toDid } from '@ocap/util';
4
+ import camelCase from 'lodash/camelCase';
4
5
  import prettyMsI18n from 'pretty-ms-i18n';
5
6
 
6
7
  import { getUserLocale } from '../../../integrations/blocklet/notification';
@@ -60,14 +61,7 @@ export class SubscriptionRenewFailedEmailTemplate
60
61
  throw new Error(`SufficientForPaymentResult.sufficient should be false: ${JSON.stringify(this.options.result)}`);
61
62
  }
62
63
 
63
- // 类似 NO_DID_WALLET 字符串将转成 noDidWallet
64
- const toCamelCase = (input: string): string => {
65
- return input.toLowerCase().replace(/_([a-z])/g, (_match, group1) => {
66
- return group1.toUpperCase();
67
- });
68
- };
69
-
70
- const i18nText = `notification.subscriptionRenewFailed.reason.${toCamelCase(this.options.result.reason as string)}`;
64
+ const i18nText = `notification.subscriptionRenewFailed.reason.${camelCase(this.options.result.reason as string)}`;
71
65
 
72
66
  const paymentDetail = await getPaymentDetail(userDid, invoice);
73
67
  const reason = translate(i18nText, locale, {
@@ -2,26 +2,35 @@
2
2
  import { toDelegateAddress } from '@arcblock/did-util';
3
3
  import { sign } from '@arcblock/jwt';
4
4
  import { getWalletDid } from '@blocklet/sdk/lib/did';
5
- import type { DelegateState } from '@ocap/client';
5
+ import type { DelegateState, TokenLimit } from '@ocap/client';
6
6
  import { toTxHash } from '@ocap/mcrypto';
7
7
  import { BN, fromUnitToToken } from '@ocap/util';
8
+ import cloneDeep from 'lodash/cloneDeep';
9
+ import type { LiteralUnion } from 'type-fest';
8
10
 
9
11
  import { Invoice, PaymentCurrency, PaymentIntent, PaymentMethod } from '../store/models';
10
12
  import type { TPaymentCurrency } from '../store/models/payment-currency';
11
13
  import { blocklet, wallet } from './auth';
12
14
  import logger from './logger';
15
+ import { OCAP_PAYMENT_TX_TYPE } from './util';
13
16
 
14
17
  export interface SufficientForPaymentResult {
15
18
  sufficient: boolean;
16
- reason?:
19
+ reason?: LiteralUnion<
17
20
  | 'NO_DID_WALLET'
18
21
  | 'NO_DELEGATION'
19
22
  | 'NO_TRANSFER_PERMISSION'
23
+ | 'NO_TRANSFER_TO'
24
+ | 'NO_TOKEN_PERMISSION'
20
25
  | 'NO_TOKEN'
26
+ | 'NO_ENOUGH_ALLOWANCE'
21
27
  | 'NO_ENOUGH_TOKEN'
22
- | 'NOT_SUPPORTED';
28
+ | 'NOT_SUPPORTED',
29
+ string
30
+ >;
23
31
  delegator?: string;
24
32
  state?: DelegateState;
33
+ token?: { address: string; balance: string };
25
34
  }
26
35
 
27
36
  export async function isDelegationSufficientForPayment(args: {
@@ -31,6 +40,8 @@ export async function isDelegationSufficientForPayment(args: {
31
40
  amount: string;
32
41
  }): Promise<SufficientForPaymentResult> {
33
42
  const { paymentCurrency, paymentMethod, userDid, amount } = args;
43
+ const tokenAddress = paymentCurrency.contract as string;
44
+
34
45
  if (paymentMethod.type === 'arcblock') {
35
46
  // user have bond wallet did?
36
47
  const { user } = await blocklet.getUser(userDid, { enableConnectedAccount: true });
@@ -48,22 +59,43 @@ export async function isDelegationSufficientForPayment(args: {
48
59
  return { sufficient: false, reason: 'NO_DELEGATION' };
49
60
  }
50
61
 
51
- // have enough permissions
52
- if (state.ops.some((x: any) => x.key === 'fg:t:transfer_v2') === false) {
62
+ // have transfer permissions?
63
+ const grant = (state as DelegateState).ops.find((x: any) => x.key === OCAP_PAYMENT_TX_TYPE)?.value;
64
+ if (!grant) {
53
65
  return { sufficient: false, reason: 'NO_TRANSFER_PERMISSION' };
54
66
  }
55
67
 
68
+ // check token limits
69
+ if (grant.limit) {
70
+ const tokenLimit = grant.limit.tokens.find((x: any) => x.address === tokenAddress);
71
+ if (!tokenLimit) {
72
+ return { sufficient: false, reason: 'NO_TOKEN_PERMISSION' };
73
+ }
74
+
75
+ // FIXME: @wangshijun check other conditions in the token limit: txCount, totalAllowance, validUntil, rateLimit
76
+
77
+ if (Array.isArray(tokenLimit.to) && tokenLimit.to.length && tokenLimit.to.includes(wallet.address) === false) {
78
+ return { sufficient: false, reason: 'NO_TRANSFER_TO' };
79
+ }
80
+
81
+ const requested = new BN(amount);
82
+ const allowance = new BN(tokenLimit.txAllowance);
83
+ if (requested.gt(allowance)) {
84
+ return { sufficient: false, reason: 'NO_ENOUGH_ALLOWANCE' };
85
+ }
86
+ }
87
+
56
88
  // balance enough token for payment?
57
- const { tokens } = await client.getAccountTokens({ address: delegator, token: paymentCurrency.contract as string });
89
+ const { tokens } = await client.getAccountTokens({ address: delegator, token: tokenAddress });
58
90
  const [token] = tokens;
59
91
  if (!token) {
60
92
  return { sufficient: false, reason: 'NO_TOKEN' };
61
93
  }
62
94
  if (new BN(token.balance).lt(new BN(amount))) {
63
- return { sufficient: false, reason: 'NO_ENOUGH_TOKEN' };
95
+ return { sufficient: false, reason: 'NO_ENOUGH_TOKEN', token };
64
96
  }
65
97
 
66
- return { sufficient: true, delegator, state };
98
+ return { sufficient: true, delegator, state, token };
67
99
  }
68
100
 
69
101
  if (paymentMethod.type === 'stripe') {
@@ -114,53 +146,82 @@ export async function getPaymentDetail(userDid: string, invoice: Invoice): Promi
114
146
  }
115
147
 
116
148
  if (paymentMethod.type === 'arcblock') {
117
- // user have bond wallet did?
118
- const { user } = await blocklet.getUser(userDid, { enableConnectedAccount: true });
119
- const delegator = getWalletDid(user);
120
- if (!delegator) {
121
- logger.error('getPayment.error', { reason: 'NO_DID_WALLET' });
122
- return defaultResult;
123
- }
124
-
125
- const client = paymentMethod.getOcapClient();
126
-
127
- // have delegated before?
128
- const address = toDelegateAddress(delegator, wallet.address);
129
- const { state } = await client.getDelegateState({ address });
130
- if (!state) {
131
- logger.error('getPayment.error', { reason: 'NO_DELEGATION' });
132
- return defaultResult;
133
- }
149
+ // balance enough token for payment?
150
+ const result = await isDelegationSufficientForPayment({
151
+ paymentMethod,
152
+ paymentCurrency,
153
+ userDid,
154
+ amount: paymentIntent.amount,
155
+ });
134
156
 
135
- // have enough permissions
136
- if (state.ops.some((x: any) => x.key === 'fg:t:transfer_v2') === false) {
137
- logger.error('getPayment.error', { reason: 'NO_TRANSFER_PERMISSION' });
157
+ // Do not have enough permission
158
+ if (!result.token) {
159
+ logger.error('getPayment.error', { reason: result.reason });
138
160
  return defaultResult;
139
161
  }
140
162
 
141
- // balance enough token for payment?
163
+ // Do not have enough token
142
164
  const amount: number = +fromUnitToToken(paymentIntent.amount, paymentCurrency.decimal);
143
- const { symbol } = paymentCurrency;
144
-
145
- const { tokens } = await client.getAccountTokens({
146
- address: delegator,
147
- token: paymentCurrency.contract as string,
148
- });
149
- const [token] = tokens;
150
- if (!token) {
165
+ if (!result.delegator) {
151
166
  return {
152
167
  balance: 0,
153
168
  price: amount,
154
- symbol,
169
+ symbol: paymentCurrency.symbol,
155
170
  };
156
171
  }
157
172
 
158
173
  return {
159
- balance: +fromUnitToToken(token.balance, paymentCurrency.decimal),
174
+ balance: +fromUnitToToken(result.token.balance, paymentCurrency.decimal),
160
175
  price: amount,
161
- symbol,
176
+ symbol: paymentCurrency.symbol,
162
177
  };
163
178
  }
164
179
 
165
180
  return defaultResult;
166
181
  }
182
+
183
+ export async function getTokenLimitsForDelegation(
184
+ paymentMethod: PaymentMethod,
185
+ paymentCurrency: PaymentCurrency,
186
+ address: string,
187
+ amount: string
188
+ ): Promise<TokenLimit[]> {
189
+ const client = paymentMethod.getOcapClient();
190
+ const { state } = await client.getDelegateState({ address });
191
+
192
+ // @ts-ignore
193
+ const entry: TokenLimit = {
194
+ address: paymentCurrency.contract as string,
195
+ to: [wallet.address], // FIXME: may broken if we have vault, migrated
196
+ txAllowance: amount,
197
+ totalAllowance: '0',
198
+ txCount: 0,
199
+ validUntil: 0,
200
+ };
201
+
202
+ // If we never delegated before
203
+ if (!state) {
204
+ return [entry];
205
+ }
206
+
207
+ const op = (state as DelegateState).ops.find((x) => x.key === OCAP_PAYMENT_TX_TYPE);
208
+ if (op && Array.isArray(op.value.limit?.tokens) && op.value.limit.tokens.length > 0) {
209
+ const tokenLimits = cloneDeep(op.value.limit.tokens);
210
+ const index = op.value.limit.tokens.findIndex((x) => x.address === paymentCurrency.contract);
211
+ // we are updating an existing token limit
212
+ if (index > -1) {
213
+ const limit = op.value.limit.tokens[index] as TokenLimit;
214
+ // If we have a previous delegation and the txAllowance is smaller than requested amount
215
+ if (new BN(limit.txAllowance).lt(new BN(amount))) {
216
+ tokenLimits[index] = entry;
217
+ }
218
+ } else {
219
+ // we are adding a new token limit
220
+ tokenLimits.push(entry);
221
+ }
222
+
223
+ return tokenLimits;
224
+ }
225
+
226
+ return [entry];
227
+ }
@@ -7,6 +7,8 @@ import type { LiteralUnion } from 'type-fest';
7
7
 
8
8
  import dayjs from './dayjs';
9
9
 
10
+ export const OCAP_PAYMENT_TX_TYPE = 'fg:t:transfer_v2';
11
+
10
12
  export const MAX_RETRY_COUNT = 18; // 2^18 seconds ~~ 3 days, total retry time: 6 days
11
13
  export const STRIPE_API_VERSION = '2023-08-16';
12
14
  export const STRIPE_ENDPOINT: string = getUrl('/api/integrations/stripe/webhook');
@@ -65,9 +65,14 @@ export default flat({
65
65
  reason: {
66
66
  noDidWallet:
67
67
  'You have not yet bound the DID Wallet, please bind the DID Wallet, make sure the balance is sufficient and then renew it',
68
- noDelegation: 'Your DID Wallet is not yet authorized, please renew it after completing the authorization',
68
+ noDelegation: 'No delegation found from your DID Wallet, please renew it after completing the authorization',
69
69
  noTransferPermission:
70
- 'Your DID Wallet transfer privileges are insufficient, please complete the authorization and renew it again',
70
+ 'Transfer permission not granted to app, please update the authorization and renew it again',
71
+ noTokenPermission:
72
+ 'Token transfer permission not granted to app, please update the authorization and renew it again',
73
+ noTransferTo:
74
+ 'App not in whitelist of the transfer permission, please update the authorization and renew it again',
75
+ noEnoughAllowance: 'Transfer amount exceeds tx allowance, please update the authorization and renew it again',
71
76
  noToken: "You don't have any tokens in your account, please replenish your tokens and renew your account",
72
77
  noEnoughToken:
73
78
  'Your account token balance is {balance}, not enough for {price}, please replenish your tokens and renew your account',
@@ -65,7 +65,10 @@ export default flat({
65
65
  reason: {
66
66
  noDidWallet: '您尚未绑定 DID Wallet,请绑定 DID Wallet,确保余额充足后重新续费',
67
67
  noDelegation: '您的 DID Wallet 尚未授权,请完成授权后重新续费',
68
- noTransferPermission: '您的 DID Wallet 转账权限不足,请完成授权后重新续费',
68
+ noTransferPermission: '您的 DID Wallet 未授予应用转账权限,请更新授权后重新续费',
69
+ noTokenPermission: '您的 DID Wallet 未授予应用对应通证的转账权限,请更新授权后重新续费',
70
+ noTransferTo: '您的 DID Wallet 未授予应用扣款权限,请更新授权后重新续费',
71
+ noEnoughAllowance: '扣款金额超出单笔转账限额,请更新授权后重新续费',
69
72
  noToken: '您的账户没有任何代币,请充值代币后重新续费',
70
73
  noEnoughToken: '您的账户代币余额为 {balance},不足 {price},请充值代币后重新续费',
71
74
  noSupported: '不支持使用代币续费,请检查您的套餐',
@@ -657,6 +657,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
657
657
  subscription: subscription.id,
658
658
  });
659
659
 
660
+ // FIXME: @wangshijun what if we have have changed subscription items when resubmit
660
661
  // create subscription items
661
662
  const items = await Promise.all(
662
663
  lineItems
@@ -753,6 +754,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
753
754
  id: stripeIntent.id,
754
755
  client_secret: stripeIntent.client_secret,
755
756
  status: stripeIntent.status,
757
+ intent_type: 'payment_intent',
756
758
  };
757
759
  }
758
760
 
@@ -6,8 +6,10 @@ 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';
10
- import { getTxMetadata } from '../../libs/util';
9
+ import { getGasPayerExtra, getTokenLimitsForDelegation } from '../../libs/payment';
10
+ import { getFastCheckoutAmount } from '../../libs/session';
11
+ import { OCAP_PAYMENT_TX_TYPE, getTxMetadata } from '../../libs/util';
12
+ import type { TLineItemExpanded } from '../../store/models';
11
13
  import { ensureSetupIntent, getAuthPrincipalClaim, getTokenRequirements } from './shared';
12
14
 
13
15
  export default {
@@ -30,6 +32,13 @@ export default {
30
32
  }
31
33
 
32
34
  if (paymentMethod.type === 'arcblock') {
35
+ const address = toDelegateAddress(userDid, wallet.address);
36
+ const amount = getFastCheckoutAmount(
37
+ checkoutSession.line_items as TLineItemExpanded[],
38
+ checkoutSession.mode,
39
+ paymentCurrency
40
+ );
41
+ const tokenLimits = await getTokenLimitsForDelegation(paymentMethod, paymentCurrency, address, amount);
33
42
  const tokenRequirements = await getTokenRequirements(checkoutSession, paymentMethod, paymentCurrency);
34
43
 
35
44
  return {
@@ -39,9 +48,9 @@ export default {
39
48
  wallet: fromPublicKey(userPk, toTypeInfo(userDid)),
40
49
  data: {
41
50
  itx: {
42
- address: toDelegateAddress(userDid, wallet.address),
51
+ address,
43
52
  to: wallet.address,
44
- ops: [{ typeUrl: 'fg:t:transfer_v2', rules: [] }],
53
+ ops: [{ typeUrl: OCAP_PAYMENT_TX_TYPE, limit: { tokens: tokenLimits, assets: [] } }],
45
54
  data: getTxMetadata({
46
55
  subscriptionId: subscription.id,
47
56
  checkoutSessionId,
@@ -8,8 +8,10 @@ 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';
12
- import { getTxMetadata } from '../../libs/util';
11
+ import { getGasPayerExtra, getTokenLimitsForDelegation } from '../../libs/payment';
12
+ import { getFastCheckoutAmount } from '../../libs/session';
13
+ import { OCAP_PAYMENT_TX_TYPE, getTxMetadata } from '../../libs/util';
14
+ import type { TLineItemExpanded } from '../../store/models';
13
15
  import { ensureInvoiceForCheckout, ensurePaymentIntent, getAuthPrincipalClaim, getTokenRequirements } from './shared';
14
16
 
15
17
  export default {
@@ -32,6 +34,13 @@ export default {
32
34
  }
33
35
 
34
36
  if (paymentMethod.type === 'arcblock') {
37
+ const address = toDelegateAddress(userDid, wallet.address);
38
+ const amount = getFastCheckoutAmount(
39
+ checkoutSession.line_items as TLineItemExpanded[],
40
+ checkoutSession.mode,
41
+ paymentCurrency
42
+ );
43
+ const tokenLimits = await getTokenLimitsForDelegation(paymentMethod, paymentCurrency, address, amount);
35
44
  const tokenRequirements = await getTokenRequirements(checkoutSession, paymentMethod, paymentCurrency);
36
45
 
37
46
  return {
@@ -41,9 +50,9 @@ export default {
41
50
  wallet: fromPublicKey(userPk, toTypeInfo(userDid)),
42
51
  data: {
43
52
  itx: {
44
- address: toDelegateAddress(userDid, wallet.address),
53
+ address,
45
54
  to: wallet.address,
46
- ops: [{ typeUrl: 'fg:t:transfer_v2', rules: [] }],
55
+ ops: [{ typeUrl: OCAP_PAYMENT_TX_TYPE, limit: { tokens: tokenLimits, assets: [] } }],
47
56
  data: getTxMetadata({
48
57
  subscriptionId: subscription.id,
49
58
  checkoutSessionId,
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.68
17
+ version: 1.13.69
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.68",
3
+ "version": "1.13.69",
4
4
  "scripts": {
5
5
  "dev": "COMPONENT_STORE_URL=https://test.store.blocklet.dev blocklet dev",
6
6
  "eject": "vite eject",
@@ -106,7 +106,7 @@
106
106
  "@abtnode/types": "^1.16.19",
107
107
  "@arcblock/eslint-config": "^0.2.4",
108
108
  "@arcblock/eslint-config-ts": "^0.2.4",
109
- "@did-pay/types": "1.13.68",
109
+ "@did-pay/types": "1.13.69",
110
110
  "@types/cookie-parser": "^1.4.6",
111
111
  "@types/cors": "^2.8.17",
112
112
  "@types/dotenv-flow": "^3.3.3",
@@ -143,5 +143,5 @@
143
143
  "parser": "typescript"
144
144
  }
145
145
  },
146
- "gitHead": "c81dabe6421359495ca922e1b21dd0c5a935f05c"
146
+ "gitHead": "fd169e0529e664ecf8257e684029f21629d39113"
147
147
  }
@@ -82,7 +82,11 @@ export default function PaymentForm({
82
82
  paying: boolean;
83
83
  paid: boolean;
84
84
  paymentIntent?: TPaymentIntent;
85
- stripeContext?: any;
85
+ stripeContext?: {
86
+ client_secret: string;
87
+ intent_type: string;
88
+ status: string;
89
+ };
86
90
  customer?: TCustomer;
87
91
  stripePaying: boolean;
88
92
  }>({
@@ -90,7 +94,7 @@ export default function PaymentForm({
90
94
  paying: false,
91
95
  paid: false,
92
96
  paymentIntent,
93
- stripeContext: null,
97
+ stripeContext: undefined,
94
98
  customer,
95
99
  stripePaying: false,
96
100
  });
@@ -3,9 +3,9 @@ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import Toast from '@arcblock/ux/lib/Toast';
4
4
  import type { TInvoiceExpanded } from '@did-pay/types';
5
5
  import { ArrowBackOutlined } from '@mui/icons-material';
6
- import { Alert, Box, Button, CircularProgress, Grid, Link, Stack, Typography } from '@mui/material';
6
+ import { Alert, Box, Button, CircularProgress, Grid, Stack, Typography } from '@mui/material';
7
7
  import { useRequest, useSetState } from 'ahooks';
8
- import { useParams } from 'react-router-dom';
8
+ import { Link, useParams } from 'react-router-dom';
9
9
 
10
10
  import TxLink from '../../components/blockchain/tx';
11
11
  import Currency from '../../components/currency';
@@ -73,7 +73,7 @@ export default function CustomerHome() {
73
73
  <Layout>
74
74
  <Grid container spacing={3} sx={{ mt: 1 }}>
75
75
  <Grid item xs={12} md={12}>
76
- <Link onClick={() => window.history.back()}>
76
+ <Link to="/customer">
77
77
  <Stack direction="row" alignItems="center" sx={{ fontWeight: 'normal' }}>
78
78
  <ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
79
79
  <Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>