payment-kit 1.13.30 → 1.13.32

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 (45) hide show
  1. package/api/src/integrations/blockchain/nft.ts +0 -1
  2. package/api/src/integrations/blocklet/passport.ts +1 -1
  3. package/api/src/integrations/stripe/handlers/invoice.ts +44 -2
  4. package/api/src/integrations/stripe/handlers/payment-intent.ts +32 -8
  5. package/api/src/integrations/stripe/resource.ts +7 -4
  6. package/api/src/jobs/subscription.ts +1 -1
  7. package/api/src/libs/payment.ts +6 -1
  8. package/api/src/libs/session.ts +78 -27
  9. package/api/src/libs/util.ts +15 -0
  10. package/api/src/routes/checkout-sessions.ts +161 -20
  11. package/api/src/routes/connect/collect.ts +5 -9
  12. package/api/src/routes/connect/pay.ts +5 -9
  13. package/api/src/routes/connect/setup.ts +22 -10
  14. package/api/src/routes/connect/shared.ts +13 -10
  15. package/api/src/routes/connect/subscribe.ts +29 -20
  16. package/api/src/routes/invoices.ts +5 -1
  17. package/api/src/routes/payment-intents.ts +5 -1
  18. package/api/src/routes/payment-links.ts +3 -2
  19. package/api/src/routes/prices.ts +32 -21
  20. package/api/src/store/migrations/20231023-upsell.ts +11 -0
  21. package/api/src/store/models/index.ts +10 -2
  22. package/api/src/store/models/price.ts +89 -23
  23. package/api/src/store/models/types.ts +1 -0
  24. package/blocklet.yml +1 -1
  25. package/package.json +17 -17
  26. package/src/components/blockchain/tx.tsx +3 -1
  27. package/src/components/checkout/pay.tsx +39 -19
  28. package/src/components/checkout/product-card.tsx +2 -6
  29. package/src/components/checkout/product-item.tsx +84 -21
  30. package/src/components/checkout/summary.tsx +11 -2
  31. package/src/components/info-row.tsx +3 -1
  32. package/src/components/invoice/table.tsx +1 -1
  33. package/src/components/price/upsell-select.tsx +83 -0
  34. package/src/components/price/upsell.tsx +74 -0
  35. package/src/components/status.tsx +1 -1
  36. package/src/components/subscription/actions/cancel.tsx +25 -27
  37. package/src/components/subscription/items/index.tsx +1 -1
  38. package/src/libs/util.ts +51 -31
  39. package/src/locales/en.tsx +23 -2
  40. package/src/locales/zh.tsx +52 -40
  41. package/src/pages/admin/billing/index.tsx +3 -3
  42. package/src/pages/admin/customers/customers/detail.tsx +1 -0
  43. package/src/pages/admin/index.tsx +1 -0
  44. package/src/pages/admin/products/prices/detail.tsx +7 -0
  45. package/src/pages/customer/invoice.tsx +7 -6
@@ -1,7 +1,7 @@
1
1
  import { Router } from 'express';
2
2
  import Joi from 'joi';
3
3
  import pick from 'lodash/pick';
4
- import { Op, WhereOptions } from 'sequelize';
4
+ import type { WhereOptions } from 'sequelize';
5
5
 
6
6
  import { authenticate } from '../libs/security';
7
7
  import { isLineItemAligned } from '../libs/session';
@@ -141,7 +141,7 @@ const paginationSchema = Joi.object<{
141
141
  });
