@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.
Files changed (53) hide show
  1. package/CHANGELOG.md +1330 -49
  2. package/__tests__/next-config.test.ts +1 -10
  3. package/__tests__/redirect.test.ts +319 -0
  4. package/api/cache.ts +41 -5
  5. package/api/image-proxy.ts +75 -0
  6. package/api/similar-product-list.ts +84 -0
  7. package/api/similar-products.ts +120 -0
  8. package/components/accordion.tsx +20 -5
  9. package/components/file-input.tsx +65 -3
  10. package/components/input.tsx +2 -0
  11. package/components/link.tsx +16 -12
  12. package/components/modal.tsx +32 -16
  13. package/components/plugin-module.tsx +30 -3
  14. package/data/client/checkout.ts +5 -4
  15. package/data/server/basket.ts +72 -0
  16. package/data/server/category.ts +52 -30
  17. package/data/server/flatpage.ts +20 -13
  18. package/data/server/form.ts +4 -1
  19. package/data/server/landingpage.ts +20 -13
  20. package/data/server/list.ts +25 -14
  21. package/data/server/menu.ts +4 -1
  22. package/data/server/product.ts +68 -40
  23. package/data/server/seo.ts +4 -1
  24. package/data/server/special-page.ts +18 -13
  25. package/data/server/widget.ts +4 -1
  26. package/data/urls.ts +5 -1
  27. package/hocs/server/with-segment-defaults.tsx +5 -2
  28. package/hooks/use-localization.ts +2 -3
  29. package/jest.config.js +7 -1
  30. package/lib/cache-handler.mjs +359 -85
  31. package/lib/cache.ts +254 -25
  32. package/middlewares/checkout-provider.ts +1 -1
  33. package/middlewares/complete-gpay.ts +2 -1
  34. package/middlewares/complete-masterpass.ts +2 -1
  35. package/middlewares/default.ts +50 -13
  36. package/middlewares/locale.ts +9 -1
  37. package/middlewares/pretty-url.ts +2 -1
  38. package/middlewares/redirection-payment.ts +2 -1
  39. package/middlewares/saved-card-redirection.ts +2 -1
  40. package/middlewares/three-d-redirection.ts +2 -1
  41. package/middlewares/url-redirection.ts +9 -15
  42. package/package.json +4 -3
  43. package/plugins.d.ts +8 -0
  44. package/plugins.js +3 -1
  45. package/redux/middlewares/checkout.ts +5 -1
  46. package/sentry/index.ts +54 -17
  47. package/types/commerce/order.ts +1 -0
  48. package/types/index.ts +43 -1
  49. package/utils/app-fetch.ts +7 -2
  50. package/utils/index.ts +34 -10
  51. package/utils/redirect-ignore.ts +35 -0
  52. package/utils/redirect.ts +31 -6
  53. 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: Handle options with ESLint rules
45
+ // TODO: Remove Zero Project DSN
21
46
 
22
- Sentry.init({
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
- beforeSend: (event, hint) => {
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
- return event;
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
  };
@@ -114,6 +114,7 @@ export interface Order {
114
114
  pk: number;
115
115
  name: string;
116
116
  slug: string;
117
+ logo: string;
117
118
  [key: string]: any;
118
119
  };
119
120
  }
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 type FileInputProps = React.HTMLProps<HTMLInputElement>;
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
+ }
@@ -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(name: string, val: string) {
24
- const date = new Date();
25
- const value = val;
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
- date.setTime(date.getTime() + 7 * 24 * 60 * 60 * 1000);
37
+ cookieParts.push(`path=${options.path ?? '/'}`);
28
38
 
29
- document.cookie =
30
- name + '=' + value + '; expires=' + date.toUTCString() + '; path=/';
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 === ServerVariables.locale
30
+ (locale) => locale.value === currentLocaleValue
15
31
  );
16
32
 
17
- const callbackUrl = pageUrl.pathname;
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.localePath ?? currentLocale.value
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: '**.akinoncloud.com'
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: {