payment-kit 1.13.45 → 1.13.47

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 (31) hide show
  1. package/api/dev.ts +1 -1
  2. package/api/src/libs/session.ts +1 -1
  3. package/api/src/routes/prices.ts +21 -18
  4. package/api/src/routes/pricing-table.ts +3 -1
  5. package/blocklet.yml +1 -1
  6. package/package.json +20 -20
  7. package/src/components/checkout/form/index.tsx +17 -14
  8. package/src/components/checkout/form/user-buttons.tsx +24 -0
  9. package/src/components/checkout/header.tsx +22 -5
  10. package/src/components/checkout/pay.tsx +50 -11
  11. package/src/components/checkout/product-item.tsx +7 -7
  12. package/src/components/checkout/summary.tsx +17 -4
  13. package/src/components/livemode.tsx +1 -1
  14. package/src/components/portal/invoice/list.tsx +16 -6
  15. package/src/components/portal/subscription/list.tsx +9 -1
  16. package/src/components/product/cross-sell-select.tsx +2 -2
  17. package/src/components/product/cross-sell.tsx +2 -1
  18. package/src/components/section/header.tsx +2 -0
  19. package/src/components/status.tsx +1 -1
  20. package/src/libs/util.ts +52 -31
  21. package/src/locales/en.tsx +36 -11
  22. package/src/locales/index.tsx +25 -0
  23. package/src/locales/zh.tsx +33 -9
  24. package/src/pages/admin/payments/links/detail.tsx +2 -1
  25. package/src/pages/admin/products/prices/detail.tsx +11 -3
  26. package/src/pages/admin/products/prices/list.tsx +4 -2
  27. package/src/pages/admin/products/pricing-tables/detail.tsx +2 -1
  28. package/src/pages/admin/products/products/create.tsx +2 -2
  29. package/src/pages/admin/products/products/detail.tsx +2 -2
  30. package/src/pages/admin/products/products/index.tsx +2 -2
  31. package/src/pages/checkout/pricing-table.tsx +66 -16
package/api/dev.ts CHANGED
@@ -3,4 +3,4 @@ import { setupClient } from 'vite-plugin-blocklet';
3
3
 
4
4
  import { app } from './src';
5
5
 
