@akinon/next 2.0.28-beta.0 → 2.0.28-rc.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.
@@ -7,224 +7,111 @@ import { getCheckoutPath } from '../utils';
7
7
 
8
8
  const withMasterpassRestCallback =
9
9
  (middleware: NextMiddleware) =>
10
- async (req: PzNextRequest, event: NextFetchEvent) => {
11
- const url = req.nextUrl.clone();
12
- const ip = req.headers.get('x-forwarded-for') ?? '';
13
- const sessionId = req.cookies.get('osessionid');
14
-
15
- if (!url.pathname.includes('/orders/masterpass-rest-callback')) {
16
- return middleware(req, event);
17
- }
18
-
19
- if (req.method !== 'POST') {
20
- logger.warn('Invalid request method for masterpass REST callback', {
21
- middleware: 'masterpass-rest-callback',
22
- method: req.method,
23
- ip
24
- });
25
-
26
- return NextResponse.redirect(
27
- `${url.origin}${getUrlPathWithLocale(
28
- '/orders/checkout/',
29
- req.cookies.get('pz-locale')?.value
30
- )}`,
31
- 303
32
- );
33
- }
34
-
35
- const responseCode = url.searchParams.get('responseCode');
36
- const token = url.searchParams.get('token');
37
-
38
- if (!responseCode || !token) {
39
- logger.warn('Missing required parameters for masterpass REST callback', {
40
- middleware: 'masterpass-rest-callback',
41
- responseCode,
42
- token,
43
- ip
44
- });
45
-
46
- return NextResponse.redirect(
47
- `${url.origin}${getUrlPathWithLocale(
48
- '/orders/checkout/',
49
- req.cookies.get('pz-locale')?.value
50
- )}`,
51
- 303
52
- );
53
- }
54
-
55
- try {
56
- const formData = await req.formData();
57
- const body: Record<string, string> = {};
58
-
59
- Array.from(formData.entries()).forEach(([key, value]) => {
60
- body[key] = value.toString();
61
- });
62
-
63
- if (!sessionId) {
64
- logger.warn(
65
- 'Make sure that the SESSION_COOKIE_SAMESITE environment variable is set to None in Commerce.',
66
- {
67
- middleware: 'masterpass-rest-callback',
68
- ip
69
- }
70
- );
10
+ async (req: PzNextRequest, event: NextFetchEvent) => {
11
+ const url = req.nextUrl.clone();
12
+ const ip = req.headers.get('x-forwarded-for') ?? '';
71
13
 
72
- return NextResponse.redirect(
73
- `${url.origin}${getUrlPathWithLocale(
74
- '/orders/checkout/',
75
- req.cookies.get('pz-locale')?.value
76
- )}`,
77
- 303
78
- );
79
- }
14
+ const isMasterpassCompletePage =
15
+ url.pathname.includes('/orders/checkout') &&
16
+ url.searchParams.get('page') === 'MasterpassRestCompletePage';
80
17
 
81
- const isPostCheckout = req.cookies.get('pz-post-checkout-flow')?.value === 'true';
82
- const requestUrl = new URL(getCheckoutPath(isPostCheckout), Settings.commerceUrl);
83
- requestUrl.searchParams.set('page', 'MasterpassRestCompletePage');
84
- requestUrl.searchParams.set('responseCode', responseCode);
85
- requestUrl.searchParams.set('token', token);
86
- requestUrl.searchParams.set(
87
- 'three_d_secure',
88
- body.transactionType?.includes('3D') ? 'true' : 'false'
89
- );
90
- requestUrl.searchParams.set(
91
- 'transactionType',
92
- body.transactionType || ''
93
- );
94
-
95
- const requestHeaders = {
96
- 'Content-Type': 'application/x-www-form-urlencoded',
97
- 'X-Requested-With': 'XMLHttpRequest',
98
- Cookie: req.headers.get('cookie') ?? '',
99
- 'x-currency': req.cookies.get('pz-currency')?.value ?? '',
100
- 'x-forwarded-for': ip,
101
- 'User-Agent': req.headers.get('user-agent') ?? ''
102
- };
103
-
104
- const request = await fetch(requestUrl.toString(), {
105
- method: 'POST',
106
- headers: requestHeaders,
107
- body: new URLSearchParams(body)
108
- });
109
-
110
- logger.info('Masterpass REST callback request', {
111
- requestUrl: requestUrl.toString(),
112
- status: request.status,
113
- requestHeaders,
114
- ip
115
- });
116
-
117
- const response = await request.json();
118
-
119
- const { context_list: contextList, errors } = response;
120
-
121
- let redirectUrl = response.redirect_url;
122
-
123
- if (!redirectUrl && contextList && contextList.length > 0) {
124
- for (const context of contextList) {
125
- if (context.page_context && context.page_context.redirect_url) {
126
- redirectUrl = context.page_context.redirect_url;
127
- break;
128
- }
129
- }
18
+ if (!isMasterpassCompletePage) {
19
+ return middleware(req, event);
130
20
  }
131
21
 
132
- if (errors && Object.keys(errors).length) {
133
- logger.error('Error while processing masterpass REST callback', {
134
- middleware: 'masterpass-rest-callback',
135
- errors,
136
- requestHeaders,
137
- ip
22
+ try {
23
+ const isPostCheckout = req.cookies.get('pz-post-checkout-flow')?.value === 'true';
24
+ const requestUrl = new URL(getCheckoutPath(isPostCheckout), Settings.commerceUrl);
25
+
26
+ url.searchParams.forEach((value, key) => {
27
+ requestUrl.searchParams.set(key, value);
138
28
  });
139
29
 
140
- const errorResponse = NextResponse.redirect(
141
- `${url.origin}${getUrlPathWithLocale(
142
- '/orders/checkout/',
143
- req.cookies.get('pz-locale')?.value
144
- )}`,
145
- {
146
- status: 303,
147
- headers: {
148
- 'Set-Cookie': `pz-pos-error=${encodeURIComponent(JSON.stringify(errors))}; path=/;`
149
- }
150
- }
151
- );
30
+ const formData = await req.formData();
31
+ const body: Record<string, string> = {};
152
32
 
153
- // Add error cookie
154
- errorResponse.cookies.set('pz-pos-error', JSON.stringify(errors), {
155
- path: '/'
33
+ Array.from(formData.entries()).forEach(([key, value]) => {
34
+ body[key] = value.toString();
156
35
  });
157
36
 
158
- return errorResponse;
159
- }
37
+ const requestHeaders = {
38
+ 'Content-Type': 'application/x-www-form-urlencoded',
39
+ 'X-Requested-With': 'XMLHttpRequest',
40
+ Cookie: req.headers.get('cookie') ?? '',
41
+ 'x-currency': req.cookies.get('pz-currency')?.value ?? '',
42
+ 'x-forwarded-for': ip,
43
+ 'User-Agent': req.headers.get('user-agent') ?? ''
44
+ };
45
+
46
+ const request = await fetch(requestUrl.toString(), {
47
+ method: 'POST',
48
+ headers: requestHeaders,
49
+ body: new URLSearchParams(body)
50
+ });
160
51
 
161
- logger.info('Masterpass REST callback response', {
162
- middleware: 'masterpass-rest-callback',
163
- contextList,
164
- redirectUrl,
165
- ip
166
- });
167
-
168
- if (!redirectUrl) {
169
- logger.warn(
170
- 'No redirection url found in response. Redirecting to checkout page.',
171
- {
52
+ const response = await request.json();
53
+ const { errors } = response;
54
+
55
+ if (errors && Object.keys(errors).length) {
56
+ logger.error('Error while processing MasterpassRestCompletePage', {
172
57
  middleware: 'masterpass-rest-callback',
173
- requestHeaders,
174
- response: JSON.stringify(response),
58
+ errors,
175
59
  ip
176
- }
177
- );
178
-
179
- const redirectUrlWithLocale = `${url.origin}${getUrlPathWithLocale(
180
- '/orders/checkout/',
181
- req.cookies.get('pz-locale')?.value
182
- )}`;
183
-
184
- return NextResponse.redirect(redirectUrlWithLocale, 303);
185
- }
186
-
187
- const redirectUrlWithLocale = `${url.origin}${getUrlPathWithLocale(
188
- redirectUrl,
189
- req.cookies.get('pz-locale')?.value
190
- )}`;
60
+ });
61
+
62
+ const errorResponse = NextResponse.redirect(
63
+ `${url.origin}${getUrlPathWithLocale(
64
+ '/orders/checkout/',
65
+ req.cookies.get('pz-locale')?.value
66
+ )}`,
67
+ 303
68
+ );
69
+
70
+ // Add error cookie
71
+ errorResponse.cookies.set('pz-pos-error', JSON.stringify(errors), {
72
+ path: '/'
73
+ });
74
+
75
+ return errorResponse;
76
+ }
191
77
 
192
- logger.info('Redirecting after masterpass REST callback', {
193
- middleware: 'masterpass-rest-callback',
194
- redirectUrl: redirectUrlWithLocale,
195
- ip
196
- });
78
+ const redirectUrl =
79
+ response.redirect_url ||
80
+ response.context_list?.[0]?.page_context?.redirect_url;
81
+
82
+ if (redirectUrl) {
83
+ const nextResponse = NextResponse.redirect(
84
+ `${url.origin}${getUrlPathWithLocale(redirectUrl, req.cookies.get('pz-locale')?.value)}`,
85
+ 303
86
+ );
87
+
88
+ // Forward set-cookie headers from the upstream response
89
+ const setCookieHeader = request.headers.get('set-cookie');
90
+ if (setCookieHeader) {
91
+ setCookieHeader.split(',').forEach((cookie) => {
92
+ nextResponse.headers.append('Set-Cookie', cookie.trim());
93
+ });
94
+ }
197
95
 
198
- const nextResponse = NextResponse.redirect(redirectUrlWithLocale, 303);
96
+ return nextResponse;
97
+ }
199
98
 
200
- // Forward set-cookie headers from the upstream response
201
- const setCookieHeader = request.headers.get('set-cookie');
202
- if (setCookieHeader) {
203
- setCookieHeader.split(',').forEach((cookie) => {
204
- nextResponse.headers.append('Set-Cookie', cookie.trim());
99
+ return NextResponse.redirect(
100
+ `${url.origin}${getUrlPathWithLocale('/orders/checkout/', req.cookies.get('pz-locale')?.value)}`,
101
+ 303
102
+ );
103
+ } catch (error) {
104
+ logger.error('Error while processing MasterpassRestCompletePage', {
105
+ middleware: 'masterpass-rest-callback',
106
+ error,
107
+ ip
205
108
  });
206
- }
207
109
 
208
- return nextResponse;
209
- } catch (error) {
210
- logger.error('Error while processing masterpass REST callback', {
211
- middleware: 'masterpass-rest-callback',
212
- error,
213
- requestHeaders: {
214
- Cookie: req.headers.get('cookie') ?? '',
215
- 'x-currency': req.cookies.get('pz-currency')?.value ?? ''
216
- },
217
- ip
218
- });
219
-
220
- return NextResponse.redirect(
221
- `${url.origin}${getUrlPathWithLocale(
222
- '/orders/checkout/',
223
- req.cookies.get('pz-locale')?.value
224
- )}`,
225
- 303
226
- );
227
- }
228
- };
110
+ return NextResponse.redirect(
111
+ `${url.origin}${getUrlPathWithLocale('/orders/checkout/', req.cookies.get('pz-locale')?.value)}`,
112
+ 303
113
+ );
114
+ }
115
+ };
229
116
 
230
117
  export default withMasterpassRestCallback;
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": "2.0.28-beta.0",
4
+ "version": "2.0.28-rc.0",
5
5
  "private": false,
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -36,7 +36,7 @@
36
36
  "set-cookie-parser": "2.6.0"
37
37
  },
38
38
  "devDependencies": {
39
- "@akinon/eslint-plugin-projectzero": "2.0.28-beta.0",
39
+ "@akinon/eslint-plugin-projectzero": "2.0.28-rc.0",
40
40
  "@babel/core": "7.26.10",
41
41
  "@babel/preset-env": "7.26.9",
42
42
  "@babel/preset-typescript": "7.27.0",
package/plugins.d.ts CHANGED
@@ -37,6 +37,16 @@ declare module '@akinon/pz-cybersource-uc/src/redux/middleware' {
37
37
  export default middleware as any;
38
38
  }
39
39
 
40
+ declare module '@akinon/pz-apple-pay' {}
41
+
42
+ declare module '@akinon/pz-similar-products' {
43
+ export const SimilarProductsModal: any;
44
+ export const SimilarProductsFilterSidebar: any;
45
+ export const SimilarProductsResultsGrid: any;
46
+ export const SimilarProductsPlugin: any;
47
+ export const SimilarProductsButtonPlugin: any;
48
+ }
49
+
40
50
  declare module '@akinon/pz-cybersource-uc/src/redux/reducer' {
41
51
  export default reducer as any;
42
52
  }
package/plugins.js CHANGED
@@ -16,6 +16,7 @@ module.exports = [
16
16
  'pz-tabby-extension',
17
17
  'pz-apple-pay',
18
18
  'pz-tamara-extension',
19
+ 'pz-similar-products',
19
20
  'pz-cybersource-uc',
20
21
  'pz-hepsipay',
21
22
  'pz-flow-payment',
@@ -26,13 +26,28 @@ import {
26
26
  } from '../../redux/reducers/checkout';
27
27
  import { RootState, TypedDispatch } from 'redux/store';
28
28
  import { checkoutApi } from '../../data/client/checkout';
29
- import { CheckoutContext, PreOrder } from '../../types';
29
+ import { CheckoutContext, MiddlewareAction, PreOrder } from '../../types';
30
30
  import { getCookie } from '../../utils';
31
31
  import settings from 'settings';
32
32
  import { LocaleUrlStrategy } from '../../localization';
33
33
  import { showMobile3dIframe } from '../../utils/mobile-3d-iframe';
34
34
  import { showRedirectionIframe } from '../../utils/redirection-iframe';
35
35
 
36
+ const IFRAME_REDIRECTION_KEY = 'pz-iframe-redirection-active';
37
+
38
+ const isIframeRedirectionActive = () =>
39
+ typeof window !== 'undefined' &&
40
+ sessionStorage.getItem(IFRAME_REDIRECTION_KEY) === 'true';
41
+
42
+ const setIframeRedirectionActive = (active: boolean) => {
43
+ if (typeof window === 'undefined') return;
44
+ if (active) {
45
+ sessionStorage.setItem(IFRAME_REDIRECTION_KEY, 'true');
46
+ } else {
47
+ sessionStorage.removeItem(IFRAME_REDIRECTION_KEY);
48
+ }
49
+ };
50
+
36
51
  interface CheckoutResult {
37
52
  payload: {
38
53
  errors?: Record<string, string[]>;
@@ -69,7 +84,7 @@ export const redirectUrlMiddleware: Middleware = () => {
69
84
  const result = next(action) as CheckoutResult;
70
85
  const redirectUrl = result?.payload?.redirect_url;
71
86
 
72
- if (redirectUrl) {
87
+ if (redirectUrl && !isIframeRedirectionActive()) {
73
88
  const currentLocale = getCookie('pz-locale');
74
89
 
75
90
  let url = redirectUrl;
@@ -99,8 +114,15 @@ export const contextListMiddleware: Middleware = ({
99
114
  const { isMobileApp, userPhoneNumber } = getState().root;
100
115
  const result = next(action) as CheckoutResult;
101
116
  const preOrder = result?.payload?.pre_order;
117
+ const act = action as MiddlewareAction;
102
118
 
103
119
  if (result?.payload?.context_list) {
120
+ const endpointName = act.meta?.arg?.endpointName;
121
+ const isBinNumberResponse = endpointName === 'setBinNumber';
122
+ const hasCardTypeInContextList = result.payload.context_list.some(
123
+ (ctx) => ctx.page_context.card_type
124
+ );
125
+
104
126
  result.payload.context_list.forEach((context) => {
105
127
  const redirectUrl = context.page_context.redirect_url;
106
128
  const isIframe = context.page_context.is_iframe ?? false;
@@ -134,6 +156,7 @@ export const contextListMiddleware: Middleware = ({
134
156
  if (isMobileDevice && isIframePaymentOptionIncluded) {
135
157
  showMobile3dIframe(urlObj.toString());
136
158
  } else if (isIframe) {
159
+ setIframeRedirectionActive(true);
137
160
  showRedirectionIframe(urlObj.toString());
138
161
  } else {
139
162
  window.location.href = urlObj.toString();
@@ -209,15 +232,34 @@ export const contextListMiddleware: Middleware = ({
209
232
  (ctx) => ctx.page_name === 'DeliveryOptionSelectionPage'
210
233
  )
211
234
  ) {
235
+ const isCreditCardPayment =
236
+ preOrder?.payment_option?.payment_type === 'credit_card' ||
237
+ preOrder?.payment_option?.payment_type === 'masterpass';
238
+
212
239
  if (context.page_context.card_type) {
213
240
  dispatch(setCardType(context.page_context.card_type));
241
+ } else if (
242
+ isCreditCardPayment &&
243
+ isBinNumberResponse &&
244
+ !hasCardTypeInContextList
245
+ ) {
246
+ dispatch(setCardType(null));
247
+ dispatch(setInstallmentOptions([]));
214
248
  }
215
249
 
216
250
  if (
217
251
  context.page_context.installments &&
218
252
  preOrder?.payment_option?.payment_type !== 'masterpass_rest'
219
253
  ) {
220
- dispatch(setInstallmentOptions(context.page_context.installments));
254
+ if (
255
+ !isCreditCardPayment ||
256
+ context.page_context.card_type ||
257
+ hasCardTypeInContextList
258
+ ) {
259
+ dispatch(
260
+ setInstallmentOptions(context.page_context.installments)
261
+ );
262
+ }
221
263
  }
222
264
  }
223
265
 
@@ -14,9 +14,17 @@ export const installmentOptionMiddleware: Middleware = ({
14
14
  return result;
15
15
  }
16
16
 
17
- const { installmentOptions } = getState().checkout;
17
+ const { installmentOptions, cardType } = getState().checkout;
18
18
  const { endpoints: apiEndpoints } = checkoutApi;
19
19
 
20
+ const isCreditCardPayment =
21
+ preOrder?.payment_option?.payment_type === 'credit_card' ||
22
+ preOrder?.payment_option?.payment_type === 'masterpass';
23
+
24
+ if (isCreditCardPayment && !cardType) {
25
+ return result;
26
+ }
27
+
20
28
  if (
21
29
  !preOrder?.installment &&
22
30
  preOrder?.payment_option?.payment_type !== 'saved_card' &&
package/types/index.ts CHANGED
@@ -85,6 +85,12 @@ export interface Settings {
85
85
  };
86
86
  usePrettyUrlRoute?: boolean;
87
87
  commerceUrl: string;
88
+ /**
89
+ * This option allows you to track Sentry events on the client side, in addition to server and edge environments.
90
+ *
91
+ * It overrides process.env.NEXT_PUBLIC_SENTRY_DSN and process.env.SENTRY_DSN.
92
+ */
93
+ sentryDsn?: string;
88
94
  /**
89
95
  * CSRF cookie hardening settings.
90
96
  *
@@ -236,6 +242,7 @@ export interface Settings {
236
242
  separator?: string;
237
243
  segments: PzSegmentDefinition[];
238
244
  };
245
+ payloadOptimization?: import('../utils/payload-optimizer').PayloadOptimizationConfig;
239
246
  }
240
247
 
241
248
  export interface CacheOptions {
@@ -295,7 +302,9 @@ export interface PzSegmentsConfig {
295
302
  }
296
303
 
297
304
  // Search params type compatible with both Next.js resolved searchParams and URLSearchParams
298
- export type SearchParams = Record<string, string | string[] | undefined> | URLSearchParams;
305
+ export type SearchParams =
306
+ | Record<string, string | string[] | undefined>
307
+ | URLSearchParams;
299
308
 
300
309
  // Raw Next 16 server prop shape, used at the middleware/HOC boundary before normalization
301
310
  export type RawSearchParams = Record<string, string | string[] | undefined>;
@@ -328,14 +337,16 @@ export interface RootLayoutProps<T = any> extends LayoutProps<T> {
328
337
 
329
338
  // Async versions for Next.js 16 generateMetadata and internal use
330
339
  export interface AsyncPageProps<T = any> {
331
- params: Promise<T & {
332
- pz?: string;
333
- commerce?: string;
334
- locale?: string;
335
- currency?: string;
336
- url?: string;
337
- [key: string]: any;
338
- }>;
340
+ params: Promise<
341
+ T & {
342
+ pz?: string;
343
+ commerce?: string;
344
+ locale?: string;
345
+ currency?: string;
346
+ url?: string;
347
+ [key: string]: any;
348
+ }
349
+ >;
339
350
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
340
351
  }
341
352
 
package/utils/csrf.ts CHANGED
@@ -31,7 +31,7 @@ export function getCsrfCookieFlags(): CsrfCookieFlags {
31
31
  const httpOnly = isCsrfHttpOnly();
32
32
  return {
33
33
  httpOnly,
34
- secure: httpOnly && process.env.NODE_ENV === 'production',
34
+ secure: process.env.NODE_ENV === 'production',
35
35
  sameSite: 'lax'
36
36
  };
37
37
  }