@akinon/next 2.0.6-rc.0 → 2.0.6-rc.2

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,21 @@
1
1
  # @akinon/next
2
2
 
3
+ ## 2.0.6-rc.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 89deabe5: ZERO-4392: Remove deprecated dependencies and update package versions in yarn.lock
8
+ - 1a345c47: ZERO-4234: Add Global Toast Notification System
9
+ - 1f1ae44e: ZERO-4230: Add Commerce API mock/replay mode for development
10
+ - 89ed4e03: ZERO-4394: Add Next.js 16 ESLint migration v9 + flat config
11
+ - 8d8fefbe: ZERO-4398: Enhance SEO metadata generation for flat pages and update FlatPage interface to include localized URLs
12
+
13
+ ## 2.0.6-rc.1
14
+
15
+ ### Patch Changes
16
+
17
+ - 51ea0688: ZERO-4377: Fix checkout card type state being cleared after valid bin number responses.
18
+
3
19
  ## 2.0.6-rc.0
4
20
 
5
21
  ### Patch Changes
package/api/auth.ts CHANGED
@@ -154,18 +154,49 @@ const getDefaultAuthConfig = () => {
154
154
  userIp
155
155
  });
156
156
 
157
- const checkCurrentUser = await getCurrentUser(
158
- cookieStore.get('osessionid')?.value ?? '',
159
- cookieStore.get('pz-currency')?.value ?? ''
160
- );
161
-
162
- if (checkCurrentUser?.pk) {
163
- const sessionCookie = reqHeaders
164
- .get('cookie')
165
- ?.match(/osessionid=\w+/)?.[0]
166
- .replace(/osessionid=/, '');
167
- if (sessionCookie) {
168
- reqHeaders.set('cookie', sessionCookie);
157
+ const existingSessionId = cookieStore.get('osessionid')?.value;
158
+
159
+ if (existingSessionId) {
160
+ const checkCurrentUser = await getCurrentUser(
161
+ existingSessionId,
162
+ cookieStore.get('pz-currency')?.value ?? ''
163
+ );
164
+
165
+ if (checkCurrentUser?.pk) {
166
+ const sessionCookie = reqHeaders
167
+ .get('cookie')
168
+ ?.match(/osessionid=\w+/)?.[0]
169
+ .replace(/osessionid=/, '');
170
+ if (sessionCookie) {
171
+ reqHeaders.set('cookie', sessionCookie);
172
+ }
173
+ } else {
174
+ // Stale session cookie — remove from headers and clear in browser
175
+ const currentCookies = reqHeaders.get('cookie') || '';
176
+ const cleanedCookies = currentCookies
177
+ .split(';')
178
+ .filter((c) => !c.trim().startsWith('osessionid='))
179
+ .join(';')
180
+ .trim();
181
+ reqHeaders.set('cookie', cleanedCookies);
182
+
183
+ const { localeUrlStrategy } = Settings.localization;
184
+ const fallbackHost =
185
+ headerStore.get('x-forwarded-host') ||
186
+ headerStore.get('host');
187
+ const hostname =
188
+ process.env.NEXT_PUBLIC_URL || `https://${fallbackHost}`;
189
+ const rootHostname =
190
+ localeUrlStrategy === LocaleUrlStrategy.Subdomain
191
+ ? getRootHostname(hostname)
192
+ : null;
193
+ const expireOptions = {
194
+ path: '/',
195
+ maxAge: 0,
196
+ ...(rootHostname ? { domain: rootHostname } : {})
197
+ };
198
+ cookieStore.set('osessionid', '', expireOptions);
199
+ cookieStore.set('sessionid', '', expireOptions);
169
200
  }
170
201
  }
171
202
 
@@ -393,18 +424,47 @@ const defaultNextAuthOptionsV4 = (req: any, res: any) => {
393
424
  userIp
394
425
  });
395
426
 
396
- const checkCurrentUser = await getCurrentUser(
397
- req.cookies['osessionid'] ?? '',
398
- req.cookies['pz-currency'] ?? ''
399
- );
400
-
401
- if (checkCurrentUser?.pk) {
402
- const sessionCookie = reqHeaders
403
- .get('cookie')
404
- ?.match(/osessionid=\w+/)?.[0]
405
- .replace(/osessionid=/, '');
406
- if (sessionCookie) {
407
- reqHeaders.set('cookie', sessionCookie);
427
+ if (req.cookies['osessionid']) {
428
+ const checkCurrentUser = await getCurrentUser(
429
+ req.cookies['osessionid'],
430
+ req.cookies['pz-currency'] ?? ''
431
+ );
432
+
433
+ if (checkCurrentUser?.pk) {
434
+ const sessionCookie = reqHeaders
435
+ .get('cookie')
436
+ ?.match(/osessionid=\w+/)?.[0]
437
+ .replace(/osessionid=/, '');
438
+ if (sessionCookie) {
439
+ reqHeaders.set('cookie', sessionCookie);
440
+ }
441
+ } else {
442
+ // Stale session cookie — remove from headers and clear in browser
443
+ const currentCookies = reqHeaders.get('cookie') || '';
444
+ const cleanedCookies = currentCookies
445
+ .split(';')
446
+ .filter((c) => !c.trim().startsWith('osessionid='))
447
+ .join(';')
448
+ .trim();
449
+ reqHeaders.set('cookie', cleanedCookies);
450
+
451
+ const { localeUrlStrategy } = Settings.localization;
452
+ const fallbackHost =
453
+ req.headers['x-forwarded-host']?.toString() ||
454
+ req.headers.host?.toString();
455
+ const hostname =
456
+ process.env.NEXT_PUBLIC_URL || `https://${fallbackHost}`;
457
+ const rootHostname =
458
+ localeUrlStrategy === LocaleUrlStrategy.Subdomain
459
+ ? getRootHostname(hostname)
460
+ : null;
461
+ const domainOption = rootHostname
462
+ ? ` Domain=${rootHostname};`
463
+ : '';
464
+ res.setHeader('Set-Cookie', [
465
+ `osessionid=; Path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT;${domainOption}`,
466
+ `sessionid=; Path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT;${domainOption}`
467
+ ]);
408
468
  }
409
469
  }
410
470
 
package/api/client.ts CHANGED
@@ -7,6 +7,7 @@ import cookieParser from 'set-cookie-parser';
7
7
  import { cookies } from 'next/headers';
8
8
  import getRootHostname from '../utils/get-root-hostname';
9
9
  import { LocaleUrlStrategy } from '../localization';
10
+ import { fixtureManager, MockMode } from '../lib/fixture-manager';
10
11
 
11
12
  interface RouteParams {
12
13
  params: {
@@ -156,6 +157,28 @@ async function proxyRequest(...args) {
156
157
  url += `?${urlSearchParams.toString()}`;
157
158
  }
158
159
 
160
+ const mockMode = process.env.PZ_MOCK;
161
+ const fixtureBody = req.method !== 'GET' ? fetchOptions.body : undefined;
162
+
163
+ // Replay mode: serve from fixtures
164
+ if (mockMode === MockMode.REPLAY) {
165
+ const { found, fixture } = await fixtureManager.read(req.method, slug, fixtureBody);
166
+
167
+ if (found) {
168
+ return NextResponse.json(
169
+ options.responseType === 'text'
170
+ ? { result: fixture.response.body }
171
+ : fixture.response.body,
172
+ { status: fixture.response.status }
173
+ );
174
+ }
175
+
176
+ return NextResponse.json(
177
+ { error: 'No fixture recorded for this endpoint' },
178
+ { status: 404 }
179
+ );
180
+ }
181
+
159
182
  try {
160
183
  const request = await fetch(url, fetchOptions);
161
184
 
@@ -181,6 +204,15 @@ async function proxyRequest(...args) {
181
204
  );
182
205
  }
183
206
 
207
+ // Record mode: save response to fixtures
208
+ if (mockMode === MockMode.RECORD) {
209
+ await fixtureManager.write(req.method, slug, fixtureBody, {
210
+ status: request.status,
211
+ headers: fixtureManager.extractHeaders(request.headers),
212
+ body: response
213
+ });
214
+ }
215
+
184
216
  const setCookieHeaders = request.headers.getSetCookie();
185
217
  const exceptCookieKeys = ['pz-locale', 'pz-currency'];
186
218
 
@@ -17,6 +17,7 @@ import {
17
17
  } from '../redux/reducers/widget';
18
18
  import { LoggerPopup } from './logger-popup';
19
19
  import { LoggerProvider } from '../hooks/use-logger-context';
20
+ import { ToastContainer } from './toast';
20
21
  import * as Sentry from '@sentry/nextjs';
21
22
  import { initSentry } from '../sentry';
22
23
 
@@ -150,6 +151,7 @@ export default function ClientRoot({
150
151
  <LoggerProvider>
151
152
  {children}
152
153
  <LoggerPopup />
154
+ <ToastContainer />
153
155
  </LoggerProvider>
154
156
  );
155
157
  }
@@ -27,7 +27,8 @@ enum Plugin {
27
27
  MasterpassRest = 'pz-masterpass-rest',
28
28
  SimilarProducts = 'pz-similar-products',
29
29
  Haso = 'pz-haso',
30
- GooglePay = 'pz-google-pay'
30
+ GooglePay = 'pz-google-pay',
31
+ ListHoverImage = 'pz-list-hover-image'
31
32
  }
32
33
 
33
34
  export enum Component {
@@ -68,7 +69,8 @@ export enum Component {
68
69
  ImageSearchButton = 'ImageSearchButton',
69
70
  HeaderImageSearchFeature = 'HeaderImageSearchFeature',
70
71
  HasoPaymentGateway = 'HasoPaymentGateway',
71
- GooglePay = 'GooglePay'
72
+ GooglePay = 'GooglePay',
73
+ ListHoverImage = 'ListHoverImage'
72
74
  }
73
75
 
74
76
  const PluginComponents = new Map([
@@ -122,7 +124,8 @@ const PluginComponents = new Map([
122
124
  [Plugin.Hepsipay, [Component.Hepsipay]],
123
125
  [Plugin.MasterpassRest, [Component.MasterpassRest]],
124
126
  [Plugin.Haso, [Component.HasoPaymentGateway]],
125
- [Plugin.GooglePay, [Component.GooglePay]]
127
+ [Plugin.GooglePay, [Component.GooglePay]],
128
+ [Plugin.ListHoverImage, [Component.ListHoverImage]]
126
129
  ]);
127
130
 
128
131
  const getPlugin = (component: Component) => {
@@ -201,6 +204,8 @@ export default function PluginModule({
201
204
  promise = import(`${'@akinon/pz-haso'}`);
202
205
  } else if (plugin === Plugin.GooglePay) {
203
206
  promise = import(`${'@akinon/pz-google-pay'}`);
207
+ } else if (plugin === Plugin.ListHoverImage) {
208
+ promise = import(`${'@akinon/pz-list-hover-image'}`);
204
209
  }
205
210
  } catch (error) {
206
211
  logger.error(error);
@@ -0,0 +1,258 @@
1
+ 'use client'
2
+
3
+ import React, { useEffect, useMemo, useRef, useState } from 'react'
4
+ import { createPortal } from 'react-dom'
5
+ import { twMerge } from 'tailwind-merge'
6
+ import { useAppDispatch, useAppSelector } from '../redux/hooks'
7
+ import {
8
+ removeToast,
9
+ Toast as ToastType,
10
+ ToastPosition
11
+ } from '../redux/reducers/toast'
12
+ import { Icon } from './icon'
13
+
14
+ type TypeStyles = {
15
+ container?: string
16
+ icon?: string
17
+ }
18
+
19
+ export interface ToastClasses {
20
+ root?: string
21
+ item?: string
22
+ icon?: string
23
+ message?: string
24
+ action?: string
25
+ dismiss?: string
26
+ animateIn?: string
27
+ animateOut?: string
28
+ types?: Partial<Record<ToastType['type'], TypeStyles>>
29
+ positions?: Partial<Record<ToastPosition, string>>
30
+ }
31
+
32
+ export interface ToastContainerProps {
33
+ classes?: ToastClasses
34
+ icons?: Partial<Record<ToastType['type'], string>>
35
+ customRender?: (props: {
36
+ toast: ToastType
37
+ onDismiss: () => void
38
+ }) => React.ReactNode
39
+ onAction?: (actionId: string, toast: ToastType) => void
40
+ }
41
+
42
+ const builtInTypeStyles: Record<ToastType['type'], TypeStyles> = {
43
+ success: {
44
+ container: 'border-l-4 border-l-success bg-white',
45
+ icon: 'text-success'
46
+ },
47
+ error: {
48
+ container: 'border-l-4 border-l-error bg-white',
49
+ icon: 'text-error'
50
+ },
51
+ warning: {
52
+ container: 'border-l-4 border-l-[#e89a0c] bg-white',
53
+ icon: 'text-[#e89a0c]'
54
+ },
55
+ info: {
56
+ container: 'border-l-4 border-l-primary bg-white',
57
+ icon: 'text-primary'
58
+ }
59
+ }
60
+
61
+ const builtInIcons: Record<ToastType['type'], string> = {
62
+ success: 'check',
63
+ error: 'close',
64
+ warning: 'info',
65
+ info: 'info'
66
+ }
67
+
68
+ const builtInPositions: Record<ToastPosition, string> = {
69
+ 'top-right': 'fixed top-4 right-4',
70
+ 'top-left': 'fixed top-4 left-4',
71
+ 'top-center': 'fixed top-4 left-1/2 -translate-x-1/2',
72
+ 'bottom-right': 'fixed bottom-4 right-4',
73
+ 'bottom-left': 'fixed bottom-4 left-4',
74
+ 'bottom-center': 'fixed bottom-4 left-1/2 -translate-x-1/2'
75
+ }
76
+
77
+ const ToastItem = React.memo(function ToastItem({
78
+ toast,
79
+ classes,
80
+ icons,
81
+ customRender,
82
+ onAction
83
+ }: {
84
+ toast: ToastType
85
+ classes: ToastClasses
86
+ icons: Record<ToastType['type'], string>
87
+ customRender?: ToastContainerProps['customRender']
88
+ onAction?: ToastContainerProps['onAction']
89
+ }) {
90
+ const dispatch = useAppDispatch()
91
+ const [exiting, setExiting] = useState(false)
92
+ const timerRef = useRef<ReturnType<typeof setTimeout>>(null)
93
+
94
+ useEffect(() => {
95
+ if (toast.duration > 0) {
96
+ timerRef.current = setTimeout(() => {
97
+ setExiting(true)
98
+ }, toast.duration)
99
+ }
100
+
101
+ return () => {
102
+ if (timerRef.current) clearTimeout(timerRef.current)
103
+ }
104
+ }, [toast.duration])
105
+
106
+ const handleAnimationEnd = () => {
107
+ if (exiting) {
108
+ dispatch(removeToast(toast.id))
109
+ }
110
+ }
111
+
112
+ const handleDismiss = () => {
113
+ if (timerRef.current) clearTimeout(timerRef.current)
114
+ setExiting(true)
115
+ }
116
+
117
+ const animIn = classes.animateIn ?? 'animate-toast-in'
118
+ const animOut = classes.animateOut ?? 'animate-toast-out'
119
+
120
+ if (customRender) {
121
+ return (
122
+ <div
123
+ className={twMerge(
124
+ 'pointer-events-auto',
125
+ exiting ? animOut : animIn
126
+ )}
127
+ onAnimationEnd={handleAnimationEnd}
128
+ >
129
+ {customRender({ toast, onDismiss: handleDismiss })}
130
+ </div>
131
+ )
132
+ }
133
+
134
+ const typeStyle = classes.types?.[toast.type] ?? builtInTypeStyles[toast.type]
135
+ const iconName = toast.icon ?? icons[toast.type]
136
+
137
+ return (
138
+ <div
139
+ role="alert"
140
+ className={twMerge(
141
+ 'flex items-center gap-3 px-4 py-3 rounded shadow-lg min-w-[300px] max-w-[420px]',
142
+ 'pointer-events-auto',
143
+ exiting ? animOut : animIn,
144
+ typeStyle.container,
145
+ classes.item,
146
+ toast.className
147
+ )}
148
+ onAnimationEnd={handleAnimationEnd}
149
+ >
150
+ <Icon
151
+ name={iconName}
152
+ size={18}
153
+ className={twMerge('shrink-0', typeStyle.icon, classes.icon)}
154
+ />
155
+ <p
156
+ className={twMerge(
157
+ 'flex-1 text-sm text-primary leading-snug',
158
+ classes.message
159
+ )}
160
+ >
161
+ {toast.message}
162
+ </p>
163
+ {toast.action && onAction && (
164
+ <button
165
+ type="button"
166
+ onClick={() => onAction(toast.action.actionId, toast)}
167
+ className={twMerge(
168
+ 'shrink-0 text-xs font-semibold text-primary underline hover:no-underline transition-colors',
169
+ classes.action
170
+ )}
171
+ >
172
+ {toast.action.label}
173
+ </button>
174
+ )}
175
+ {toast.dismissible !== false && (
176
+ <button
177
+ type="button"
178
+ onClick={handleDismiss}
179
+ className={twMerge(
180
+ 'shrink-0 p-1 text-gray-600 hover:text-primary transition-colors',
181
+ classes.dismiss
182
+ )}
183
+ aria-label="Dismiss"
184
+ >
185
+ <Icon name="close" size={12} />
186
+ </button>
187
+ )}
188
+ </div>
189
+ )
190
+ })
191
+
192
+ const EMPTY_CLASSES: ToastClasses = {}
193
+
194
+ export function ToastContainer({
195
+ classes = EMPTY_CLASSES,
196
+ icons,
197
+ customRender,
198
+ onAction
199
+ }: ToastContainerProps = {}) {
200
+ const toasts = useAppSelector((state) => state.toast.toasts)
201
+ const [mounted, setMounted] = useState(false)
202
+
203
+ const mergedIcons = useMemo(
204
+ () => ({ ...builtInIcons, ...icons }),
205
+ [icons]
206
+ )
207
+
208
+ const posStyles = useMemo(
209
+ () => ({ ...builtInPositions, ...classes.positions }),
210
+ [classes.positions]
211
+ )
212
+
213
+ const grouped = useMemo(
214
+ () =>
215
+ toasts.reduce<Record<string, ToastType[]>>((acc, toast) => {
216
+ const pos = toast.position ?? 'top-right'
217
+ if (!acc[pos]) acc[pos] = []
218
+ acc[pos].push(toast)
219
+ return acc
220
+ }, {}),
221
+ [toasts]
222
+ )
223
+
224
+ useEffect(() => {
225
+ setMounted(true)
226
+ }, [])
227
+
228
+ if (!mounted || toasts.length === 0) return null
229
+
230
+ return createPortal(
231
+ <>
232
+ {Object.entries(grouped).map(([position, items]) => (
233
+ <div
234
+ key={position}
235
+ aria-live="polite"
236
+ aria-label="Notifications"
237
+ className={twMerge(
238
+ 'z-[9999] flex flex-col gap-3 pointer-events-none',
239
+ posStyles[position as ToastPosition],
240
+ classes.root
241
+ )}
242
+ >
243
+ {items.map((toast) => (
244
+ <ToastItem
245
+ key={toast.id}
246
+ toast={toast}
247
+ classes={classes}
248
+ icons={mergedIcons}
249
+ customRender={customRender}
250
+ onAction={onAction}
251
+ />
252
+ ))}
253
+ </div>
254
+ ))}
255
+ </>,
256
+ document.body
257
+ )
258
+ }
@@ -738,7 +738,6 @@ export const checkoutApi = api.injectEndpoints({
738
738
  },
739
739
  async onQueryStarted(arg, { dispatch, queryFulfilled }) {
740
740
  dispatch(setPaymentStepBusy(true));
741
- dispatch(setCardType(arg));
742
741
  await queryFulfilled;
743
742
  dispatch(setPaymentStepBusy(false));
744
743
  }
@@ -0,0 +1,54 @@
1
+ import { defineConfig, globalIgnores } from 'eslint/config'
2
+ import nextVitals from 'eslint-config-next/core-web-vitals'
3
+ import nextTs from 'eslint-config-next/typescript'
4
+ import prettier from 'eslint-config-prettier/flat'
5
+ import projectzero from '@akinon/eslint-plugin-projectzero'
6
+
7
+ export default defineConfig([
8
+ ...nextVitals,
9
+ ...nextTs,
10
+ prettier,
11
+ {
12
+ plugins: {
13
+ '@akinon/projectzero': projectzero
14
+ },
15
+ rules: {
16
+ ...projectzero.configs.core.rules,
17
+ ...projectzero.configs.recommended.rules,
18
+ // typescript-eslint v6 → v8 renamed rules (override legacy `recommended` preset).
19
+ '@typescript-eslint/no-require-imports': 'off',
20
+ '@typescript-eslint/no-empty-object-type': [
21
+ 'error',
22
+ { allowInterfaces: 'never' }
23
+ ],
24
+ '@typescript-eslint/no-unused-expressions': 'warn',
25
+ '@typescript-eslint/no-unused-vars': 'warn',
26
+ '@typescript-eslint/no-explicit-any': 'warn',
27
+ // react-hooks v7 new strict rules — softened to warn for incremental adoption.
28
+ 'react-hooks/set-state-in-effect': 'warn',
29
+ 'react-hooks/static-components': 'warn',
30
+ 'react-hooks/error-boundaries': 'warn',
31
+ 'react-hooks/preserve-manual-memoization': 'warn',
32
+ 'react-hooks/incompatible-library': 'warn',
33
+ 'react-hooks/immutability': 'warn'
34
+ }
35
+ },
36
+ {
37
+ files: ['eslint.config.{js,cjs,mjs}', 'middlewares/default.ts'],
38
+ rules: {
39
+ '@akinon/projectzero/check-middleware-order': 'error'
40
+ }
41
+ },
42
+ {
43
+ files: ['redux/middlewares/pre-order/index.ts'],
44
+ rules: {
45
+ '@akinon/projectzero/check-pre-order-middleware-order': 'error'
46
+ }
47
+ },
48
+ globalIgnores([
49
+ '.next/**',
50
+ 'out/**',
51
+ 'build/**',
52
+ 'next-env.d.ts'
53
+ ])
54
+ ])
package/hooks/index.ts CHANGED
@@ -14,3 +14,4 @@ export * from './use-logger';
14
14
  export * from './use-logger-context';
15
15
  export * from './use-sentry-uncaught-errors';
16
16
  export * from './use-pz-params';
17
+ export * from './use-toast';
@@ -0,0 +1,56 @@
1
+ 'use client'
2
+
3
+ import { useCallback } from 'react'
4
+ import { useAppDispatch } from '../redux/hooks'
5
+ import {
6
+ addToast,
7
+ removeToast,
8
+ ToastType,
9
+ ToastInput
10
+ } from '../redux/reducers/toast'
11
+
12
+ export type ToastOptions = Omit<ToastInput, 'type' | 'message'>
13
+
14
+ export const useToast = () => {
15
+ const dispatch = useAppDispatch()
16
+
17
+ const toast = useCallback(
18
+ (type: ToastType, message: string, options?: ToastOptions) => {
19
+ dispatch(addToast({ type, message, ...options }))
20
+ },
21
+ [dispatch]
22
+ )
23
+
24
+ const success = useCallback(
25
+ (message: string, options?: ToastOptions) =>
26
+ toast('success', message, options),
27
+ [toast]
28
+ )
29
+
30
+ const error = useCallback(
31
+ (message: string, options?: ToastOptions) =>
32
+ toast('error', message, options),
33
+ [toast]
34
+ )
35
+
36
+ const warning = useCallback(
37
+ (message: string, options?: ToastOptions) =>
38
+ toast('warning', message, options),
39
+ [toast]
40
+ )
41
+
42
+ const info = useCallback(
43
+ (message: string, options?: ToastOptions) =>
44
+ toast('info', message, options),
45
+ [toast]
46
+ )
47
+
48
+ const dismiss = useCallback(
49
+ (id: string) => {
50
+ dispatch(removeToast(id))
51
+ },
52
+ [dispatch]
53
+ )
54
+
55
+ return { toast, success, error, warning, info, dismiss }
56
+ }
@@ -3,6 +3,7 @@ import * as Sentry from '@sentry/nextjs';
3
3
 
4
4
  export async function register() {
5
5
  if (process.env.NEXT_RUNTIME === 'nodejs') {
6
+ await import('./node');
6
7
  initSentry('Server');
7
8
  }
8
9
 
@@ -1,2 +1,224 @@
1
- // OpenTelemetry tracing is handled by Sentry.
2
- // Custom NodeSDK setup removed due to version incompatibility with Sentry 10's OpenTelemetry v2.x requirements.
1
+ import { context, trace, type SpanAttributes } from '@opentelemetry/api';
2
+ import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
3
+ import { ExportResultCode, type ExportResult } from '@opentelemetry/core';
4
+ import {
5
+ BasicTracerProvider,
6
+ SimpleSpanProcessor,
7
+ type ReadableSpan,
8
+ type SpanExporter
9
+ } from '@opentelemetry/sdk-trace-base';
10
+
11
+ type OTelAttributeValue =
12
+ | { stringValue: string }
13
+ | { intValue: number }
14
+ | { doubleValue: number }
15
+ | { boolValue: boolean };
16
+
17
+ const PZ_DASHBOARD_TRACING_STARTED_KEY = '__pzDashboardTracingStarted__';
18
+ const SERVICE_NAME = 'pz-next-app';
19
+ const DASHBOARD_FALLBACK_URL = 'http://localhost:3005';
20
+
21
+ type PzDashboardGlobal = typeof globalThis & {
22
+ [PZ_DASHBOARD_TRACING_STARTED_KEY]?: boolean;
23
+ };
24
+
25
+ const getDashboardTraceUrl = () => {
26
+ const baseUrl = process.env.PZ_DASHBOARD_URL ?? DASHBOARD_FALLBACK_URL;
27
+
28
+ return `${baseUrl.replace(/\/$/, '')}/api/traces`;
29
+ };
30
+
31
+ const hrTimeToUnixNano = ([seconds, nanos]: [number, number]) =>
32
+ (BigInt(seconds) * BigInt(1000000000) + BigInt(nanos)).toString();
33
+
34
+ const getAttributeValue = (value: unknown): OTelAttributeValue => {
35
+ if (typeof value === 'string') {
36
+ return { stringValue: value };
37
+ }
38
+
39
+ if (typeof value === 'number') {
40
+ return Number.isInteger(value)
41
+ ? { intValue: value }
42
+ : { doubleValue: value };
43
+ }
44
+
45
+ if (typeof value === 'boolean') {
46
+ return { boolValue: value };
47
+ }
48
+
49
+ return { stringValue: JSON.stringify(value ?? '') };
50
+ };
51
+
52
+ const getAttributes = (attributes: SpanAttributes = {}) =>
53
+ Object.entries(attributes)
54
+ .filter(([, value]) => value !== undefined)
55
+ .map(([key, value]) => ({
56
+ key,
57
+ value: getAttributeValue(value)
58
+ }));
59
+
60
+ const getParentSpanId = (span: ReadableSpan) => {
61
+ if ('parentSpanId' in span && typeof span.parentSpanId === 'string') {
62
+ return span.parentSpanId;
63
+ }
64
+
65
+ if (
66
+ 'parentSpanContext' in span &&
67
+ span.parentSpanContext &&
68
+ typeof span.parentSpanContext === 'object' &&
69
+ 'spanId' in span.parentSpanContext
70
+ ) {
71
+ return String(span.parentSpanContext.spanId);
72
+ }
73
+
74
+ return undefined;
75
+ };
76
+
77
+ const getResourceAttributes = (span: ReadableSpan) => ({
78
+ ...(span.resource?.attributes ?? {}),
79
+ 'service.name':
80
+ process.env.OTEL_SERVICE_NAME ??
81
+ process.env.NEXT_PUBLIC_APP_NAME ??
82
+ SERVICE_NAME
83
+ });
84
+
85
+ const getScope = (span: ReadableSpan) => {
86
+ const scopedSpan = span as ReadableSpan & {
87
+ instrumentationLibrary?: {
88
+ name?: string;
89
+ version?: string;
90
+ };
91
+ instrumentationScope?: {
92
+ name?: string;
93
+ version?: string;
94
+ };
95
+ };
96
+ const scope =
97
+ scopedSpan.instrumentationScope ?? scopedSpan.instrumentationLibrary;
98
+
99
+ return {
100
+ name: scope?.name ?? SERVICE_NAME,
101
+ version: scope?.version ?? ''
102
+ };
103
+ };
104
+
105
+ const getSpanPayload = (span: ReadableSpan) => ({
106
+ traceId: span.spanContext().traceId,
107
+ spanId: span.spanContext().spanId,
108
+ parentSpanId: getParentSpanId(span),
109
+ name: span.name,
110
+ kind: span.kind,
111
+ startTimeUnixNano: hrTimeToUnixNano(span.startTime),
112
+ endTimeUnixNano: hrTimeToUnixNano(span.endTime),
113
+ attributes: getAttributes(span.attributes),
114
+ droppedAttributesCount: span.droppedAttributesCount ?? 0,
115
+ events:
116
+ span.events?.map((event) => ({
117
+ name: event.name,
118
+ timeUnixNano: hrTimeToUnixNano(event.time),
119
+ attributes: getAttributes(event.attributes),
120
+ droppedAttributesCount: event.droppedAttributesCount ?? 0
121
+ })) ?? [],
122
+ droppedEventsCount: span.droppedEventsCount ?? 0,
123
+ status: span.status,
124
+ links:
125
+ span.links?.map((link) => ({
126
+ traceId: link.context.traceId,
127
+ spanId: link.context.spanId,
128
+ attributes: getAttributes(link.attributes),
129
+ droppedAttributesCount: link.droppedAttributesCount ?? 0
130
+ })) ?? [],
131
+ droppedLinksCount: span.droppedLinksCount ?? 0
132
+ });
133
+
134
+ class PzDashboardTraceExporter implements SpanExporter {
135
+ constructor(private readonly url: string) {}
136
+
137
+ export(
138
+ spans: ReadableSpan[],
139
+ resultCallback: (result: ExportResult) => void
140
+ ) {
141
+ const [firstSpan] = spans;
142
+
143
+ if (!firstSpan) {
144
+ resultCallback({ code: ExportResultCode.SUCCESS });
145
+ return;
146
+ }
147
+
148
+ void this.sendSpans(spans, firstSpan, resultCallback);
149
+ }
150
+
151
+ private async sendSpans(
152
+ spans: ReadableSpan[],
153
+ firstSpan: ReadableSpan,
154
+ resultCallback: (result: ExportResult) => void
155
+ ) {
156
+ try {
157
+ const response = await fetch(this.url, {
158
+ method: 'POST',
159
+ headers: { 'Content-Type': 'application/json' },
160
+ body: JSON.stringify({
161
+ resourceSpans: [
162
+ {
163
+ resource: {
164
+ attributes: getAttributes(getResourceAttributes(firstSpan)),
165
+ droppedAttributesCount: 0
166
+ },
167
+ scopeSpans: spans.map((span) => ({
168
+ scope: getScope(span),
169
+ spans: [getSpanPayload(span)]
170
+ }))
171
+ }
172
+ ]
173
+ })
174
+ });
175
+
176
+ if (!response.ok) {
177
+ throw new Error(`PZ Dashboard trace export failed: ${response.status}`);
178
+ }
179
+
180
+ resultCallback({ code: ExportResultCode.SUCCESS });
181
+ } catch (error) {
182
+ resultCallback({
183
+ code: ExportResultCode.FAILED,
184
+ error: error instanceof Error ? error : new Error(String(error))
185
+ });
186
+ }
187
+ }
188
+
189
+ shutdown() {
190
+ return Promise.resolve();
191
+ }
192
+ }
193
+
194
+ const startDashboardTracing = () => {
195
+ const pzDashboardGlobal = globalThis as PzDashboardGlobal;
196
+
197
+ if (pzDashboardGlobal[PZ_DASHBOARD_TRACING_STARTED_KEY]) {
198
+ return;
199
+ }
200
+
201
+ const provider = new BasicTracerProvider({
202
+ spanProcessors: [
203
+ new SimpleSpanProcessor(
204
+ new PzDashboardTraceExporter(getDashboardTraceUrl())
205
+ )
206
+ ]
207
+ });
208
+
209
+ context.setGlobalContextManager(
210
+ new AsyncLocalStorageContextManager().enable()
211
+ );
212
+
213
+ const registered = trace.setGlobalTracerProvider(provider);
214
+
215
+ if (!registered) {
216
+ return;
217
+ }
218
+
219
+ pzDashboardGlobal[PZ_DASHBOARD_TRACING_STARTED_KEY] = true;
220
+ };
221
+
222
+ if (process.env.NODE_ENV === 'development') {
223
+ startDashboardTracing();
224
+ }
@@ -0,0 +1,146 @@
1
+ import * as fs from 'fs'
2
+ import * as path from 'path'
3
+ import * as crypto from 'crypto'
4
+ import logger from '../utils/log'
5
+
6
+ export const MockMode = {
7
+ RECORD: 'record',
8
+ REPLAY: 'replay'
9
+ } as const
10
+
11
+ export type MockMode = (typeof MockMode)[keyof typeof MockMode]
12
+
13
+ interface FixtureData {
14
+ request: {
15
+ method: string
16
+ url: string
17
+ body?: any
18
+ }
19
+ response: {
20
+ status: number
21
+ headers: Record<string, string>
22
+ body: any
23
+ }
24
+ recorded_at: string
25
+ }
26
+
27
+ const SENSITIVE_HEADERS = [
28
+ 'authorization',
29
+ 'cookie',
30
+ 'set-cookie',
31
+ 'x-csrftoken',
32
+ 'x-forwarded-for'
33
+ ]
34
+
35
+ class FixtureManager {
36
+ private fixturesDir: string
37
+ private dirEnsured = false
38
+
39
+ constructor() {
40
+ this.fixturesDir = path.join(process.cwd(), '.pz-fixtures')
41
+ }
42
+
43
+ private ensureDir(): void {
44
+ if (this.dirEnsured) return
45
+ fs.mkdirSync(this.fixturesDir, { recursive: true })
46
+ this.dirEnsured = true
47
+ }
48
+
49
+ generateKey(method: string, url: string, body?: any): string {
50
+ const normalized = method?.toUpperCase() ?? 'GET'
51
+ const parts = [normalized, url]
52
+
53
+ if (body && normalized !== 'GET') {
54
+ const bodyStr =
55
+ typeof body === 'string' ? body : JSON.stringify(body)
56
+ parts.push(bodyStr)
57
+ }
58
+
59
+ return crypto.createHash('md5').update(parts.join(':')).digest('hex')
60
+ }
61
+
62
+ extractHeaders(headers: Headers): Record<string, string> {
63
+ const result: Record<string, string> = {}
64
+ headers.forEach((value, key) => {
65
+ result[key] = value
66
+ })
67
+ return result
68
+ }
69
+
70
+ private stripSensitiveHeaders(
71
+ headers: Record<string, string>
72
+ ): Record<string, string> {
73
+ const cleaned: Record<string, string> = {}
74
+
75
+ for (const [key, value] of Object.entries(headers)) {
76
+ if (!SENSITIVE_HEADERS.includes(key.toLowerCase())) {
77
+ cleaned[key] = value
78
+ }
79
+ }
80
+
81
+ return cleaned
82
+ }
83
+
84
+ async write(
85
+ method: string,
86
+ url: string,
87
+ body: any,
88
+ response: { status: number; headers: Record<string, string>; body: any }
89
+ ): Promise<void> {
90
+ try {
91
+ this.ensureDir()
92
+
93
+ const normalized = method?.toUpperCase() ?? 'GET'
94
+ const key = this.generateKey(normalized, url, body)
95
+ const fixture: FixtureData = {
96
+ request: {
97
+ method: normalized,
98
+ url
99
+ },
100
+ response: {
101
+ status: response.status,
102
+ headers: this.stripSensitiveHeaders(response.headers),
103
+ body: response.body
104
+ },
105
+ recorded_at: new Date().toISOString()
106
+ }
107
+
108
+ if (body && normalized !== 'GET') {
109
+ fixture.request.body = body
110
+ }
111
+
112
+ const filePath = path.join(this.fixturesDir, `${key}.json`)
113
+ await fs.promises.writeFile(filePath, JSON.stringify(fixture, null, 2))
114
+
115
+ logger.debug(`[pz-mock] Recorded fixture: ${normalized} ${url} → ${filePath}`)
116
+ } catch (error) {
117
+ logger.error(`[pz-mock] Failed to write fixture`, { url, error })
118
+ }
119
+ }
120
+
121
+ async read(
122
+ method: string,
123
+ url: string,
124
+ body?: any
125
+ ): Promise<{ found: boolean; fixture?: FixtureData }> {
126
+ try {
127
+ const key = this.generateKey(method, url, body)
128
+ const filePath = path.join(this.fixturesDir, `${key}.json`)
129
+
130
+ const raw = await fs.promises.readFile(filePath, 'utf-8')
131
+ const fixture = JSON.parse(raw) as FixtureData
132
+
133
+ logger.debug(`[pz-mock] Replaying fixture: ${method} ${url} → ${key}`)
134
+ return { found: true, fixture }
135
+ } catch (error) {
136
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
137
+ logger.warn(`[pz-mock] No fixture found: ${method} ${url}`)
138
+ return { found: false }
139
+ }
140
+ logger.error(`[pz-mock] Failed to read fixture`, { url, error })
141
+ return { found: false }
142
+ }
143
+ }
144
+ }
145
+
146
+ export const fixtureManager = new FixtureManager()
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.6-rc.0",
4
+ "version": "2.0.6-rc.2",
5
5
  "private": false,
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -19,11 +19,10 @@
19
19
  "dependencies": {
20
20
  "@mongodb-js/zstd": "^2.0.1",
21
21
  "@neshca/cache-handler": "1.9.0",
22
- "@opentelemetry/exporter-trace-otlp-http": "0.46.0",
23
- "@opentelemetry/resources": "1.19.0",
24
- "@opentelemetry/sdk-node": "0.46.0",
25
- "@opentelemetry/sdk-trace-node": "1.19.0",
26
- "@opentelemetry/semantic-conventions": "1.19.0",
22
+ "@opentelemetry/api": "^1.9.0",
23
+ "@opentelemetry/context-async-hooks": "^2.5.0",
24
+ "@opentelemetry/core": "^2.5.0",
25
+ "@opentelemetry/sdk-trace-base": "^2.5.0",
27
26
  "@reduxjs/toolkit": "1.9.7",
28
27
  "@sentry/nextjs": "10.39.0",
29
28
  "cross-spawn": "7.0.3",
@@ -37,19 +36,17 @@
37
36
  "set-cookie-parser": "2.6.0"
38
37
  },
39
38
  "devDependencies": {
40
- "@akinon/eslint-plugin-projectzero": "2.0.6-rc.0",
39
+ "@akinon/eslint-plugin-projectzero": "2.0.6-rc.2",
41
40
  "@babel/core": "7.26.10",
42
41
  "@babel/preset-env": "7.26.9",
43
42
  "@babel/preset-typescript": "7.27.0",
44
43
  "@types/jest": "29.5.14",
45
44
  "@types/react-redux": "7.1.30",
46
45
  "@types/set-cookie-parser": "2.4.7",
47
- "@typescript-eslint/eslint-plugin": "6.7.4",
48
- "@typescript-eslint/parser": "6.7.4",
49
46
  "babel-jest": "29.7.0",
50
- "eslint": "8.56.0",
47
+ "eslint": "9.39.4",
51
48
  "eslint-config-next": "16.2.4",
52
- "eslint-config-prettier": "8.5.0",
49
+ "eslint-config-prettier": "10.1.1",
53
50
  "jest": "29.7.0",
54
51
  "ts-jest": "29.3.2",
55
52
  "typescript": "5.9.3"
package/redux/actions.ts CHANGED
@@ -3,6 +3,7 @@ export * from './reducers/root';
3
3
  export * from './reducers/header';
4
4
  export * from './reducers/checkout';
5
5
  export * from './reducers/config';
6
+ export * from './reducers/toast';
6
7
 
7
8
  // Import RTK Query APIs
8
9
  import { basketApi } from '../data/client/basket';
@@ -26,7 +26,7 @@ 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';
@@ -114,8 +114,15 @@ export const contextListMiddleware: Middleware = ({
114
114
  const { isMobileApp, userPhoneNumber } = getState().root;
115
115
  const result = next(action) as CheckoutResult;
116
116
  const preOrder = result?.payload?.pre_order;
117
+ const act = action as MiddlewareAction;
117
118
 
118
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
+
119
126
  result.payload.context_list.forEach((context) => {
120
127
  const redirectUrl = context.page_context.redirect_url;
121
128
  const isIframe = context.page_context.is_iframe ?? false;
@@ -231,15 +238,24 @@ export const contextListMiddleware: Middleware = ({
231
238
 
232
239
  if (context.page_context.card_type) {
233
240
  dispatch(setCardType(context.page_context.card_type));
234
- } else if (isCreditCardPayment) {
241
+ } else if (
242
+ isCreditCardPayment &&
243
+ isBinNumberResponse &&
244
+ !hasCardTypeInContextList
245
+ ) {
235
246
  dispatch(setCardType(null));
247
+ dispatch(setInstallmentOptions([]));
236
248
  }
237
249
 
238
250
  if (
239
251
  context.page_context.installments &&
240
252
  preOrder?.payment_option?.payment_type !== 'masterpass_rest'
241
253
  ) {
242
- if (!isCreditCardPayment || context.page_context.card_type) {
254
+ if (
255
+ !isCreditCardPayment ||
256
+ context.page_context.card_type ||
257
+ hasCardTypeInContextList
258
+ ) {
243
259
  dispatch(
244
260
  setInstallmentOptions(context.page_context.installments)
245
261
  );
@@ -2,6 +2,7 @@ import rootReducer from './root';
2
2
  import checkoutReducer from './checkout';
3
3
  import configReducer from './config';
4
4
  import headerReducer from './header';
5
+ import toastReducer from './toast';
5
6
  import widgetReducer from './widget';
6
7
  import { api } from '../../data/client/api';
7
8
 
@@ -20,6 +21,7 @@ const reducers = {
20
21
  checkout: checkoutReducer,
21
22
  config: configReducer,
22
23
  header: headerReducer,
24
+ toast: toastReducer,
23
25
  widget: widgetReducer,
24
26
  masterpass: masterpassReducer || fallbackReducer,
25
27
  otp: otpReducer || fallbackReducer,
@@ -0,0 +1,70 @@
1
+ import { createSlice, PayloadAction } from '@reduxjs/toolkit'
2
+
3
+ export type ToastType = 'success' | 'error' | 'warning' | 'info'
4
+
5
+ export type ToastPosition =
6
+ | 'top-right'
7
+ | 'top-left'
8
+ | 'top-center'
9
+ | 'bottom-right'
10
+ | 'bottom-left'
11
+ | 'bottom-center'
12
+
13
+ export interface Toast {
14
+ id: string
15
+ type: ToastType
16
+ message: string
17
+ duration?: number
18
+ position?: ToastPosition
19
+ icon?: string
20
+ className?: string
21
+ dismissible?: boolean
22
+ action?: {
23
+ label: string
24
+ actionId: string
25
+ }
26
+ }
27
+
28
+ export type ToastInput = Omit<Toast, 'id'>
29
+
30
+ export interface ToastState {
31
+ toasts: Toast[]
32
+ }
33
+
34
+ const MAX_TOASTS = 5
35
+
36
+ const initialState: ToastState = {
37
+ toasts: []
38
+ }
39
+
40
+ const toastSlice = createSlice({
41
+ name: 'toast',
42
+ initialState,
43
+ reducers: {
44
+ addToast: {
45
+ reducer: (state, action: PayloadAction<Toast>) => {
46
+ state.toasts.push(action.payload)
47
+ if (state.toasts.length > MAX_TOASTS) {
48
+ state.toasts.shift()
49
+ }
50
+ },
51
+ prepare: (toast: ToastInput) => ({
52
+ payload: {
53
+ ...toast,
54
+ id: `toast-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
55
+ duration: toast.duration ?? 4000,
56
+ position: toast.position ?? 'top-right',
57
+ dismissible: toast.dismissible ?? true
58
+ }
59
+ })
60
+ },
61
+ removeToast: (state, action: PayloadAction<string>) => {
62
+ const idx = state.toasts.findIndex((t) => t.id === action.payload)
63
+ if (idx !== -1) state.toasts.splice(idx, 1)
64
+ }
65
+ }
66
+ })
67
+
68
+ export const { addToast, removeToast } = toastSlice.actions
69
+
70
+ export default toastSlice.reducer
@@ -1,7 +1,14 @@
1
+ export interface FlatPagePrettyUrl {
2
+ pk: number;
3
+ url: string;
4
+ language: string;
5
+ }
6
+
1
7
  export interface FlatPage {
2
8
  flat_page: {
3
9
  url: string;
4
10
  title: string;
5
11
  content: string;
12
+ flatpageprettyurl_set?: FlatPagePrettyUrl[];
6
13
  };
7
14
  }
@@ -3,6 +3,7 @@ import logger from '../utils/log';
3
3
  import { headers, cookies } from 'next/headers';
4
4
  import { ServerVariables } from './server-variables';
5
5
  import { notFound } from 'next/navigation';
6
+ import { fixtureManager, MockMode } from '../lib/fixture-manager';
6
7
 
7
8
  export enum FetchResponseType {
8
9
  JSON = 'json',
@@ -42,6 +43,23 @@ const appFetch = async <T>({
42
43
  }
43
44
 
44
45
  const requestURL = `${decodeURIComponent(commerceUrl)}${url}`;
46
+ const mockMode = process.env.PZ_MOCK;
47
+ const method = init.method?.toUpperCase() ?? 'GET';
48
+
49
+ // Replay mode: serve from fixtures without hitting the API
50
+ if (mockMode === MockMode.REPLAY) {
51
+ const { found, fixture } = await fixtureManager.read(method, String(url), init.body);
52
+
53
+ if (found) {
54
+ status = fixture.response.status;
55
+ response = (responseType === FetchResponseType.JSON
56
+ ? fixture.response.body
57
+ : JSON.stringify(fixture.response.body)) as T;
58
+ return response;
59
+ }
60
+
61
+ return undefined;
62
+ }
45
63
 
46
64
  init.headers = {
47
65
  cookie: nextCookies.toString(),
@@ -67,6 +85,15 @@ const appFetch = async <T>({
67
85
  response = (await req.text()) as unknown as T;
68
86
  }
69
87
 
88
+ // Record mode: save response to fixtures
89
+ if (mockMode === MockMode.RECORD) {
90
+ await fixtureManager.write(method, String(url), init.body, {
91
+ status: req.status,
92
+ headers: fixtureManager.extractHeaders(req.headers),
93
+ body: response
94
+ });
95
+ }
96
+
70
97
  logger.trace(`FETCH RESPONSE`, { url, response, ip });
71
98
  } catch (error) {
72
99
  const logType = status === 500 ? 'fatal' : 'error';
@@ -0,0 +1,7 @@
1
+ export const formatErrorMessage = (errors: any): string => {
2
+ if (typeof errors === 'string') return errors
3
+ if (Array.isArray(errors)) return errors.join(', ')
4
+ if (typeof errors === 'object' && errors !== null)
5
+ return Object.values(errors).flat().join(', ')
6
+ return 'An error occurred'
7
+ }
package/utils/index.ts CHANGED
@@ -9,6 +9,7 @@ export * from './generate-commerce-search-params';
9
9
  export * from './get-currency-label';
10
10
  export * from './pz-segments';
11
11
  export * from './get-checkout-path';
12
+ export * from './format-error-message';
12
13
 
13
14
  export function getCookie(name: string) {
14
15
  if (typeof document === 'undefined') {
@@ -62,7 +63,13 @@ export function setCookie(
62
63
  export function removeCookie(name: string) {
63
64
  const date = 'Thu, 01 Jan 1970 00:00:00 UTC';
64
65
 
65
- document.cookie = `${name}=; expires=${date}; path=/;`;
66
+ const domain =
67
+ settings.localization.localeUrlStrategy === LocaleUrlStrategy.Subdomain
68
+ ? getRootHostname(document.location.href)
69
+ : null;
70
+
71
+ const domainStr = domain ? ` domain=${domain};` : '';
72
+ document.cookie = `${name}=; expires=${date}; path=/;${domainStr}`;
66
73
  }
67
74
 
68
75
  /**
package/with-pz-config.js CHANGED
@@ -12,7 +12,8 @@ const defaultConfig = {
12
12
  output: 'standalone',
13
13
  compress: false,
14
14
  env: {
15
- NEXT_PUBLIC_SENTRY_DSN: process.env.SENTRY_DSN
15
+ NEXT_PUBLIC_SENTRY_DSN: process.env.SENTRY_DSN,
16
+ ...(process.env.PZ_MOCK && { PZ_MOCK: process.env.PZ_MOCK })
16
17
  },
17
18
  images: {
18
19
  remotePatterns: [