@akinon/next 1.5.0 → 1.7.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.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Reset pre-order is_redirected state
8
+ - Show IP addresses in logs
9
+
10
+ ## 1.6.0
11
+
12
+ ### Minor Changes
13
+
14
+ - ZERO-2286: Find custom payment option view by slug instead of pk
15
+ - ZERO-2169: Add OTP plugin module support
16
+ - ZERO-2250: Show checkout iframe on mobile web
17
+ - ZERO-2288: Allow custom response in middleware.ts
18
+ - ZERO-2272: Add confirm email services
19
+ - ZERO-2250: Add expire date to currency and locale cookies
20
+
21
+ ### Patch Changes
22
+
23
+ - Fix Metadata type
24
+
3
25
  ## 1.5.0
4
26
 
5
27
  ### Patch 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
  }
@@ -9,7 +9,8 @@ enum Plugin {
9
9
  OneClickCheckout = 'pz-one-click-checkout',
10
10
  PayOnDelivery = 'pz-pay-on-delivery',
11
11
  CheckoutGiftPack = 'pz-checkout-gift-pack',
12
- GPay = 'pz-gpay'
12
+ GPay = 'pz-gpay',
13
+ Otp = 'pz-otp'
13
14
  }
14
15
 
15
16
  export enum Component {
@@ -18,7 +19,8 @@ export enum Component {
18
19
  OneClickCheckoutButtons = 'OneClickCheckoutButtons',
19
20
  PayOnDelivery = 'PayOnDelivery',
20
21
  CheckoutGiftPack = 'CheckoutGiftPack',
21
- GPay = 'GPayOption'
22
+ GPay = 'GPayOption',
23
+ Otp = 'Otp'
22
24
  }
23
25
 
24
26
  const PluginComponents = new Map([
@@ -27,11 +29,12 @@ const PluginComponents = new Map([
27
29
  [Plugin.OneClickCheckout, [Component.OneClickCheckoutButtons]],
28
30
  [Plugin.PayOnDelivery, [Component.PayOnDelivery]],
29
31
  [Plugin.CheckoutGiftPack, [Component.CheckoutGiftPack]],
30
- [Plugin.GPay, [Component.GPay]]
32
+ [Plugin.GPay, [Component.GPay]],
33
+ [Plugin.Otp, [Component.Otp]]
31
34
  ]);
32
35
 
33
36
  const getPlugin = (component: Component) => {
34
- for (let [key, value] of Array.from(PluginComponents.entries())) {
37
+ for (const [key, value] of Array.from(PluginComponents.entries())) {
35
38
  if (value.includes(component)) return key;
36
39
  }
37
40
 
@@ -70,6 +73,8 @@ export default function PluginModule({
70
73
  promise = import(`${'pz-checkout-gift-pack'}`);
71
74
  } else if (plugin === Plugin.GPay) {
72
75
  promise = import(`${'pz-gpay'}`);
76
+ } else if (plugin === Plugin.Otp) {
77
+ promise = import(`${'pz-otp'}`);
73
78
  }
74
79
  } catch (error) {
75
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
+ }
@@ -20,6 +20,8 @@ const resetBasket = async (req: PzNextRequest) => {
20
20
  const withCurrency =
21
21
  (middleware: NextMiddleware) =>
22
22
  async (req: PzNextRequest, event: NextFetchEvent) => {
23
+ const ip = req.headers.get('x-forwarded-for') ?? '';
24
+
23
25
  try {
24
26
  const { currencies, defaultCurrencyCode, defaultLocaleValue } =
25
27
  settings.localization;
@@ -27,7 +29,9 @@ const withCurrency =
27
29
  const url = req.nextUrl.clone();
28
30
 
29
31
  if (!defaultCurrencyCode) {
30
- logger.error('Default currency code is not defined in settings.');
32
+ logger.error('Default currency code is not defined in settings.', {
33
+ ip
34
+ });
31
35
  throw new Error(
32
36
  'Default currency code is not defined. Use `defaultCurrencyCode` property in `localization` object in `settings.js` file.'
33
37
  );
@@ -57,7 +61,10 @@ const withCurrency =
57
61
  !currencies.find((c) => c.code === activeCurrency)
58
62
  ) {
59
63
  logger.warn(
60
- `Currency ${activeCurrency} is not defined in settings. Using default currency ${defaultCurrencyCode} instead.`
64
+ `Currency ${activeCurrency} is not defined in settings. Using default currency ${defaultCurrencyCode} instead.`,
65
+ {
66
+ ip
67
+ }
61
68
  );
62
69
  activeCurrency = defaultCurrencyCode;
63
70
  }
@@ -72,7 +79,8 @@ const withCurrency =
72
79
  logger.info('Currency changed. Resetting basket...', {
73
80
  sessionid: req.cookies.get('osessionid')?.value ?? '',
74
81
  oldCurrency: req.cookies.get('pz-currency')?.value,
75
- newCurrency: activeCurrency
82
+ newCurrency: activeCurrency,
83
+ ip
76
84
  });
77
85
 
78
86
  resetBasket(req);
@@ -80,7 +88,10 @@ const withCurrency =
80
88
 
81
89
  req.middlewareParams.rewrites.currency = activeCurrency;
82
90
  } catch (error) {
83
- logger.error('withCurrency error', error);
91
+ logger.error('withCurrency error', {
92
+ error,
93
+ ip
94
+ });
84
95
  }
85
96
 
86
97
  return middleware(req, event);
@@ -13,6 +13,7 @@ import withCurrency from './currency';
13
13
  import withLocale from './locale';
14
14
  import logger from '../utils/log';
15
15
  import { user } from '../data/urls';
16
+ import { getUrlPathWithLocale } from '../utils/localization';
16
17
 
17
18
  const withPzDefault =
18
19
  (middleware: NextMiddleware) =>
@@ -20,10 +21,12 @@ const withPzDefault =
20
21
  const url = req.nextUrl.clone();
21
22
  const commerceUrl = encodeURIComponent(decodeURI(Settings.commerceUrl)); // encodeURI doesn't work as expected in middleware
22
23
  const searchParams = new URLSearchParams(url.search);
24
+ const ip = req.headers.get('x-forwarded-for') ?? '';
23
25
 
24
26
  logger.debug('withPzDefault', {
25
27
  url: url.href,
26
- middlewareParams: req.middlewareParams
28
+ middlewareParams: req.middlewareParams,
29
+ ip
27
30
  });
28
31
 
29
32
  // Support legacy ?format=json query param
@@ -50,7 +53,10 @@ const withPzDefault =
50
53
 
51
54
  return NextResponse.json(await request.json());
52
55
  } catch (error) {
53
- logger.error('?format=json error', error);
56
+ logger.error('?format=json error', {
57
+ error,
58
+ ip
59
+ });
54
60
  return NextResponse.next();
55
61
  }
56
62
  }
@@ -87,6 +93,23 @@ const withPzDefault =
87
93
  );
88
94
  }
89
95
 
96
+ // If commerce redirects to /orders/checkout/ without locale
97
+ if (
98
+ req.nextUrl.pathname.match(new RegExp('^/orders/checkout/$')) &&
99
+ req.nextUrl.searchParams.size === 0 &&
100
+ getUrlPathWithLocale(
101
+ '/orders/checkout/',
102
+ req.cookies.get('pz-locale')?.value
103
+ ) !== req.nextUrl.pathname
104
+ ) {
105
+ const redirectUrlWithLocale = `${url.origin}${getUrlPathWithLocale(
106
+ '/orders/checkout/',
107
+ req.cookies.get('pz-locale')?.value
108
+ )}`;
109
+
110
+ return NextResponse.redirect(redirectUrlWithLocale, 303);
111
+ }
112
+
90
113
  req.middlewareParams = {
91
114
  commerceUrl,
92
115
  rewrites: {}
@@ -130,12 +153,23 @@ const withPzDefault =
130
153
  event
131
154
  )) as NextResponse | void;
132
155
 
156
+ // if middleware.ts has a return value for current url
133
157
  if (middlewareResult instanceof NextResponse) {
134
- middlewareResult.headers.set(
135
- 'x-middleware-rewrite',
136
- url.href
137
- );
158
+ // pz-override-response header is used to prevent 404 page for custom responses.
159
+ if (
160
+ middlewareResult.headers.get(
161
+ 'pz-override-response'
162
+ ) !== 'true'
163
+ ) {
164
+ middlewareResult.headers.set(
165
+ 'x-middleware-rewrite',
166
+ url.href
167
+ );
168
+ }
138
169
  } else {
170
+ // if middleware.ts doesn't have a return value.
171
+ // e.g. NextResponse.next() doesn't exist in middleware.ts
172
+
139
173
  middlewareResult = NextResponse.rewrite(url);
140
174
  }
141
175
 
@@ -145,13 +179,17 @@ const withPzDefault =
145
179
  locale?.length > 0 ? locale : defaultLocaleValue,
146
180
  {
147
181
  sameSite: 'none',
148
- secure: true
182
+ secure: true,
183
+ expires: new Date(
184
+ Date.now() + 1000 * 60 * 60 * 24 * 7
185
+ ) // 7 days
149
186
  }
150
187
  );
151
188
  }
