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 +1 -1
- package/api/src/routes/checkout-sessions.ts +17 -14
- package/blocklet.yml +1 -1
- package/package.json +20 -20
- package/src/components/checkout/form/index.tsx +21 -14
- package/src/components/checkout/form/user-buttons.tsx +24 -0
- package/src/components/checkout/header.tsx +22 -5
- package/src/components/checkout/pay.tsx +50 -11
- package/src/components/checkout/product-card.tsx +3 -3
- package/src/components/checkout/summary.tsx +77 -31
- package/src/components/livemode.tsx +1 -1
- package/src/components/portal/invoice/list.tsx +16 -6
- package/src/components/portal/subscription/list.tsx +9 -1
- package/src/components/section/header.tsx +2 -0
- package/src/components/status.tsx +1 -1
- package/src/libs/api.ts +2 -1
- package/src/locales/en.tsx +1 -0
- package/src/pages/checkout/pricing-table.tsx +62 -12
package/api/dev.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.13.
|
|
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.
|
|
43
|
+
"@arcblock/did": "^1.18.95",
|
|
44
44
|
"@arcblock/did-auth-storage-nedb": "^1.7.1",
|
|
45
|
-
"@arcblock/did-connect": "^2.8.
|
|
46
|
-
"@arcblock/did-util": "^1.18.
|
|
47
|
-
"@arcblock/ux": "^2.8.
|
|
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.
|
|
50
|
+
"@blocklet/ui-react": "^2.8.7",
|
|
51
51
|
"@blocklet/uploader": "^0.0.33",
|
|
52
|
-
"@mui/icons-material": "^5.14.
|
|
53
|
-
"@mui/lab": "^5.0.0-alpha.
|
|
54
|
-
"@mui/material": "^5.14.
|
|
55
|
-
"@mui/styles": "^5.14.
|
|
56
|
-
"@mui/system": "^5.14.
|
|
57
|
-
"@ocap/asset": "^1.18.
|
|
58
|
-
"@ocap/client": "^1.18.
|
|
59
|
-
"@ocap/mcrypto": "^1.18.
|
|
60
|
-
"@ocap/util": "^1.18.
|
|
61
|
-
"@ocap/wallet": "^1.18.
|
|
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.
|
|
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.
|
|
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.
|
|
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": "
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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
|
|
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-
|
|
272
|
+
flex-direction: column;
|
|
273
273
|
justify-content: center;
|
|
274
|
-
|
|
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:
|
|
293
|
+
max-width: 1000px;
|
|
292
294
|
display: flex;
|
|
293
|
-
|
|
295
|
+
flex-direction: row;
|
|
294
296
|
justify-content: space-between;
|
|
295
297
|
position: relative;
|
|
296
|
-
|
|
298
|
+
padding: 0 16px;
|
|
297
299
|
}
|
|
298
300
|
|
|
299
301
|
.cko-overview {
|
|
300
|
-
width:
|
|
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:
|
|
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:
|
|
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="
|
|
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:
|
|
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
|
|
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
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
</
|
|
193
|
+
</Grow>
|
|
148
194
|
)}
|
|
149
195
|
</Stack>
|
|
150
196
|
</Fade>
|
|
@@ -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"
|
|
65
|
+
<Stack direction="column" gap={3} sx={{ mt: 1 }}>
|
|
67
66
|
{Object.entries(grouped).map(([date, invoices]) => (
|
|
68
|
-
<
|
|
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
|
|
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
|
-
</
|
|
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
|
|
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:
|
|
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
|
},
|
package/src/locales/en.tsx
CHANGED
|
@@ -77,18 +77,32 @@ export default function PricingTable({ id }: Props) {
|
|
|
77
77
|
|
|
78
78
|
if (error) {
|
|
79
79
|
return (
|
|
80
|
-
<
|
|
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
|
-
</
|
|
92
|
+
</Box>
|
|
86
93
|
);
|
|
87
94
|
}
|
|
88
95
|
|
|
89
96
|
if (loading || !data) {
|
|
90
97
|
return (
|
|
91
|
-
<
|
|
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
|
|
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
|
-
</
|
|
122
|
+
</Box>
|
|
109
123
|
);
|
|
110
124
|
}
|
|
111
125
|
|
|
112
126
|
if (data.items.length === 0) {
|
|
113
127
|
return (
|
|
114
|
-
<
|
|
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
|
-
</
|
|
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
|
-
<
|
|
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
|
|
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
|
|
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
|
|
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
|
-
</
|
|
279
|
+
</Box>
|
|
230
280
|
);
|
|
231
281
|
}
|