@akinon/next 1.82.0-rc.2 → 1.82.0-rc.20

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/.eslintrc.js CHANGED
@@ -24,6 +24,18 @@ module.exports = {
24
24
  parserOptions: {
25
25
  sourceType: 'script'
26
26
  }
27
+ },
28
+ {
29
+ env: {
30
+ node: true
31
+ },
32
+ files: ['redux/middlewares/pre-order/index.ts'],
33
+ rules: {
34
+ '@akinon/projectzero/check-pre-order-middleware-order': 'error'
35
+ },
36
+ parserOptions: {
37
+ sourceType: 'script'
38
+ }
27
39
  }
28
40
  ],
29
41
  parser: '@typescript-eslint/parser',
package/CHANGELOG.md CHANGED
@@ -1,5 +1,141 @@
1
1
  # @akinon/next
2
2
 
3
+ ## 1.82.0-rc.20
4
+
5
+ ### Minor Changes
6
+
7
+ - e5529cd: ZERO-3267: Update error-page component to use root path for links instead of ROUTES
8
+
9
+ ## 1.82.0-rc.19
10
+
11
+ ### Minor Changes
12
+
13
+ - 33377cf: ZERO-3267: Refactor import statement for ROUTES in error-page component
14
+
15
+ ## 1.82.0-rc.18
16
+
17
+ ### Minor Changes
18
+
19
+ - e4761d2: Refactor import statement for ROUTES in error-page component
20
+
21
+ ## 1.82.0-rc.17
22
+
23
+ ### Minor Changes
24
+
25
+ - e2c6d426: ZERO-2935: Add @sentry/nextjs dependency to akinon-next and remove from projectzeronext
26
+ - 70bc0aed: ZERO-3284: Set tracesSampleRate in Sentry configuration
27
+
28
+ ## 1.82.0-rc.16
29
+
30
+ ### Minor Changes
31
+
32
+ - 757ee539: ZERO-3207: Add SMS send & verify endpoints with state management
33
+ - 6f506af: ZERO-3229: Implement mini basket query for basket total quantity
34
+ - 5dfeea04: ZERO-2801: Revert ZERO-2801
35
+ - 2d9b2b2c: ZERO-2816: Add segment to headers
36
+ - 5e1feca: Revert "ZERO-3286: Add notFound handling for chunk URLs starting with \_next"
37
+ - c0c1962: ZERO-3258: Add new API endpoints for fetching Bukalemun image URL and bundle product data
38
+ - 40a46853: ZERO-3182: Optimize basket update mutation with optimistic update
39
+ - f49bb74f: ZERO-3097: Add setCookie to logging in payment redirection middlewares
40
+ - 0e05135: ZERO-3244: Encode URL search parameters
41
+ - e9541a13: ZERO-2816: Add headers to url
42
+ - 4d3deb4f: ZERO-2935: sentry 8 upgrade
43
+ - 72fd4d67: ZERO-3084: Fix URL search parameters encoding in default middleware
44
+ - c53ef7b9: ZERO-2668: The Link component has been updated to improve the logic for handling href values. Previously, if the href was not a string or started with 'http', it would return the href as is. Now, if the href is not provided, it will default to '#' to prevent any potential errors. Additionally, if the href is a string and does not start with 'http', it will be formatted with the locale and pathname, based on the localeUrlStrategy and defaultLocaleValue. This ensures that the correct href is generated based on the localization settings.
45
+ - 64699d3f: ZERO-2761: Fix invalid import for plugin module
46
+ - 9abd011: ZERO-3267: Refactor error handling in ErrorPage component to set error details in Sentry scope
47
+ - 7727ae55: ZERO-3073: Refactor basket page to use server-side data fetching and simplify component structure
48
+ - d552629: ZERO-3182: Refactor basketApi to use invalidatesTags and comment out onQueryStarted logic
49
+ - c3b2f3f: ZERO-3267: Enable sentry client errors and filter them by log type
50
+ - 17f87524: ZERO-2816: Make the incoming currency lowercase
51
+ - 65d3b862: ZERO-3054: Update headers in appFetch
52
+ - bbe18b9f: ZERO-2575: Fix build error
53
+ - 17bfadc: ZERO-3275: Disable OpenTelemetry monitoring in production environment
54
+ - 4920742c: Disable getCachedTranslations
55
+ - b6e5b62: ZERO-3257: Enhance locale middleware to redirect using existing or default locale and support 303 status for POST requests
56
+ - ac65ca9: ZERO-3269: Enhance locale handling by adding Subdomain strategy and updating related functions
57
+ - 7e56d6b6: ZERO-2841: Update api tagTypes
58
+ - 43c182ee: ZERO-3054: Update Redis variable checks to conditionally include CACHE_SECRET
59
+ - 2d305aaf: ZERO-2935: Update Sentry configuration: remove hideSourceMaps option and add it to withPzConfig
60
+ - eeb20bea: Revert "ZERO-3054: Refactor cache handler to use custom Redis handler and implement key hashing"
61
+ - 3bf63c8: ZERO-3286: Add notFound handling for chunk URLs starting with \_next
62
+ - 9be2c08: ZERO-3243: Improve basket update query handling with optimistic updates
63
+ - f2c92d5c: ZERO-2816: Update cookie name
64
+ - 2f3588f: ZERO-3287: Add user session handling in authentication flow
65
+ - 7bd3d992: ZERO-2801: Refactor locale middleware to handle single locale configuration
66
+ - fdd255ee: ZERO-3054: Refactor cache handler to use custom Redis handler and implement key hashing
67
+ - 0b1bd07: ZERO-3240: Remove unused preOrderMiddleware
68
+ - 49eeebfa: ZERO-2909: Add deleteCollectionItem query to wishlistApi
69
+ - 3f9b8d7e: ZERO-2761: Update plugins.js for akinon-next
70
+
71
+ ## 1.82.0-rc.15
72
+
73
+ ### Minor Changes
74
+
75
+ - 2f3588fb: ZERO-3287: Add user session handling in authentication flow
76
+
77
+ ## 1.82.0-rc.14
78
+
79
+ ### Minor Changes
80
+
81
+ - 5e1feca: Revert "ZERO-3286: Add notFound handling for chunk URLs starting with \_next"
82
+
83
+ ## 1.82.0-rc.13
84
+
85
+ ### Minor Changes
86
+
87
+ - 3bf63c8: ZERO-3286: Add notFound handling for chunk URLs starting with \_next
88
+
89
+ ## 1.82.0-rc.12
90
+
91
+ ### Minor Changes
92
+
93
+ - ac65ca9: ZERO-3269: Enhance locale handling by adding Subdomain strategy and updating related functions
94
+
95
+ ## 1.82.0-rc.11
96
+
97
+ ## 1.82.0-rc.10
98
+
99
+ ## 1.82.0-rc.9
100
+
101
+ ### Minor Changes
102
+
103
+ - 9abd011: ZERO-3267: Refactor error handling in ErrorPage component to set error details in Sentry scope
104
+
105
+ ## 1.82.0-rc.8
106
+
107
+ ### Minor Changes
108
+
109
+ - 17bfadc: ZERO-3275: Disable OpenTelemetry monitoring in production environment
110
+
111
+ ## 1.82.0-rc.7
112
+
113
+ ### Minor Changes
114
+
115
+ - c3b2f3f: ZERO-3267: Enable sentry client errors and filter them by log type
116
+
117
+ ## 1.82.0-rc.6
118
+
119
+ ### Minor Changes
120
+
121
+ - 0b1bd07f: ZERO-3240: Remove unused preOrderMiddleware
122
+
123
+ ## 1.82.0-rc.5
124
+
125
+ ### Minor Changes
126
+
127
+ - 6f506afc: ZERO-3229: Implement mini basket query for basket total quantity
128
+ - c0c19629: ZERO-3258: Add new API endpoints for fetching Bukalemun image URL and bundle product data
129
+ - 9be2c081: ZERO-3243: Improve basket update query handling with optimistic updates
130
+
131
+ ## 1.82.0-rc.4
132
+
133
+ ### Minor Changes
134
+
135
+ - b6e5b624: ZERO-3257: Enhance locale middleware to redirect using existing or default locale and support 303 status for POST requests
136
+
137
+ ## 1.82.0-rc.3
138
+
3
139
  ## 1.82.0-rc.2
