@akinon/next 1.4.0 → 1.6.0
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/CHANGELOG.md +22 -0
- package/api/auth.ts +5 -1
- package/assets/styles/index.scss +9 -4
- package/components/client-root.tsx +3 -12
- package/components/mobile-app-toggler.tsx +12 -1
- package/components/plugin-module.tsx +14 -4
- package/components/redirect-three-d/content/index.tsx +13 -3
- package/components/selected-payment-option-view.tsx +1 -1
- package/data/client/checkout.ts +4 -2
- package/data/client/user.ts +8 -0
- package/data/urls.ts +1 -0
- package/hooks/index.ts +1 -0
- package/hooks/use-mobile-iframe-handler.ts +23 -0
- package/middlewares/default.ts +21 -6
- package/package.json +1 -1
- package/plugins.js +2 -1
- package/redux/middlewares/checkout.ts +4 -1
- package/types/commerce/checkout.ts +2 -1
- package/types/index.ts +1 -0
- package/types/metadata.ts +7 -0
- package/utils/mobile-3d-iframe.ts +28 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @akinon/next
|
|
2
2
|
|
|
3
|
+
## 1.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- ZERO-2286: Find custom payment option view by slug instead of pk
|
|
8
|
+
- ZERO-2169: Add OTP plugin module support
|
|
9
|
+
- ZERO-2250: Show checkout iframe on mobile web
|
|
10
|
+
- ZERO-2288: Allow custom response in middleware.ts
|
|
11
|
+
- ZERO-2272: Add confirm email services
|
|
12
|
+
- ZERO-2250: Add expire date to currency and locale cookies
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- Fix Metadata type
|
|
17
|
+
|
|
18
|
+
## 1.5.0
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- Support GPay plugin
|
|
23
|
+
- Extend Next Metadata type
|
|
24
|
+
|
|
3
25
|
## 1.4.0
|
|
4
26
|
|
|
5
27
|
### Minor Changes
|
package/api/auth.ts
CHANGED
|
@@ -130,7 +130,7 @@ const nextAuthOptions = (req: NextApiRequest, res: NextApiResponse) => {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
if (!response.key) {
|
|
133
|
-
|
|
133
|
+
const errors = [] as AuthError[];
|
|
134
134
|
|
|
135
135
|
const fieldErrors = Object.keys(response ?? {})
|
|
136
136
|
.filter((key) => key !== 'non_field_errors')
|
|
@@ -150,6 +150,10 @@ const nextAuthOptions = (req: NextApiRequest, res: NextApiResponse) => {
|
|
|
150
150
|
email: credentials.email,
|
|
151
151
|
formType: credentials.formType
|
|
152
152
|
});
|
|
153
|
+
} else if (apiRequest.status === 202) {
|
|
154
|
+
errors.push({
|
|
155
|
+
type: 'otp'
|
|
156
|
+
});
|
|
153
157
|
} else if (fieldErrors.length) {
|
|
154
158
|
errors.push({
|
|
155
159
|
type: 'field_errors',
|
package/assets/styles/index.scss
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
.checkout-payment-iframe-wrapper {
|
|
2
|
+
position: fixed;
|
|
3
|
+
top: 0;
|
|
4
|
+
left: 0;
|
|
5
|
+
width: 100%;
|
|
6
|
+
height: 100%;
|
|
7
|
+
border: none;
|
|
8
|
+
z-index: 1000;
|
|
9
|
+
background-color: white;
|
|
10
|
+
|
|
2
11
|
iframe {
|
|
3
|
-
position: fixed;
|
|
4
|
-
top: 0;
|
|
5
|
-
left: 0;
|
|
6
12
|
width: 100%;
|
|
7
13
|
height: 100%;
|
|
8
14
|
border: none;
|
|
9
|
-
z-index: 1000;
|
|
10
15
|
background-color: white;
|
|
11
16
|
}
|
|
12
17
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { useAppSelector } from '../redux/hooks';
|
|
3
|
+
import { useMobileIframeHandler } from '../hooks';
|
|
5
4
|
|
|
6
5
|
export default function ClientRoot({
|
|
7
6
|
children,
|
|
@@ -10,17 +9,9 @@ export default function ClientRoot({
|
|
|
10
9
|
children: React.ReactNode;
|
|
11
10
|
sessionId?: string;
|
|
12
11
|
}) {
|
|
13
|
-
const
|
|
14
|
-
const { isMobileApp } = useAppSelector((state) => state.root);
|
|
15
|
-
|
|
16
|
-
if (isMobileApp && pathname.includes('/orders/completed')) {
|
|
17
|
-
((window.parent || window) as any)?.ReactNativeWebView?.postMessage?.(
|
|
18
|
-
JSON.stringify({
|
|
19
|
-
url: pathname,
|
|
20
|
-
sessionId
|
|
21
|
-
})
|
|
22
|
-
);
|
|
12
|
+
const { preventPageRender } = useMobileIframeHandler({ sessionId });
|
|
23
13
|
|
|
14
|
+
if (preventPageRender) {
|
|
24
15
|
return null;
|
|
25
16
|
}
|
|
26
17
|
|
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useAppSelector } from '../redux/hooks';
|
|
4
|
+
import { useState, useEffect } from 'react';
|
|
5
|
+
import { useSearchParams } from 'next/navigation';
|
|
4
6
|
|
|
5
7
|
export default function MobileAppToggler({
|
|
6
8
|
children
|
|
7
9
|
}: {
|
|
8
10
|
children: React.ReactNode;
|
|
9
11
|
}) {
|
|
12
|
+
const searchParams = useSearchParams();
|
|
10
13
|
const { isMobileApp } = useAppSelector((state) => state.root);
|
|
14
|
+
const [isInIframe, setIsInIframe] = useState(false);
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (window.frameElement) {
|
|
18
|
+
setIsInIframe(true);
|
|
19
|
+
}
|
|
20
|
+
}, []);
|
|
21
|
+
|
|
22
|
+
if (isMobileApp || isInIframe || searchParams.get('iframe') === 'true')
|
|
23
|
+
return null;
|
|
13
24
|
|
|
14
25
|
return <>{children}</>;
|
|
15
26
|
}
|
|
@@ -8,7 +8,9 @@ enum Plugin {
|
|
|
8
8
|
ClickCollect = 'pz-click-collect',
|
|
9
9
|
OneClickCheckout = 'pz-one-click-checkout',
|
|
10
10
|
PayOnDelivery = 'pz-pay-on-delivery',
|
|
11
|
-
CheckoutGiftPack = 'pz-checkout-gift-pack'
|
|
11
|
+
CheckoutGiftPack = 'pz-checkout-gift-pack',
|
|
12
|
+
GPay = 'pz-gpay',
|
|
13
|
+
Otp = 'pz-otp'
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export enum Component {
|
|
@@ -16,7 +18,9 @@ export enum Component {
|
|
|
16
18
|
ClickCollect = 'ClickCollect',
|
|
17
19
|
OneClickCheckoutButtons = 'OneClickCheckoutButtons',
|
|
18
20
|
PayOnDelivery = 'PayOnDelivery',
|
|
19
|
-
CheckoutGiftPack = 'CheckoutGiftPack'
|
|
21
|
+
CheckoutGiftPack = 'CheckoutGiftPack',
|
|
22
|
+
GPay = 'GPayOption',
|
|
23
|
+
Otp = 'Otp'
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
const PluginComponents = new Map([
|
|
@@ -24,11 +28,13 @@ const PluginComponents = new Map([
|
|
|
24
28
|
[Plugin.ClickCollect, [Component.ClickCollect]],
|
|
25
29
|
[Plugin.OneClickCheckout, [Component.OneClickCheckoutButtons]],
|
|
26
30
|
[Plugin.PayOnDelivery, [Component.PayOnDelivery]],
|
|
27
|
-
[Plugin.CheckoutGiftPack, [Component.CheckoutGiftPack]]
|
|
31
|
+
[Plugin.CheckoutGiftPack, [Component.CheckoutGiftPack]],
|
|
32
|
+
[Plugin.GPay, [Component.GPay]],
|
|
33
|
+
[Plugin.Otp, [Component.Otp]]
|
|
28
34
|
]);
|
|
29
35
|
|
|
30
36
|
const getPlugin = (component: Component) => {
|
|
31
|
-
for (
|
|
37
|
+
for (const [key, value] of Array.from(PluginComponents.entries())) {
|
|
32
38
|
if (value.includes(component)) return key;
|
|
33
39
|
}
|
|
34
40
|
|
|
@@ -65,6 +71,10 @@ export default function PluginModule({
|
|
|
65
71
|
promise = import(`${'pz-pay-on-delivery'}`);
|
|
66
72
|
} else if (plugin === Plugin.CheckoutGiftPack) {
|
|
67
73
|
promise = import(`${'pz-checkout-gift-pack'}`);
|
|
74
|
+
} else if (plugin === Plugin.GPay) {
|
|
75
|
+
promise = import(`${'pz-gpay'}`);
|
|
76
|
+
} else if (plugin === Plugin.Otp) {
|
|
77
|
+
promise = import(`${'pz-otp'}`);
|
|
68
78
|
}
|
|
69
79
|
} catch (error) {
|
|
70
80
|
logger.error(error);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useRouter } from '@akinon/next/hooks';
|
|
4
3
|
import { useEffect, useState } from 'react';
|
|
5
4
|
import { ROUTES } from 'routes';
|
|
6
5
|
import { useGet3dRedirectFormQuery } from '@akinon/next/data/client/checkout';
|
|
7
6
|
import { LoaderSpinner } from 'components';
|
|
8
7
|
import { useLocalization } from '../../../hooks/use-localization';
|
|
8
|
+
import { getUrlPathWithLocale } from '../../../utils/localization';
|
|
9
|
+
import { useSearchParams } from 'next/navigation';
|
|
9
10
|
|
|
10
11
|
interface RedirectThreeDContentProps {
|
|
11
12
|
sessionId: string;
|
|
@@ -14,9 +15,9 @@ interface RedirectThreeDContentProps {
|
|
|
14
15
|
export default function RedirectThreeDContent({
|
|
15
16
|
sessionId
|
|
16
17
|
}: RedirectThreeDContentProps) {
|
|
18
|
+
const searchParams = useSearchParams();
|
|
17
19
|
const { data } = useGet3dRedirectFormQuery();
|
|
18
20
|
const [error, setError] = useState(null);
|
|
19
|
-
const router = useRouter();
|
|
20
21
|
const { locale } = useLocalization();
|
|
21
22
|
|
|
22
23
|
useEffect(() => {
|
|
@@ -31,7 +32,16 @@ export default function RedirectThreeDContent({
|
|
|
31
32
|
setError('Redirecting to checkout page. Please wait...');
|
|
32
33
|
|
|
33
34
|
setTimeout(() => {
|
|
34
|
-
|
|
35
|
+
let checkoutUrl = `${ROUTES.CHECKOUT}`;
|
|
36
|
+
|
|
37
|
+
// iframe param is used to prevent header and footer rendering
|
|
38
|
+
if (searchParams.get('iframe') === 'true') {
|
|
39
|
+
checkoutUrl = `${checkoutUrl}?iframe=true`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Use `window.location.href` instead of `router.push`
|
|
43
|
+
// to capture the url change event in iframe
|
|
44
|
+
location.href = getUrlPathWithLocale(checkoutUrl, locale);
|
|
35
45
|
}, 3000);
|
|
36
46
|
return;
|
|
37
47
|
}
|
package/data/client/checkout.ts
CHANGED
|
@@ -42,6 +42,7 @@ interface CompleteCreditCardParams {
|
|
|
42
42
|
card_number: string;
|
|
43
43
|
card_month: string;
|
|
44
44
|
card_year: string;
|
|
45
|
+
use_three_d?: boolean;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
interface GetContractResponse {
|
|
@@ -96,7 +97,8 @@ export const checkoutApi = api.injectEndpoints({
|
|
|
96
97
|
card_cvv,
|
|
97
98
|
card_number,
|
|
98
99
|
card_month,
|
|
99
|
-
card_year
|
|
100
|
+
card_year,
|
|
101
|
+
use_three_d = true
|
|
100
102
|
}) => ({
|
|
101
103
|
url: buildClientRequestUrl(checkout.completeCreditCardPayment, {
|
|
102
104
|
useFormData: true
|
|
@@ -104,7 +106,7 @@ export const checkoutApi = api.injectEndpoints({
|
|
|
104
106
|
method: 'POST',
|
|
105
107
|
body: {
|
|
106
108
|
agreement: '1',
|
|
107
|
-
use_three_d: '1',
|
|
109
|
+
use_three_d: use_three_d ? '1' : '0',
|
|
108
110
|
card_cvv,
|
|
109
111
|
card_holder,
|
|
110
112
|
card_month,
|
package/data/client/user.ts
CHANGED
|
@@ -78,6 +78,13 @@ const userApi = api.injectEndpoints({
|
|
|
78
78
|
})
|
|
79
79
|
})
|
|
80
80
|
}),
|
|
81
|
+
confirmEmailVerification: build.query<void, string>({
|
|
82
|
+
query: (token) => ({
|
|
83
|
+
url: buildClientRequestUrl(user.confirmEmailVerification(token), {
|
|
84
|
+
contentType: 'application/json'
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
})
|
|
81
88
|
}),
|
|
82
89
|
overrideExisting: false
|
|
83
90
|
});
|
|
@@ -85,6 +92,7 @@ const userApi = api.injectEndpoints({
|
|
|
85
92
|
export const {
|
|
86
93
|
useGetCaptchaQuery,
|
|
87
94
|
useChangeEmailVerificationQuery,
|
|
95
|
+
useConfirmEmailVerificationQuery,
|
|
88
96
|
useValidateCaptchaMutation,
|
|
89
97
|
useLogoutMutation,
|
|
90
98
|
useForgotPasswordMutation
|
package/data/urls.ts
CHANGED
|
@@ -143,6 +143,7 @@ export const user = {
|
|
|
143
143
|
profiles: '/users/profile',
|
|
144
144
|
forgotPassword: '/users/password/reset',
|
|
145
145
|
changeEmailVerification: (token: string) => `/users/email-set-primary/${token}`,
|
|
146
|
+
confirmEmailVerification: (token: string) => `/users/registration/account-confirm-email/${token}`,
|
|
146
147
|
csrfToken: '/csrf_token'
|
|
147
148
|
};
|
|
148
149
|
|
package/hooks/index.ts
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { usePathname } from 'next/navigation';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { useAppSelector } from '../redux/hooks';
|
|
4
|
+
|
|
5
|
+
export function useMobileIframeHandler({ sessionId }: { sessionId: string }) {
|
|
6
|
+
const pathname = usePathname();
|
|
7
|
+
const { isMobileApp } = useAppSelector((state) => state.root);
|
|
8
|
+
const [preventPageRender, setPreventPageRender] = useState(false);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (
|
|
12
|
+
pathname.includes('/orders/completed') &&
|
|
13
|
+
(isMobileApp || /iPad|iPhone|iPod|Android/i.test(navigator.userAgent)) &&
|
|
14
|
+
window.frameElement // Check if the page is inside an iframe
|
|
15
|
+
) {
|
|
16
|
+
setPreventPageRender(true);
|
|
17
|
+
}
|
|
18
|
+
}, [pathname]);
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
preventPageRender
|
|
22
|
+
};
|
|
23
|
+
}
|
package/middlewares/default.ts
CHANGED
|
@@ -130,12 +130,23 @@ const withPzDefault =
|
|
|
130
130
|
event
|
|
131
131
|
)) as NextResponse | void;
|
|
132
132
|
|
|
133
|
+
// if middleware.ts has a return value for current url
|
|
133
134
|
if (middlewareResult instanceof NextResponse) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
// pz-override-response header is used to prevent 404 page for custom responses.
|
|
136
|
+
if (
|
|
137
|
+
middlewareResult.headers.get(
|
|
138
|
+
'pz-override-response'
|
|
139
|
+
) !== 'true'
|
|
140
|
+
) {
|
|
141
|
+
middlewareResult.headers.set(
|
|
142
|
+
'x-middleware-rewrite',
|
|
143
|
+
url.href
|
|
144
|
+
);
|
|
145
|
+
}
|
|
138
146
|
} else {
|
|
147
|
+
// if middleware.ts doesn't have a return value.
|
|
148
|
+
// e.g. NextResponse.next() doesn't exist in middleware.ts
|
|
149
|
+
|
|
139
150
|
middlewareResult = NextResponse.rewrite(url);
|
|
140
151
|
}
|
|
141
152
|
|
|
@@ -145,13 +156,17 @@ const withPzDefault =
|
|
|
145
156
|
locale?.length > 0 ? locale : defaultLocaleValue,
|
|
146
157
|
{
|
|
147
158
|
sameSite: 'none',
|
|
148
|
-
secure: true
|
|
159
|
+
secure: true,
|
|
160
|
+
expires: new Date(
|
|
161
|
+
Date.now() + 1000 * 60 * 60 * 24 * 7
|
|
162
|
+
) // 7 days
|
|
149
163
|
}
|
|
150
164
|
);
|
|
151
165
|
}
|
|
152
166
|
middlewareResult.cookies.set('pz-currency', currency, {
|
|
153
167
|
sameSite: 'none',
|
|
154
|
-
secure: true
|
|
168
|
+
secure: true,
|
|
169
|
+
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7) // 7 days
|
|
155
170
|
});
|
|
156
171
|
|
|
157
172
|
if (
|
package/package.json
CHANGED
package/plugins.js
CHANGED
|
@@ -164,7 +164,10 @@ export const contextListMiddleware: Middleware = ({
|
|
|
164
164
|
: `/${currentLocale}${redirectUrl}`;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
if (
|
|
167
|
+
if (
|
|
168
|
+
isMobileApp ||
|
|
169
|
+
/iPad|iPhone|iPod|Android/i.test(navigator.userAgent)
|
|
170
|
+
) {
|
|
168
171
|
showMobile3dIframe(url);
|
|
169
172
|
} else {
|
|
170
173
|
window.location.href = url;
|
package/types/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ const removeIframe = async () => {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
iframeSelector.remove();
|
|
21
|
+
document.body.style.overflow = 'auto';
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
export const showMobile3dIframe = (redirectUrl: string) => {
|
|
@@ -27,12 +28,20 @@ export const showMobile3dIframe = (redirectUrl: string) => {
|
|
|
27
28
|
|
|
28
29
|
iframeWrapper.className = 'checkout-payment-iframe-wrapper';
|
|
29
30
|
closeButton.className = 'close-button';
|
|
30
|
-
|
|
31
|
+
|
|
32
|
+
// iframe param is used to prevent header and footer rendering
|
|
33
|
+
iframe.setAttribute(
|
|
34
|
+
'src',
|
|
35
|
+
redirectUrl.match(new RegExp(`^/orders/redirect-three-d/$`))
|
|
36
|
+
? `${redirectUrl}?iframe=true`
|
|
37
|
+
: redirectUrl
|
|
38
|
+
);
|
|
31
39
|
closeButton.innerHTML = '✕';
|
|
32
40
|
closeButton.addEventListener('click', removeIframe);
|
|
33
41
|
|
|
34
42
|
iframeWrapper.append(iframe, closeButton);
|
|
35
43
|
document.body.appendChild(iframeWrapper);
|
|
44
|
+
document.body.style.overflow = 'hidden';
|
|
36
45
|
|
|
37
46
|
iframeURLChange(iframe, (location) => {
|
|
38
47
|
if (location.origin !== window.location.origin) {
|
|
@@ -41,18 +50,28 @@ export const showMobile3dIframe = (redirectUrl: string) => {
|
|
|
41
50
|
|
|
42
51
|
const searchParams = new URLSearchParams(location.search);
|
|
43
52
|
const isOrderCompleted = location.href.includes('/orders/completed');
|
|
44
|
-
const data = {
|
|
45
|
-
url: location.pathname
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
if (searchParams.has('success') || isOrderCompleted) {
|
|
49
|
-
removeIframe();
|
|
50
|
-
}
|
|
51
53
|
|
|
52
54
|
if (isOrderCompleted) {
|
|
53
55
|
(window.parent as any)?.ReactNativeWebView?.postMessage?.(
|
|
54
|
-
JSON.stringify(
|
|
56
|
+
JSON.stringify({
|
|
57
|
+
url: location.pathname
|
|
58
|
+
})
|
|
55
59
|
);
|
|
60
|
+
|
|
61
|
+
if (localStorage.isMobileApp !== 'true') {
|
|
62
|
+
window.location.href = location.pathname;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (
|
|
68
|
+
searchParams.has('success') ||
|
|
69
|
+
isOrderCompleted ||
|
|
70
|
+
location.href.includes('/orders/checkout')
|
|
71
|
+
) {
|
|
72
|
+
setTimeout(() => {
|
|
73
|
+
removeIframe();
|
|
74
|
+
}, 0);
|
|
56
75
|
}
|
|
57
76
|
});
|
|
58
77
|
};
|