payment-kit 1.13.46 → 1.13.48

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/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 });
@@ -198,15 +198,14 @@ export async function getCheckoutSessionAmounts(checkoutSession: CheckoutSession
198
198
 
199
199
  export async function ensureCheckoutSessionOpen(req: Request, res: Response, next: NextFunction) {
200
200
  const doc = await CheckoutSession.findByPk(req.params.id);
201
-
202
201
  if (!doc) {
203
- return res.status(404).json({ error: 'Checkout session not found' });
202
+ return res.status(404).json({ code: 'CHECKOUT_NOT_FOUND', error: 'Checkout session not found' });
204
203
  }
205
204
  if (doc.status === 'complete') {
206
- return res.status(403).json({ error: 'Checkout session completed' });
205
+ return res.status(403).json({ code: 'CHECKOUT_COMPLETED', error: 'Checkout session completed' });
207
206
  }
208
207
  if (doc.status === 'expired') {
209
- return res.status(403).json({ error: 'Checkout session already expired' });
208
+ return res.status(403).json({ code: 'CHECKOUT_EXPIRED', error: 'Checkout session already expired' });
210
209
  }
211
210
 
212
211
  req.doc = doc;
@@ -405,7 +404,7 @@ router.get('/retrieve/:id', user, async (req, res) => {
405
404
  router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
406
405
  try {
407
406
  if (!req.user) {
408
- return res.status(403).json({ error: 'Please login to continue' });
407
+ return res.status(403).json({ code: 'REQUIRE_LOGIN', error: 'Please login to continue' });
409
408
  }
410
409
 
411
410
  const checkoutSession = req.doc as CheckoutSession;
@@ -416,7 +415,9 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
416
415
  const result = await getCrossSellItem(checkoutSession);
417
416
  // @ts-ignore
418
417
  if (result.id) {
419
- return res.status(400).json({ error: 'Please select cross sell product to continue' });
418
+ return res
419
+ .status(400)
420
+ .json({ code: 'REQUIRE_CROSS_SELL', error: 'Please select cross sell product to continue' });
420
421
  }
421
422
  }
422
423
  }
@@ -496,13 +497,13 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
496
497
  if (paymentIntent) {
497
498
  // Check payment intent, if we have a payment intent, we should not create a new one
498
499
  if (paymentIntent.status === 'succeeded') {
499
- return res.status(403).json({ error: 'Checkout session payment completed' });
500
+ return res.status(403).json({ code: 'PAYMENT_SUCCEEDED', error: 'Checkout session payment completed' });
500
501
  }
501
502
  if (paymentIntent.status === 'canceled') {
502
- return res.status(403).json({ error: 'Checkout session payment canceled' });
503
+ return res.status(403).json({ code: 'PAYMENT_CANCELLED', error: 'Checkout session payment canceled' });
503
504
  }
504
505
  if (paymentIntent.status === 'processing') {
505
- return res.status(403).json({ error: 'Checkout session payment processing' });
506
+ return res.status(403).json({ code: 'PAYMENT_PROCESSING', error: 'Checkout session payment processing' });
506
507
  }
507
508
  paymentIntent = await paymentIntent.update({
508
509
  amount: checkoutSession.amount_total,
@@ -553,13 +554,13 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
553
554
 
554
555
  if (setupIntent) {
555
556
  if (setupIntent.status === 'succeeded') {
556
- return res.status(403).json({ error: 'Checkout session setup completed' });
557
+ return res.status(403).json({ code: 'SETUP_SUCCEEDED', error: 'Checkout session setup completed' });
557
558
  }
558
559
  if (setupIntent.status === 'canceled') {
559
- return res.status(403).json({ error: 'Checkout session setup canceled' });
560
+ return res.status(403).json({ code: 'SETUP_CANCELED', error: 'Checkout session setup canceled' });
560
561
  }
561
562
  if (setupIntent.status === 'processing') {
562
- return res.status(403).json({ error: 'Checkout session setup processing' });
563
+ return res.status(403).json({ code: 'SETUP_PROCESSING', error: 'Checkout session setup processing' });
563
564
  }
564
565
  await setupIntent.update({
565
566
  customer_id: customer.id,
@@ -597,7 +598,9 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
597
598
  }
598
599
  if (subscription) {
599
600
  if (subscription.status !== 'incomplete') {
600
- return res.status(403).json({ error: 'Checkout session subscription status unexpected' });
601
+ return res
602
+ .status(403)
603
+ .json({ code: 'SUBSCRIPTION_INVALID', error: 'Checkout session subscription status unexpected' });
601
604
  }
602
605
  subscription = await subscription.update({
603
606
  currency_id: paymentCurrency.id,
@@ -768,7 +771,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
768
771
  return res.json({ paymentIntent, setupIntent, stripeContext, subscription, checkoutSession, customer, delegation });
769
772
  } catch (err) {
770
773
  console.error(err);
771
- res.status(500).json({ error: err.message });
774
+ res.status(500).json({ code: err.code, error: err.message });
772
775
  }
773
776
  });
774
777
 
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.46
17
+ version: 1.13.48
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.46",
3
+ "version": "1.13.48",
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.46",
106
+ "@did-pay/types": "1.13.48",
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": "7c37d3e6f4814056ddc98033b257707fbefba2d9"
143
+ "gitHead": "55d5b96dd4ab54fb3a4400cb7b78924db51de16d"
144
144
  }
@@ -1,17 +1,17 @@
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';
14
13
  import { Controller, useFormContext, useWatch } from 'react-hook-form';
14
+ import { dispatch } from 'use-bus';
15
15
  import isEmail from 'validator/es/lib/isEmail';
16
16
 
17
17
  import { useSessionContext } from '../../../contexts/session';
@@ -21,6 +21,7 @@ import FormInput from '../../input';
21
21
  import AddressForm from './address';
22
22
  import PhoneInput from './phone';
23
23
  import StripeCheckout from './stripe';
24
+ import UserButtons from './user-buttons';
24
25
 
25
26
  const phoneUtil = PhoneNumberUtil.getInstance();
26
27
 
@@ -72,6 +73,7 @@ export default function PaymentForm({
72
73
  onPaid,
73
74
  onError,
74
75
  }: PageData) {
76
+ const theme = useTheme();
75
77
  const { t } = useLocaleContext();
76
78
  const { session, connectApi } = useSessionContext();
77
79
  const { control, getValues, setValue, handleSubmit } = useFormContext();
@@ -112,6 +114,17 @@ export default function PaymentForm({
112
114
  const paymentCurrency = useWatch({ control, name: 'payment_currency' });
113
115
  const paymentCurrencies = paymentMethods.find((x) => x.id === paymentMethod)?.payment_currencies || [];
114
116
 
117
+ const domSize = useSize(document.body);
118
+
119
+ const isColumnLayout = useCreation(() => {
120
+ if (domSize) {
121
+ if (domSize?.width <= theme.breakpoints.values.md) {
122
+ return true;
123
+ }
124
+ }
125
+ return false;
126
+ }, [domSize, theme]);
127
+
115
128
  const payee = getStatementDescriptor(checkoutSession.line_items);
116
129
  const buttonText = session.user
117
130
  ? t(`checkout.${checkoutSession.mode}`)
@@ -217,6 +230,9 @@ export default function PaymentForm({
217
230
  }
218
231
  }
219
232
  } catch (err) {
233
+ if (err.response?.data?.code) {
234
+ dispatch(`error.${err.response?.data?.code}`);
235
+ }
220
236
  Toast.error(formatError(err));
221
237
  } finally {
222
238
  setState({ submitting: false });
@@ -249,16 +265,7 @@ export default function PaymentForm({
249
265
  <Stack className="cko-payment-contact">
250
266
  <Stack direction="row" sx={{ mb: 1 }} alignItems="center" justifyContent="space-between">
251
267
  <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>
268
+ {isColumnLayout ? null : <UserButtons />}
262
269
  </Stack>
263
270
  <Stack direction="column" className="cko-payment-form" spacing={0}>
264
271
  <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
  `;
@@ -14,7 +14,7 @@ type Props = {
14
14
  export default function ProductCard({ size, variant, name, logo, description, extra }: Props) {
15
15
  const s = { width: size, height: size };
16
16
  return (
17
- <Stack direction="row" alignItems="center" spacing={1} flex={2}>
17
+ <Stack direction="row" alignItems="flex-start" spacing={1} flex={2}>
18
18
  {logo ? (
19
19
  // @ts-ignore
20
20
  <Avatar src={logo} alt={name} variant={variant} sx={s} />
@@ -25,11 +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">
28
+ <Typography variant="body1" sx={{ fontWeight: 500, mb: 0.5, lineHeight: 1 }} color="text.primary">
29
29
  {name}
30
30
  </Typography>
31
31
  {description && (
32
- <Typography variant="body1" sx={{ fontSize: '0.85rem', lineHeight: '120%' }} color="text.secondary">
32
+ <Typography variant="body1" sx={{ fontSize: '0.85rem', mb: 0.5, lineHeight: 1 }} color="text.secondary">
33
33
  {description}
34
34
  </Typography>
35
35
  )}
@@ -1,9 +1,10 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
2
  import type { TCheckoutSessionExpanded, TLineItemExpanded, TPaymentCurrency } from '@did-pay/types';
3
3
  import { LoadingButton } from '@mui/lab';
4
- import { Fade, Stack, Typography } from '@mui/material';
4
+ import { Fade, Grow, Stack, Typography, keyframes } from '@mui/material';
5
5
  import { useRequest, useSetState } from 'ahooks';
6
6
  import noop from 'lodash/noop';
7
+ import useBus from 'use-bus';
7
8
 
8
9
  import api from '../../libs/api';
9
10
  import { formatCheckoutHeadlines } from '../../libs/util';
@@ -11,6 +12,24 @@ import Status from '../status';
11
12
  import PaymentAmount from './amount';
12
13
  import ProductItem from './product-item';
13
14
 
15
+ const shake = keyframes`
16
+ 0% {
17
+ transform: rotate(0deg);
18
+ }
19
+ 25% {
20
+ transform: rotate(2deg);
21
+ }
22
+ 50% {
23
+ transform: rotate(0eg);
24
+ }
25
+ 75% {
26
+ transform: rotate(-2deg);
27
+ }
28
+ 100% {
29
+ transform: rotate(0deg);
30
+ }
31
+ `;
32
+
14
33
  type Props = {
15
34
  checkoutSession: TCheckoutSessionExpanded;
16
35
  currency: TPaymentCurrency;
@@ -42,10 +61,21 @@ export default function PaymentSummary({
42
61
  onCancelCrossSell,
43
62
  }: Props) {
44
63
  const { t, locale } = useLocaleContext();
45
- const [state, setState] = useSetState({ loading: false });
64
+ const [state, setState] = useSetState({ loading: false, shake: false });
46
65
  const { data, runAsync } = useRequest(() => fetchCrossSell(checkoutSession.id));
47
66
  const headlines = formatCheckoutHeadlines(checkoutSession, currency, locale);
48
67
 
68
+ useBus(
69
+ 'error.REQUIRE_CROSS_SELL',
70
+ () => {
71
+ setState({ shake: true });
72
+ setTimeout(() => {
73
+ setState({ shake: false });
74
+ }, 1000);
75
+ },
76
+ []
77
+ );
78
+
49
79
  const handleUpsell = async (from: string, to: string) => {
50
80
  await onUpsell(from, to);
51
81
  runAsync();
@@ -82,7 +112,12 @@ export default function PaymentSummary({
82
112
 
83
113
  return (
84
114
  <Fade in>
85
- <Stack className="cko-product" direction="column" sx={{ mt: 4 }}>
115
+ <Stack
116
+ className="cko-product"
117
+ direction="column"
118
+ sx={{
119
+ mt: 8,
120
+ }}>
86
121
  <Stack className="cko-product-summary" sx={{ mb: 4 }}>
87
122
  <Typography sx={{ fontWeight: 500, fontSize: '1.15rem', color: 'text.secondary' }}>
88
123
  {headlines.action}
@@ -116,35 +151,46 @@ export default function PaymentSummary({
116
151
  ))}
117
152
  </Stack>
118
153
  {data && checkoutSession.line_items.some((x) => x.price_id === data.id) === false && (
119
- <Stack
120
- direction="column"
121
- alignItems="flex-end"
122
- spacing={0.5}
123
- sx={{ border: '1px solid #eee', borderRadius: 1, padding: 1, mt: 8, mb: 4 }}>
124
- <ProductItem
125
- item={{ quantity: 1, price: data, price_id: data.id, cross_sell: true } as TLineItemExpanded}
126
- session={checkoutSession}
127
- currency={currency}
128
- onUpsell={noop}
129
- onDownsell={noop}
130
- />
131
- <Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ width: 1 }}>
132
- <Typography>
133
- {checkoutSession.cross_sell_behavior === 'required' && (
134
- <Status label={t('checkout.required')} color="info" variant="outlined" sx={{ mr: 1 }} />
135
- )}
136
- </Typography>
137
- <LoadingButton
138
- size="small"
139
- loadingPosition="end"
140
- color={checkoutSession.cross_sell_behavior === 'required' ? 'info' : 'info'}
141
- variant={checkoutSession.cross_sell_behavior === 'required' ? 'text' : 'text'}
142
- loading={state.loading}
143
- onClick={handleApplyCrossSell}>
144
- {t('checkout.cross_sell.add')}
145
- </LoadingButton>
154
+ <Grow in>
155
+ <Stack
156
+ direction="column"
157
+ alignItems="flex-end"
158
+ spacing={0.5}
159
+ sx={{
160
+ border: '1px solid #eee',
161
+ borderRadius: 1,
162
+ padding: 1,
163
+ animation: state.shake ? `${shake} 0.2s 5 ease-in-out` : 'none',
164
+ mt: {
165
+ xs: 4,
166
+ md: 8,
167
+ },
168
+ }}>
169
+ <ProductItem
170
+ item={{ quantity: 1, price: data, price_id: data.id, cross_sell: true } as TLineItemExpanded}
171
+ session={checkoutSession}
172
+ currency={currency}
173
+ onUpsell={noop}
174
+ onDownsell={noop}
175
+ />
176
+ <Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ width: 1 }}>
177
+ <Typography>
178
+ {checkoutSession.cross_sell_behavior === 'required' && (
179
+ <Status label={t('checkout.required')} color="info" variant="outlined" sx={{ mr: 1 }} />
180
+ )}
181
+ </Typography>
182
+ <LoadingButton
183
+ size="small"
184
+ loadingPosition="end"
185
+ color={checkoutSession.cross_sell_behavior === 'required' ? 'info' : 'info'}
186
+ variant={checkoutSession.cross_sell_behavior === 'required' ? 'text' : 'text'}
187
+ loading={state.loading}
188
+ onClick={handleApplyCrossSell}>
189
+ {t('checkout.cross_sell.add')}
190
+ </LoadingButton>
191
+ </Stack>
146
192
  </Stack>
147
- </Stack>
193
+ </Grow>
148
194
  )}
149
195
  </Stack>
150
196
  </Fade>
@@ -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}>
@@ -14,6 +14,8 @@ export default function SectionHeader(props: Props) {
14
14
  direction="row"
15
15
  justifyContent="space-between"
16
16
  alignItems="center"
17
+ flexWrap="wrap"
18
+ gap={1}
17
19
  sx={{ mb: props.mb, pb: 1, width: 1, borderBottom: '1px solid #eee' }}>
18
20
  <Typography variant="h6" sx={{ fontWeight: 600 }}>
19
21
  {props.title}
@@ -6,7 +6,7 @@ export default function Status(props: ChipProps) {
6
6
  size="small"
7
7
  variant="outlined"
8
8
  {...props}
9
- sx={{ ...(props.sx || {}), borderRadius: '4px', height: 20, lineHeight: 20, textTransform: 'capitalize' }}
9
+ sx={{ ...(props.sx || {}), borderRadius: '4px', height: 20, lineHeight: 1, textTransform: 'capitalize' }}
10
10
  />
11
11
  );
12
12
  }
package/src/libs/api.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { getLocale } from '@arcblock/ux/lib/Locale/context';
2
2
  import axios from 'axios';
3
+ import isNull from 'lodash/isNull';
3
4
 
4
5
  axios.interceptors.request.use(
5
6
  (config) => {
@@ -9,7 +10,7 @@ axios.interceptors.request.use(
9
10
 
10
11
  const livemode = localStorage.getItem('livemode');
11
12
  const locale = getLocale(window.blocklet.languages);
12
- config.params = { ...(config.params || {}), livemode, locale };
13
+ config.params = { ...(config.params || {}), livemode: isNull(livemode) ? true : JSON.parse(livemode), locale };
13
14
 
14
15
  return config;
15
16
  },
@@ -513,6 +513,7 @@ export default flat({
513
513
  portal: 'Manage subscriptions',
514
514
  cardPay: '{action} with card',
515
515
  empty: 'No thing to pay',
516
+ per: 'per',
516
517
  pay: 'Pay {payee}',
517
518
  try1: 'Try {name}',
518
519
  try2: 'Try {name} and {count} more',
@@ -77,18 +77,32 @@ export default function PricingTable({ id }: Props) {
77
77
 
78
78
  if (error) {
79
79
  return (
80
- <div style={{ height: '90vh', width: '100vw' }}>
80
+ <Box
81
+ sx={{
82
+ width: '100vw',
83
+ minHeight: '90vh',
84
+ pb: 4,
85
+ display: 'flex',
86
+ flexDirection: 'column',
87
+ }}>
81
88
  <Header />
82
89
  <Center relative="parent">
83
90
  <Alert severity="error">{error.message}</Alert>
84
91
  </Center>
85
- </div>
92
+ </Box>
86
93
  );
87
94
  }
88
95
 
89
96
  if (loading || !data) {
90
97
  return (
91
- <div style={{ height: '90vh', width: '100vw' }}>
98
+ <Box
99
+ sx={{
100
+ width: '100vw',
101
+ minHeight: '90vh',
102
+ pb: 4,
103
+ display: 'flex',
104
+ flexDirection: 'column',
105
+ }}>
92
106
  <Header />
93
107
  <Center>
94
108
  <Stack direction="column" alignItems="center" spacing={4}>
@@ -98,25 +112,32 @@ export default function PricingTable({ id }: Props) {
98
112
  <Typography component="div" variant="h6" sx={{ width: '10%' }}>
99
113
  <Skeleton />
100
114
  </Typography>
101
- <Stack direction="row" flexWrap="wrap" spacing={5}>
115
+ <Stack flexWrap="wrap" direction="row" gap={{ xs: 3, sm: 5, md: 10 }} justifyContent="center">
102
116
  <ProductSkeleton key={1} count={2} />
103
117
  <ProductSkeleton key={2} count={3} />
104
118
  <ProductSkeleton key={3} count={4} />
105
119
  </Stack>
106
120
  </Stack>
107
121
  </Center>
108
- </div>
122
+ </Box>
109
123
  );
110
124
  }
111
125
 
112
126
  if (data.items.length === 0) {
113
127
  return (
114
- <div style={{ height: '90vh', width: '100vw' }}>
128
+ <Box
129
+ sx={{
130
+ width: '100vw',
131
+ minHeight: '90vh',
132
+ pb: 4,
133
+ display: 'flex',
134
+ flexDirection: 'column',
135
+ }}>
115
136
  <Header />
116
137
  <Center relative="parent">
117
138
  <Alert severity="warning">{t('checkout.noPricing')}</Alert>
118
139
  </Center>
119
- </div>
140
+ </Box>
120
141
  );
121
142
  }
122
143
 
@@ -137,16 +158,45 @@ export default function PricingTable({ id }: Props) {
137
158
  };
138
159
 
139
160
  return (
140
- <div style={{ height: '90vh', width: '100vw' }}>
161
+ <Box
162
+ sx={{
163
+ width: '100vw',
164
+ minHeight: '90vh',
165
+ pb: 4,
166
+ display: {
167
+ xs: 'block',
168
+ sm: 'flex',
169
+ },
170
+ flexDirection: 'column',
171
+ }}>
141
172
  <Header />
142
173
  <Center relative="parent">
143
- <Stack direction="column" alignItems="center" spacing={5}>
174
+ <Stack
175
+ direction="column"
176
+ alignItems="center"
177
+ sx={{
178
+ pt: {
179
+ xs: 4,
180
+ sm: 2,
181
+ },
182
+ gap: {
183
+ xs: 3,
184
+ sm: 5,
185
+ },
186
+ }}>
144
187
  <Typography variant="h4" color="text.primary" fontWeight={600}>
145
188
  {data.name}
146
189
  {!livemode && <Livemode />}
147
190
  </Typography>
148
191
  {Object.keys(recurring).length > 1 && (
149
- <ToggleButtonGroup value={state.interval} onChange={(_, value) => setState({ interval: value })} exclusive>
192
+ <ToggleButtonGroup
193
+ value={state.interval}
194
+ onChange={(_, value) => {
195
+ if (value !== null) {
196
+ setState({ interval: value });
197
+ }
198
+ }}
199
+ exclusive>
150
200
  {Object.keys(recurring).map((x) => (
151
201
  <ToggleButton key={x} value={x} sx={{ textTransform: 'capitalize' }}>
152
202
  {formatRecurring(recurring[x] as PriceRecurring, true, '', locale)}
@@ -154,7 +204,7 @@ export default function PricingTable({ id }: Props) {
154
204
  ))}
155
205
  </ToggleButtonGroup>
156
206
  )}
157
- <Stack direction="row" flexWrap="wrap" spacing={5}>
207
+ <Stack flexWrap="wrap" direction="row" gap={{ xs: 3, sm: 5, md: 10 }} justifyContent="center">
158
208
  {grouped[state.interval]?.map((x) => {
159
209
  return (
160
210
  <Fade in>
@@ -226,6 +276,6 @@ export default function PricingTable({ id }: Props) {
226
276
  </Stack>
227
277
  </Stack>
228
278
  </Center>
229
- </div>
279
+ </Box>
230
280
  );
231
281
  }