payment-kit 1.16.10 → 1.16.12

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.
package/api/src/index.ts CHANGED
@@ -37,6 +37,7 @@ import rechargeHandlers from './routes/connect/recharge';
37
37
  import payHandlers from './routes/connect/pay';
38
38
  import setupHandlers from './routes/connect/setup';
39
39
  import subscribeHandlers from './routes/connect/subscribe';
40
+ import delegationHandlers from './routes/connect/delegation';
40
41
  import { initialize } from './store/models';
41
42
  import { sequelize } from './store/sequelize';
42
43
  import { initUserHandler } from './integrations/blocklet/user';
@@ -71,7 +72,7 @@ handlers.attach(Object.assign({ app: router }, subscribeHandlers));
71
72
  handlers.attach(Object.assign({ app: router }, changePaymentHandlers));
72
73
  handlers.attach(Object.assign({ app: router }, changePlanHandlers));
73
74
  handlers.attach(Object.assign({ app: router }, rechargeHandlers));
74
-
75
+ handlers.attach(Object.assign({ app: router }, delegationHandlers));
75
76
  router.use('/api', routes);
76
77
 
77
78
  const isProduction = process.env.BLOCKLET_MODE === 'production';
@@ -263,7 +263,7 @@ export async function handleStripeInvoiceCreated(event: TEventExpanded, client:
263
263
  const usageReportStart = stripeInvoice.period_start;
264
264
  const usageReportEnd = stripeInvoice.period_end;
265
265
  const usageReportEmpty = await checkUsageReportEmpty(subscription, usageReportStart, usageReportEnd);
266
- if (usageReportEmpty) {
266
+ if (usageReportEmpty && subscription.status !== 'trialing') {
267
267
  createEvent('Subscription', 'usage.report.empty', subscription, {
268
268
  usageReportStart,
269
269
  usageReportEnd,
@@ -73,6 +73,10 @@ export async function isDelegationSufficientForPayment(args: {
73
73
  return { sufficient: false, reason: 'NO_DELEGATION' };
74
74
  }
75
75
 
76
+ if (!state.ops || state.ops?.length === 0) {
77
+ return { sufficient: false, reason: 'NO_DELEGATION' };
78
+ }
79
+
76
80
  // have transfer permissions?
77
81
  const grant = (state as DelegateState).ops.find((x: any) => x.key === OCAP_PAYMENT_TX_TYPE)?.value;
78
82
  if (!grant) {
@@ -276,12 +280,11 @@ export async function getTokenLimitsForDelegation(
276
280
  return [entry];
277
281
  }
278
282
 
279
- // If we have metered items, we should not limit tx allowance(set to 0)
280
- if (hasMetered) {
283
+ if (!state.ops || state.ops?.length === 0) {
281
284
  return [entry];
282
285
  }
283
286
 
284
- const op = (state as DelegateState).ops.find((x) => x.key === OCAP_PAYMENT_TX_TYPE);
287
+ const op = state.ops.find((x) => x.key === OCAP_PAYMENT_TX_TYPE);
285
288
  if (op && Array.isArray(op.value.limit?.tokens) && op.value.limit.tokens.length > 0) {
286
289
  const tokenLimits = cloneDeep(op.value.limit.tokens);
287
290
  const index = op.value.limit.tokens.findIndex((x) => x.address === paymentCurrency.contract);
@@ -119,18 +119,20 @@ const doHandleSubscriptionInvoice = async ({
119
119
  const usageReportStart = usageStart || start - offset;
120
120
  const usageReportEnd = usageEnd || end - offset;
121
121
 
122
- // check if usage report is empty
123
- const usageReportEmpty = await checkUsageReportEmpty(subscription, usageReportStart, usageReportEnd);
124
- if (usageReportEmpty) {
125
- createEvent('Subscription', 'usage.report.empty', subscription, {
126
- usageReportStart,
127
- usageReportEnd,
128
- }).catch(console.error);
129
- logger.info('create usage report empty event', {
130
- subscriptionId: subscription.id,
131
- usageReportStart,
132
- usageReportEnd,
133
- });
122
+ if (subscription.status !== 'trialing') {
123
+ // check if usage report is empty
124
+ const usageReportEmpty = await checkUsageReportEmpty(subscription, usageReportStart, usageReportEnd);
125
+ if (usageReportEmpty) {
126
+ createEvent('Subscription', 'usage.report.empty', subscription, {
127
+ usageReportStart,
128
+ usageReportEnd,
129
+ }).catch(console.error);
130
+ logger.info('create usage report empty event', {
131
+ subscriptionId: subscription.id,
132
+ usageReportStart,
133
+ usageReportEnd,
134
+ });
135
+ }
134
136
  }
135
137
 
136
138
  // get usage summaries for this billing cycle
@@ -3,8 +3,8 @@ import type { Transaction } from '@ocap/client';
3
3
  import { fromAddress } from '@ocap/wallet';
4
4
 
5
5
  import { toBase58 } from '@ocap/util';
6
- import { encodeTransferItx } from 'api/src/integrations/ethereum/token';
7
- import { waitForEvmTxConfirm, waitForEvmTxReceipt } from 'api/src/integrations/ethereum/tx';
6
+ import { encodeTransferItx } from '../../integrations/ethereum/token';
7
+ import { waitForEvmTxConfirm, waitForEvmTxReceipt } from '../../integrations/ethereum/tx';
8
8
  import type { CallbackArgs } from '../../libs/auth';
9
9
  import { ethWallet, wallet } from '../../libs/auth';
10
10
  import { getGasPayerExtra } from '../../libs/payment';
@@ -0,0 +1,70 @@
1
+ import type { TLineItemExpanded } from '../../store/models';
2
+ import type { CallbackArgs } from '../../libs/auth';
3
+ import { getTxMetadata } from '../../libs/util';
4
+ import {
5
+ ensureSubscriptionDelegation,
6
+ executeOcapTransactions,
7
+ getAuthPrincipalClaim,
8
+ getDelegationTxClaim,
9
+ } from './shared';
10
+
11
+ export default {
12
+ action: 'delegation',
13
+ authPrincipal: false,
14
+ claims: {
15
+ authPrincipal: async ({ extraParams }: CallbackArgs) => {
16
+ const { paymentMethod } = await ensureSubscriptionDelegation(extraParams.subscriptionId);
17
+ return getAuthPrincipalClaim(paymentMethod, 'continue');
18
+ },
19
+ },
20
+ onConnect: async (args: CallbackArgs) => {
21
+ const { userDid, userPk, extraParams } = args;
22
+ const { paymentMethod, paymentCurrency, subscription, payerAddress } = await ensureSubscriptionDelegation(
23
+ extraParams.subscriptionId
24
+ );
25
+ const billingThreshold = Number(subscription.billing_thresholds?.amount_gte || 0);
26
+ if (userDid !== payerAddress) {
27
+ throw new Error(
28
+ `You are not the payer for this subscription. Expected payer: ${payerAddress}, but found: ${userDid}.`
29
+ );
30
+ }
31
+ const claims: { [type: string]: [string, object] } = {
32
+ delegation: [
33
+ 'signature',
34
+ await getDelegationTxClaim({
35
+ mode: 'delegation',
36
+ userDid,
37
+ userPk,
38
+ nonce: subscription.id,
39
+ data: getTxMetadata({ subscriptionId: subscription.id }),
40
+ paymentCurrency,
41
+ paymentMethod,
42
+ trialing: true,
43
+ billingThreshold,
44
+ items: subscription!.items as TLineItemExpanded[],
45
+ }),
46
+ ],
47
+ };
48
+ return claims;
49
+ },
50
+ onAuth: async (args: CallbackArgs) => {
51
+ const { request, userDid, userPk, claims, extraParams } = args;
52
+ const { subscriptionId } = extraParams;
53
+ const { paymentMethod, paymentCurrency } = await ensureSubscriptionDelegation(subscriptionId);
54
+
55
+ if (paymentMethod.type === 'arcblock') {
56
+ const { stakingAmount, ...paymentDetails } = await executeOcapTransactions(
57
+ userDid,
58
+ userPk,
59
+ claims,
60
+ paymentMethod,
61
+ request,
62
+ subscriptionId,
63
+ paymentCurrency?.contract
64
+ );
65
+ return { hash: paymentDetails.tx_hash };
66
+ }
67
+
68
+ throw new Error(`Payment method ${paymentMethod.type} not supported`);
69
+ },
70
+ };
@@ -1,8 +1,8 @@
1
1
  import type { Transaction } from '@ocap/client';
2
2
  import { fromAddress } from '@ocap/wallet';
3
3
  import { fromTokenToUnit, toBase58 } from '@ocap/util';
4
- import { encodeTransferItx } from 'api/src/integrations/ethereum/token';
5
- import { executeEvmTransaction, waitForEvmTxConfirm } from 'api/src/integrations/ethereum/tx';
4
+ import { encodeTransferItx } from '../../integrations/ethereum/token';
5
+ import { executeEvmTransaction, waitForEvmTxConfirm } from '../../integrations/ethereum/tx';
6
6
  import type { CallbackArgs } from '../../libs/auth';
7
7
  import { getGasPayerExtra } from '../../libs/payment';
8
8
  import { getTxMetadata } from '../../libs/util';
@@ -259,6 +259,26 @@ export async function ensureSetupIntent(checkoutSessionId: string, userDid?: str
259
259
  };
260
260
  }
261
261
 
262
+ export async function ensureSubscriptionDelegation(subscriptionId: string) {
263
+ const subscription = (await Subscription.findOne({
264
+ where: { id: subscriptionId },
265
+ include: [{ model: PaymentCurrency, as: 'paymentCurrency' }, { model: PaymentMethod, as: 'paymentMethod' }],
266
+ })) as (Subscription & { paymentCurrency: PaymentCurrency; paymentMethod: PaymentMethod; items?: TLineItemExpanded[] }) | null;
267
+ if (!subscription) {
268
+ throw new Error('Subscription not found');
269
+ }
270
+ if (!['arcblock', 'ethereum'].includes(subscription.paymentMethod?.type)) {
271
+ throw new Error(`Payment method ${subscription.paymentMethod?.type} not supported for delegation`);
272
+ }
273
+ subscription.items = await expandSubscriptionItems(subscription.id);
274
+ return {
275
+ paymentCurrency: subscription.paymentCurrency!,
276
+ paymentMethod: subscription.paymentMethod!,
277
+ subscription,
278
+ payerAddress: getSubscriptionPaymentAddress(subscription, subscription.paymentMethod?.type),
279
+ };
280
+ }
281
+
262
282
  type Args = {
263
283
  checkoutSession: CheckoutSession;
264
284
  customer: Customer;
@@ -689,7 +709,7 @@ export async function getDelegationTxClaim({
689
709
  const amount = getFastCheckoutAmount(items, mode, paymentCurrency.id);
690
710
  const address = toDelegateAddress(userDid, wallet.address);
691
711
  const tokenLimits = await getTokenLimitsForDelegation(items, paymentMethod, paymentCurrency, address, amount);
692
- const tokenRequirements = await getTokenRequirements({
712
+ let tokenRequirements = await getTokenRequirements({
693
713
  items,
694
714
  mode,
695
715
  trialing,
@@ -697,7 +717,9 @@ export async function getDelegationTxClaim({
697
717
  paymentMethod,
698
718
  paymentCurrency,
699
719
  });
700
-
720
+ if (mode === 'delegation') {
721
+ tokenRequirements = [];
722
+ }
701
723
  if (paymentMethod.type === 'arcblock') {
702
724
  return {
703
725
  type: 'DelegateTx',
@@ -884,7 +906,7 @@ export async function getTokenRequirements({
884
906
  }
885
907
 
886
908
  // Add stake requirement to token requirement
887
- if (paymentMethod.type === 'arcblock' || mode === 'setup') {
909
+ if ((paymentMethod.type === 'arcblock' && mode !== 'delegation') || mode === 'setup') {
888
910
  const staking = getSubscriptionStakeSetup(
889
911
  items,
890
912
  paymentCurrency.id,
@@ -48,7 +48,9 @@ const donationSchema = Joi.object<DonationSettings>({
48
48
  router.post('/', async (req, res) => {
49
49
  try {
50
50
  const payload = await donationSchema.validateAsync(req.body, { stripUnknown: true, convert: true });
51
- const link = await PaymentLink.findOne({ where: { 'donation_settings.target': payload.target } });
51
+ const link = await PaymentLink.findOne({
52
+ where: { 'donation_settings.target': payload.target, livemode: !!req.livemode },
53
+ });
52
54
  if (link) {
53
55
  await link.update({
54
56
  name: payload.title,
@@ -67,12 +69,13 @@ router.post('/', async (req, res) => {
67
69
  }
68
70
 
69
71
  logger.info('No existing payment link found, creating new one');
70
- let price = await Price.findByPkOrLookupKey(payload.target);
72
+ const lookupKey = `${payload.target}-${req.livemode ? 'live' : 'test'}`;
73
+ let price = await Price.findByPkOrLookupKey(lookupKey);
71
74
  if (!price) {
72
75
  logger.info('No existing price found, creating new product and price');
73
76
  const result = await createProductAndPrices({
74
77
  type: 'service',
75
- livemode: req.livemode,
78
+ livemode: !!req.livemode,
76
79
  name: payload.title,
77
80
  description: payload.description,
78
81
  currency_id: req.currency.id,
@@ -82,7 +85,7 @@ router.post('/', async (req, res) => {
82
85
  type: 'one_time',
83
86
  unit_amount: '0',
84
87
  billing_schema: 'per_unit',
85
- lookup_key: payload.target,
88
+ lookup_key: lookupKey,
86
89
  custom_unit_amount: {
87
90
  presets: payload.amount.presets || [],
88
91
  preset: payload.amount.preset || null,
@@ -1918,4 +1918,45 @@ router.get('/:id/overdue/invoices', authPortal, async (req, res) => {
1918
1918
  return res.status(400).json({ error: err.message });
1919
1919
  }
1920
1920
  });
1921
+
1922
+ router.get('/:id/delegation', authPortal, async (req, res) => {
1923
+ if (!req.user) {
1924
+ return res.status(403).json({ error: 'Unauthorized' });
1925
+ }
1926
+ try {
1927
+ const subscription = (await Subscription.findByPk(req.params.id, {
1928
+ include: [
1929
+ { model: PaymentCurrency, as: 'paymentCurrency' },
1930
+ { model: PaymentMethod, as: 'paymentMethod' },
1931
+ ],
1932
+ })) as (Subscription & { paymentMethod: PaymentMethod; paymentCurrency: PaymentCurrency }) | null;
1933
+ if (!subscription) {
1934
+ return res.status(404).json({ error: 'Subscription not found' });
1935
+ }
1936
+ const payer = getSubscriptionPaymentAddress(subscription, subscription.paymentMethod?.type);
1937
+ const delegator = await isDelegationSufficientForPayment({
1938
+ paymentMethod: subscription.paymentMethod,
1939
+ paymentCurrency: subscription.paymentCurrency,
1940
+ userDid: payer as string,
1941
+ amount: '0',
1942
+ });
1943
+ if (
1944
+ !delegator.sufficient &&
1945
+ [
1946
+ 'NO_DELEGATION',
1947
+ 'NO_TOKEN_PERMISSION',
1948
+ 'NO_TRANSFER_PERMISSION',
1949
+ 'NO_TRANSFER_TO',
1950
+ 'NO_ENOUGH_ALLOWANCE',
1951
+ 'NO_ENOUGH_TOKEN',
1952
+ ].includes(delegator?.reason || '')
1953
+ ) {
1954
+ return res.json(delegator);
1955
+ }
1956
+ return res.json(null);
1957
+ } catch (err) {
1958
+ console.error(err);
1959
+ return res.json(null);
1960
+ }
1961
+ });
1921
1962
  export default router;
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.16.10
17
+ version: 1.16.12
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.16.10",
3
+ "version": "1.16.12",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -53,7 +53,7 @@
53
53
  "@arcblock/validator": "^1.18.150",
54
54
  "@blocklet/js-sdk": "^1.16.33",
55
55
  "@blocklet/logger": "^1.16.33",
56
- "@blocklet/payment-react": "1.16.10",
56
+ "@blocklet/payment-react": "1.16.12",
57
57
  "@blocklet/sdk": "^1.16.33",
58
58
  "@blocklet/ui-react": "^2.10.74",
59
59
  "@blocklet/uploader": "^0.1.53",
@@ -120,7 +120,7 @@
120
120
  "devDependencies": {
121
121
  "@abtnode/types": "^1.16.33",
122
122
  "@arcblock/eslint-config-ts": "^0.3.3",
123
- "@blocklet/payment-types": "1.16.10",
123
+ "@blocklet/payment-types": "1.16.12",
124
124
  "@types/cookie-parser": "^1.4.7",
125
125
  "@types/cors": "^2.8.17",
126
126
  "@types/debug": "^4.1.12",
@@ -166,5 +166,5 @@
166
166
  "parser": "typescript"
167
167
  }
168
168
  },
169
- "gitHead": "ee0052f6d37257507d69b979ea11ae22ffd622b7"
169
+ "gitHead": "1af1e91e65dfb78b39d65a1d8d3396be83a9c04f"
170
170
  }
@@ -638,5 +638,12 @@ export default flat({
638
638
  intervals: 'intervals',
639
639
  history: 'Fund History',
640
640
  },
641
+ delegation: {
642
+ title:
643
+ 'Seems your delegation to this blocklet is revoked or insufficient, which will cause automatic payment failures for your subscription.',
644
+ btn: 'Delegate Now',
645
+ success: 'Delegate successful',
646
+ error: 'Delegate failed',
647
+ },
641
648
  },
642
649
  });
@@ -625,5 +625,11 @@ export default flat({
625
625
  intervals: '个周期',
626
626
  history: '充值记录',
627
627
  },
628
+ delegation: {
629
+ title: '检测到你未完成账户的授权,为了不影响订阅的扣费,请尽快完成授权',
630
+ btn: '支付授权',
631
+ success: '授权成功',
632
+ error: '授权失败',
633
+ },
628
634
  },
629
635
  });
@@ -199,7 +199,7 @@ export default function PaymentMethods() {
199
199
  </IconButton>
200
200
  }>
201
201
  <ListItemAvatar>
202
- <Avatar src={currency.logo} />
202
+ <Avatar src={currency.logo} alt={currency.name} />
203
203
  </ListItemAvatar>
204
204
  <ListItemText primary={currency.name} secondary={currency.description} />
205
205
  </ListItem>
@@ -104,6 +104,7 @@ export default function CustomerHome() {
104
104
  const { data, error, loading, runAsync } = useRequest(fetchData, {
105
105
  manual: true,
106
106
  });
107
+
107
108
  const countryDetail = useMemo(() => {
108
109
  const item = defaultCountries.find((v) => v[1] === data?.address?.country);
109
110
  return item ? parseCountry(item) : { name: '' };
@@ -465,24 +466,6 @@ export default function CustomerHome() {
465
466
  )}
466
467
  </Content>
467
468
  );
468
-
469
- // return (
470
- // <>
471
- // <ProgressBar pending={isPending} />
472
- // <Grid container spacing={5}>
473
- // <Grid item xs={12} md={8}>
474
- // <Root direction="column" spacing={3} sx={{ my: 2 }}>
475
-
476
- // </Root>
477
- // </Grid>
478
- // <Grid item xs={12} md={4}>
479
- // <Root direction="column" spacing={4} sx={{ my: 2 }}>
480
-
481
- // </Root>
482
- // </Grid>
483
- // </Grid>
484
- // </>
485
- // );
486
469
  }
487
470
 
488
471
  const Content = styled(Stack)`
@@ -1,13 +1,23 @@
1
1
  /* eslint-disable react/no-unstable-nested-components */
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
- import { CustomerInvoiceList, TxLink, api, formatTime, hasDelegateTxHash } from '@blocklet/payment-react';
3
+ import {
4
+ CustomerInvoiceList,
5
+ TxLink,
6
+ api,
7
+ formatError,
8
+ formatTime,
9
+ getPrefix,
10
+ hasDelegateTxHash,
11
+ usePaymentContext,
12
+ } from '@blocklet/payment-react';
4
13
  import type { TSubscriptionExpanded } from '@blocklet/payment-types';
5
14
  import { ArrowBackOutlined } from '@mui/icons-material';
6
- import { Alert, Box, Button, CircularProgress, Divider, Stack, Typography } from '@mui/material';
15
+ import { Alert, Box, Button, CircularProgress, Divider, Stack, Tooltip, Typography } from '@mui/material';
7
16
  import { useRequest } from 'ahooks';
8
17
  import { Link, useNavigate, useParams } from 'react-router-dom';
9
-
18
+ import Toast from '@arcblock/ux/lib/Toast';
10
19
  import { styled } from '@mui/system';
20
+ import { joinURL } from 'ufo';
11
21
  import Currency from '../../../components/currency';
12
22
  import CustomerLink from '../../../components/customer/link';
13
23
  import InfoRow from '../../../components/info-row';
@@ -22,6 +32,10 @@ const fetchData = (id: string | undefined): Promise<TSubscriptionExpanded> => {
22
32
  return api.get(`/api/subscriptions/${id}`).then((res) => res.data);
23
33
  };
24
34
 
35
+ const fetchSubscriptionDelegation = (id: string): Promise<{ sufficient: boolean }> => {
36
+ return api.get(`/api/subscriptions/${id}/delegation`).then((res) => res.data);
37
+ };
38
+
25
39
  const InfoDirection = 'column';
26
40
  const InfoAlignItems = 'flex-start';
27
41
 
@@ -31,6 +45,33 @@ export default function CustomerSubscriptionDetail() {
31
45
  const { t } = useLocaleContext();
32
46
  const { session } = useSessionContext();
33
47
  const { loading, error, data, refresh } = useRequest(() => fetchData(id));
48
+ const { connect } = usePaymentContext();
49
+
50
+ const { data: delegation = { sufficient: true }, runAsync: runDelegation } = useRequest(() =>
51
+ fetchSubscriptionDelegation(id)
52
+ );
53
+ const noDelegation = delegation && typeof delegation === 'object' && !delegation.sufficient;
54
+
55
+ const handleDelegate = () => {
56
+ connect.open({
57
+ containerEl: undefined as unknown as Element,
58
+ saveConnect: false,
59
+ action: 'delegation',
60
+ prefix: joinURL(getPrefix(), '/api/did'),
61
+ extraParams: { subscriptionId: id },
62
+ onSuccess: () => {
63
+ connect.close();
64
+ Toast.success(t('customer.delegation.success'));
65
+ runDelegation();
66
+ },
67
+ onClose: () => {
68
+ connect.close();
69
+ },
70
+ onError: (err: any) => {
71
+ Toast.error(formatError(err));
72
+ },
73
+ });
74
+ };
34
75
  if (data?.customer?.did && session?.user?.did && data.customer.did !== session.user.did) {
35
76
  return <Alert severity="error">You do not have permission to access other customer data</Alert>;
36
77
  }
@@ -62,6 +103,13 @@ export default function CustomerSubscriptionDetail() {
62
103
  </Typography>
63
104
  </Stack>
64
105
  <Stack direction="row" gap={1}>
106
+ {noDelegation && ['active', 'trialing', 'past_due'].includes(data?.status) && (
107
+ <Tooltip title={t('customer.delegation.title')}>
108
+ <Button variant="outlined" color="primary" onClick={() => handleDelegate()}>
109
+ {t('customer.delegation.btn')}
110
+ </Button>
111
+ </Tooltip>
112
+ )}
65
113
  <SubscriptionActions
66
114
  subscription={data}
67
115
  onChange={() => refresh()}