@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 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
- let errors = [] as AuthError[];
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',
@@ -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 { usePathname } from 'next/navigation';
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 pathname = usePathname();
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
- if (isMobileApp) return null;
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 (let [key, value] of Array.from(PluginComponents.entries())) {
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
- router.replace(ROUTES.CHECKOUT);
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
  }
@@ -16,7 +16,7 @@ export default function SelectedPaymentOptionView() {
16
16
  dynamic(
17
17
  () => {
18
18
  const customOption = PaymentOptionViews.find(
19
- (opt) => opt.pk == payment_option.pk
19
+ (opt) => opt.slug === payment_option.slug
20
20
  );
21
21
 
22
22
  if (customOption) {
@@ -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,
@@ -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
@@ -6,3 +6,4 @@ export * from './use-common-product-attributes';
6
6
  export * from './use-debounce';
7
7
  export * from './use-media-query';
8
8
  export * from './use-on-click-outside';
9
+ export * from './use-mobile-iframe-handler';
@@ -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
+ }
@@ -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
- middlewareResult.headers.set(
135
- 'x-middleware-rewrite',
136
- url.href
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@akinon/next",
3
3
  "description": "Core package for Project Zero Next",
4
- "version": "1.4.0",
4
+ "version": "1.6.0",
5
5
  "private": false,
6
6
  "license": "MIT",
7
7
  "bin": {
package/plugins.js CHANGED
@@ -4,5 +4,6 @@ module.exports = [
4
4
  'pz-one-click-checkout',
5
5
  'pz-pay-on-delivery',
6
6
  'pz-checkout-gift-pack',
7
- 'pz-gpay'
7
+ 'pz-gpay',
8
+ 'pz-otp'
8
9
  ];
@@ -164,7 +164,10 @@ export const contextListMiddleware: Middleware = ({
164
164
  : `/${currentLocale}${redirectUrl}`;
165
165
  }
166
166
 
167
- if (isMobileApp) {
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;
@@ -41,7 +41,8 @@ export interface CheckoutAddressType {
41
41
  }
42
42
 
43
43
  export interface CheckoutPaymentOption {
44
- pk: number;
44
+ pk?: number;
45
+ slug?: string;
45
46
  view?: (...args) => JSX.Element;
46
47
  viewProps?: any;
47
48
  }
package/types/index.ts CHANGED
@@ -9,6 +9,7 @@ declare global {
9
9
 
10
10
  export * from './commerce';
11
11
  export * from './gtm';
12
+ export * from './metadata';
12
13
 
13
14
  export interface Locale {
14
15
  label: string;
@@ -0,0 +1,7 @@
1
+ import { Metadata as NextMetaData } from 'next';
2
+
3
+ export interface Metadata extends NextMetaData {
4
+ alternates?: NextMetaData['alternates'] & {
5
+ languages?: Record<string, string>;
6
+ };
7
+ }
@@ -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
- iframe.setAttribute('src', redirectUrl);
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 = '&#x2715';
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(data)
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
  };