@akinon/next 2.0.23-rc.0 → 2.0.24-beta.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,111 +7,224 @@ 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
-
14
- const isMasterpassCompletePage =
15
- url.pathname.includes('/orders/checkout') &&
16
- url.searchParams.get('page') === 'MasterpassRestCompletePage';
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
+ );
17
71
 
18
- if (!isMasterpassCompletePage) {
19
- return middleware(req, event);
72
+ return NextResponse.redirect(
73
+ `${url.origin}${getUrlPathWithLocale(
74
+ '/orders/checkout/',
75
+ req.cookies.get('pz-locale')?.value
76
+ )}`,
77
+ 303
78
+ );
20
79
  }
21
80
 
22
- try {
23
- const isPostCheckout = req.cookies.get('pz-post-checkout-flow')?.value === 'true';
24
- const requestUrl = new URL(getCheckoutPath(isPostCheckout), Settings.commerceUrl);
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
+ }
130
+ }
25
131
 
26
- url.searchParams.forEach((value, key) => {
27
- requestUrl.searchParams.set(key, value);
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
28
138
  });
29
139
 
30
- const formData = await req.formData();
31
- const body: Record<string, string> = {};
32
-
33
- Array.from(formData.entries()).forEach(([key, value]) => {
34
- body[key] = value.toString();
35
- });
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
+ );
36
152
 
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)
153
+ // Add error cookie
154
+ errorResponse.cookies.set('pz-pos-error', JSON.stringify(errors), {
155
+ path: '/'
50
156
  });
51
157
 
52
- const response = await request.json();
53
- const { errors } = response;
158
+ return errorResponse;
159
+ }
54
160
 
55
- if (errors && Object.keys(errors).length) {
56
- logger.error('Error while processing MasterpassRestCompletePage', {
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
+ {
57
172
  middleware: 'masterpass-rest-callback',
58
- errors,
173
+ requestHeaders,
174
+ response: JSON.stringify(response),
59
175
  ip
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
- }
77
-
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
176
  }
177
+ );
95
178
 
96
- return nextResponse;
97
- }
179
+ const redirectUrlWithLocale = `${url.origin}${getUrlPathWithLocale(
180
+ '/orders/checkout/',
181
+ req.cookies.get('pz-locale')?.value
182
+ )}`;
98
183
 
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
108
- });
184
+ return NextResponse.redirect(redirectUrlWithLocale, 303);
185
+ }
109
186
 
110
- return NextResponse.redirect(
111
- `${url.origin}${getUrlPathWithLocale('/orders/checkout/', req.cookies.get('pz-locale')?.value)}`,
112
- 303
113
- );
187
+ const redirectUrlWithLocale = `${url.origin}${getUrlPathWithLocale(
188
+ redirectUrl,
189
+ req.cookies.get('pz-locale')?.value
190
+ )}`;
191
+
192
+ logger.info('Redirecting after masterpass REST callback', {
193
+ middleware: 'masterpass-rest-callback',
194
+ redirectUrl: redirectUrlWithLocale,
195
+ ip
196
+ });
197
+
198
+ const nextResponse = NextResponse.redirect(redirectUrlWithLocale, 303);
199
+
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());
205
+ });
114
206
  }
115
- };
207
+
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
+ };
116
229
 
117
230
  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.23-rc.0",
4
+ "version": "2.0.24-beta.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.23-rc.0",
39
+ "@akinon/eslint-plugin-projectzero": "2.0.24-beta.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,16 +37,6 @@ 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
-
50
40
  declare module '@akinon/pz-cybersource-uc/src/redux/reducer' {
51
41
  export default reducer as any;
52
42
  }