142
142
  router.get('/', auth, async (req, res) => {
143
143
  const { page, pageSize, ...query } = await paginationSchema.validateAsync(req.query, { stripUnknown: true });
144
- const where: WhereOptions<PaymentLink> = { id: { [Op.notIn]: [`plink_${req.user?.did}`] } };
144
+ const where: WhereOptions<PaymentLink> = {};
145
145
 
146
146
  if (typeof query.active === 'boolean') {
147
147
  where.active = query.active;
@@ -255,6 +255,7 @@ router.post('/stash', auth, async (req, res) => {
255
255
  raw.livemode = !!req.livemode;
256
256
  raw.created_via = req.user?.via;
257
257
  raw.currency_id = raw.currency_id || req.currency.id;
258
+ raw.metadata = { preview: '1' };
258
259
 
259
260
  let doc = await PaymentLink.findByPk(raw.id);
260
261
  if (doc) {
@@ -3,6 +3,7 @@ import { Router } from 'express';
3
3
  import pick from 'lodash/pick';
4
4
 
5
5
  import { authenticate } from '../libs/security';
6
+ import { canUpsell } from '../libs/session';
6
7
  import { PaymentCurrency } from '../store/models/payment-currency';
7
8
  import { Price } from '../store/models/price';
8
9
  import { Product } from '../store/models/product';
@@ -55,32 +56,42 @@ router.get('/:id', auth, async (req, res) => {
55
56
  });
56
57
 
57
58
  if (price) {
58
- const doc = price.toJSON();
59
- if (doc.currency_options) {
60
- const currencies = await PaymentCurrency.findAll();
61
- doc.currency_options.forEach((x) => {
59
+ const currencies = await PaymentCurrency.findAll();
60
+ const doc = Price.formatAfterRead(price.toJSON(), currencies);
61
+
62
+ if (doc.upsell?.upsells_to_id) {
63
+ const to = await Price.findByPk(doc.upsell.upsells_to_id);
64
+ if (to) {
62
65
  // @ts-ignore
63
- x.currency = currencies.find((c) => c.id === x.currency_id);
64
- });
65
- } else {
66
- doc.currency_options = [
67
- {
68
- currency_id: doc.currency_id,
69
- unit_amount: doc.unit_amount,
70
- // @ts-ignore
71
- currency: doc.currency,
72
- tiers: null,
73
- custom_unit_amount: null,
74
- },
75
- ];
66
+ doc.upsell.upsells_to = Price.formatAfterRead(to.toJSON(), currencies);
67
+ }
76
68
  }
69
+
77
70
  res.json(doc);
78
71
  } else {
79
72
  res.json(null);
80
73
  }
81
74
  });
82
75
 
76
+ router.get('/:id/upsell', auth, async (req, res) => {
77
+ const price = await Price.findByPkOrLookupKey(req.params.id as string, {
78
+ include: [{ model: PaymentCurrency, as: 'currency' }],
79
+ });
80
+
81
+ if (price) {
82
+ const prices = await Price.findAll({
83
+ where: { product_id: price.product_id },
84
+ include: [{ model: PaymentCurrency, as: 'currency' }],
85
+ });
86
+ const upsells = prices.filter((x) => canUpsell(price, x));
87
+ res.json(upsells);
88
+ } else {
89
+ res.json(null);
90
+ }
91
+ });
92
+
83
93
  // update price
94
+ // FIXME: upsell validate https://stripe.com/docs/payments/checkout/upsells
84
95
  router.put('/:id', auth, async (req, res) => {
85
96
  const price = await Price.findByPkOrLookupKey(req.params.id as string);
86
97
 
@@ -92,12 +103,12 @@ router.put('/:id', auth, async (req, res) => {
92
103
  return res.status(403).json({ error: 'price archived' });
93
104
  }
94
105
 
95
- const updates: Partial<Price> = Price.format(
106
+ const updates: Partial<Price> = Price.formatBeforeSave(
96
107
  pick(
97
108
  req.body,
98
109
  price.locked
99
- ? ['nickname', 'description', 'metadata']
100
- : ['type', 'model', 'active', 'livemode', 'nickname', 'recurring', 'description', 'tiers', 'unit_amount', 'transform_quantity', 'metadata', 'lookup_key', 'currency_options'] // prettier-ignore
110
+ ? ['nickname', 'description', 'metadata', 'upsell']
111
+ : ['type', 'model', 'active', 'livemode', 'nickname', 'recurring', 'description', 'tiers', 'unit_amount', 'transform_quantity', 'metadata', 'lookup_key', 'currency_options', 'upsell'] // prettier-ignore
101
112
  )
102
113
  );
103
114
 
@@ -140,7 +151,7 @@ router.put('/:id', auth, async (req, res) => {
140
151
  }
141
152
  }
142
153
 
143
- await price.update(Price.format(updates));
154
+ await price.update(Price.formatBeforeSave(updates));
144
155
 
145
156
  return res.json(price);
146
157
  });
@@ -0,0 +1,11 @@
1
+ import { DataTypes } from 'sequelize';
2
+
3
+ import type { Migration } from '../migrate';
4
+
5
+ export const up: Migration = async ({ context }) => {
6
+ await context.addColumn('prices', 'upsell', { type: DataTypes.JSON, allowNull: true });
7
+ };
8
+
9
+ export const down: Migration = async ({ context }) => {
10
+ await context.removeColumn('prices', 'upsell');
11
+ };
@@ -85,9 +85,17 @@ export * from './webhook-attempt';
85
85
  export * from './webhook-endpoint';
86
86
  export * from './types';
87
87
 
88
- export type TPriceExpanded = TPrice & { object: 'price'; product: TProduct; currency: TPaymentCurrency };
88
+ export type TPriceExpanded = TPrice & {
89
+ object: 'price';
90
+ product: TProduct;
91
+ currency: TPaymentCurrency;
92
+ upsell?: {
93
+ upsells_to: TPriceExpanded;
94
+ upsells_to_id: string;
95
+ };
96
+ };
89
97
 
90
- export type TLineItemExpanded = LineItem & { price: TPriceExpanded };
98
+ export type TLineItemExpanded = LineItem & { price: TPriceExpanded; upsell_price: TPriceExpanded };
91
99
 
92
100
  export type TProductExpanded = TProduct & { object: 'price'; prices: TPrice[]; default_price: TPrice };
93
101
 
@@ -20,6 +20,18 @@ import type { CustomUnitAmount, LineItem, PriceCurrency, PriceRecurring, PriceTi
20
20
 
21
21
  const nextId = createIdGenerator('price', 24);
22
22
 
23
+ // Duplicate here
24
+ type TPriceExpanded = TPrice & {
25
+ object: 'price';
26
+ product: any;
27
+ currency: TPaymentCurrency;
28
+ upsell?: {
29
+ upsells_to: TPriceExpanded;
30
+ upsells_to_id: string;
31
+ };
32
+ };
33
+ type TLineItemExpanded = LineItem & { price: TPriceExpanded; upsell_price: TPriceExpanded };
34
+
23
35
  // @link https://stripe.com/docs/api/prices
24
36
  export class Price extends Model<InferAttributes<Price>, InferCreationAttributes<Price>> {
25
37
  // Unique identifier for the object.
@@ -148,6 +160,10 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
148
160
  type: DataTypes.JSON,
149
161
  defaultValue: [],
150
162
  },
163
+ upsell: {
164
+ type: DataTypes.JSON,
165
+ allowNull: true,
166
+ },
151
167
  created_at: {
152
168
  type: DataTypes.DATE,
153
169
  defaultValue: DataTypes.NOW,
@@ -207,29 +223,33 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
207
223
  return 'standard';
208
224
  }
209
225
 
210
- public static format(price: Partial<TPrice & { model: string }>) {
211
- if (price.type === 'recurring') {
212
- if (!price.recurring) {
213
- throw new Error('recurring config is required for recurring prices');
226
+ public static formatBeforeSave(price: Partial<TPrice & { model: string }>) {
227
+ if (price.type) {
228
+ if (price.type === 'recurring') {
229
+ if (!price.recurring) {
230
+ throw new Error('recurring config is required for recurring prices');
231
+ }
232
+ price.recurring.interval_count = Number(price.recurring.interval_count);
233
+ } else {
234
+ price.recurring = null;
214
235
  }
215
- price.recurring.interval_count = Number(price.recurring.interval_count);
216
- } else {
217
- price.recurring = null;
218
236
  }
219
237
 
220
- if (price.model && ['graduated', 'volume'].includes(price.model)) {
221
- price.billing_scheme = 'tiered';
222
- price.tiers_mode = price.model;
223
- if (isEmpty(price.tiers)) {
224
- throw new Error('tiers is required for graduated and volume prices');
238
+ if (price.model) {
239
+ if (['graduated', 'volume'].includes(price.model)) {
240
+ price.billing_scheme = 'tiered';
241
+ price.tiers_mode = price.model;
242
+ if (isEmpty(price.tiers)) {
243
+ throw new Error('tiers is required for graduated and volume prices');
244
+ }
245
+ } else {
246
+ price.billing_scheme = 'per_unit';
247
+ price.tiers = null;
225
248
  }
226
- } else {
227
- price.billing_scheme = 'per_unit';
228
- price.tiers = null;
229
- }
230
249
 
231
- if (price.model !== 'package') {
232
- price.transform_quantity = null;
250
+ if (price.model !== 'package') {
251
+ price.transform_quantity = null;
252
+ }
233
253
  }
234
254
 
235
255
  price.metadata = formatMetadata(price.metadata);
@@ -237,6 +257,28 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
237
257
  return price;
238
258
  }
239
259
 
260
+ public static formatAfterRead(doc: TPrice, currencies: TPaymentCurrency[]) {
261
+ if (doc.currency_options) {
262
+ doc.currency_options.forEach((x) => {
263
+ // @ts-ignore
264
+ x.currency = currencies.find((c) => c.id === x.currency_id);
265
+ });
266
+ } else {
267
+ doc.currency_options = [
268
+ {
269
+ currency_id: doc.currency_id,
270
+ unit_amount: doc.unit_amount,
271
+ // @ts-ignore
272
+ currency: doc.currency,
273
+ tiers: null,
274
+ custom_unit_amount: null,
275
+ },
276
+ ];
277
+ }
278
+
279
+ return doc;
280
+ }
281
+
240
282
  public static formatCurrencies(options: PriceCurrency[], currencies: TPaymentCurrency[]) {
241
283
  return options.map((x) => {
242
284
  const currency = currencies.find((c) => c.id === x.currency_id);
@@ -254,13 +296,20 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
254
296
  });
255
297
  }
256
298
 
257
- public static async expand(items: LineItem[], deep: boolean = true): Promise<(LineItem & { price: TPrice })[]> {
258
- const priceIds: string[] = items.map((i) => i.price_id);
299
+ public static async expand(
300
+ items: LineItem[],
301
+ { product = true, upsell }: { product?: boolean; upsell?: boolean } = {}
302
+ ): Promise<TLineItemExpanded[]> {
303
+ const priceIds: string[] = items
304
+ .map((i) => i.price_id)
305
+ .concat(items.map((i) => i.upsell_price_id as string))
306
+ .filter(Boolean);
259
307
  const prices = await Price.findAll({
260
308
  where: { id: priceIds },
261
- include: deep ? [{ model: sequelize.models.Product, as: 'product' }] : [],
309
+ include: product ? [{ model: sequelize.models.Product, as: 'product' }] : [],
262
310
  });
263
311
 
312
+ // expand currency_options
264
313
  prices.forEach((x) => {
265
314
  if (!x.currency_options || x.currency_options?.length === 0) {
266
315
  x.currency_options = [
@@ -274,10 +323,27 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
274
323
  }
275
324
  });
276
325
 
326
+ // expand upsell
327
+ if (upsell) {
328
+ await Promise.all(
329
+ prices.map(async (x) => {
330
+ if (x.upsell?.upsells_to_id) {
331
+ const to = await Price.findByPk(x.upsell?.upsells_to_id);
332
+ if (to) {
333
+ // @ts-ignore
334
+ x.upsell.upsells_to = to;
335
+ }
336
+ }
337
+ })
338
+ );
339
+ }
340
+
341
+ // @ts-ignore
277
342
  return items.map((x) => ({
278
343
  ...x,
279
344
  price: prices.find((p) => p.id === x.price_id),
280
- })) as (LineItem & { price: TPrice })[];
345
+ upsell_price: x.upsell_price_id ? prices.find((p) => p.id === x.upsell_price_id) : null,
346
+ })) as TLineItemExpanded[];
281
347
  }
282
348
 
283
349
  public static async insert(price: TPrice & { model: string }) {
@@ -289,7 +355,7 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
289
355
  }
290
356
 
291
357
  // @ts-ignore
292
- return this.create(this.format(price));
358
+ return this.create(this.formatBeforeSave(price));
293
359
  }
294
360
 
295
361
  public static findByPkOrLookupKey(id: string, options: FindOptions<Price> = {}) {
@@ -146,6 +146,7 @@ export type LineItem = {
146
146
  maximum: number;
147
147
  minimum: number;
148
148
  };
149
+ upsell_price_id?: string;
149
150
  // TODO: following are not supported
150
151
  // price_data?: any;
151
152
  // dynamic_tax_rates?: any;
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.30
17
+ version: 1.13.32
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.30",
3
+ "version": "1.13.32",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev",
6
6
  "eject": "vite eject",
@@ -40,25 +40,25 @@
40
40
  ]
41
41
  },
42
42
  "dependencies": {
43
- "@arcblock/did": "^1.18.92",
43
+ "@arcblock/did": "^1.18.93",
44
44
  "@arcblock/did-auth-storage-nedb": "^1.7.1",
45
- "@arcblock/did-connect": "^2.7.28",
46
- "@arcblock/did-util": "^1.18.92",
47
- "@arcblock/ux": "^2.7.28",
48
- "@blocklet/logger": "1.16.17-beta-703fb879",
49
- "@blocklet/sdk": "1.16.17-beta-703fb879",
50
- "@blocklet/ui-react": "^2.7.28",
51
- "@blocklet/uploader": "^0.0.26",
45
+ "@arcblock/did-connect": "^2.8.2",
46
+ "@arcblock/did-util": "^1.18.93",
47
+ "@arcblock/ux": "^2.8.2",
48
+ "@blocklet/logger": "1.16.17-beta-952ef53d",
49
+ "@blocklet/sdk": "1.16.17-beta-952ef53d",
50
+ "@blocklet/ui-react": "^2.8.2",
51
+ "@blocklet/uploader": "^0.0.32",
52
52
  "@mui/icons-material": "^5.14.13",
53
53
  "@mui/lab": "^5.0.0-alpha.148",
54
54
  "@mui/material": "^5.14.13",
55
55
  "@mui/styles": "^5.14.13",
56
56
  "@mui/system": "^5.14.13",
57
- "@ocap/asset": "^1.18.92",
58
- "@ocap/client": "^1.18.92",
59
- "@ocap/mcrypto": "^1.18.92",
60
- "@ocap/util": "^1.18.92",
61
- "@ocap/wallet": "^1.18.92",
57
+ "@ocap/asset": "^1.18.93",
58
+ "@ocap/client": "^1.18.93",
59
+ "@ocap/mcrypto": "^1.18.93",
60
+ "@ocap/util": "^1.18.93",
61
+ "@ocap/wallet": "^1.18.93",
62
62
  "@stripe/react-stripe-js": "^2.3.1",
63
63
  "@stripe/stripe-js": "^2.1.7",
64
64
  "ahooks": "^3.7.8",
@@ -100,10 +100,10 @@
100
100
  "validator": "^13.11.0"
101
101
  },
102
102
  "devDependencies": {
103
- "@abtnode/types": "1.16.17-beta-703fb879",
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.30",
106
+ "@did-pay/types": "1.13.32",
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": "eb3b428db29ac33c697ca9ae3796d377ec779335"
143
+ "gitHead": "354fff1bd72e201fb51e8cf0ee6d26dbf5211b7c"
144
144
  }
@@ -50,7 +50,9 @@ export default function TxLink(props: { details: PaymentDetails; method: TPaymen
50
50
  return (
51
51
  <Link href={link} target="_blank" rel="noopener noreferrer">
52
52
  <Stack component="span" direction="row" alignItems="center" spacing={1}>
53
- <Typography component="span">{text}</Typography>
53
+ <Typography component="span" color="primary">
54
+ {text.length > 40 ? [text.slice(0, 8), text.slice(-8)].join('...') : text}
55
+ </Typography>
54
56
  <OpenInNewOutlined fontSize="small" />
55
57
  </Stack>
56
58
  </Link>
@@ -1,4 +1,5 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import Toast from '@arcblock/ux/lib/Toast';
2
3
  import type {
3
4
  TCheckoutSessionExpanded,
4
5
  TCustomer,
@@ -9,11 +10,13 @@ import type {
9
10
  } from '@did-pay/types';
10
11
  import { Box, Fade, Stack } from '@mui/material';
11
12
  import { styled } from '@mui/system';
13
+ import { useSetState } from 'ahooks';
12
14
  import { useEffect } from 'react';
13
15
  import { FormProvider, useForm } from 'react-hook-form';
14
16
 
15
17
  import { useSessionContext } from '../../contexts/session';
16
18
  import { useSettingsContext } from '../../contexts/settings';
19
+ import api from '../../libs/api';
17
20
  import { findCurrency, formatError, getStatementDescriptor, isValidCountry } from '../../libs/util';
18
21
  import PaymentError from './error';
19
22
  import CheckoutFooter from './footer';
@@ -57,6 +60,7 @@ export default function CheckoutPay({
57
60
  onPaid,
58
61
  onError,
59
62
  }: Props) {
63
+ const { t } = useLocaleContext();
60
64
  const { refresh, livemode, setLivemode } = useSettingsContext();
61
65
 
62
66
  useEffect(() => {
@@ -92,22 +96,12 @@ export default function CheckoutPay({
92
96
 
93
97
  // expired session
94
98
  if (checkoutSession.expires_at <= Math.round(Date.now() / 1000)) {
95
- return (
96
- <PaymentError
97
- title="Expired Link"
98
- description="This link has expired. This means that your payment has already been processed or your session has expired."
99
- />
100
- );
99
+ return <PaymentError title={t('checkout.expired.title')} description={t('checkout.expired.description')} />;
101
100
  }
102
101
 
103
102
  // completed session
104
103
  if (checkoutSession.status === 'complete') {
105
- return (
106
- <PaymentError
107
- title="Checkout Completed"
108
- description="This checkout session has completed. This means that your payment has already been successfully processed."
109
- />
110
- );
104
+ return <PaymentError title={t('checkout.complete.title')} description={t('checkout.complete.description')} />;
111
105
  }
112
106
 
113
107
  return (
@@ -154,8 +148,9 @@ export function CheckoutPayMain({
154
148
  const { t } = useLocaleContext();
155
149
  const { session } = useSessionContext();
156
150
  const { settings } = useSettingsContext();
151
+ const [state, setState] = useSetState({ checkoutSession });
157
152
 
158
- const defaultCurrencyId = checkoutSession?.currency_id || checkoutSession?.line_items[0]?.price.currency_id;
153
+ const defaultCurrencyId = state.checkoutSession.currency_id || state.checkoutSession.line_items[0]?.price.currency_id;
159
154
  const defaultMethodId = paymentMethods.find((m) => m.payment_currencies.some((c) => c.id === defaultCurrencyId))?.id;
160
155
 
161
156
  const methods = useForm({
@@ -185,30 +180,55 @@ export function CheckoutPayMain({
185
180
  (findCurrency(paymentMethods as TPaymentMethodExpanded[], currencyId as string) as TPaymentCurrency) ||
186
181
  settings.baseCurrency;
187
182
 
183
+ const onUpsell = async (from: string, to: string) => {
184
+ try {
185
+ const { data } = await api.put(`/api/checkout-sessions/${state.checkoutSession.id}/upsell`, { from, to });
186
+ setState({ checkoutSession: data });
187
+ } catch (err) {
188
+ console.error(err);
189
+ Toast.error(formatError(err));
190
+ }
191
+ };
192
+
193
+ const onDownsell = async (from: string) => {
194
+ try {
195
+ const { data } = await api.put(`/api/checkout-sessions/${state.checkoutSession.id}/downsell`, { from });
196
+ setState({ checkoutSession: data });
197
+ } catch (err) {
198
+ console.error(err);
199
+ Toast.error(formatError(err));
200
+ }
201
+ };
202
+
188
203
  return (
189
204
  <FormProvider {...methods}>
190
205
  <PaymentRoot>
191
206
  <Stack direction="row" className="cko-container">
192
207
  <Fade in>
193
208
  <Stack className="cko-overview" direction="column">
194
- <PaymentHeader checkoutSession={checkoutSession} />
195
- <PaymentSummary checkoutSession={checkoutSession} currency={currency} />
209
+ <PaymentHeader checkoutSession={state.checkoutSession} />
210
+ <PaymentSummary
211
+ checkoutSession={state.checkoutSession}
212
+ currency={currency}
213
+ onUpsell={onUpsell}
214
+ onDownsell={onDownsell}
215
+ />
196
216
  </Stack>
197
217
  </Fade>
198
218
  <Stack className="cko-payment" direction="column" spacing={4}>
199
219
  {completed && (
200
220
  <PaymentSuccess
201
- payee={getStatementDescriptor(checkoutSession.line_items)}
202
- action={checkoutSession.mode}
221
+ payee={getStatementDescriptor(state.checkoutSession.line_items)}
222
+ action={state.checkoutSession.mode}
203
223
  message={
204
224
  paymentLink?.after_completion?.hosted_confirmation?.custom_message ||
205
- t(`checkout.completed.${checkoutSession.mode}`)
225
+ t(`checkout.completed.${state.checkoutSession.mode}`)
206
226
  }
207
227
  />
208
228
  )}
209
229
  {!completed && (
210
230
  <PaymentForm
211
- checkoutSession={checkoutSession}
231
+ checkoutSession={state.checkoutSession}
212
232
  paymentMethods={paymentMethods as TPaymentMethodExpanded[]}
213
233
  paymentIntent={paymentIntent}
214
234
  customer={customer}
@@ -25,15 +25,11 @@ export default function ProductCard({ size, variant, name, logo, description, ex
25
25
  </Avatar>
26
26
  )}
27
27
  <Stack direction="column" alignItems="flex-start" justifyContent="space-around">
28
- <Typography variant="body1" sx={{ fontWeight: 500 }} color="text.primary" gutterBottom>
28
+ <Typography variant="body1" sx={{ fontWeight: 500 }} color="text.primary">
29
29
  {name}
30
30
  </Typography>
31
31
  {description && (
32
- <Typography
33
- variant="body1"
34
- sx={{ fontSize: '0.85rem', lineHeight: '120%' }}
35
- color="text.secondary"
36
- gutterBottom>
32
+ <Typography variant="body1" sx={{ fontSize: '0.85rem', lineHeight: '120%' }} color="text.secondary">
37
33
  {description}
38
34
  </Typography>
39
35
  )}