4
140
 
5
141
  ### Minor Changes
package/api/auth.ts CHANGED
@@ -99,6 +99,21 @@ const nextAuthOptions = (req: NextApiRequest, res: NextApiResponse) => {
99
99
  userIp
100
100
  });
101
101
 
102
+ const checkCurrentUser = await getCurrentUser(
103
+ req.cookies['osessionid'] ?? '',
104
+ req.cookies['pz-currency'] ?? ''
105
+ );
106
+
107
+ if (checkCurrentUser?.pk) {
108
+ const sessionCookie = headers
109
+ .get('cookie')
110
+ ?.match(/osessionid=\w+/)?.[0]
111
+ .replace(/osessionid=/, '');
112
+ if (sessionCookie) {
113
+ headers.set('cookie', sessionCookie);
114
+ }
115
+ }
116
+
102
117
  const apiRequest = await fetch(
103
118
  `${Settings.commerceUrl}${user[credentials.formType]}`,
104
119
  {
@@ -1,6 +1,9 @@
1
1
  'use client';
2
2
 
3
3
  import { useMobileIframeHandler } from '../hooks';
4
+ import * as Sentry from '@sentry/nextjs';
5
+ import { initSentry } from '../sentry';
6
+ import { useEffect } from 'react';
4
7
 
5
8
  export default function ClientRoot({
6
9
  children,
@@ -11,6 +14,23 @@ export default function ClientRoot({
11
14
  }) {
12
15
  const { preventPageRender } = useMobileIframeHandler({ sessionId });
13
16
 
17
+ const initializeSentry = async () => {
18
+ const response = await fetch('/api/sentry', { next: { revalidate: 0 } });
19
+ const data = await response.json();
20
+
21
+ const options = {
22
+ dsn: data.dsn
23
+ };
24
+
25
+ initSentry('Client', options);
26
+ };
27
+
28
+ useEffect(() => {
29
+ if (!Sentry.isInitialized()) {
30
+ initializeSentry();
31
+ }
32
+ }, []);
33
+
14
34
  if (preventPageRender) {
15
35
  return null;
16
36
  }
@@ -24,6 +24,26 @@ export const basketApi = api.injectEndpoints({
24
24
  transformResponse: (response: { basket: Basket }) => response.basket,
25
25
  providesTags: ['Basket']
26
26
  }),
27
+ getMiniBasket: build.query<
28
+ { pk: number | null; total_quantity: number },
29
+ void
30
+ >({
31
+ query: () =>
32
+ buildClientRequestUrl(basket.getMiniBasket, {
33
+ contentType: 'application/json'
34
+ }),
35
+ providesTags: ['Basket']
36
+ }),
37
+ getMiniBasketDetail: build.query<
38
+ { pk: number | null; total_quantity: number },
39
+ { namespace: string }
40
+ >({
41
+ query: ({ namespace }) =>
42
+ buildClientRequestUrl(basket.getMiniBasketDetail(namespace), {
43
+ contentType: 'application/json'
44
+ }),
45
+ providesTags: ['MultiBasket']
46
+ }),
27
47
  getBasketDetail: build.query<Basket, { namespace: string }>({
28
48
  query: ({ namespace }) =>
29
49
  buildClientRequestUrl(basket.getBasketDetail(namespace)),
@@ -84,23 +104,45 @@ export const basketApi = api.injectEndpoints({
84
104
  method: 'PUT',
85
105
  body
86
106
  }),
87
- // TODO: It doesn't work with namespaced baskets. Fix it and remove invalidatesTags.
88
- invalidatesTags: ['MultiBasket', 'Basket']
107
+ async onQueryStarted(_, { dispatch, queryFulfilled }) {
108
+ try {
109
+ const { data } = await queryFulfilled;
110
+
111
+ dispatch(
112
+ basketApi.util.updateQueryData(
113
+ 'getBasket',
114
+ undefined,
115
+ () => data.basket
116
+ )
117
+ );
89
118
 
90
- // async onQueryStarted(_, { dispatch, queryFulfilled }) {
91
- // try {
92
- // const { data } = await queryFulfilled;
93
- // dispatch(
94
- // basketApi.util.updateQueryData(
95
- // 'getBasket',
96
- // undefined,
97
- // () => data.basket
98
- // )
99
- // );
100
- // } catch {
101
- // dispatch(basketApi.util.invalidateTags(['MultiBasket', 'Basket']));
102
- // }
103
- // }
119
+ if (data.basket.namespace) {
120
+ dispatch(
121
+ basketApi.util.updateQueryData(
122
+ 'getBasketDetail',
123
+ { namespace: data.basket.namespace },
124
+ () => data.basket
125
+ )
126
+ );
127
+ }
128
+
129
+ dispatch(
130
+ basketApi.util.updateQueryData(
131
+ 'getAllBaskets',
132
+ undefined,
133
+ (baskets) => {
134
+ if (!baskets) return baskets;
135
+
136
+ return baskets.map((basket) =>
137
+ basket.pk === data.basket.pk ? data.basket : basket
138
+ );
139
+ }
140
+ )
141
+ );
142
+ } catch (error) {
143
+ console.error('Error updating quantity:', error);
144
+ }
145
+ }
104
146
  }),
105
147
  clearBasket: build.mutation<Basket, void>({
106
148
  query: (body) => ({
@@ -141,6 +183,8 @@ export const basketApi = api.injectEndpoints({
141
183
 
142
184
  export const {
143
185
  useGetBasketQuery,
186
+ useGetMiniBasketQuery,
187
+ useGetMiniBasketDetailQuery,
144
188
  useLazyGetBasketDetailQuery,
145
189
  useGetAllBasketsQuery,
146
190
  useRemoveBasketMutation,
@@ -92,6 +92,29 @@ export const miscApi = api.injectEndpoints({
92
92
  transformResponse: (response: { menu: MenuItemType[] }) => {
93
93
  return response.menu;
94
94
  }
95
+ }),
96
+ getBukalemunImageUrl: builder.query<
97
+ any,
98
+ { current_chapter: string; sku: string; selected_attributes: any }
99
+ >({
100
+ query: ({ current_chapter, sku, selected_attributes }) => {
101
+ const data = {
102
+ ...selected_attributes,
103
+ current_chapter,
104
+ sku
105
+ };
106
+
107
+ const params = new URLSearchParams(data);
108
+
109
+ return {
110
+ url: buildClientRequestUrl(
111
+ misc.bukalemunImageUrl(params.toString()),
112
+ {
113
+ responseType: 'json'
114
+ }
115
+ )
116
+ };
117
+ }
95
118
  })
96
119
  }),
97
120
  overrideExisting: true
@@ -102,5 +125,6 @@ export const {
102
125
  useEmailSubscriptionMutation,
103
126
  useSetLanguageMutation,
104
127
  useGetWidgetQuery,
105
- useGetMenuQuery
128
+ useGetMenuQuery,
129
+ useGetBukalemunImageUrlQuery
106
130
  } = miscApi;
@@ -75,6 +75,21 @@ export const productApi = api.injectEndpoints({
75
75
  query: (productPk) => ({
76
76
  url: buildClientRequestUrl(product.installments(productPk))
77
77
  })
78
+ }),
79
+ getBundleProductData: build.query<
80
+ any,
81
+ { productPk: string; queryString: string }
82
+ >({
83
+ query: ({ productPk, queryString }) => {
84
+ return {
85
+ url: buildClientRequestUrl(
86
+ product.bundleProduct(productPk, queryString),
87
+ {
88
+ responseType: 'json'
89
+ }
90
+ )
91
+ };
92
+ }
78
93
  })
79
94
  }),
80
95
  overrideExisting: true
@@ -85,5 +100,6 @@ export const {
85
100
  useGetProductByPkQuery,
86
101
  useGetRetailStoreStockMutation,
87
102
  useGetInstallmentsQuery,
88
- useGetProductByParamsQuery
103
+ useGetProductByParamsQuery,
104
+ useGetBundleProductDataQuery
89
105
  } = productApi;
package/data/urls.ts CHANGED
@@ -74,7 +74,10 @@ export const address = {
74
74
 
75
75
  export const basket = {
76
76
  getBasket: '/baskets/basket/',
77
+ getMiniBasket: '/baskets/basket/mini/',
77
78
  getBasketDetail: (namespace: string) => `/baskets/basket/${namespace}/`,
79
+ getMiniBasketDetail: (namespace: string) =>
80
+ `/baskets/basket/${namespace}/mini/`,
78
81
  getAllBaskets: '/baskets/basket-list/',
79
82
  removeBasket: (pk: number) => `/baskets/basket-list/${pk}/`,
80
83
  selectMainBasket: (pk: number) => `/baskets/basket-list/id/${pk}/main/`,
@@ -162,7 +165,8 @@ export const misc = {
162
165
  parent ? `&parent=${parent}` : ''
163
166
  }`,
164
167
  cmsSeo: (slug: string | string[]) => `/cms/seo/?url=${slug ? slug : '/'}`,
165
- setCurrency: '/users/activate-currency/'
168
+ setCurrency: '/users/activate-currency/',
169
+ bukalemunImageUrl: (params: string) => `/bukalemun/?${params}`
166
170
  };
167
171
 
168
172
  export const product = {
@@ -176,7 +180,9 @@ export const product = {
176
180
  slug: (slug: string) => `/${slug}/`,
177
181
  categoryUrl: (pk: number) => `/products/${pk}/category_nodes/?limit=1`,
178
182
  breadcrumbUrl: (menuitemmodel: string) =>
179
- `/menus/generate_breadcrumb/?item=${menuitemmodel}&generator_name=menu_item`
183
+ `/menus/generate_breadcrumb/?item=${menuitemmodel}&generator_name=menu_item`,
184
+ bundleProduct: (productPk: string, queryString: string) =>
185
+ `/bundle-product/${productPk}/?${queryString}`
180
186
  };
181
187
 
182
188
  export const wishlist = {
package/hooks/index.ts CHANGED
@@ -10,3 +10,4 @@ export * from './use-mobile-iframe-handler';
10
10
  export * from './use-payment-options';
11
11
  export * from './use-pagination';
12
12
  export * from './use-message-listener';
13
+ export * from './use-sentry-uncaught-errors';
@@ -27,8 +27,11 @@ export const useRouter = () => {
27
27
  );
28
28
 
29
29
  url.pathname = `${
30
- locale === defaultLocale?.value &&
31
- localeUrlStrategy === LocaleUrlStrategy.HideDefaultLocale
30
+ localeUrlStrategy === LocaleUrlStrategy.Subdomain ||
31
+ (locale === defaultLocale?.value &&
32
+ [LocaleUrlStrategy.HideDefaultLocale].includes(
33
+ localeUrlStrategy as LocaleUrlStrategy
34
+ ))
32
35
  ? ''
33
36
  : `/${locale}`
34
37
  }${pathnameWithoutLocale}`;
@@ -0,0 +1,24 @@
1
+ import { useEffect } from 'react';
2
+ import * as Sentry from '@sentry/nextjs';
3
+ import { ClientLogType } from '@akinon/next/sentry';
4
+
5
+ export const useSentryUncaughtErrors = (error: Error & { digest?: string }) => {
6
+ useEffect(() => {
7
+ Sentry.withScope(function (scope) {
8
+ scope.setLevel('fatal');
9
+ scope.setTags({
10
+ APP_TYPE: 'ProjectZeroNext',
11
+ TYPE: 'Client',
12
+ LOG_TYPE: ClientLogType.UNCAUGHT_ERROR_PAGE
13
+ });
14
+ scope.setExtra('error', error);
15
+
16
+ const error_ = new Error('FATAL: Uncaught client error');
17
+ error_.name = 'UNCAUGHT_ERROR_PAGE';
18
+
19
+ Sentry.captureException(error_, {
20
+ fingerprint: ['UNCAUGHT_ERROR_PAGE', error.digest]
21
+ });
22
+ });
23
+ }, [error]);
24
+ };
@@ -1,4 +1,5 @@
1
1
  import { initSentry } from '../sentry';
2
+ import * as Sentry from '@sentry/nextjs';
2
3
 
3
4
  export async function register() {
4
5
  if (process.env.NEXT_RUNTIME === 'nodejs') {
@@ -10,3 +11,5 @@ export async function register() {
10
11
  initSentry('Edge');
11
12
  }
12
13
  }
14
+
15
+ export const onRequestError = Sentry.captureRequestError;
@@ -4,17 +4,19 @@ import { Resource } from '@opentelemetry/resources';
4
4
  import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
5
5
  import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-node';
6
6
 
7
- const sdk = new NodeSDK({
8
- resource: new Resource({
9
- [SemanticResourceAttributes.SERVICE_NAME]: 'pz-next-app'
10
- }),
11
- spanProcessor: new SimpleSpanProcessor(
12
- new OTLPTraceExporter({
13
- url: `${
14
- process.env.PZ_DASHBOARD_URL ?? 'http://localhost:3005'
15
- }/api/traces`
16
- })
17
- )
18
- });
7
+ if (process.env.NODE_ENV === 'development') {
8
+ const sdk = new NodeSDK({
9
+ resource: new Resource({
10
+ [SemanticResourceAttributes.SERVICE_NAME]: 'pz-next-app'
11
+ }),
12
+ spanProcessor: new SimpleSpanProcessor(
13
+ new OTLPTraceExporter({
14
+ url: `${
15
+ process.env.PZ_DASHBOARD_URL ?? 'http://localhost:3005'
16
+ }/api/traces`
17
+ })
18
+ )
19
+ });
19
20
 
20
- sdk.start();
21
+ sdk.start();
22
+ }
@@ -1,5 +1,6 @@
1
1
  export enum LocaleUrlStrategy {
2
2
  HideAllLocales = 'hide-all-locales',
3
3
  HideDefaultLocale = 'hide-default-locale',
4
- ShowAllLocales = 'show-all-locales'
4
+ ShowAllLocales = 'show-all-locales',
5
+ Subdomain = 'subdomain'
5
6
  }
@@ -4,17 +4,32 @@ import { PzNextRequest } from '.';
4
4
  import { LocaleUrlStrategy } from '../localization';
5
5
  import { urlLocaleMatcherRegex } from '../utils';
6
6
  import logger from '../utils/log';
7
+ import { getUrlPathWithLocale } from '../utils/localization';
7
8
 
8
- const getMatchedLocale = (pathname: string) => {
9
+ const getMatchedLocale = (pathname: string, req: PzNextRequest) => {
9
10
  let matchedLocale = pathname.match(urlLocaleMatcherRegex)?.[0] ?? '';
10
11
  matchedLocale = matchedLocale.replace('/', '');
11
12
 
13
+ const { localeUrlStrategy, defaultLocaleValue } = settings.localization;
14
+
15
+ if (localeUrlStrategy === LocaleUrlStrategy.Subdomain) {
16
+ const host = req.headers.get('x-forwarded-host');
17
+
18
+ if (host) {
19
+ const subDomain = host.split('.')[0] || '';
20
+ const subDomainLocaleMatched = `/${subDomain}`.match(
21
+ urlLocaleMatcherRegex
22
+ );
23
+
24
+ if (subDomainLocaleMatched && subDomainLocaleMatched[0]) {
25
+ matchedLocale = subDomainLocaleMatched[0].slice(1);
26
+ }
27
+ }
28
+ }
29
+
12
30
  if (!matchedLocale.length) {
13
- if (
14
- settings.localization.localeUrlStrategy !==
15
- LocaleUrlStrategy.ShowAllLocales
16
- ) {
17
- matchedLocale = settings.localization.defaultLocaleValue;
31
+ if (localeUrlStrategy !== LocaleUrlStrategy.ShowAllLocales) {
32
+ matchedLocale = defaultLocaleValue;
18
33
  }
19
34
  }
20
35
 
@@ -28,7 +43,7 @@ const withLocale =
28
43
 
29
44
  try {
30
45
  const url = req.nextUrl.clone();
31
- const matchedLocale = getMatchedLocale(url.pathname);
46
+ const matchedLocale = getMatchedLocale(url.pathname, req);
32
47
  let { localeUrlStrategy, defaultLocaleValue, redirectToDefaultLocale } =
33
48
  settings.localization;
34
49
 
@@ -50,8 +65,15 @@ const withLocale =
50
65
  redirectToDefaultLocale &&
51
66
  req.method === 'GET'
52
67
  ) {
53
- url.pathname = `/${defaultLocaleValue}${url.pathname}`;
54
- return NextResponse.redirect(url);
68
+ // Redirect to existing or default locale
69
+
70
+ url.pathname = getUrlPathWithLocale(
71
+ url.pathname,
72
+ req.cookies.get('pz-locale')?.value
73
+ );
74
+
75
+ // Use 303 for POST requests
76
+ return NextResponse.redirect(url, 303);
55
77
  }
56
78
 
57
79
  req.middlewareParams.rewrites.locale = matchedLocale;
@@ -61,7 +83,6 @@ const withLocale =
61
83
  ip
62
84
  });
63
85
  }
64
-
65
86
  return middleware(req, event);
66
87
  };
67
88
 
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": "1.82.0-rc.2",
4
+ "version": "1.82.0-rc.20",
5
5
  "private": false,
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -21,6 +21,7 @@
21
21
  "@opentelemetry/semantic-conventions": "1.19.0",
22
22
  "@reduxjs/toolkit": "1.9.7",
23
23
  "@neshca/cache-handler": "1.9.0",
24
+ "@sentry/nextjs": "9.5.0",
24
25
  "cross-spawn": "7.0.3",
25
26
  "generic-pool": "3.9.0",
26
27
  "react-redux": "8.1.3",
@@ -30,7 +31,7 @@
30
31
  "set-cookie-parser": "2.6.0"
31
32
  },
32
33
  "devDependencies": {
33
- "@akinon/eslint-plugin-projectzero": "1.82.0-rc.2",
34
+ "@akinon/eslint-plugin-projectzero": "1.82.0-rc.20",
34
35
  "@types/react-redux": "7.1.30",
35
36
  "@types/set-cookie-parser": "2.4.7",
36
37
  "@typescript-eslint/eslint-plugin": "6.7.4",
@@ -16,10 +16,8 @@ import {
16
16
  setLoyaltyBalance,
17
17
  setPaymentChoices,
18
18
  setPaymentOptions,
19
- setPreOrder,
20
19
  setRetailStores,
21
20
  setShippingOptions,
22
- setShippingStepCompleted,
23
21
  setHepsipayAvailability,
24
22
  setWalletPaymentData,
25
23
  setPayOnDeliveryOtpModalActive
@@ -59,146 +57,6 @@ export const errorMiddleware: Middleware = ({ dispatch }: MiddlewareParams) => {
59
57
  };
60
58
  };
61
59
 
62
- export const preOrderMiddleware: Middleware = ({
63
- getState,
64
- dispatch
65
- }: MiddlewareParams) => {
66
- return (next) => (action) => {
67
- const result: CheckoutResult = next(action);
68
- const preOrder = result?.payload?.pre_order;
69
-
70
- if (
71
- !preOrder ||
72
- action?.meta?.arg?.endpointName === 'guestLogin' ||
73
- action?.meta?.arg?.endpointName === 'getCheckoutLoyaltyBalance'
74
- ) {
75
- return result;
76
- }
77
-
78
- const {
79
- deliveryOptions,
80
- addressList: addresses,
81
- shippingOptions,
82
- dataSourceShippingOptions,
83
- paymentOptions,
84
- installmentOptions,
85
- attributeBasedShippingOptions
86
- } = getState().checkout;
87
- const { endpoints: apiEndpoints } = checkoutApi;
88
-
89
- if (preOrder.is_redirected) {
90
- const contextList = result?.payload?.context_list;
91
-
92
- if (
93
- contextList.find(
94
- (ctx) => ctx.page_name === 'RedirectionPaymentSelectedPage'
95
- )
96
- ) {
97
- dispatch(
98
- apiEndpoints.setPaymentOption.initiate(preOrder.payment_option?.pk)
99
- );
100
- return;
101
- }
102
- }
103
-
104
- dispatch(setPreOrder(preOrder));
105
-
106
- if (!preOrder.delivery_option && deliveryOptions.length > 0) {
107
- dispatch(
108
- apiEndpoints.setDeliveryOption.initiate(
109
- deliveryOptions.find((opt) => opt.delivery_option_type === 'customer')
110
- ?.pk
111
- )
112
- );
113
- }
114
-
115
- if (
116
- (!preOrder.shipping_address || !preOrder.billing_address) &&
117
- addresses.length > 0 &&
118
- (!preOrder.delivery_option ||
119
- preOrder.delivery_option.delivery_option_type === 'customer')
120
- ) {
121
- dispatch(
122
- apiEndpoints.setAddresses.initiate({
123
- shippingAddressPk: addresses[0].pk,
124
- billingAddressPk: addresses[0].pk
125
- })
126
- );
127
- }
128
-
129
- if (
130
- shippingOptions.length > 0 &&
131
- (!preOrder.shipping_option ||
132
- !shippingOptions.find((opt) => opt.pk === preOrder.shipping_option?.pk))
133
- ) {
134
- dispatch(apiEndpoints.setShippingOption.initiate(shippingOptions[0].pk));
135
- }
136
-
137
- if (
138
- dataSourceShippingOptions.length > 0 &&
139
- !preOrder.data_source_shipping_options
140
- ) {
141
- const selectedDataSourceShippingOptionsPks =
142
- dataSourceShippingOptions.map(
143
- (opt) => opt.data_source_shipping_options[0].pk
144
- );
145
-
146
- dispatch(
147
- apiEndpoints.setDataSourceShippingOptions.initiate(
148
- selectedDataSourceShippingOptionsPks
149
- )
150
- );
151
- }
152
-
153
- if (
154
- Object.keys(attributeBasedShippingOptions).length > 0 &&
155
- !preOrder.attribute_based_shipping_options
156
- ) {
157
- const initialSelectedOptions: Record<string, number> = Object.fromEntries(
158
- Object.entries(attributeBasedShippingOptions).map(([key, options]) => [
159
- key,
160
- options.attribute_based_shipping_options[0].pk
161
- ])
162
- );
163
-
164
- dispatch(
165
- apiEndpoints.setAttributeBasedShippingOptions.initiate(
166
- initialSelectedOptions
167
- )
168
- );
169
- }
170
-
171
- if (!preOrder.payment_option && paymentOptions.length > 0) {
172
- dispatch(apiEndpoints.setPaymentOption.initiate(paymentOptions[0].pk));
173
- }
174
-
175
- if (
176
- !preOrder.installment &&
177
- preOrder.payment_option?.payment_type !== 'saved_card' &&
178
- installmentOptions.length > 0
179
- ) {
180
- dispatch(
181
- apiEndpoints.setInstallmentOption.initiate(installmentOptions[0].pk)
182
- );
183
- }
184
-
185
- dispatch(
186
- setShippingStepCompleted(
187
- [
188
- preOrder.delivery_option?.delivery_option_type === 'retail_store'
189
- ? true
190
- : preOrder.shipping_address?.pk,
191
- preOrder.billing_address?.pk,
192
- preOrder.shipping_option?.pk,
193
- addresses.length > 0
194
- ].every(Boolean)
195
- )
196
- );
197
-
198
- return result;
199
- };
200
- };
201
-
202
60
  export const contextListMiddleware: Middleware = ({
203
61
  dispatch,
204
62
  getState
@@ -37,6 +37,8 @@ export const setAddressMiddleware: Middleware = ({
37
37
  billingAddressPk: firstAddressPk
38
38
  })
39
39
  );
40
+
41
+ return null;
40
42
  }
41
43
 
42
44
  return result;
@@ -33,6 +33,8 @@ export const attributeBasedShippingOptionMiddleware: Middleware = ({
33
33
  initialSelectedOptions
34
34
  )
35
35
  );
36
+
37
+ return null;
36
38
  }
37
39
 
38
40
  return result;
@@ -29,6 +29,8 @@ export const dataSourceShippingOptionMiddleware: Middleware = ({
29
29
  dispatch(
30
30
  apiEndpoints.setDataSourceShippingOptions.initiate(selectedPks)
31
31
  );
32
+
33
+ return null;
32
34
  }
33
35
  }
34
36
 
@@ -26,6 +26,8 @@ export const deliveryOptionMiddleware: Middleware = ({
26
26
  dispatch(
27
27
  apiEndpoints.setDeliveryOption.initiate(customerDeliveryOption)
28
28
  );
29
+
30
+ return null;
29
31
  }
30
32
  }
31
33
 
@@ -10,16 +10,20 @@ import { paymentOptionMiddleware } from './payment-option';
10
10
  import { installmentOptionMiddleware } from './installment-option';
11
11
  import { shippingStepMiddleware } from './shipping-step';
12
12
 
13
+ // ⚠️ WARNING: Redux Toolkit applies middlewares in reverse order (from last to first).
14
+ // This list is written **in reverse execution order** to ensure they run in the correct sequence.
15
+ // If you add a new middleware, make sure to insert it **in reverse order** based on execution priority.
16
+
13
17
  export const preOrderMiddlewares = [
14
- preOrderValidationMiddleware,
15
- redirectionMiddleware,
16
- setPreOrderMiddleware,
17
- deliveryOptionMiddleware,
18
- setAddressMiddleware,
19
- shippingOptionMiddleware,
20
- dataSourceShippingOptionMiddleware,
21
- attributeBasedShippingOptionMiddleware,
22
- paymentOptionMiddleware,
18
+ shippingStepMiddleware,
23
19
  installmentOptionMiddleware,
24
- shippingStepMiddleware
20
+ paymentOptionMiddleware,
21
+ attributeBasedShippingOptionMiddleware,
22
+ dataSourceShippingOptionMiddleware,
23
+ shippingOptionMiddleware,
24
+ setAddressMiddleware,
25
+ deliveryOptionMiddleware,
26
+ setPreOrderMiddleware,
27
+ redirectionMiddleware,
28
+ preOrderValidationMiddleware
25
29
  ];
@@ -28,6 +28,8 @@ export const installmentOptionMiddleware: Middleware = ({
28
28
  dispatch(
29
29
  apiEndpoints.setInstallmentOption.initiate(firstInstallmentOptionPk)
30
30
  );
31
+
32
+ return null;
31
33
  }
32
34
  }
33
35
 
@@ -21,6 +21,8 @@ export const paymentOptionMiddleware: Middleware = ({
21
21
  const firstPaymentOptionPk = paymentOptions[0]?.pk;
22
22
 
23
23
  dispatch(apiEndpoints.setPaymentOption.initiate(firstPaymentOptionPk));
24
+
25
+ return null;
24
26
  }
25
27
 
26
28
  return result;
@@ -26,6 +26,8 @@ export const redirectionMiddleware: Middleware = ({
26
26
  preOrder.payment_option?.pk
27
27
  )
28
28
  );
29
+
30
+ return null;
29
31
  }
30
32
  }
31
33
 
@@ -30,6 +30,8 @@ export const shippingOptionMiddleware: Middleware = ({
30
30
  dispatch(
31
31
  apiEndpoints.setShippingOption.initiate(defaultShippingOption)
32
32
  );
33
+
34
+ return null;
33
35
  }
34
36
  }
35
37
 
package/sentry/index.ts CHANGED
@@ -1,29 +1,48 @@
1
1
  import * as Sentry from '@sentry/nextjs';
2
2
 
3
- const SENTRY_DSN: string =
3
+ const SENTRY_DSN: string | undefined =
4
4
  process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;
5
5
 
6
+ export enum ClientLogType {
7
+ UNCAUGHT_ERROR_PAGE = 'UNCAUGHT_ERROR_PAGE',
8
+ CHECKOUT = 'CHECKOUT'
9
+ }
10
+
11
+ const ALLOWED_CLIENT_LOG_TYPES: ClientLogType[] = [
12
+ ClientLogType.UNCAUGHT_ERROR_PAGE,
13
+ ClientLogType.CHECKOUT
14
+ ];
15
+
6
16
  export const initSentry = (
7
17
  type: 'Server' | 'Client' | 'Edge',
8
18
  options: Sentry.BrowserOptions | Sentry.NodeOptions | Sentry.EdgeOptions = {}
9
19
  ) => {
10
20
  // TODO: Handle options with ESLint rules
11
21
 
12
- // TODO: Remove Zero Project DSN
22
+ Sentry.init({
23
+ dsn:
24
+ options.dsn ||
25
+ SENTRY_DSN ||
26
+ 'https://d8558ef8997543deacf376c7d8d7cf4b@o64293.ingest.sentry.io/4504338423742464',
27
+ initialScope: {
28
+ tags: {
29
+ APP_TYPE: 'ProjectZeroNext',
30
+ TYPE: type
31
+ }
32
+ },
33
+ 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
+ }
13
44
 
14
- if (type === 'Server' || type === 'Edge') {
15
- Sentry.init({
16
- dsn:
17
- SENTRY_DSN ||
18
- 'https://d8558ef8997543deacf376c7d8d7cf4b@o64293.ingest.sentry.io/4504338423742464',
19
- initialScope: {
20
- tags: {
21
- APP_TYPE: 'ProjectZeroNext',
22
- TYPE: type
23
- }
24
- },
25
- tracesSampleRate: 1.0,
26
- integrations: []
27
- });
28
- }
45
+ return event;
46
+ }
47
+ });
29
48
  };
package/utils/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import settings from 'settings';
2
2
  import { LocaleUrlStrategy } from '../localization';
3
- import { CDNOptions, ClientRequestOptions } from '../types';
3
+ import { CDNOptions, ClientRequestOptions, Locale } from '../types';
4
4
 
5
5
  export * from './get-currency';
6
6
  export * from './menu-generator';
@@ -152,14 +152,17 @@ export function buildCDNUrl(url: string, config?: CDNOptions) {
152
152
  return `${rootWithoutOptions}${options}${fileExtension}`;
153
153
  }
154
154
 
155
+ const { locales, localeUrlStrategy, defaultLocaleValue } =
156
+ settings.localization;
157
+
158
+ const isLocaleExcluded = (locale: Locale) =>
159
+ ![LocaleUrlStrategy.ShowAllLocales, LocaleUrlStrategy.Subdomain].includes(
160
+ localeUrlStrategy
161
+ ) && locale.value !== defaultLocaleValue;
162
+
155
163
  export const urlLocaleMatcherRegex = new RegExp(
156
- `^/(${settings.localization.locales
157
- .filter((l) =>
158
- settings.localization.localeUrlStrategy !==
159
- LocaleUrlStrategy.ShowAllLocales
160
- ? l.value !== settings.localization.defaultLocaleValue
161
- : l
162
- )
164
+ `^/(${locales
165
+ .filter((l) => !isLocaleExcluded(l))
163
166
  .map((l) => l.value)
164
167
  .join('|')})(?=/|$)`
165
168
  );
@@ -11,6 +11,10 @@ export const getUrlPathWithLocale = (
11
11
  currentLocale = defaultLocaleValue;
12
12
  }
13
13
 
14
+ if (localeUrlStrategy === LocaleUrlStrategy.Subdomain) {
15
+ return pathname;
16
+ }
17
+
14
18
  if (localeUrlStrategy === LocaleUrlStrategy.HideAllLocales) {
15
19
  return pathname;
16
20
  }
@@ -0,0 +1,92 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useLocalization } from '../hooks';
3
+ import { Button, Link } from '../components';
4
+
5
+ export default function PzErrorPage({
6
+ error,
7
+ reset
8
+ }: {
9
+ error: Error & { digest?: string; isServerError?: boolean };
10
+ reset: () => void;
11
+ }) {
12
+ const [isServerError, setIsServerError] = useState(false);
13
+
14
+ useEffect(() => {
15
+ if ('isServerError' in error) {
16
+ setIsServerError(true);
17
+ return;
18
+ }
19
+
20
+ setIsServerError(!!error.digest);
21
+ }, [error]);
22
+
23
+ return isServerError ? (
24
+ <ServerErrorUI />
25
+ ) : (
26
+ <ClientErrorUI error={error} reset={reset} />
27
+ );
28
+ }
29
+
30
+ function ClientErrorUI({
31
+ error,
32
+ reset
33
+ }: {
34
+ error: Error & { digest?: string };
35
+ reset: () => void;
36
+ }) {
37
+ const { t } = useLocalization();
38
+
39
+ const errorMessage = error?.message || 'Unknown error';
40
+
41
+ return (
42
+ <section className="text-center px-6 my-14 md:px-0 md:m-14">
43
+ <div className="text-4xl font-bold md:text-6xl text-red-500">Oops!</div>
44
+ <h1 className="text-lg md:text-xl mt-4">
45
+ {t('common.client_error.title')}
46
+ </h1>
47
+ <p className="text-lg md:text-xl mt-2">
48
+ {t('common.client_error.description')}
49
+ </p>
50
+
51
+ <div className="mt-4 mx-auto max-w-lg">
52
+ <p className="text-xs text-gray-600 font-mono bg-gray-100 p-3 rounded overflow-auto text-left">
53
+ <span className="font-semibold">Error:</span> {errorMessage}
54
+ </p>
55
+ </div>
56
+
57
+ <div className="mt-6 flex flex-col gap-4 items-center justify-center">
58
+ <Link href="/" className="text-lg underline">
59
+ {t('common.client_error.link_text')}
60
+ </Link>
61
+ <Button onClick={reset} className="text-lg w-28">
62
+ {t('common.try_again')}
63
+ </Button>
64
+ </div>
65
+ </section>
66
+ );
67
+ }
68
+
69
+ function ServerErrorUI() {
70
+ const { t } = useLocalization();
71
+
72
+ const reloadPage = () => {
73
+ window.location.reload();
74
+ };
75
+
76
+ return (
77
+ <section className="text-center px-6 my-14 md:px-0 md:m-14">
78
+ <div className="text-7xl font-bold md:text-8xl">500</div>
79
+ <h1 className="text-lg md:text-xl"> {t('common.page_500.title')} </h1>
80
+ <p className="text-lg md:text-xl"> {t('common.page_500.description')} </p>
81
+
82
+ <div className="mt-6 flex flex-col gap-4 items-center justify-center">
83
+ <Link href="/" className="text-lg underline">
84
+ {t('common.page_500.link_text')}
85
+ </Link>
86
+ <Button onClick={reloadPage} className="text-lg w-28">
87
+ {t('common.try_again')}
88
+ </Button>
89
+ </div>
90
+ </section>
91
+ );
92
+ }