@akinon/next 1.94.0 → 1.95.0-snapshot-ZERO-3586-20250901132537
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 +1330 -49
- package/__tests__/next-config.test.ts +1 -10
- package/__tests__/redirect.test.ts +319 -0
- package/api/cache.ts +41 -5
- package/api/image-proxy.ts +75 -0
- package/api/similar-product-list.ts +84 -0
- package/api/similar-products.ts +120 -0
- package/components/accordion.tsx +20 -5
- package/components/file-input.tsx +65 -3
- package/components/input.tsx +2 -0
- package/components/link.tsx +16 -12
- package/components/modal.tsx +32 -16
- package/components/plugin-module.tsx +30 -3
- package/data/client/checkout.ts +5 -4
- package/data/server/basket.ts +72 -0
- package/data/server/category.ts +52 -30
- package/data/server/flatpage.ts +20 -13
- package/data/server/form.ts +4 -1
- package/data/server/landingpage.ts +20 -13
- package/data/server/list.ts +25 -14
- package/data/server/menu.ts +4 -1
- package/data/server/product.ts +68 -40
- package/data/server/seo.ts +4 -1
- package/data/server/special-page.ts +18 -13
- package/data/server/widget.ts +4 -1
- package/data/urls.ts +5 -1
- package/hocs/server/with-segment-defaults.tsx +5 -2
- package/hooks/use-localization.ts +2 -3
- package/jest.config.js +7 -1
- package/lib/cache-handler.mjs +359 -85
- package/lib/cache.ts +254 -25
- package/middlewares/checkout-provider.ts +1 -1
- package/middlewares/complete-gpay.ts +2 -1
- package/middlewares/complete-masterpass.ts +2 -1
- package/middlewares/default.ts +50 -13
- package/middlewares/locale.ts +9 -1
- package/middlewares/pretty-url.ts +2 -1
- package/middlewares/redirection-payment.ts +2 -1
- package/middlewares/saved-card-redirection.ts +2 -1
- package/middlewares/three-d-redirection.ts +2 -1
- package/middlewares/url-redirection.ts +9 -15
- package/package.json +4 -3
- package/plugins.d.ts +8 -0
- package/plugins.js +3 -1
- package/redux/middlewares/checkout.ts +5 -1
- package/sentry/index.ts +54 -17
- package/types/commerce/order.ts +1 -0
- package/types/index.ts +43 -1
- package/utils/app-fetch.ts +7 -2
- package/utils/index.ts +34 -10
- package/utils/redirect-ignore.ts +35 -0
- package/utils/redirect.ts +31 -6
- package/with-pz-config.js +2 -5
package/sentry/index.ts
CHANGED
|
@@ -13,36 +13,73 @@ const ALLOWED_CLIENT_LOG_TYPES: ClientLogType[] = [
|
|
|
13
13
|
ClientLogType.CHECKOUT
|
|
14
14
|
];
|
|
15
15
|
|
|
16
|
+
const isNetworkError = (exception: unknown): boolean => {
|
|
17
|
+
if (!(exception instanceof Error)) return false;
|
|
18
|
+
|
|
19
|
+
const networkErrorPatterns = [
|
|
20
|
+
'networkerror',
|
|
21
|
+
'failed to fetch',
|
|
22
|
+
'network request failed',
|
|
23
|
+
'network error',
|
|
24
|
+
'loading chunk',
|
|
25
|
+
'chunk load failed'
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
if (exception.name === 'NetworkError') return true;
|
|
29
|
+
|
|
30
|
+
if (exception.name === 'TypeError') {
|
|
31
|
+
return networkErrorPatterns.some((pattern) =>
|
|
32
|
+
exception.message.toLowerCase().includes(pattern)
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return networkErrorPatterns.some((pattern) =>
|
|
37
|
+
exception.message.toLowerCase().includes(pattern)
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
16
41
|
export const initSentry = (
|
|
17
42
|
type: 'Server' | 'Client' | 'Edge',
|
|
18
43
|
options: Sentry.BrowserOptions | Sentry.NodeOptions | Sentry.EdgeOptions = {}
|
|
19
44
|
) => {
|
|
20
|
-
// TODO:
|
|
45
|
+
// TODO: Remove Zero Project DSN
|
|
21
46
|
|
|
22
|
-
|
|
47
|
+
const baseConfig = {
|
|
23
48
|
dsn:
|
|
24
|
-
options.dsn ||
|
|
25
49
|
SENTRY_DSN ||
|
|
50
|
+
options.dsn ||
|
|
26
51
|
'https://d8558ef8997543deacf376c7d8d7cf4b@o64293.ingest.sentry.io/4504338423742464',
|
|
27
52
|
initialScope: {
|
|
28
53
|
tags: {
|
|
29
54
|
APP_TYPE: 'ProjectZeroNext',
|
|
30
|
-
TYPE: type
|
|
55
|
+
TYPE: type,
|
|
56
|
+
...((options.initialScope as any)?.tags || {})
|
|
31
57
|
}
|
|
32
58
|
},
|
|
33
59
|
tracesSampleRate: 0,
|
|
34
|
-
integrations: []
|
|
35
|
-
|
|
36
|
-
if (
|
|
37
|
-
type === 'Client' &&
|
|
38
|
-
!ALLOWED_CLIENT_LOG_TYPES.includes(
|
|
39
|
-
event.tags?.LOG_TYPE as ClientLogType
|
|
40
|
-
)
|
|
41
|
-
) {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
60
|
+
integrations: []
|
|
61
|
+
};
|
|
44
62
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
})
|
|
63
|
+
if (type === 'Server' || type === 'Edge') {
|
|
64
|
+
Sentry.init(baseConfig);
|
|
65
|
+
} else if (type === 'Client') {
|
|
66
|
+
Sentry.init({
|
|
67
|
+
...baseConfig,
|
|
68
|
+
beforeSend: (event, hint) => {
|
|
69
|
+
if (
|
|
70
|
+
!ALLOWED_CLIENT_LOG_TYPES.includes(
|
|
71
|
+
event.tags?.LOG_TYPE as ClientLogType
|
|
72
|
+
)
|
|
73
|
+
) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (isNetworkError(hint?.originalException)) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return event;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
48
85
|
};
|
package/types/commerce/order.ts
CHANGED
package/types/index.ts
CHANGED
|
@@ -83,6 +83,12 @@ export interface Settings {
|
|
|
83
83
|
};
|
|
84
84
|
usePrettyUrlRoute?: boolean;
|
|
85
85
|
commerceUrl: string;
|
|
86
|
+
/**
|
|
87
|
+
* This option allows you to track Sentry events on the client side, in addition to server and edge environments.
|
|
88
|
+
*
|
|
89
|
+
* It overrides process.env.NEXT_PUBLIC_SENTRY_DSN and process.env.SENTRY_DSN.
|
|
90
|
+
*/
|
|
91
|
+
sentryDsn?: string;
|
|
86
92
|
redis: {
|
|
87
93
|
defaultExpirationTime: number;
|
|
88
94
|
};
|
|
@@ -216,6 +222,15 @@ export interface CacheOptions {
|
|
|
216
222
|
cache?: boolean;
|
|
217
223
|
expire?: number;
|
|
218
224
|
useProxy?: boolean;
|
|
225
|
+
compressed?: boolean;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export interface SetCookieOptions {
|
|
229
|
+
expires?: number; // days
|
|
230
|
+
path?: string;
|
|
231
|
+
domain?: string;
|
|
232
|
+
secure?: boolean;
|
|
233
|
+
sameSite?: 'strict' | 'lax' | 'none';
|
|
219
234
|
}
|
|
220
235
|
|
|
221
236
|
export interface ClientRequestOptions {
|
|
@@ -283,7 +298,13 @@ export interface ButtonProps
|
|
|
283
298
|
target?: '_blank' | '_self' | '_parent' | '_top';
|
|
284
299
|
}
|
|
285
300
|
|
|
286
|
-
export
|
|
301
|
+
export interface FileInputProps extends React.HTMLProps<HTMLInputElement> {
|
|
302
|
+
fileClassName?: string;
|
|
303
|
+
fileNameWrapperClassName?: string;
|
|
304
|
+
fileInputClassName?: string;
|
|
305
|
+
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
306
|
+
buttonClassName?: string;
|
|
307
|
+
}
|
|
287
308
|
|
|
288
309
|
export interface PriceProps {
|
|
289
310
|
currencyCode?: string;
|
|
@@ -304,15 +325,19 @@ export interface InputProps extends React.HTMLProps<HTMLInputElement> {
|
|
|
304
325
|
|
|
305
326
|
export interface AccordionProps {
|
|
306
327
|
isCollapse?: boolean;
|
|
328
|
+
collapseClassName?: string;
|
|
307
329
|
title?: string;
|
|
308
330
|
subTitle?: string;
|
|
309
331
|
icons?: string[];
|
|
310
332
|
iconSize?: number;
|
|
311
333
|
iconColor?: string;
|
|
312
334
|
children?: ReactNode;
|
|
335
|
+
headerClassName?: string;
|
|
313
336
|
className?: string;
|
|
314
337
|
titleClassName?: string;
|
|
338
|
+
subTitleClassName?: string;
|
|
315
339
|
dataTestId?: string;
|
|
340
|
+
contentClassName?: string;
|
|
316
341
|
}
|
|
317
342
|
|
|
318
343
|
export interface PluginModuleComponentProps {
|
|
@@ -337,3 +362,20 @@ export interface PaginationProps {
|
|
|
337
362
|
direction?: 'next' | 'prev';
|
|
338
363
|
isLoading?: boolean;
|
|
339
364
|
}
|
|
365
|
+
|
|
366
|
+
export interface ModalProps {
|
|
367
|
+
portalId: string;
|
|
368
|
+
children?: React.ReactNode;
|
|
369
|
+
open?: boolean;
|
|
370
|
+
setOpen?: (open: boolean) => void;
|
|
371
|
+
title?: React.ReactNode;
|
|
372
|
+
showCloseButton?: React.ReactNode;
|
|
373
|
+
className?: string;
|
|
374
|
+
overlayClassName?: string;
|
|
375
|
+
headerWrapperClassName?: string;
|
|
376
|
+
titleClassName?: string;
|
|
377
|
+
closeButtonClassName?: string;
|
|
378
|
+
iconName?: string;
|
|
379
|
+
iconSize?: number;
|
|
380
|
+
iconClassName?: string;
|
|
381
|
+
}
|
package/utils/app-fetch.ts
CHANGED
|
@@ -43,12 +43,12 @@ const appFetch = async <T>({
|
|
|
43
43
|
const requestURL = `${decodeURIComponent(commerceUrl)}${url}`;
|
|
44
44
|
|
|
45
45
|
init.headers = {
|
|
46
|
+
cookie: nextCookies.toString(),
|
|
46
47
|
...(init.headers ?? {}),
|
|
47
48
|
...(ServerVariables.globalHeaders ?? {}),
|
|
48
49
|
'Accept-Language': currentLocale.apiValue,
|
|
49
50
|
'x-currency': currency,
|
|
50
|
-
'x-forwarded-for': ip
|
|
51
|
-
cookie: nextCookies.toString()
|
|
51
|
+
'x-forwarded-for': ip
|
|
52
52
|
};
|
|
53
53
|
|
|
54
54
|
init.next = {
|
|
@@ -60,6 +60,11 @@ const appFetch = async <T>({
|
|
|
60
60
|
status = req.status;
|
|
61
61
|
logger.debug(`FETCH END ${url}`, { status: req.status, ip });
|
|
62
62
|
|
|
63
|
+
if (!req.ok) {
|
|
64
|
+
const errorMessage = `HTTP ${req.status}: ${req.statusText}`;
|
|
65
|
+
throw new Error(errorMessage);
|
|
66
|
+
}
|
|
67
|
+
|
|
63
68
|
if (responseType === FetchResponseType.JSON) {
|
|
64
69
|
response = (await req.json()) as T;
|
|
65
70
|
} else {
|
package/utils/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import settings from 'settings';
|
|
2
2
|
import { LocaleUrlStrategy } from '../localization';
|
|
3
|
-
import { CDNOptions, ClientRequestOptions } from '../types';
|
|
3
|
+
import { CDNOptions, ClientRequestOptions, SetCookieOptions } from '../types';
|
|
4
|
+
import getRootHostname from './get-root-hostname';
|
|
4
5
|
|
|
5
6
|
export * from './get-currency';
|
|
6
7
|
export * from './menu-generator';
|
|
@@ -20,14 +21,40 @@ export function getCookie(name: string) {
|
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
export function setCookie(
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
export function setCookie(
|
|
25
|
+
name: string,
|
|
26
|
+
value: string,
|
|
27
|
+
options: SetCookieOptions = {}
|
|
28
|
+
) {
|
|
29
|
+
const cookieParts = [`${name}=${value}`];
|
|
30
|
+
|
|
31
|
+
if (options.expires) {
|
|
32
|
+
const date = new Date();
|
|
33
|
+
date.setTime(date.getTime() + options.expires * 24 * 60 * 60 * 1000);
|
|
34
|
+
cookieParts.push(`expires=${date.toUTCString()}`);
|
|
35
|
+
}
|
|
26
36
|
|
|
27
|
-
|
|
37
|
+
cookieParts.push(`path=${options.path ?? '/'}`);
|
|
28
38
|
|
|
29
|
-
|
|
30
|
-
|
|
39
|
+
if (options.secure) {
|
|
40
|
+
cookieParts.push('secure');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (options.sameSite) {
|
|
44
|
+
cookieParts.push(`sameSite=${options.sameSite}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const domain =
|
|
48
|
+
options.domain ??
|
|
49
|
+
(settings.localization.localeUrlStrategy === LocaleUrlStrategy.Subdomain
|
|
50
|
+
? getRootHostname(document.location.href)
|
|
51
|
+
: null);
|
|
52
|
+
|
|
53
|
+
if (domain) {
|
|
54
|
+
cookieParts.push(`domain=${domain}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
document.cookie = cookieParts.join('; ');
|
|
31
58
|
}
|
|
32
59
|
|
|
33
60
|
export function removeCookie(name: string) {
|
|
@@ -152,9 +179,6 @@ export function buildCDNUrl(url: string, config?: CDNOptions) {
|
|
|
152
179
|
return `${rootWithoutOptions}${options}${fileExtension}`;
|
|
153
180
|
}
|
|
154
181
|
|
|
155
|
-
const { locales, localeUrlStrategy, defaultLocaleValue } =
|
|
156
|
-
settings.localization;
|
|
157
|
-
|
|
158
182
|
export const urlLocaleMatcherRegex = new RegExp(
|
|
159
183
|
`^/(${settings.localization.locales
|
|
160
184
|
.filter((l) =>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import settings from 'settings';
|
|
2
|
+
import { getUrlPathWithLocale } from './localization';
|
|
3
|
+
|
|
4
|
+
type IgnorePath = string | RegExp;
|
|
5
|
+
|
|
6
|
+
const defaultIgnoreList: string[] = [];
|
|
7
|
+
|
|
8
|
+
const extraIgnores: IgnorePath[] = Array.isArray(
|
|
9
|
+
settings.commerceRedirectionIgnoreList
|
|
10
|
+
)
|
|
11
|
+
? settings.commerceRedirectionIgnoreList.map((path) => {
|
|
12
|
+
if (path === '/users/reset') {
|
|
13
|
+
return /^\/users\/reset\/[^/]+\/[^/]+\/$/;
|
|
14
|
+
}
|
|
15
|
+
return path;
|
|
16
|
+
})
|
|
17
|
+
: [];
|
|
18
|
+
|
|
19
|
+
export function shouldIgnoreRedirect(
|
|
20
|
+
pathname: string,
|
|
21
|
+
locale: string
|
|
22
|
+
): boolean {
|
|
23
|
+
if (!pathname) return false;
|
|
24
|
+
|
|
25
|
+
const rawIgnoreList: IgnorePath[] = [...defaultIgnoreList, ...extraIgnores];
|
|
26
|
+
|
|
27
|
+
return rawIgnoreList.some((ignorePath) => {
|
|
28
|
+
if (ignorePath instanceof RegExp) {
|
|
29
|
+
return ignorePath.test(pathname);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const localized = getUrlPathWithLocale(ignorePath, locale);
|
|
33
|
+
return localized === pathname;
|
|
34
|
+
});
|
|
35
|
+
}
|
package/utils/redirect.ts
CHANGED
|
@@ -1,23 +1,48 @@
|
|
|
1
1
|
import { redirect as nextRedirect, RedirectType } from 'next/navigation';
|
|
2
2
|
import Settings from 'settings';
|
|
3
|
-
import { headers } from 'next/headers';
|
|
4
|
-
import { ServerVariables } from '@akinon/next/utils/server-variables';
|
|
3
|
+
import { headers, cookies } from 'next/headers';
|
|
5
4
|
import { getUrlPathWithLocale } from '@akinon/next/utils/localization';
|
|
5
|
+
import { urlLocaleMatcherRegex } from '@akinon/next/utils';
|
|
6
6
|
|
|
7
7
|
export const redirect = (path: string, type?: RedirectType) => {
|
|
8
8
|
const nextHeaders = headers();
|
|
9
|
+
const nextCookies = cookies();
|
|
9
10
|
const pageUrl = new URL(
|
|
10
|
-
nextHeaders.get('pz-url') ?? process.env.NEXT_PUBLIC_URL
|
|
11
|
+
nextHeaders.get('pz-url') ?? process.env.NEXT_PUBLIC_URL ?? ''
|
|
11
12
|
);
|
|
12
13
|
|
|
14
|
+
let currentLocaleValue = Settings.localization.defaultLocaleValue;
|
|
15
|
+
const urlLocaleMatch = pageUrl.pathname.match(urlLocaleMatcherRegex);
|
|
16
|
+
|
|
17
|
+
if (urlLocaleMatch && urlLocaleMatch[0]) {
|
|
18
|
+
currentLocaleValue = urlLocaleMatch[0].replace('/', '');
|
|
19
|
+
} else {
|
|
20
|
+
const cookieLocale = nextCookies.get('pz-locale')?.value;
|
|
21
|
+
if (
|
|
22
|
+
cookieLocale &&
|
|
23
|
+
Settings.localization.locales.find((l) => l.value === cookieLocale)
|
|
24
|
+
) {
|
|
25
|
+
currentLocaleValue = cookieLocale;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
13
29
|
const currentLocale = Settings.localization.locales.find(
|
|
14
|
-
(locale) => locale.value ===
|
|
30
|
+
(locale) => locale.value === currentLocaleValue
|
|
15
31
|
);
|
|
16
32
|
|
|
17
|
-
|
|
33
|
+
if (!currentLocale) {
|
|
34
|
+
currentLocaleValue = Settings.localization.defaultLocaleValue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const searchParams = new URLSearchParams(pageUrl.search);
|
|
38
|
+
|
|
39
|
+
const callbackUrl =
|
|
40
|
+
pageUrl.pathname.replace(urlLocaleMatcherRegex, '') +
|
|
41
|
+
(searchParams.toString() ? `?${searchParams.toString()}` : '');
|
|
42
|
+
|
|
18
43
|
const redirectUrlWithLocale = getUrlPathWithLocale(
|
|
19
44
|
path,
|
|
20
|
-
currentLocale
|
|
45
|
+
currentLocale?.value
|
|
21
46
|
);
|
|
22
47
|
|
|
23
48
|
const redirectUrl = `${redirectUrlWithLocale}?callbackUrl=${callbackUrl}`;
|
package/with-pz-config.js
CHANGED
|
@@ -16,11 +16,7 @@ const defaultConfig = {
|
|
|
16
16
|
remotePatterns: [
|
|
17
17
|
{
|
|
18
18
|
protocol: 'https',
|
|
19
|
-
hostname: '
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
protocol: 'https',
|
|
23
|
-
hostname: '**.akinoncdn.com'
|
|
19
|
+
hostname: '**'
|
|
24
20
|
}
|
|
25
21
|
]
|
|
26
22
|
},
|
|
@@ -61,6 +57,7 @@ const defaultConfig = {
|
|
|
61
57
|
}, {}),
|
|
62
58
|
translations: false
|
|
63
59
|
};
|
|
60
|
+
|
|
64
61
|
return config;
|
|
65
62
|
},
|
|
66
63
|
sentry: {
|