payment-kit 1.13.34 → 1.13.36

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.
@@ -205,44 +205,44 @@ router.post('/', auth, async (req, res) => {
205
205
  });
206
206
 
207
207
  export async function startCheckoutSessionFromPaymentLink(id: string, req: Request, res: Response) {
208
- const link = await PaymentLink.findByPk(id);
209
- if (!link) {
210
- res.status(400).json({ error: 'Payment link not found, please contact the source of the payment link.' });
211
- return;
212
- }
213
- if (!link.active) {
214
- res.status(400).json({ error: 'Payment link archived, we can not create new checkout session.' });
215
- return;
216
- }
208
+ try {
209
+ const link = await PaymentLink.findByPk(id);
210
+ if (!link) {
211
+ res.status(400).json({ error: 'Payment link not found, please contact the source of the payment link.' });
212
+ return;
213
+ }
214
+ if (!link.active) {
215
+ res.status(400).json({ error: 'Payment link archived, we can not create new checkout session.' });
216
+ return;
217
+ }
217
218
 
218
- const items = await Price.expand(link.line_items, { upsell: true });
219
-
220
- const raw: Partial<CheckoutSession> = await formatCheckoutSession(link, false);
221
- raw.livemode = link.livemode;
222
- raw.created_via = 'portal';
223
- raw.currency_id = link.currency_id || req.currency.id;
224
- raw.payment_link_id = link.id;
225
-
226
- if (link.after_completion?.hosted_confirmation?.custom_message) {
227
- raw.payment_intent_data = {
228
- description: link.after_completion?.hosted_confirmation?.custom_message,
229
- };
230
- } else {
231
- raw.payment_intent_data = {
232
- // TODO: bake default into this
233
- description: 'Thanks for your purchase',
234
- };
235
- }
236
- if (link.after_completion?.redirect?.url) {
237
- raw.success_url = link.after_completion?.redirect?.url;
238
- }
219
+ const items = await Price.expand(link.line_items, { upsell: true });
239
220
 
240
- if (req.query.redirect) {
241
- raw.success_url = req.query.redirect as string;
242
- raw.cancel_url = req.query.redirect as string;
243
- }
221
+ const raw: Partial<CheckoutSession> = await formatCheckoutSession(link, false);
222
+ raw.livemode = link.livemode;
223
+ raw.created_via = 'portal';
224
+ raw.currency_id = link.currency_id || req.currency.id;
225
+ raw.payment_link_id = link.id;
226
+
227
+ if (link.after_completion?.hosted_confirmation?.custom_message) {
228
+ raw.payment_intent_data = {
229
+ description: link.after_completion?.hosted_confirmation?.custom_message,
230
+ };
231
+ } else {
232
+ raw.payment_intent_data = {
233
+ // TODO: bake default into this
234
+ description: 'Thanks for your purchase',
235
+ };
236
+ }
237
+ if (link.after_completion?.redirect?.url) {
238
+ raw.success_url = link.after_completion?.redirect?.url;
239
+ }
240
+
241
+ if (req.query.redirect) {
242
+ raw.success_url = req.query.redirect as string;
243
+ raw.cancel_url = req.query.redirect as string;
244
+ }
244
245
 
245
- try {
246
246
  let doc;
247
247
  if (req.query.preview === '1') {
248
248
  doc = await CheckoutSession.findOne({ where: { payment_link_id: link.id, metadata: { preview: '1' } } });
@@ -1,6 +1,8 @@
1
1
  import { fromTokenToUnit } from '@ocap/util';
2
2
  import { Router } from 'express';
3
+ import Joi from 'joi';
3
4
  import pick from 'lodash/pick';
5
+ import type { WhereOptions } from 'sequelize';
4
6
 
5
7
  import { authenticate } from '../libs/security';
6
8
  import { canUpsell } from '../libs/session';
@@ -213,4 +215,63 @@ router.delete('/:id', auth, async (req, res) => {
213
215
  return res.json(price);
214
216
  });
215
217
 
218
+ // list products and prices
219
+ const paginationSchema = Joi.object<{
220
+ page: number;
221
+ pageSize: number;
222
+ livemode?: boolean;
223
+ active?: boolean;
224
+ type?: string;
225
+ currency_id?: string;
226
+ product_id?: string;
227
+ lookup_key?: string;
228
+ }>({
229
+ page: Joi.number().integer().min(1).default(1),
230
+ pageSize: Joi.number().integer().min(1).max(100).default(20),
231
+ livemode: Joi.boolean().empty(''),
232
+ active: Joi.boolean().empty(''),
233
+ type: Joi.string().empty(''),
234
+ currency_id: Joi.string().empty(''),
235
+ product_id: Joi.string().empty(''),
236
+ lookup_key: Joi.string().empty(''),
237
+ });
238
+ router.get('/', auth, async (req, res) => {
239
+ const { page, pageSize, active, livemode, ...query } = await paginationSchema.validateAsync(req.query, {
240
+ stripUnknown: false,
241
+ allowUnknown: true,
242
+ });
243
+ const where: WhereOptions<Price> = {};
244
+
245
+ if (typeof active === 'boolean') {
246
+ where.active = active;
247
+ }
248
+ if (typeof livemode === 'boolean') {
249
+ where.livemode = livemode;
250
+ }
251
+ ['type', 'currency_id', 'product_id', 'lookup_key'].forEach((key: string) => {
252
+ // @ts-ignore
253
+ if (query[key]) {
254
+ // @ts-ignore
255
+ where[key] = query[key].split(',').map((x: string) => x.trim()).filter(Boolean); // prettier-ignore
256
+ }
257
+ });
258
+
259
+ Object.keys(query)
260
+ .filter((x) => x.startsWith('recurring.'))
261
+ .forEach((key: string) => {
262
+ // @ts-ignore
263
+ where[key] = query[key];
264
+ });
265
+
266
+ const { rows, count } = await Price.findAndCountAll({
267
+ where,
268
+ attributes: ['id'],
269
+ order: [['created_at', 'DESC']],
270
+ offset: (page - 1) * pageSize,
271
+ limit: pageSize,
272
+ });
273
+
274
+ res.json({ count, list: await Promise.all(rows.map((x) => getExpandedPrice(x.id))) });
275
+ });
276
+
216
277
  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.13.34
17
+ version: 1.13.36
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.34",
3
+ "version": "1.13.36",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev",
6
6
  "eject": "vite eject",
@@ -103,7 +103,7 @@
103
103
  "@abtnode/types": "1.16.17-beta-952ef53d",
104
104
  "@arcblock/eslint-config": "^0.2.4",
105
105
  "@arcblock/eslint-config-ts": "^0.2.4",
106
- "@did-pay/types": "1.13.34",
106
+ "@did-pay/types": "1.13.36",
107
107
  "@types/cookie-parser": "^1.4.4",
108
108
  "@types/cors": "^2.8.14",
109
109
  "@types/dotenv-flow": "^3.3.1",
@@ -140,5 +140,5 @@
140
140
  "parser": "typescript"
141
141
  }
142
142
  },
143
- "gitHead": "136d14107082c64e9fdd822dc3f815cb5c8497b4"
143
+ "gitHead": "9f921f0988c77ca3d0e9c6f383631f6e61fcd32f"
144
144
  }
@@ -15,7 +15,7 @@ export default function PaymentHeader({ checkoutSession }: Props) {
15
15
 
16
16
  return (
17
17
  <Stack className="cko-header" direction="row" spacing={1} alignItems="center">
18
- <Avatar src={window.blocklet.appLogo} sx={{ width: 32, height: 32 }} />
18
+ <Avatar variant="square" src={window.blocklet.appLogo} sx={{ width: 32, height: 32 }} />
19
19
  <Typography sx={{ color: 'text.primary', fontWeight: 600 }}>{brand}</Typography>
20
20
  {!livemode && <Livemode />}
21
21
  </Stack>
@@ -13,16 +13,18 @@ type Props = {
13
13
  addons: React.ReactNode;
14
14
  children: React.ReactNode;
15
15
  width?: number;
16
+ open?: boolean;
16
17
  style?: Record<string, any>;
17
18
  };
18
19
 
19
20
  DrawerForm.defaultProps = {
20
21
  style: {},
22
+ open: false,
21
23
  width: 960,
22
24
  };
23
25
 
24
26
  export default function DrawerForm(props: Props) {
25
- const [open, setOpen] = useState(false);
27
+ const [open, setOpen] = useState(props.open);
26
28
  const settings = useSettingsContext();
27
29
 
28
30
  useBus('drawer.submitted', () => setOpen(false), []);
@@ -3,6 +3,7 @@ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import { Checkbox, FormControlLabel, Stack, TextField, Typography } from '@mui/material';
4
4
  import { useEffect, useState } from 'react';
5
5
  import { Controller, useFieldArray, useFormContext, useWatch } from 'react-hook-form';
6
+ import { useSearchParams } from 'react-router-dom';
6
7
 
7
8
  import { useProductsContext } from '../../contexts/products';
8
9
  import { getProductByPriceId, isPriceAligned } from '../../libs/util';
@@ -12,6 +13,7 @@ import ProductSelect from './product-select';
12
13
 
13
14
  export default function BeforePay() {
14
15
  const { t } = useLocaleContext();
16
+ const [params, setParams] = useSearchParams();
15
17
  const { products, refresh } = useProductsContext();
16
18
  const { control, setValue, getValues } = useFormContext();
17
19
  const items = useFieldArray({ control, name: 'line_items' });
@@ -47,6 +49,15 @@ export default function BeforePay() {
47
49
  }
48
50
  };
49
51
 
52
+ useEffect(() => {
53
+ if (params.get('price_id') && getValues().line_items.length === 0) {
54
+ onProductSelected(params.get('price_id') as string);
55
+ params.set('price_id', '');
56
+ setParams(params);
57
+ }
58
+ // eslint-disable-next-line react-hooks/exhaustive-deps
59
+ }, []);
60
+
50
61
  const onProductCreated = () => {
51
62
  setState({ creating: false });
52
63
  refresh();
@@ -3,6 +3,7 @@ import { Checkbox, FormControlLabel, MenuItem, Select, Stack, Typography } from
3
3
  import { useSetState } from 'ahooks';
4
4
  import { useEffect } from 'react';
5
5
  import { Controller, useFieldArray, useFormContext, useWatch } from 'react-hook-form';
6
+ import { useSearchParams } from 'react-router-dom';
6
7
 
7
8
  import { useProductsContext } from '../../contexts/products';
8
9
  import { getProductByPriceId, groupPricingTableItems, isPriceCurrencyAligned } from '../../libs/util';
@@ -12,6 +13,7 @@ import ProductItem from './product-item';
12
13
 
13
14
  export default function PricingTableProductSettings() {
14
15
  const { t } = useLocaleContext();
16
+ const [params, setParams] = useSearchParams();
15
17
  const { products, refresh } = useProductsContext();
16
18
  const { control, setValue, getValues } = useFormContext();
17
19
  const items = useFieldArray({ control, name: 'items' });
@@ -79,6 +81,15 @@ export default function PricingTableProductSettings() {
79
81
  }
80
82
  };
81
83
 
84
+ useEffect(() => {
85
+ if (params.get('price_id') && products.length && getValues().items.length === 0) {
86
+ onProductSelected(params.get('price_id') as string);
87
+ params.set('price_id', '');
88
+ setParams(params);
89
+ }
90
+ // eslint-disable-next-line react-hooks/exhaustive-deps
91
+ }, [products.length]);
92
+
82
93
  const onProductCreated = () => {
83
94
  setState({ creating: false });
84
95
  refresh();
package/src/libs/util.ts CHANGED
@@ -626,8 +626,8 @@ export function formatSubscriptionProduct(items: TSubscriptionItemExpanded[], ma
626
626
  );
627
627
  }
628
628
 
629
- export function formatAmount(amount: string, decimals: number, points = 2) {
630
- return Number(fromUnitToToken(amount, decimals)).toFixed(points);
629
+ export function formatAmount(amount: string, decimals: number) {
630
+ return fromUnitToToken(amount, decimals);
631
631
  }
632
632
 
633
633
  export function findCurrency(methods: TPaymentMethodExpanded[], currencyId: string) {
@@ -72,6 +72,7 @@ export default flat({
72
72
  paymentMethods: 'Payment methods',
73
73
  customers: 'Customers',
74
74
  products: 'Products',
75
+ pricing: 'Pricing',
75
76
  coupons: 'Coupons',
76
77
  pricingTables: 'Pricing tables',
77
78
  billing: 'Billing',
@@ -73,6 +73,7 @@ export default flat({
73
73
  customers: '客户管理',
74
74
  products: '产品定价',
75
75
  coupons: '优惠券',
76
+ pricing: '定价',
76
77
  pricingTables: '价格表',
77
78
  billing: '订阅和发票',
78
79
  invoices: '发票',
@@ -7,6 +7,7 @@ import { AddOutlined } from '@mui/icons-material';
7
7
  import { Button, Stack, Typography } from '@mui/material';
8
8
  import { useEffect, useState } from 'react';
9
9
  import { FormProvider, useForm } from 'react-hook-form';
10
+ import { useSearchParams } from 'react-router-dom';
10
11
  import { dispatch } from 'use-bus';
11
12
 
12
13
  import DrawerForm from '../../../../components/drawer-form';
@@ -25,6 +26,7 @@ type PaymentLink = InferFormType<TPaymentLink> & {
25
26
 
26
27
  export default function CreatePaymentLink() {
27
28
  const { t } = useLocaleContext();
29
+ const [params] = useSearchParams();
28
30
  const { session } = useSessionContext();
29
31
  const [current, setCurrent] = useState('beforePay');
30
32
  const [stashed, setStashed] = useState(0);
@@ -128,6 +130,7 @@ export default function CreatePaymentLink() {
128
130
  <DrawerForm
129
131
  icon={<AddOutlined />}
130
132
  text={t('admin.paymentLink.add')}
133
+ open={!!params.get('price_id')}
131
134
  width={1280}
132
135
  addons={
133
136
  // @ts-ignore
@@ -2,6 +2,8 @@ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
2
  import Toast from '@arcblock/ux/lib/Toast';
3
3
  import type { TPrice } from '@did-pay/types';
4
4
  import { useSetState } from 'ahooks';
5
+ import noop from 'lodash/noop';
6
+ import { useNavigate } from 'react-router-dom';
5
7
 
6
8
  import Actions from '../../../../components/actions';
7
9
  import ConfirmDialog from '../../../../components/confirm';
@@ -21,12 +23,13 @@ PriceActions.defaultProps = {
21
23
  setAsDefault: false,
22
24
  };
23
25
 
24
- export default function PriceActions(props: Props) {
26
+ export default function PriceActions({ data, onChange, variant, setAsDefault }: Props) {
25
27
  const { t } = useLocaleContext();
28
+ const navigate = useNavigate();
26
29
 
27
- const canEdit = props.data.active;
28
- const canArchive = props.data.active;
29
- const canRemove = !props.data.locked && !props.setAsDefault;
30
+ const canEdit = data.active;
31
+ const canArchive = data.active;
32
+ const canRemove = !data.locked && !setAsDefault;
30
33
 
31
34
  const [state, setState] = useSetState({
32
35
  action: '',
@@ -36,9 +39,9 @@ export default function PriceActions(props: Props) {
36
39
  const onEditPrice = async (updates: TPrice) => {
37
40
  try {
38
41
  setState({ loading: true });
39
- await api.put(`/api/prices/${props.data.id}`, updates).then((res) => res.data);
42
+ await api.put(`/api/prices/${data.id}`, updates).then((res) => res.data);
40
43
  Toast.success(t('common.saved'));
41
- props.onChange(state.action);
44
+ onChange(state.action);
42
45
  } catch (err) {
43
46
  console.error(err);
44
47
  Toast.error(formatError(err));
@@ -49,9 +52,9 @@ export default function PriceActions(props: Props) {
49
52
  const onArchivePrice = async () => {
50
53
  try {
51
54
  setState({ loading: true });
52
- await api.put(`/api/prices/${props.data.id}/archive`).then((res) => res.data);
55
+ await api.put(`/api/prices/${data.id}/archive`).then((res) => res.data);
53
56
  Toast.success(t('common.saved'));
54
- props.onChange(state.action);
57
+ onChange(state.action);
55
58
  } catch (err) {
56
59
  console.error(err);
57
60
  Toast.error(formatError(err));
@@ -62,9 +65,9 @@ export default function PriceActions(props: Props) {
62
65
  const onRemovePrice = async () => {
63
66
  try {
64
67
  setState({ loading: true });
65
- await api.delete(`/api/prices/${props.data.id}`).then((res) => res.data);
68
+ await api.delete(`/api/prices/${data.id}`).then((res) => res.data);
66
69
  Toast.success(t('common.removed'));
67
- props.onChange(state.action);
70
+ onChange(state.action);
68
71
  } catch (err) {
69
72
  console.error(err);
70
73
  Toast.error(formatError(err));
@@ -75,11 +78,9 @@ export default function PriceActions(props: Props) {
75
78
  const onSetAsDefault = async () => {
76
79
  try {
77
80
  setState({ loading: true });
78
- await api
79
- .put(`/api/products/${props.data.product_id}`, { default_price_id: props.data.id })
80
- .then((res) => res.data);
81
+ await api.put(`/api/products/${data.product_id}`, { default_price_id: data.id }).then((res) => res.data);
81
82
  Toast.success(t('common.removed'));
82
- props.onChange(state.action);
83
+ onChange(state.action);
83
84
  } catch (err) {
84
85
  console.error(err);
85
86
  Toast.error(formatError(err));
@@ -88,7 +89,21 @@ export default function PriceActions(props: Props) {
88
89
  }
89
90
  };
90
91
 
92
+ const onCreatePaymentLink = () => {
93
+ navigate(`/admin/payments/links?price_id=${data.id}`);
94
+ };
95
+
96
+ const onCreatePricingTable = () => {
97
+ navigate(`/admin/products/pricing-tables?price_id=${data.id}`);
98
+ };
99
+
91
100
  const actions = [
101
+ {
102
+ label: t('admin.pricing'),
103
+ handler: noop,
104
+ color: 'text.primary',
105
+ disabled: true,
106
+ },
92
107
  {
93
108
  label: t('admin.price.edit'),
94
109
  handler: () => setState({ action: 'edit' }),
@@ -100,35 +115,40 @@ export default function PriceActions(props: Props) {
100
115
  handler: () => setState({ action: 'archive' }),
101
116
  color: canArchive ? 'text.primary' : 'text.disabled',
102
117
  disabled: !canArchive,
103
- divider: true,
104
118
  },
105
119
  {
106
120
  label: t('admin.price.remove'),
107
121
  handler: () => setState({ action: 'remove' }),
108
122
  color: canRemove ? 'error.main' : 'text.disabled',
109
123
  disabled: !canRemove,
110
- divider: props.setAsDefault,
124
+ divider: !setAsDefault,
125
+ },
126
+ {
127
+ label: t('admin.payments'),
128
+ handler: noop,
129
+ color: 'text.primary',
130
+ disabled: true,
111
131
  },
112
- // FIXME: add these features later
113
- // { label: 'Create payment link', handler: props.onRemove, color: 'error', divider: true },
114
- // { label: 'Create pricing table', handler: props.onRemove, color: 'error', divider: true },
132
+ { label: 'Create payment link', handler: onCreatePaymentLink, color: 'primary' },
133
+ { label: 'Create pricing table', handler: onCreatePricingTable, color: 'primary' },
115
134
  ];
116
135
 
117
- if (props.setAsDefault) {
118
- actions.push({
136
+ if (setAsDefault) {
137
+ actions.splice(4, 0, {
119
138
  label: t('admin.price.setAsDefault'),
120
139
  handler: onSetAsDefault,
121
140
  color: 'text.primary',
122
141
  disabled: false,
142
+ divider: true,
123
143
  });
124
144
  }
125
145
 
126
146
  return (
127
147
  <>
128
- <Actions variant={props.variant} actions={actions} />
148
+ <Actions variant={variant} actions={actions} />
129
149
  {state.action === 'edit' && (
130
150
  // @ts-ignore
131
- <EditPrice price={props.data} onSave={onEditPrice} onCancel={() => setState({ action: '' })} />
151
+ <EditPrice price={data} onSave={onEditPrice} onCancel={() => setState({ action: '' })} />
132
152
  )}
133
153
  {state.action === 'archive' && (
134
154
  <ConfirmDialog
@@ -6,6 +6,7 @@ import { AddOutlined } from '@mui/icons-material';
6
6
  import { Box, Button, Stack, Typography } from '@mui/material';
7
7
  import { useEffect, useState } from 'react';
8
8
  import { FormProvider, useForm } from 'react-hook-form';
9
+ import { useSearchParams } from 'react-router-dom';
9
10
  import { dispatch } from 'use-bus';
10
11
 
11
12
  import DrawerForm from '../../../../components/drawer-form';
@@ -20,6 +21,7 @@ import { formatError } from '../../../../libs/util';
20
21
 
21
22
  export default function CreatePricingTable() {
22
23
  const { t } = useLocaleContext();
24
+ const [params] = useSearchParams();
23
25
  const { session } = useSessionContext();
24
26
  const [step, setStep] = useState(0); // ['products', 'payment', 'portal']
25
27
  const [stashed, setStashed] = useState(0);
@@ -91,6 +93,7 @@ export default function CreatePricingTable() {
91
93
  <DrawerForm
92
94
  icon={<AddOutlined />}
93
95
  text={t('admin.pricingTable.add')}
96
+ open={!!params.get('price_id')}
94
97
  width={1280}
95
98
  addons={
96
99
  // @ts-ignore
@@ -135,4 +135,8 @@ export default function CustomerHome() {
135
135
  );
136
136
  }
137
137
 
138
- const Root = styled(Stack)``;
138
+ const Root = styled(Stack)`
139
+ a {
140
+ text-decoration: underline;
141
+ }
142
+ `;