package/plugins.js CHANGED
@@ -16,7 +16,6 @@ module.exports = [
16
16
  'pz-tabby-extension',
17
17
  'pz-apple-pay',
18
18
  'pz-tamara-extension',
19
- 'pz-similar-products',
20
19
  'pz-cybersource-uc',
21
20
  'pz-hepsipay',
22
21
  'pz-flow-payment',
@@ -26,28 +26,13 @@ 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, MiddlewareAction, PreOrder } from '../../types';
29
+ import { CheckoutContext, 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
-
51
36
  interface CheckoutResult {
52
37
  payload: {
53
38
  errors?: Record<string, string[]>;
@@ -84,7 +69,7 @@ export const redirectUrlMiddleware: Middleware = () => {
84
69
  const result = next(action) as CheckoutResult;
85
70
  const redirectUrl = result?.payload?.redirect_url;
86
71
 
87
- if (redirectUrl && !isIframeRedirectionActive()) {
72
+ if (redirectUrl) {
88
73
  const currentLocale = getCookie('pz-locale');
89
74
 
90
75
  let url = redirectUrl;
@@ -114,15 +99,8 @@ export const contextListMiddleware: Middleware = ({
114
99
  const { isMobileApp, userPhoneNumber } = getState().root;
115
100
  const result = next(action) as CheckoutResult;
116
101
  const preOrder = result?.payload?.pre_order;
117
- const act = action as MiddlewareAction;
118
102
 
119
103
  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
-
126
104
  result.payload.context_list.forEach((context) => {
127
105
  const redirectUrl = context.page_context.redirect_url;
128
106
  const isIframe = context.page_context.is_iframe ?? false;
@@ -156,7 +134,6 @@ export const contextListMiddleware: Middleware = ({
156
134
  if (isMobileDevice && isIframePaymentOptionIncluded) {
157
135
  showMobile3dIframe(urlObj.toString());
158
136
  } else if (isIframe) {
159
- setIframeRedirectionActive(true);
160
137
  showRedirectionIframe(urlObj.toString());
161
138
  } else {
162
139
  window.location.href = urlObj.toString();
@@ -232,34 +209,15 @@ export const contextListMiddleware: Middleware = ({
232
209
  (ctx) => ctx.page_name === 'DeliveryOptionSelectionPage'
233
210
  )
234
211
  ) {
235
- const isCreditCardPayment =
236
- preOrder?.payment_option?.payment_type === 'credit_card' ||
237
- preOrder?.payment_option?.payment_type === 'masterpass';
238
-
239
212
  if (context.page_context.card_type) {
240
213
  dispatch(setCardType(context.page_context.card_type));
241
- } else if (
242
- isCreditCardPayment &&
243
- isBinNumberResponse &&
244
- !hasCardTypeInContextList
245
- ) {
246
- dispatch(setCardType(null));
247
- dispatch(setInstallmentOptions([]));
248
214
  }
249
215
 
250
216
  if (
251
217
  context.page_context.installments &&
252
218
  preOrder?.payment_option?.payment_type !== 'masterpass_rest'
253
219
  ) {
254
- if (
255
- !isCreditCardPayment ||
256
- context.page_context.card_type ||
257
- hasCardTypeInContextList
258
- ) {
259
- dispatch(
260
- setInstallmentOptions(context.page_context.installments)
261
- );
262
- }
220
+ dispatch(setInstallmentOptions(context.page_context.installments));
263
221
  }
264
222
  }
265
223
 
@@ -14,17 +14,9 @@ export const installmentOptionMiddleware: Middleware = ({
14
14
  return result;
15
15
  }
16
16
 
17
- const { installmentOptions, cardType } = getState().checkout;
17
+ const { installmentOptions } = 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
-
28
20
  if (
29
21
  !preOrder?.installment &&
30
22
  preOrder?.payment_option?.payment_type !== 'saved_card' &&
package/types/index.ts CHANGED
@@ -85,12 +85,6 @@ 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;
94
88
  /**
95
89
  * CSRF cookie hardening settings.
96
90
  *
@@ -242,7 +236,6 @@ export interface Settings {
242
236
  separator?: string;
243
237
  segments: PzSegmentDefinition[];
244
238
  };
245
- payloadOptimization?: import('../utils/payload-optimizer').PayloadOptimizationConfig;
246
239
  }
247
240
 
248
241
  export interface CacheOptions {
@@ -302,9 +295,7 @@ export interface PzSegmentsConfig {
302
295
  }
303
296
 
304
297
  // Search params type compatible with both Next.js resolved searchParams and URLSearchParams
305
- export type SearchParams =
306
- | Record<string, string | string[] | undefined>
307
- | URLSearchParams;
298
+ export type SearchParams = Record<string, string | string[] | undefined> | URLSearchParams;
308
299
 
309
300
  // Raw Next 16 server prop shape, used at the middleware/HOC boundary before normalization
310
301
  export type RawSearchParams = Record<string, string | string[] | undefined>;
@@ -337,16 +328,14 @@ export interface RootLayoutProps<T = any> extends LayoutProps<T> {
337
328
 
338
329
  // Async versions for Next.js 16 generateMetadata and internal use
339
330
  export interface AsyncPageProps<T = any> {
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
- >;
331
+ params: Promise<T & {
332
+ pz?: string;
333
+ commerce?: string;
334
+ locale?: string;
335
+ currency?: string;
336
+ url?: string;
337
+ [key: string]: any;
338
+ }>;
350
339
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
351
340
  }
352
341
 
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: process.env.NODE_ENV === 'production',
34
+ secure: httpOnly && process.env.NODE_ENV === 'production',
35
35
  sameSite: 'lax'
36
36
  };
37
37
  }
package/utils/index.ts CHANGED
@@ -6,6 +6,7 @@ import getRootHostname from './get-root-hostname';
6
6
  export * from './get-currency';
7
7
  export * from './menu-generator';
8
8
  export * from './generate-commerce-search-params';
9
+ export * from './normalize-search-params';
9
10
  export * from './get-currency-label';
10
11
  export * from './pz-segments';
11
12
  export * from './get-checkout-path';
@@ -0,0 +1,42 @@
1
+ import { SearchParams } from '../types';
2
+
3
+ /**
4
+ * Normalize SearchParams to a plain Record<string, string | string[]>.
5
+ *
6
+ * Accepts either a URLSearchParams instance (Next 16 HOC output / v2 brands)
7
+ * or a plain Record (v1 brands / direct callers) and always returns a plain
8
+ * object. Downstream code can then safely:
9
+ * - JSON.stringify it for stable cache keys (avoids the URLSearchParams
10
+ * `JSON.stringify === "{}"` cache collision)
11
+ * - iterate with Object.keys / Object.entries (avoids the dropped query
12
+ * param bug on the PDP outbound URL builder)
13
+ * - spread with { ...obj }
14
+ *
15
+ * Repeated keys in URLSearchParams (e.g. ?color=red&color=blue) collapse to
16
+ * a string[] value so multi-value filter semantics survive.
17
+ *
18
+ * Returns undefined when the input is undefined so downstream
19
+ * `if (searchParams)` guards keep working unchanged.
20
+ */
21
+ export const normalizeSearchParams = (
22
+ searchParams?: SearchParams
23
+ ): Record<string, string | string[]> | undefined => {
24
+ if (!searchParams) return undefined;
25
+
26
+ if (searchParams instanceof URLSearchParams) {
27
+ const out: Record<string, string | string[]> = {};
28
+ for (const key of new Set(searchParams.keys())) {
29
+ const all = searchParams.getAll(key);
30
+ out[key] = all.length > 1 ? all : all[0];
31
+ }
32
+ return out;
33
+ }
34
+
35
+ // Plain object — drop undefined values so cache key serialization is stable
36
+ // across calls that omit some keys.
37
+ const out: Record<string, string | string[]> = {};
38
+ for (const [key, value] of Object.entries(searchParams)) {
39
+ if (value !== undefined) out[key] = value;
40
+ }
41
+ return out;
42
+ };
package/with-pz-config.js CHANGED
@@ -69,8 +69,7 @@ const defaultConfig = {
69
69
  acc[`@akinon/${plugin}`] = false;
70
70
  return acc;
71
71
  }, {}),
72
- translations: false,
73
- '@opentelemetry/exporter-jaeger': false
72
+ translations: false
74
73
  };
75
74
  // Ensure webpack can resolve deps from the app's node_modules when
76
75
  // compiling transpiled packages (e.g. @akinon/next) whose imports