6
- setupClient(app);
6
+ setupClient(app, { port: 47069 });
@@ -287,7 +287,7 @@ export function getFastCheckoutAmount(
287
287
  mode: string,
288
288
  currency: TPaymentCurrency,
289
289
  includeFreeTrial = false,
290
- minimumCycle = 2
290
+ minimumCycle = 1
291
291
  ) {
292
292
  if (minimumCycle < 1) {
293
293
  // eslint-disable-next-line no-param-reassign
@@ -1,4 +1,4 @@
1
- import { fromTokenToUnit } from '@ocap/util';
1
+ import { fromTokenToUnit, fromUnitToToken } from '@ocap/util';
2
2
  import { Router } from 'express';
3
3
  import Joi from 'joi';
4
4
  import pick from 'lodash/pick';
@@ -114,20 +114,20 @@ router.get('/:id/upsell', auth, async (req, res) => {
114
114
  // update price
115
115
  // FIXME: upsell validate https://stripe.com/docs/payments/checkout/upsells
116
116
  router.put('/:id', auth, async (req, res) => {
117
- const price = await Price.findByPkOrLookupKey(req.params.id as string);
117
+ const doc = await Price.findByPkOrLookupKey(req.params.id as string);
118
118
 
119
- if (!price) {
119
+ if (!doc) {
120
120
  return res.status(404).json({ error: 'price not found' });
121
121
  }
122
122
 
123
- if (price.active === false) {
123
+ if (doc.active === false) {
124
124
  return res.status(403).json({ error: 'price archived' });
125
125
  }
126
126
 
127
127
  const updates: Partial<Price> = Price.formatBeforeSave(
128
128
  pick(
129
129
  req.body,
130
- price.locked
130
+ doc.locked
131
131
  ? ['nickname', 'description', 'metadata', 'currency_options', 'upsell']
132
132
  : ['type', 'model', 'active', 'livemode', 'nickname', 'recurring', 'description', 'tiers', 'unit_amount', 'transform_quantity', 'metadata', 'lookup_key', 'currency_options', 'upsell'] // prettier-ignore
133
133
  )
@@ -135,38 +135,41 @@ router.put('/:id', auth, async (req, res) => {
135
135
 
136
136
  if (updates.lookup_key) {
137
137
  const exist = await Price.findOne({ where: { lookup_key: updates.lookup_key } });
138
- if (exist && exist.id !== price.id) {
138
+ if (exist && exist.id !== doc.id) {
139
139
  return res.status(400).json({ error: `lookup_key ${updates.lookup_key} already used by ${exist.id}` });
140
140
  }
141
141
  }
142
142
 
143
143
  const currencies = await PaymentCurrency.findAll({ where: { active: true } });
144
- const currency = currencies.find((x) => x.id === price.currency_id);
144
+ const currency = currencies.find((x) => x.id === doc.currency_id);
145
145
  if (!currency) {
146
- return res.status(400).json({ error: `currency used in price not found or not active: ${price.currency_id}` });
146
+ return res.status(400).json({ error: `currency used in price not found or not active: ${doc.currency_id}` });
147
147
  }
148
148
  if (updates.unit_amount) {
149
149
  updates.unit_amount = fromTokenToUnit(updates.unit_amount, currency.decimal).toString();
150
+ if (updates.currency_options) {
151
+ const exist = updates.currency_options.find((x) => x.currency_id === doc.currency_id);
152
+ if (exist) {
153
+ exist.unit_amount = fromUnitToToken(updates.unit_amount as string, currency.decimal);
154
+ }
155
+ }
150
156
  }
151
157
  if (updates.currency_options) {
152
158
  updates.currency_options = Price.formatCurrencies(updates.currency_options, currencies);
153
- if (updates.currency_options.some((x) => x.currency_id === price.currency_id) === false) {
159
+ const index = updates.currency_options.findIndex((x) => x.currency_id === doc.currency_id);
160
+ if (index > -1) {
161
+ updates.unit_amount = updates.currency_options[index]?.unit_amount;
162
+ } else {
154
163
  updates.currency_options.unshift({
155
- currency_id: price.currency_id,
156
- unit_amount: price.unit_amount,
164
+ currency_id: doc.currency_id,
165
+ unit_amount: doc.unit_amount,
157
166
  tiers: null,
158
167
  custom_unit_amount: null,
159
168
  });
160
169
  }
161
- if (updates.unit_amount) {
162
- const base = price.currency_options.find((x) => x.currency_id === price.currency_id);
163
- if (base) {
164
- base.unit_amount = updates.unit_amount;
165
- }
166
- }
167
170
  }
168
171
 
169
- await price.update(Price.formatBeforeSave(updates));
172
+ await doc.update(Price.formatBeforeSave(updates));
170
173
 
171
174
  return res.json(await getExpandedPrice(req.params.id as string));
172
175
  });
@@ -344,9 +344,11 @@ router.post('/:id/checkout/:priceId', async (req, res) => {
344
344
  },
345
345
  });
346
346
 
347
+ const currency = await PaymentCurrency.findOne({ where: { livemode: doc.livemode, is_base_currency: true } });
348
+
347
349
  raw.livemode = doc.livemode;
348
350
  raw.created_via = 'portal';
349
- raw.currency_id = req.currency.id;
351
+ raw.currency_id = currency?.id;
350
352
 
351
353
  if (req.query.redirect) {
352
354
  raw.success_url = req.query.redirect as string;
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.45
17
+ version: 1.13.47
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.45",
3
+ "version": "1.13.47",
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.93",
43
+ "@arcblock/did": "^1.18.95",
44
44
  "@arcblock/did-auth-storage-nedb": "^1.7.1",
45
- "@arcblock/did-connect": "^2.8.6",
46
- "@arcblock/did-util": "^1.18.93",
47
- "@arcblock/ux": "^2.8.6",
45
+ "@arcblock/did-connect": "^2.8.7",
46
+ "@arcblock/did-util": "^1.18.95",
47
+ "@arcblock/ux": "^2.8.7",
48
48
  "@blocklet/logger": "1.16.17",
49
49
  "@blocklet/sdk": "1.16.17",
50
- "@blocklet/ui-react": "^2.8.6",
50
+ "@blocklet/ui-react": "^2.8.7",
51
51
  "@blocklet/uploader": "^0.0.33",
52
- "@mui/icons-material": "^5.14.15",
53
- "@mui/lab": "^5.0.0-alpha.150",
54
- "@mui/material": "^5.14.15",
55
- "@mui/styles": "^5.14.15",
56
- "@mui/system": "^5.14.15",
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",
52
+ "@mui/icons-material": "^5.14.16",
53
+ "@mui/lab": "^5.0.0-alpha.151",
54
+ "@mui/material": "^5.14.16",
55
+ "@mui/styles": "^5.14.16",
56
+ "@mui/system": "^5.14.16",
57
+ "@ocap/asset": "^1.18.95",
58
+ "@ocap/client": "^1.18.95",
59
+ "@ocap/mcrypto": "^1.18.95",
60
+ "@ocap/util": "^1.18.95",
61
+ "@ocap/wallet": "^1.18.95",
62
62
  "@stripe/react-stripe-js": "^2.3.1",
63
63
  "@stripe/stripe-js": "^2.1.10",
64
64
  "ahooks": "^3.7.8",
@@ -88,7 +88,7 @@
88
88
  "react-error-boundary": "^4.0.11",
89
89
  "react-hook-form": "^7.47.0",
90
90
  "react-international-phone": "^3.1.2",
91
- "react-router-dom": "^6.17.0",
91
+ "react-router-dom": "^6.18.0",
92
92
  "rimraf": "^3.0.2",
93
93
  "sequelize": "^6.33.0",
94
94
  "sqlite3": "^5.1.6",
@@ -103,12 +103,12 @@
103
103
  "@abtnode/types": "1.16.17",
104
104
  "@arcblock/eslint-config": "^0.2.4",
105
105
  "@arcblock/eslint-config-ts": "^0.2.4",
106
- "@did-pay/types": "1.13.45",
106
+ "@did-pay/types": "1.13.47",
107
107
  "@types/cookie-parser": "^1.4.5",
108
108
  "@types/cors": "^2.8.15",
109
109
  "@types/dotenv-flow": "^3.3.2",
110
110
  "@types/express": "^4.17.20",
111
- "@types/node": "^18.18.7",
111
+ "@types/node": "^18.18.8",
112
112
  "@types/react": "^18.2.33",
113
113
  "@types/react-dom": "^18.2.14",
114
114
  "@vitejs/plugin-react": "^2.2.0",
@@ -140,5 +140,5 @@
140
140
  "parser": "typescript"
141
141
  }
142
142
  },
143
- "gitHead": "6dacad185a8f180bbb63ec062aa196dc07c56535"
143
+ "gitHead": "e917c1f665d97114b37ddb566329a1f9c6a9ba6b"
144
144
  }
@@ -1,13 +1,12 @@
1
1
  import 'react-international-phone/style.css';
2
2
 
3
- import SessionManager from '@arcblock/did-connect/lib/SessionManager';
4
3
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
5
- import LocaleSelector from '@arcblock/ux/lib/Locale/selector';
4
+ import { useTheme } from '@arcblock/ux/lib/Theme';
6
5
  import Toast from '@arcblock/ux/lib/Toast';
7
6
  import type { TCheckoutSessionExpanded, TCustomer, TPaymentIntent, TPaymentMethodExpanded } from '@did-pay/types';
8
7
  import { LoadingButton } from '@mui/lab';
9
- import { Avatar, Fade, InputAdornment, MenuItem, Select, Stack, Tooltip, Typography } from '@mui/material';
10
- import { useSetState } from 'ahooks';
8
+ import { Avatar, Fade, InputAdornment, MenuItem, Select, Stack, Typography } from '@mui/material';
9
+ import { useCreation, useSetState, useSize } from 'ahooks';
11
10
  import { PhoneNumberUtil } from 'google-libphonenumber';
12
11
  import pWaitFor from 'p-wait-for';
13
12
  import { useEffect } from 'react';
@@ -21,6 +20,7 @@ import FormInput from '../../input';
21
20
  import AddressForm from './address';
22
21
  import PhoneInput from './phone';
23
22
  import StripeCheckout from './stripe';
23
+ import UserButtons from './user-buttons';
24
24
 
25
25
  const phoneUtil = PhoneNumberUtil.getInstance();
26
26
 
@@ -72,6 +72,7 @@ export default function PaymentForm({
72
72
  onPaid,
73
73
  onError,
74
74
  }: PageData) {
75
+ const theme = useTheme();
75
76
  const { t } = useLocaleContext();
76
77
  const { session, connectApi } = useSessionContext();
77
78
  const { control, getValues, setValue, handleSubmit } = useFormContext();
@@ -112,6 +113,17 @@ export default function PaymentForm({
112
113
  const paymentCurrency = useWatch({ control, name: 'payment_currency' });
113
114
  const paymentCurrencies = paymentMethods.find((x) => x.id === paymentMethod)?.payment_currencies || [];
114
115
 
116
+ const domSize = useSize(document.body);
117
+
118
+ const isCloumnLayout = useCreation(() => {
119
+ if (domSize) {
120
+ if (domSize?.width <= theme.breakpoints.values.md) {
121
+ return true;
122
+ }
123
+ }
124
+ return false;
125
+ }, [domSize, theme]);
126
+
115
127
  const payee = getStatementDescriptor(checkoutSession.line_items);
116
128
  const buttonText = session.user
117
129
  ? t(`checkout.${checkoutSession.mode}`)
@@ -249,16 +261,7 @@ export default function PaymentForm({
249
261
  <Stack className="cko-payment-contact">
250
262
  <Stack direction="row" sx={{ mb: 1 }} alignItems="center" justifyContent="space-between">
251
263
  <Typography sx={{ color: 'text.primary', fontWeight: 600 }}>{t('checkout.contact')}</Typography>
252
- <Stack direction="row" alignItems="center" justifyContent="space-between">
253
- <LocaleSelector showText={false} />
254
- {session.user ? (
255
- <SessionManager session={session} />
256
- ) : (
257
- <Tooltip title={t('checkout.login')} arrow>
258
- <SessionManager session={session} />
259
- </Tooltip>
260
- )}
261
- </Stack>
264
+ {isCloumnLayout ? null : <UserButtons />}
262
265
  </Stack>
263
266
  <Stack direction="column" className="cko-payment-form" spacing={0}>
264
267
  <FormInput
@@ -0,0 +1,24 @@
1
+ import SessionManager from '@arcblock/did-connect/lib/SessionManager';
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import LocaleSelector from '@arcblock/ux/lib/Locale/selector';
4
+ import { Stack, Tooltip } from '@mui/material';
5
+
6
+ import { useSessionContext } from '../../../contexts/session';
7
+
8
+ export default function UserButtons() {
9
+ const { t } = useLocaleContext();
10
+ const { session } = useSessionContext();
11
+
12
+ return (
13
+ <Stack direction="row" alignItems="center" justifyContent="space-between">
14
+ <LocaleSelector showText={false} />
15
+ {session.user ? (
16
+ <SessionManager session={session} />
17
+ ) : (
18
+ <Tooltip title={t('checkout.login')} arrow>
19
+ <SessionManager session={session} />
20
+ </Tooltip>
21
+ )}
22
+ </Stack>
23
+ );
24
+ }
@@ -1,9 +1,11 @@
1
+ import { useTheme } from '@arcblock/ux/lib/Theme';
1
2
  import type { TCheckoutSessionExpanded } from '@did-pay/types';
2
3
  import { Avatar, Stack, Typography } from '@mui/material';
3
- import { useLocalStorageState } from 'ahooks';
4
+ import { useCreation, useLocalStorageState, useSize } from 'ahooks';
4
5
 
5
6
  import { getStatementDescriptor } from '../../libs/util';
6
7
  import Livemode from '../livemode';
8
+ import UserButtons from './form/user-buttons';
7
9
 
8
10
  type Props = {
9
11
  checkoutSession: TCheckoutSessionExpanded;
@@ -12,12 +14,27 @@ type Props = {
12
14
  export default function PaymentHeader({ checkoutSession }: Props) {
13
15
  const [livemode] = useLocalStorageState('livemode', { defaultValue: true });
14
16
  const brand = getStatementDescriptor(checkoutSession.line_items);
17
+ const theme = useTheme();
18
+
19
+ const domSize = useSize(document.body);
20
+
21
+ const isCloumnLayout = useCreation(() => {
22
+ if (domSize) {
23
+ if (domSize?.width <= theme.breakpoints.values.md) {
24
+ return true;
25
+ }
26
+ }
27
+ return false;
28
+ }, [domSize, theme]);
15
29
 
16
30
  return (
17
- <Stack className="cko-header" direction="row" spacing={1} alignItems="center">
18
- <Avatar variant="square" src={window.blocklet.appLogo} sx={{ width: 32, height: 32 }} />
19
- <Typography sx={{ color: 'text.primary', fontWeight: 600 }}>{brand}</Typography>
20
- {!livemode && <Livemode />}
31
+ <Stack className="cko-header" direction="row" spacing={1} alignItems="center" justifyContent="space-between">
32
+ <Stack direction="row" spacing={1} alignItems="center">
33
+ <Avatar variant="square" src={window.blocklet.appLogo} sx={{ width: 32, height: 32 }} />
34
+ <Typography sx={{ color: 'text.primary', fontWeight: 600 }}>{brand}</Typography>
35
+ {!livemode && <Livemode />}
36
+ </Stack>
37
+ {isCloumnLayout ? <UserButtons /> : null}
21
38
  </Stack>
22
39
  );
23
40
  }
@@ -81,7 +81,7 @@ export default function CheckoutPay({
81
81
  if (!checkoutSession) {
82
82
  return (
83
83
  <PaymentRoot>
84
- <Stack direction="row" className="cko-container">
84
+ <Stack className="cko-container">
85
85
  <Stack className="cko-overview">
86
86
  <OverviewSkeleton />
87
87
  </Stack>
@@ -223,7 +223,7 @@ export function CheckoutPayMain({
223
223
  return (
224
224
  <FormProvider {...methods}>
225
225
  <PaymentRoot>
226
- <Stack direction="row" className="cko-container">
226
+ <Stack className="cko-container">
227
227
  <Fade in>
228
228
  <Stack className="cko-overview" direction="column">
229
229
  <PaymentHeader checkoutSession={state.checkoutSession} />
@@ -269,14 +269,16 @@ export function CheckoutPayMain({
269
269
  export const PaymentRoot = styled(Box)`
270
270
  box-sizing: border-box;
271
271
  display: flex;
272
- flex-wrap: nowrap;
272
+ flex-direction: column;
273
273
  justify-content: center;
274
- min-height: 0;
274
+ align-items: center;
275
+ min-height: 100vh;
276
+ position: relative;
275
277
 
276
278
  &:before {
277
279
  animation-fill-mode: both;
278
280
  background: #ffffff;
279
- content: ' ';
281
+ content: '';
280
282
  height: 100%;
281
283
  position: fixed;
282
284
  right: 0;
@@ -288,21 +290,30 @@ export const PaymentRoot = styled(Box)`
288
290
 
289
291
  .cko-container {
290
292
  width: 100%;
291
- max-width: 920px;
293
+ max-width: 1000px;
292
294
  display: flex;
293
- align-items: flex-start;
295
+ flex-direction: row;
294
296
  justify-content: space-between;
295
297
  position: relative;
296
- transform: translateY(max(48px, calc(50vh - 55%)));
298
+ padding: 0 16px;
297
299
  }
298
300
 
299
301
  .cko-overview {
300
- width: 380px;
302
+ width: 400px;
301
303
  min-height: 540px;
304
+ position: relative;
305
+ }
306
+ .cko-header {
307
+ left: 0;
308
+ margin-bottom: 0;
309
+ position: absolute;
310
+ right: 0;
311
+ top: 0;
312
+ transition: background-color 0.15s ease, box-shadow 0.15s ease-out;
302
313
  }
303
314
 
304
315
  .cko-payment {
305
- width: 380px;
316
+ width: 400px;
306
317
  .MuiInputBase-root {
307
318
  border-radius: 0;
308
319
  }
@@ -337,10 +348,38 @@ export const PaymentRoot = styled(Box)`
337
348
  }
338
349
  }
339
350
 
351
+ .cko-header {
352
+ }
353
+
340
354
  .cko-footer {
341
355
  position: absolute;
342
356
  bottom: 0;
343
- left: 0;
357
+ left: 12px;
344
358
  margin: 12px 0;
345
359
  }
360
+
361
+ @media (max-width: ${({ theme }) => theme.breakpoints.values.md}px) {
362
+ &:before {
363
+ display: none;
364
+ }
365
+ .cko-container {
366
+ flex-direction: column;
367
+ align-items: center;
368
+ gap: 24px;
369
+ min-width: 350px;
370
+ max-width: 400px;
371
+ }
372
+ .cko-overview {
373
+ width: 100%;
374
+ min-height: auto;
375
+ }
376
+ .cko-payment {
377
+ width: 100%;
378
+ }
379
+
380
+ .cko-footer {
381
+ position: static;
382
+ margin-top: 0;
383
+ }
384
+ }
346
385
  `;
@@ -23,10 +23,10 @@ ProductItem.defaultProps = {
23
23
  };
24
24
 
25
25
  export default function ProductItem({ item, session, currency, mode, children, onUpsell, onDownsell }: Props) {
26
- const { t } = useLocaleContext();
27
- const pricing = formatLineItemPricing(item, currency, session.subscription_data?.trial_period_days || 0);
26
+ const { t, locale } = useLocaleContext();
27
+ const pricing = formatLineItemPricing(item, currency, session.subscription_data?.trial_period_days || 0, locale);
28
28
  const saving = formatUpsellSaving(session, currency);
29
- const metered = item.price?.recurring?.usage_type === 'metered' ? ' based on usage' : '';
29
+ const metered = item.price?.recurring?.usage_type === 'metered' ? t('common.metered') : '';
30
30
  const canUpsell = mode === 'normal' && session.line_items.length === 1;
31
31
  return (
32
32
  <Stack direction="column" alignItems="flex-start" spacing={1} sx={{ width: '100%' }}>
@@ -38,7 +38,7 @@ export default function ProductItem({ item, session, currency, mode, children, o
38
38
  description={item.price.product?.description}
39
39
  extra={
40
40
  item.price.type === 'recurring' && item.price.recurring
41
- ? [pricing.quantity, `billed ${formatRecurring(item.upsell_price?.recurring || item.price.recurring)} ${metered}`].filter(Boolean).join(', ') // prettier-ignore
41
+ ? [pricing.quantity, t('common.billed', { rule: `${formatRecurring(item.upsell_price?.recurring || item.price.recurring, true, 'per', locale)} ${metered}` })].filter(Boolean).join(', ') // prettier-ignore
42
42
  : pricing.quantity
43
43
  }
44
44
  />
@@ -71,12 +71,12 @@ export default function ProductItem({ item, session, currency, mode, children, o
71
71
  onChange={() => onUpsell(item.price_id, item.price.upsell?.upsells_to_id)}
72
72
  />
73
73
  {t('checkout.upsell.save', {
74
- recurring: t(`common.${formatRecurring(item.price.upsell.upsells_to.recurring as PriceRecurring)}`),
74
+ recurring: formatRecurring(item.price.upsell.upsells_to.recurring as PriceRecurring, true, 'per', locale),
75
75
  })}
76
76
  <Status label={t('checkout.upsell.off', { saving })} color="primary" variant="outlined" sx={{ ml: 1 }} />
77
77
  </Typography>
78
78
  <Typography component="span" sx={{ fontSize: 12 }}>
79
- {formatPrice(item.price.upsell.upsells_to, currency, item.price.product?.unit_label)}
79
+ {formatPrice(item.price.upsell.upsells_to, currency, item.price.product?.unit_label, 1, true, locale)}
80
80
  </Typography>
81
81
  </Stack>
82
82
  )}
@@ -102,7 +102,7 @@ export default function ProductItem({ item, session, currency, mode, children, o
102
102
  })}
103
103
  </Typography>
104
104
  <Typography component="span" sx={{ fontSize: 12 }}>
105
- {formatPrice(item.price, currency, item.price.product?.unit_label)}
105
+ {formatPrice(item.price, currency, item.price.product?.unit_label, 1, true, locale)}
106
106
  </Typography>
107
107
  </Stack>
108
108
  )}
@@ -41,10 +41,10 @@ export default function PaymentSummary({
41
41
  onApplyCrossSell,
42
42
  onCancelCrossSell,
43
43
  }: Props) {
44
- const { t } = useLocaleContext();
44
+ const { t, locale } = useLocaleContext();
45
45
  const [state, setState] = useSetState({ loading: false });
46
46
  const { data, runAsync } = useRequest(() => fetchCrossSell(checkoutSession.id));
47
- const headlines = formatCheckoutHeadlines(checkoutSession, currency);
47
+ const headlines = formatCheckoutHeadlines(checkoutSession, currency, locale);
48
48
 
49
49
  const handleUpsell = async (from: string, to: string) => {
50
50
  await onUpsell(from, to);
@@ -82,7 +82,12 @@ export default function PaymentSummary({
82
82
 
83
83
  return (
84
84
  <Fade in>
85
- <Stack className="cko-product" direction="column" sx={{ mt: 4 }}>
85
+ <Stack
86
+ className="cko-product"
87
+ direction="column"
88
+ sx={{
89
+ mt: 8,
90
+ }}>
86
91
  <Stack className="cko-product-summary" sx={{ mb: 4 }}>
87
92
  <Typography sx={{ fontWeight: 500, fontSize: '1.15rem', color: 'text.secondary' }}>
88
93
  {headlines.action}
@@ -120,7 +125,15 @@ export default function PaymentSummary({
120
125
  direction="column"
121
126
  alignItems="flex-end"
122
127
  spacing={0.5}
123
- sx={{ border: '1px solid #eee', borderRadius: 1, padding: 1, mt: 8, mb: 4 }}>
128
+ sx={{
129
+ border: '1px solid #eee',
130
+ borderRadius: 1,
131
+ padding: 1,
132
+ mt: {
133
+ xs: 4,
134
+ md: 8,
135
+ },
136
+ }}>
124
137
  <ProductItem
125
138
  item={{ quantity: 1, price: data, price_id: data.id, cross_sell: true } as TLineItemExpanded}
126
139
  session={checkoutSession}
@@ -10,7 +10,7 @@ export default function Livemode() {
10
10
  sx={{
11
11
  ml: 2,
12
12
  height: 18,
13
- lineHeight: 18,
13
+ lineHeight: 1,
14
14
  textTransform: 'uppercase',
15
15
  fontSize: '0.8rem',
16
16
  fontWeight: 'bold',
@@ -4,7 +4,6 @@ import type { Paginated, TInvoiceExpanded } from '@did-pay/types';
4
4
  import { Box, Button, CircularProgress, Stack, Typography } from '@mui/material';
5
5
  import { fromUnitToToken } from '@ocap/util';
6
6
  import { useInfiniteScroll } from 'ahooks';
7
- import React from 'react';
8
7
  import { Link } from 'react-router-dom';
9
8
 
10
9
  import api from '../../../libs/api';
@@ -63,12 +62,23 @@ export default function CustomerInvoiceList({ customer_id }: Props) {
63
62
  const grouped = groupByDate(data.list);
64
63
 
65
64
  return (
66
- <Stack direction="column" spacing={3} sx={{ mt: 1 }}>
65
+ <Stack direction="column" gap={3} sx={{ mt: 1 }}>
67
66
  {Object.entries(grouped).map(([date, invoices]) => (
68
- <React.Fragment key={date}>
69
- <Typography sx={{ fontWeight: 'bold', color: 'text.secondary', mt: 2 }}>{date}</Typography>
67
+ <Box key={date}>
68
+ <Typography sx={{ fontWeight: 'bold', color: 'text.secondary', mt: 2, mb: 1 }}>{date}</Typography>
70
69
  {invoices.map((invoice) => (
71
- <Stack key={invoice.id} direction="row" justifyContent="space-between" spacing={3} flexWrap="nowrap">
70
+ <Stack
71
+ key={invoice.id}
72
+ direction={{
73
+ xs: 'column',
74
+ sm: 'row',
75
+ }}
76
+ gap={{
77
+ xs: 0.5,
78
+ sm: 1.5,
79
+ md: 4,
80
+ }}
81
+ flexWrap="nowrap">
72
82
  <Box flex={2}>
73
83
  <Link to={`/customer/invoice/${invoice.id}`}>
74
84
  <Typography component="span">{invoice.number}</Typography>
@@ -91,7 +101,7 @@ export default function CustomerInvoiceList({ customer_id }: Props) {
91
101
  </Box>
92
102
  </Stack>
93
103
  ))}
94
- </React.Fragment>
104
+ </Box>
95
105
  ))}
96
106
  <Box>
97
107
  {hasMore && (
@@ -99,7 +99,15 @@ export function CurrentSubscriptionsInner({ id, onChange }: Props) {
99
99
  return (
100
100
  <Stack direction="column" spacing={4} sx={{ mt: 2 }}>
101
101
  {data.list.map((subscription) => (
102
- <Stack key={subscription.id} direction="row" justifyContent="space-between" spacing={3}>
102
+ <Stack
103
+ key={subscription.id}
104
+ direction="row"
105
+ justifyContent="space-between"
106
+ gap={{
107
+ xs: 1,
108
+ sm: 3,
109
+ }}
110
+ flexWrap="wrap">
103
111
  <Stack direction="column" spacing={0.5}>
104
112
  <Stack direction="row" spacing={1} alignItems="center">
105
113
  <AvatarGroup max={3}>
@@ -13,7 +13,7 @@ type Props = {
13
13
  };
14
14
 
15
15
  export default function CrossSellSelect({ data, onSelect }: Props) {
16
- const { t } = useLocaleContext();
16
+ const { t, locale } = useLocaleContext();
17
17
  const { products } = useProductsContext();
18
18
  const { settings } = useSettingsContext();
19
19
 
@@ -41,7 +41,7 @@ export default function CrossSellSelect({ data, onSelect }: Props) {
41
41
  <MenuItem key={x.id} value={x.id}>
42
42
  <InfoCard
43
43
  name={x.name}
44
- description={formatProductPrice(x as any, settings.baseCurrency)}
44
+ description={formatProductPrice(x as any, settings.baseCurrency, locale)}
45
45
  logo={x.images[0]}
46
46
  />
47
47
  </MenuItem>
@@ -14,6 +14,7 @@ import InfoRow from '../info-row';
14
14
  import CrossSellSelect from './cross-sell-select';
15
15
 
16
16
  export function CrossSellForm({ data, onChange }: { data: TProductExpanded; onChange: Function }) {
17
+ const { locale } = useLocaleContext();
17
18
  const { settings } = useSettingsContext();
18
19
  const [state, setState] = useSetState({
19
20
  loading: false,
@@ -55,7 +56,7 @@ export function CrossSellForm({ data, onChange }: { data: TProductExpanded; onCh
55
56
  <Stack spacing={1} direction="row" alignItems="center">
56
57
  <InfoCard
57
58
  name={to.name}
58
- description={formatProductPrice(to as any, settings.baseCurrency)}
59
+ description={formatProductPrice(to as any, settings.baseCurrency, locale)}
59
60
  logo={to.images[0]}
60
61
  />
61
62
  <IconButton size="small" sx={{ ml: 1 }} onClick={onRemoveUpsell}>