152
189
  middlewareResult.cookies.set('pz-currency', currency, {
153
190
  sameSite: 'none',
154
- secure: true
191
+ secure: true,
192
+ expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7) // 7 days
155
193
  });
156
194
 
157
195
  if (
@@ -160,7 +198,8 @@ const withPzDefault =
160
198
  ) {
161
199
  logger.debug('Locale changed', {
162
200
  locale,
163
- oldLocale: req.cookies.get('pz-locale')?.value
201
+ oldLocale: req.cookies.get('pz-locale')?.value,
202
+ ip
164
203
  });
165
204
  }
166
205
 
@@ -191,10 +230,16 @@ const withPzDefault =
191
230
  middlewareResult.cookies.set('csrftoken', csrf_token);
192
231
  }
193
232
  } catch (error) {
194
- logger.error('CSRF Error', error);
233
+ logger.error('CSRF Error', {
234
+ error,
235
+ ip
236
+ });
195
237
  }
196
238
  } catch (error) {
197
- logger.error('withPzDefault Error', error);
239
+ logger.error('withPzDefault Error', {
240
+ error,
241
+ ip
242
+ });
198
243
  }
199
244
 
200
245
  return middlewareResult;
@@ -24,6 +24,8 @@ const getMatchedLocale = (pathname: string) => {
24
24
  const withLocale =
25
25
  (middleware: NextMiddleware) =>
26
26
  async (req: PzNextRequest, event: NextFetchEvent) => {
27
+ const ip = req.headers.get('x-forwarded-for') ?? '';
28
+
27
29
  try {
28
30
  const url = req.nextUrl.clone();
29
31
  const matchedLocale = getMatchedLocale(url.pathname);
@@ -34,7 +36,9 @@ const withLocale =
34
36
  localeUrlStrategy ?? LocaleUrlStrategy.HideDefaultLocale;
35
37
 
36
38
  if (!defaultLocaleValue) {
37
- logger.error('Default locale value is not defined in settings.');
39
+ logger.error('Default locale value is not defined in settings.', {
40
+ ip
41
+ });
38
42
  throw new Error(
39
43
  'Default locale value is not defined. Use `defaultLocaleValue` property in `localization` object in `settings.js` file.'
40
44
  );
@@ -52,7 +56,10 @@ const withLocale =
52
56
 
53
57
  req.middlewareParams.rewrites.locale = matchedLocale;
54
58
  } catch (error) {
55
- logger.error('withLocale error', error);
59
+ logger.error('withLocale error', {
60
+ error,
61
+ ip
62
+ });
56
63
  }
57
64
 
58
65
  return middleware(req, event);
@@ -12,40 +12,41 @@ type PrettyUrlResult = {
12
12
  path?: string;
13
13
  };
14
14
 
15
- const resolvePrettyUrlHandler = (pathname: string) => async () => {
16
- let results = <{ old_path: string }[]>[];
17
- let prettyUrlResult: PrettyUrlResult = {
18
- matched: false
19
- };
15
+ const resolvePrettyUrlHandler =
16
+ (pathname: string, ip: string | null) => async () => {
17
+ let results = <{ old_path: string }[]>[];
18
+ let prettyUrlResult: PrettyUrlResult = {
19
+ matched: false
20
+ };
20
21
 
21
- try {
22
- const requestUrl = URLS.misc.prettyUrls(pathname);
22
+ try {
23
+ const requestUrl = URLS.misc.prettyUrls(pathname);
23
24
 
24
- logger.debug(`Resolving pretty url`, { pathname, requestUrl });
25
+ logger.debug(`Resolving pretty url`, { pathname, requestUrl, ip });
25
26
 
26
- const apiResponse = await fetch(requestUrl);
27
- const data = await apiResponse.json();
28
- ({ results } = data);
27
+ const apiResponse = await fetch(requestUrl);
28
+ const data = await apiResponse.json();
29
+ ({ results } = data);
29
30
 
30
- const matched = results.length > 0;
31
- const [{ old_path: path } = { old_path: '' }] = results;
32
- prettyUrlResult = {
33
- matched,
34
- path
35
- };
31
+ const matched = results.length > 0;
32
+ const [{ old_path: path } = { old_path: '' }] = results;
33
+ prettyUrlResult = {
34
+ matched,
35
+ path
36
+ };
36
37
 
37
- logger.trace('Pretty url result', prettyUrlResult);
38
- } catch (error) {
39
- logger.error('Error resolving pretty url', { error, pathname });
40
- }
38
+ logger.trace('Pretty url result', { prettyUrlResult, ip });
39
+ } catch (error) {
40
+ logger.error('Error resolving pretty url', { error, pathname, ip });
41
+ }
41
42
 
42
- return prettyUrlResult;
43
- };
43
+ return prettyUrlResult;
44
+ };
44
45
 
45
- const resolvePrettyUrl = async (pathname: string) => {
46
+ const resolvePrettyUrl = async (pathname: string, ip: string | null) => {
46
47
  return Cache.wrap(
47
48
  CacheKey.PrettyUrl(pathname),
48
- resolvePrettyUrlHandler(pathname),
49
+ resolvePrettyUrlHandler(pathname, ip),
49
50
  {
50
51
  useProxy: true
51
52
  }
@@ -68,6 +69,7 @@ const withPrettyUrl =
68
69
  )
69
70
  );
70
71
  };
72
+ const ip = req.headers.get('x-forwarded-for') ?? '';
71
73
 
72
74
  if (
73
75
  !isValidPrettyUrlPath(prettyUrlPathname) ||
@@ -81,14 +83,16 @@ const withPrettyUrl =
81
83
  prettyUrlPathname
82
84
  )
83
85
  ? url.pathname
84
- : prettyUrlPathname
86
+ : prettyUrlPathname,
87
+ ip
85
88
  );
86
89
 
87
90
  if (prettyUrlResult.matched) {
88
91
  req.middlewareParams.rewrites.prettyUrl = prettyUrlResult.path;
89
92
  logger.debug('Resolved pretty url', {
90
93
  source: prettyUrlPathname,
91
- dest: prettyUrlResult.path
94
+ dest: prettyUrlResult.path,
95
+ ip
92
96
  });
93
97
 
94
98
  return middleware(req, event);
@@ -38,81 +38,106 @@ const withRedirectionPayment =
38
38
  ''
39
39
  );
40
40
  const searchParams = new URLSearchParams(url.search);
41
+ const ip = req.headers.get('x-forwarded-for') ?? '';
41
42
 
42
- if (
43
- !pathnameWithoutLocale.startsWith('/orders') ||
44
- searchParams.get('page') !== 'RedirectionPageCompletePage' ||
45
- req.method !== 'POST'
46
- ) {
43
+ if (searchParams.get('page') !== 'RedirectionPageCompletePage') {
47
44
  return middleware(req, event);
48
45
  }
49
46
 
50
- const body = await streamToString(req.body);
51
47
  const requestUrl = `${Settings.commerceUrl}/orders/checkout/${url.search}`;
48
+ const requestHeaders = {
49
+ 'X-Requested-With': 'XMLHttpRequest',
50
+ 'Content-Type': 'application/x-www-form-urlencoded',
51
+ Cookie: req.headers.get('cookie') ?? '',
52
+ 'x-currency': req.cookies.get('pz-currency')?.value ?? ''
53
+ };
52
54
 
53
- const request = await fetch(requestUrl, {
54
- method: 'POST',
55
- headers: {
56
- 'X-Requested-With': 'XMLHttpRequest',
57
- 'Content-Type': 'application/x-www-form-urlencoded',
58
- Cookie: req.headers.get('cookie') ?? '',
59
- 'x-currency': req.cookies.get('pz-currency')?.value ?? ''
60
- },
61
- body
62
- });
63
-
64
- logger.info('Complete redirection payment request', {
65
- requestUrl,
66
- status: request.status
67
- });
68
-
69
- const response = await request.json();
70
-
71
- const { context_list: contextList } = response;
72
- const redirectionContext = contextList?.find(
73
- (context) => context.page_context?.redirect_url
74
- );
75
- const redirectUrl = redirectionContext?.page_context?.redirect_url;
55
+ try {
56
+ const body = await streamToString(req.body);
57
+
58
+ const request = await fetch(requestUrl, {
59
+ method: 'POST',
60
+ headers: requestHeaders,
61
+ body
62
+ });
63
+
64
+ logger.info('Complete redirection payment request', {
65
+ requestUrl,
66
+ status: request.status,
67
+ requestHeaders,
68
+ ip
69
+ });
70
+
71
+ const response = await request.json();
72
+
73
+ const { context_list: contextList } = response;
74
+ const redirectionContext = contextList?.find(
75
+ (context) => context.page_context?.redirect_url
76
+ );
77
+ const redirectUrl = redirectionContext?.page_context?.redirect_url;
76
78
 
77
- if (!redirectUrl) {
78
79
  logger.info('Order success page context list', {
79
80
  middleware: 'redirection-payment',
80
- contextList
81
+ contextList,
82
+ ip
81
83
  });
82
84
 
83
- logger.warn(
84
- 'No redirection url for order success page found in page_context',
85
- { middleware: 'redirection-payment' }
85
+ if (!redirectUrl) {
86
+ logger.warn(
87
+ 'No redirection url for order success page found in page_context',
88
+ {
89
+ middleware: 'redirection-payment',
90
+ requestHeaders,
91
+ response: JSON.stringify(response),
92
+ ip
93
+ }
94
+ );
95
+
96
+ const redirectUrlWithLocale = `${url.origin}${getUrlPathWithLocale(
97
+ '/orders/checkout/',
98
+ req.cookies.get('pz-locale')?.value
99
+ )}`;
100
+
101
+ return NextResponse.redirect(redirectUrlWithLocale, 303);
102
+ }
103
+
104
+ const redirectUrlWithLocale = `${url.origin}${getUrlPathWithLocale(
105
+ redirectUrl,
106
+ req.cookies.get('pz-locale')?.value
107
+ )}`;
108
+
109
+ logger.info('Redirecting to order success page', {
110
+ middleware: 'redirection-payment',
111
+ redirectUrlWithLocale,
112
+ ip
113
+ });
114
+
115
+ // Using POST method while redirecting causes an error,
116
+ // So we use 303 status code to change the method to GET
117
+ const nextResponse = NextResponse.redirect(redirectUrlWithLocale, 303);
118
+
119
+ nextResponse.headers.set(
120
+ 'Set-Cookie',
121
+ request.headers.get('set-cookie') ?? ''
86
122
  );
123
+
124
+ return nextResponse;
125
+ } catch (error) {
126
+ logger.error('Error while completing redirection payment', {
127
+ middleware: 'redirection-payment',
128
+ error,
129
+ requestHeaders,
130
+ ip
131
+ });
132
+
87
133
  return NextResponse.redirect(
88
134
  `${url.origin}${getUrlPathWithLocale(
89
- `/orders/checkout?${searchParams.toString()}`,
135
+ '/orders/checkout/',
90
136
  req.cookies.get('pz-locale')?.value
91
137
  )}`,
92
138
  303
93
139
  );
94
140
  }
95
-
96
- const redirectUrlWithLocale = `${url.origin}${getUrlPathWithLocale(
97
- redirectUrl,
98
- req.cookies.get('pz-locale')?.value
99
- )}`;
100
-
101
- logger.info('Redirecting to order success page', {
102
- middleware: 'redirection-payment',
103
- redirectUrlWithLocale
104
- });
105
-
106
- // Using POST method while redirecting causes an error,
107
- // So we use 303 status code to change the method to GET
108
- const nextResponse = NextResponse.redirect(redirectUrlWithLocale, 303);
109
-
110
- nextResponse.headers.set(
111
- 'Set-Cookie',
112
- request.headers.get('set-cookie') ?? ''
113
- );
114
-
115
- return nextResponse;
116
141
  };
117
142
 
118
143
  export default withRedirectionPayment;
@@ -32,6 +32,7 @@ const withThreeDRedirection =
32
32
  (middleware: NextMiddleware) =>
33
33
  async (req: PzNextRequest, event: NextFetchEvent) => {
34
34
  const url = req.nextUrl.clone();
35
+ const ip = req.headers.get('x-forwarded-for') ?? '';
35
36
 
36
37
  if (url.search.indexOf('CreditCardThreeDSecurePage') === -1) {
37
38
  return middleware(req, event);
@@ -57,7 +58,8 @@ const withThreeDRedirection =
57
58
  logger.info('Complete 3D payment request', {
58
59
  requestUrl,
59
60
  status: request.status,
60
- requestHeaders
61
+ requestHeaders,
62
+ ip
61
63
  });
62
64
 
63
65
  const response = await request.json();
@@ -70,7 +72,8 @@ const withThreeDRedirection =
70
72
 
71
73
  logger.info('Order success page context list', {
72
74
  middleware: 'three-d-redirection',
73
- contextList
75
+ contextList,
76
+ ip
74
77
  });
75
78
 
76
79
  if (!redirectUrl) {
@@ -79,7 +82,8 @@ const withThreeDRedirection =
79
82
  {
80
83
  middleware: 'three-d-redirection',
81
84
  requestHeaders,
82
- response: JSON.stringify(response)
85
+ response: JSON.stringify(response),
86
+ ip
83
87
  }
84
88
  );
85
89
 
@@ -98,7 +102,8 @@ const withThreeDRedirection =
98
102
 
99
103
  logger.info('Redirecting to order success page', {
100
104
  middleware: 'three-d-redirection',
101
- redirectUrlWithLocale
105
+ redirectUrlWithLocale,
106
+ ip
102
107
  });
103
108
 
104
109
  // Using POST method while redirecting causes an error,
@@ -115,7 +120,8 @@ const withThreeDRedirection =
115
120
  logger.error('Error while completing 3D payment', {
116
121
  middleware: 'three-d-redirection',
117
122
  error,
118
- requestHeaders
123
+ requestHeaders,
124
+ ip
119
125
  });
120
126
 
121
127
  return NextResponse.redirect(
@@ -12,6 +12,7 @@ const withUrlRedirection =
12
12
  (middleware: NextMiddleware) =>
13
13
  async (req: PzNextRequest, event: NextFetchEvent) => {
14
14
  const url = req.nextUrl.clone();
15
+ const ip = req.headers.get('x-forwarded-for') ?? '';
15
16
  const pathnameWithoutLocale = url.pathname.replace(
16
17
  urlLocaleMatcherRegex,
17
18
  ''
@@ -52,7 +53,10 @@ const withUrlRedirection =
52
53
  return Response.redirect(redirectUrl.toString(), request.status);
53
54
  }
54
55
  } catch (error) {
55
- logger.error('withUrlRedirection error', error);
56
+ logger.error('withUrlRedirection error', {
57
+ error,
58
+ ip
59
+ });
56
60
  }
57
61
 
58
62
  return middleware(req, event);
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.5.0",
4
+ "version": "1.7.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
  ];
@@ -77,6 +77,21 @@ export const preOrderMiddleware: Middleware = ({
77
77
  } = getState().checkout;
78
78
  const { endpoints: apiEndpoints } = checkoutApi;
79
79
 
80
+ if (preOrder.is_redirected) {
81
+ const contextList = result?.payload?.context_list;
82
+
83
+ if (
84
+ contextList.find(
85
+ (ctx) => ctx.page_name === 'RedirectionPaymentSelectedPage'
86
+ )
87
+ ) {
88
+ dispatch(
89
+ apiEndpoints.setPaymentOption.initiate(preOrder.payment_option?.pk)
90
+ );
91
+ return;
92
+ }
93
+ }
94
+
80
95
  dispatch(setPreOrder(preOrder));
81
96
 
82
97
  if (!preOrder.delivery_option && deliveryOptions.length > 0) {
@@ -164,7 +179,10 @@ export const contextListMiddleware: Middleware = ({
164
179
  : `/${currentLocale}${redirectUrl}`;
165
180
  }
166
181
 
167
- if (isMobileApp) {
182
+ if (
183
+ isMobileApp ||
184
+ /iPad|iPhone|iPod|Android/i.test(navigator.userAgent)
185
+ ) {
168
186
  showMobile3dIframe(url);
169
187
  } else {
170
188
  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
  }
@@ -71,6 +72,7 @@ export interface PreOrder {
71
72
  currency_type_label?: string;
72
73
  is_guest?: boolean;
73
74
  is_post_order?: boolean;
75
+ is_redirected?: boolean;
74
76
  installment: InstallmentOption;
75
77
  retail_store?: RetailStore;
76
78
  gift_box?: GiftBox;
package/types/metadata.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Metadata as NextMetaData } from 'next';
2
2
 
3
3
  export interface Metadata extends NextMetaData {
4
- alternates?: {
5
- languages: Record<string, string>;
4
+ alternates?: NextMetaData['alternates'] & {
5
+ languages?: Record<string, string>;
6
6
  };
7
7
  }
@@ -1,6 +1,7 @@
1
1
  import Settings from 'settings';
2
2
  import { ServerVariables } from './server-variables';
3
3
  import logger from '../utils/log';
4
+ import { headers } from 'next/headers';
4
5
 
5
6
  export enum FetchResponseType {
6
7
  JSON = 'json',
@@ -14,8 +15,12 @@ const appFetch = async <T>(
14
15
  ) => {
15
16
  let response: T;
16
17
  let status: number;
18
+ let ip = '';
17
19
 
18
20
  try {
21
+ const nextHeaders = headers();
22
+ ip = nextHeaders.get('x-forwarded-for') ?? '';
23
+
19
24
  const commerceUrl = Settings.commerceUrl;
20
25
  const currentLocale = Settings.localization.locales.find(
21
26
  (locale) => locale.value === ServerVariables.locale
@@ -38,10 +43,10 @@ const appFetch = async <T>(
38
43
  revalidate: 60
39
44
  };
40
45
 
41
- logger.debug(`FETCH START ${url}`, { requestURL, init });
46
+ logger.debug(`FETCH START ${url}`, { requestURL, init, ip });
42
47
  const req = await fetch(requestURL, init);
43
48
  status = req.status;
44
- logger.debug(`FETCH END ${url}`, { status: req.status });
49
+ logger.debug(`FETCH END ${url}`, { status: req.status, ip });
45
50
 
46
51
  const rawData = await req.text();
47
52
 
@@ -51,9 +56,9 @@ const appFetch = async <T>(
51
56
  response = rawData as unknown as T;
52
57
  }
53
58
 
54
- logger.trace(`FETCH RESPONSE`, { url, response });
59
+ logger.trace(`FETCH RESPONSE`, { url, response, ip });
55
60
  } catch (error) {
56
- logger.error(`FETCH FAILED`, { url, status, error });
61
+ logger.error(`FETCH FAILED`, { url, status, error, ip });
57
62
  }
58
63
 
59
64
  return response;
@@ -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
  };