@akinon/next 2.0.0-beta.7 → 2.0.0-beta.8

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 (38) hide show
  1. package/.eslintrc.js +12 -0
  2. package/CHANGELOG.md +6 -0
  3. package/api/auth.ts +15 -0
  4. package/bin/run-prebuild-tests.js +46 -0
  5. package/components/client-root.tsx +20 -0
  6. package/data/client/api.ts +1 -0
  7. package/data/client/basket.ts +27 -5
  8. package/data/client/checkout.ts +23 -1
  9. package/data/client/misc.ts +25 -1
  10. package/data/client/product.ts +19 -2
  11. package/data/urls.ts +11 -3
  12. package/hooks/index.ts +1 -0
  13. package/hooks/use-router.ts +5 -2
  14. package/hooks/use-sentry-uncaught-errors.ts +24 -0
  15. package/instrumentation/index.ts +10 -0
  16. package/lib/cache.ts +4 -4
  17. package/localization/index.ts +2 -1
  18. package/middlewares/default.ts +6 -2
  19. package/middlewares/locale.ts +31 -10
  20. package/package.json +3 -2
  21. package/redux/middlewares/checkout.ts +16 -144
  22. package/redux/middlewares/pre-order/address.ts +2 -0
  23. package/redux/middlewares/pre-order/attribute-based-shipping-option.ts +2 -0
  24. package/redux/middlewares/pre-order/data-source-shipping-option.ts +2 -0
  25. package/redux/middlewares/pre-order/delivery-option.ts +2 -0
  26. package/redux/middlewares/pre-order/index.ts +14 -10
  27. package/redux/middlewares/pre-order/installment-option.ts +2 -0
  28. package/redux/middlewares/pre-order/payment-option.ts +2 -0
  29. package/redux/middlewares/pre-order/redirection.ts +2 -0
  30. package/redux/middlewares/pre-order/shipping-option.ts +2 -0
  31. package/redux/reducers/checkout.ts +8 -2
  32. package/redux/reducers/root.ts +7 -2
  33. package/sentry/index.ts +36 -17
  34. package/types/commerce/checkout.ts +9 -0
  35. package/utils/index.ts +11 -8
  36. package/utils/localization.ts +4 -0
  37. package/views/error-page.tsx +93 -0
  38. package/with-pz-config.js +3 -2
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,11 @@
1
1
  # @akinon/next
2
2
 
3
+ ## 2.0.0-beta.8
4
+
5
+ ### Minor Changes
6
+
7
+ - 071d0f5: ZERO-3352: Resolve Single item size exceeds maxSize error and upgrade dependencies
8
+
3
9
  ## 2.0.0-beta.7
4
10
 
5
11
  ## 2.0.0-beta.6
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
  {
@@ -0,0 +1,46 @@
1
+ const path = require('path')
2
+ const fs = require('fs')
3
+
4
+ function runPrebuildTests() {
5
+ const workspaceRoot = process.cwd()
6
+ const configPath = path.join(workspaceRoot, 'config/prebuild-tests.json')
7
+
8
+ try {
9
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'))
10
+
11
+ if (config.tests && Array.isArray(config.tests)) {
12
+ for (const testScript of config.tests) {
13
+ console.log(`🧪 Running test script: ${testScript}`)
14
+ const result = require('child_process').spawnSync('yarn', [testScript], {
15
+ stdio: 'inherit',
16
+ shell: true
17
+ })
18
+
19
+ if (result.status !== 0) {
20
+ console.error(`❌ Test script '${testScript}' failed`)
21
+ process.exit(1)
22
+ }
23
+ }
24
+ console.log('✅ All prebuild tests passed successfully!')
25
+ }
26
+ } catch (error) {
27
+ if (error.code === 'ENOENT') {
28
+ console.log('🧪 Running default test: test:middleware')
29
+ const result = require('child_process').spawnSync('yarn', ['test:middleware'], {
30
+ stdio: 'inherit',
31
+ shell: true
32
+ })
33
+
34
+ if (result.status !== 0) {
35
+ console.error('❌ Middleware test failed')
36
+ process.exit(1)
37
+ }
38
+ console.log('✅ Default test passed successfully!')
39
+ } else {
40
+ console.error('❌ Error reading test configuration:', error)
41
+ process.exit(1)
42
+ }
43
+ }
44
+ }
45
+
46
+ module.exports = runPrebuildTests
@@ -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
  }
@@ -68,6 +68,7 @@ export const api = createApi({
68
68
  tagTypes: [
69
69
  'Basket',
70
70
  'MultiBasket',
71
+ 'MiniBasket',
71
72
  'BasketB2b',
72
73
  'DraftsB2b',
73
74
  'Product',
@@ -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: ['MiniBasket']
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: ['MiniBasket']
46
+ }),
27
47
  getBasketDetail: build.query<Basket, { namespace: string }>({
28
48
  query: ({ namespace }) =>
29
49
  buildClientRequestUrl(basket.getBasketDetail(namespace)),
@@ -46,7 +66,7 @@ export const basketApi = api.injectEndpoints({
46
66
  method: 'DELETE',
47
67
  body: { pk }
48
68
  }),
49
- invalidatesTags: ['MultiBasket', 'Basket']
69
+ invalidatesTags: ['MultiBasket', 'Basket', 'MiniBasket']
50
70
  }),
51
71
  selectMainBasket: build.mutation<Basket, { pk: number }>({
52
72
  query: ({ pk }) => ({
@@ -57,7 +77,7 @@ export const basketApi = api.injectEndpoints({
57
77
  body: { pk }
58
78
  }),
59
79
  transformResponse: (response: { baskets: Basket }) => response.baskets,
60
- invalidatesTags: ['MultiBasket', 'Basket']
80
+ invalidatesTags: ['MultiBasket', 'Basket', 'MiniBasket']
61
81
  }),
62
82
  selectNameSpaceMainBasket: build.mutation<Basket, { namespace: string }>({
63
83
  query: ({ namespace }) => ({
@@ -71,7 +91,7 @@ export const basketApi = api.injectEndpoints({
71
91
  body: { namespace }
72
92
  }),
73
93
  transformResponse: (response: { baskets: Basket }) => response.baskets,
74
- invalidatesTags: ['MultiBasket', 'Basket']
94
+ invalidatesTags: ['MultiBasket', 'Basket', 'MiniBasket']
75
95
  }),
76
96
  updateQuantity: build.mutation<
77
97
  UpdateQuantityResponse,
@@ -84,7 +104,7 @@ export const basketApi = api.injectEndpoints({
84
104
  method: 'PUT',
85
105
  body
86
106
  }),
87
- invalidatesTags: ['MultiBasket', 'Basket']
107
+ invalidatesTags: ['MultiBasket', 'Basket', 'MiniBasket']
88
108
  }),
89
109
  clearBasket: build.mutation<Basket, void>({
90
110
  query: (body) => ({
@@ -95,7 +115,7 @@ export const basketApi = api.injectEndpoints({
95
115
  body
96
116
  }),
97
117
  transformResponse: (response: { basket: Basket }) => response.basket,
98
- invalidatesTags: ['Basket']
118
+ invalidatesTags: ['Basket', 'MiniBasket', 'MiniBasket']
99
119
  }),
100
120
  applyVoucherCode: build.mutation<Basket, { voucher_code: string }>({
101
121
  query: (body) => ({
@@ -125,6 +145,8 @@ export const basketApi = api.injectEndpoints({
125
145
 
126
146
  export const {
127
147
  useGetBasketQuery,
148
+ useGetMiniBasketQuery,
149
+ useGetMiniBasketDetailQuery,
128
150
  useLazyGetBasketDetailQuery,
129
151
  useGetAllBasketsQuery,
130
152
  useRemoveBasketMutation,
@@ -13,7 +13,9 @@ import {
13
13
  ExtraField,
14
14
  GuestLoginFormParams,
15
15
  Order,
16
- PreOrder
16
+ PreOrder,
17
+ SendSmsType,
18
+ VerifySmsType
17
19
  } from '../../types';
18
20
  import { buildClientRequestUrl } from '../../utils';
19
21
  import { api } from './api';
@@ -847,6 +849,24 @@ export const checkoutApi = api.injectEndpoints({
847
849
  selected_loyalty_amount: amount
848
850
  }
849
851
  })
852
+ }),
853
+ sendSms: build.mutation<CheckoutResponse, SendSmsType>({
854
+ query: (body) => ({
855
+ url: buildClientRequestUrl(checkout.sendSmsPage, {
856
+ useFormData: true
857
+ }),
858
+ method: 'POST',
859
+ body
860
+ })
861
+ }),
862
+ verifySms: build.mutation<CheckoutResponse, VerifySmsType>({
863
+ query: (body) => ({
864
+ url: buildClientRequestUrl(checkout.verifySmsPage, {
865
+ useFormData: true
866
+ }),
867
+ method: 'POST',
868
+ body
869
+ })
850
870
  })
851
871
  }),
852
872
  overrideExisting: false
@@ -892,5 +912,7 @@ export const {
892
912
  useSetWalletSelectionPageMutation,
893
913
  useSetWalletPaymentPageMutation,
894
914
  useSetWalletCompletePageMutation,
915
+ useSendSmsMutation,
916
+ useVerifySmsMutation,
895
917
  useResetCheckoutStateQuery
896
918
  } = checkoutApi;
@@ -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;
@@ -69,12 +69,28 @@ export const productApi = api.injectEndpoints({
69
69
  }),
70
70
  method: 'POST',
71
71
  body
72
- })
72
+ }),
73
+ invalidatesTags: ['MiniBasket']
73
74
  }),
74
75
  getInstallments: build.query<any, any>({
75
76
  query: (productPk) => ({
76
77
  url: buildClientRequestUrl(product.installments(productPk))
77
78
  })
79
+ }),
80
+ getBundleProductData: build.query<
81
+ any,
82
+ { productPk: string; queryString: string }
83
+ >({
84
+ query: ({ productPk, queryString }) => {
85
+ return {
86
+ url: buildClientRequestUrl(
87
+ product.bundleProduct(productPk, queryString),
88
+ {
89
+ responseType: 'json'
90
+ }
91
+ )
92
+ };
93
+ }
78
94
  })
79
95
  }),
80
96
  overrideExisting: true
@@ -85,5 +101,6 @@ export const {
85
101
  useGetProductByPkQuery,
86
102
  useGetRetailStoreStockMutation,
87
103
  useGetInstallmentsQuery,
88
- useGetProductByParamsQuery
104
+ useGetProductByParamsQuery,
105
+ useGetBundleProductDataQuery
89
106
  } = 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/`,
@@ -137,7 +140,9 @@ export const checkout = {
137
140
  '/orders/checkout/?page=AttributeBasedShippingOptionSelectionPage',
138
141
  deliveryBagsPage: '/orders/checkout/?page=DeliveryBagsPage',
139
142
  setOrderSelectionPage: '/orders/checkout/?page=OrderSelectionPage',
140
- loyaltyCardPage: '/orders/checkout/?page=LoyaltyCardPage'
143
+ loyaltyCardPage: '/orders/checkout/?page=LoyaltyCardPage',
144
+ sendSmsPage: '/orders/checkout/?page=SendSmsPage',
145
+ verifySmsPage: '/orders/checkout/?page=VerifySmsPage'
141
146
  };
142
147
 
143
148
  export const flatpage = {
@@ -160,7 +165,8 @@ export const misc = {
160
165
  parent ? `&parent=${parent}` : ''
161
166
  }`,
162
167
  cmsSeo: (slug: string | string[]) => `/cms/seo/?url=${slug ? slug : '/'}`,
163
- setCurrency: '/users/activate-currency/'
168
+ setCurrency: '/users/activate-currency/',
169
+ bukalemunImageUrl: (params: string) => `/bukalemun/?${params}`
164
170
  };
165
171
 
166
172
  export const product = {
@@ -174,7 +180,9 @@ export const product = {
174
180
  slug: (slug: string) => `/${slug}/`,
175
181
  categoryUrl: (pk: number) => `/products/${pk}/category_nodes/?limit=1`,
176
182
  breadcrumbUrl: (menuitemmodel: string) =>
177
- `/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}`
178
186
  };
179
187
 
180
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,5 +1,15 @@
1
+ import { initSentry } from '../sentry';
2
+ import * as Sentry from '@sentry/nextjs';
3
+
1
4
  export async function register() {
2
5
  if (process.env.NEXT_RUNTIME === 'nodejs') {
3
6
  await import('./node');
7
+ initSentry('Server');
8
+ }
9
+
10
+ if (process.env.NEXT_RUNTIME === 'edge') {
11
+ initSentry('Edge');
4
12
  }
5
13
  }
14
+
15
+ export const onRequestError = Sentry.captureRequestError;
package/lib/cache.ts CHANGED
@@ -156,10 +156,6 @@ export class Cache {
156
156
  handler: () => Promise<T>,
157
157
  options?: CacheOptions
158
158
  ): Promise<T> {
159
- if (Settings.usePrettyUrlRoute) {
160
- return await handler();
161
- }
162
-
163
159
  const requiredVariables = [
164
160
  process.env.CACHE_HOST,
165
161
  process.env.CACHE_PORT,
@@ -178,6 +174,10 @@ export class Cache {
178
174
  const _options = Object.assign(defaultOptions, options);
179
175
  const formattedKey = Cache.formatKey(key, locale);
180
176
 
177
+ if (Settings.usePrettyUrlRoute) {
178
+ _options.expire = 120;
179
+ }
180
+
181
181
  logger.debug('Cache wrap', { key, formattedKey, _options });
182
182
 
183
183
  if (_options.cache) {
@@ -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
  }
@@ -95,7 +95,9 @@ const withPzDefault =
95
95
 
96
96
  if (
97
97
  req.nextUrl.pathname.includes('/orders/hooks/') ||
98
- req.nextUrl.pathname.includes('/orders/checkout-with-token/')
98
+ req.nextUrl.pathname.includes('/orders/checkout-with-token/') ||
99
+ req.nextUrl.pathname.includes('/hooks/cash_register/complete/') ||
100
+ req.nextUrl.pathname.includes('/hooks/cash_register/pre_order/')
99
101
  ) {
100
102
  const queryString = searchParams.toString();
101
103
  const currency = searchParams.get('currency')?.toLowerCase();
@@ -285,7 +287,9 @@ const withPzDefault =
285
287
  url.pathname =
286
288
  url.pathname +
287
289
  (/\/$/.test(url.pathname) ? '' : '/') +
288
- `searchparams|${url.searchParams.toString()}`;
290
+ `searchparams|${encodeURIComponent(
291
+ url.searchParams.toString()
292
+ )}`;
289
293
  }
290
294
 
291
295
  Settings.rewrites.forEach((rewrite) => {
@@ -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": "2.0.0-beta.7",
4
+ "version": "2.0.0-beta.8",
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.5.1",
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": "2.0.0-beta.7",
34
+ "@akinon/eslint-plugin-projectzero": "2.0.0-beta.8",
34
35
  "@types/react-redux": "7.1.30",
35
36
  "@types/set-cookie-parser": "2.4.7",
36
37
  "@typescript-eslint/eslint-plugin": "8.18.2",
@@ -16,12 +16,11 @@ import {
16
16
  setLoyaltyBalance,
17
17
  setPaymentChoices,
18
18
  setPaymentOptions,
19
- setPreOrder,
20
19
  setRetailStores,
21
20
  setShippingOptions,
22
- setShippingStepCompleted,
23
21
  setHepsipayAvailability,
24
- setWalletPaymentData
22
+ setWalletPaymentData,
23
+ setPayOnDeliveryOtpModalActive
25
24
  } from '../../redux/reducers/checkout';
26
25
  import { RootState, TypedDispatch } from 'redux/store';
27
26
  import { checkoutApi } from '../../data/client/checkout';
@@ -58,153 +57,14 @@ export const errorMiddleware: Middleware = ({ dispatch }: MiddlewareParams) => {
58
57
  };
59
58
  };
60
59
 
61
- export const preOrderMiddleware: Middleware = ({
62
- getState,
63
- dispatch
64
- }: MiddlewareParams) => {
65
- return (next) => (action) => {
66
- const result: CheckoutResult = next(action);
67
- const preOrder = result?.payload?.pre_order;
68
-
69
- if (
70
- !preOrder ||
71
- action?.meta?.arg?.endpointName === 'guestLogin' ||
72
- action?.meta?.arg?.endpointName === 'getCheckoutLoyaltyBalance'
73
- ) {
74
- return result;
75
- }
76
-
77
- const {
78
- deliveryOptions,
79
- addressList: addresses,
80
- shippingOptions,
81
- dataSourceShippingOptions,
82
- paymentOptions,
83
- installmentOptions,
84
- attributeBasedShippingOptions
85
- } = getState().checkout;
86
- const { endpoints: apiEndpoints } = checkoutApi;
87
-
88
- if (preOrder.is_redirected) {
89
- const contextList = result?.payload?.context_list;
90
-
91
- if (
92
- contextList.find(
93
- (ctx) => ctx.page_name === 'RedirectionPaymentSelectedPage'
94
- )
95
- ) {
96
- dispatch(
97
- apiEndpoints.setPaymentOption.initiate(preOrder.payment_option?.pk)
98
- );
99
- return;
100
- }
101
- }
102
-
103
- dispatch(setPreOrder(preOrder));
104
-
105
- if (!preOrder.delivery_option && deliveryOptions.length > 0) {
106
- dispatch(
107
- apiEndpoints.setDeliveryOption.initiate(
108
- deliveryOptions.find((opt) => opt.delivery_option_type === 'customer')
109
- ?.pk
110
- )
111
- );
112
- }
113
-
114
- if (
115
- (!preOrder.shipping_address || !preOrder.billing_address) &&
116
- addresses.length > 0 &&
117
- (!preOrder.delivery_option ||
118
- preOrder.delivery_option.delivery_option_type === 'customer')
119
- ) {
120
- dispatch(
121
- apiEndpoints.setAddresses.initiate({
122
- shippingAddressPk: addresses[0].pk,
123
- billingAddressPk: addresses[0].pk
124
- })
125
- );
126
- }
127
-
128
- if (
129
- shippingOptions.length > 0 &&
130
- (!preOrder.shipping_option ||
131
- !shippingOptions.find((opt) => opt.pk === preOrder.shipping_option?.pk))
132
- ) {
133
- dispatch(apiEndpoints.setShippingOption.initiate(shippingOptions[0].pk));
134
- }
135
-
136
- if (
137
- dataSourceShippingOptions.length > 0 &&
138
- !preOrder.data_source_shipping_options
139
- ) {
140
- const selectedDataSourceShippingOptionsPks =
141
- dataSourceShippingOptions.map(
142
- (opt) => opt.data_source_shipping_options[0].pk
143
- );
144
-
145
- dispatch(
146
- apiEndpoints.setDataSourceShippingOptions.initiate(
147
- selectedDataSourceShippingOptionsPks
148
- )
149
- );
150
- }
151
-
152
- if (
153
- Object.keys(attributeBasedShippingOptions).length > 0 &&
154
- !preOrder.attribute_based_shipping_options
155
- ) {
156
- const initialSelectedOptions: Record<string, number> = Object.fromEntries(
157
- Object.entries(attributeBasedShippingOptions).map(([key, options]) => [
158
- key,
159
- options.attribute_based_shipping_options[0].pk
160
- ])
161
- );
162
-
163
- dispatch(
164
- apiEndpoints.setAttributeBasedShippingOptions.initiate(
165
- initialSelectedOptions
166
- )
167
- );
168
- }
169
-
170
- if (!preOrder.payment_option && paymentOptions.length > 0) {
171
- dispatch(apiEndpoints.setPaymentOption.initiate(paymentOptions[0].pk));
172
- }
173
-
174
- if (
175
- !preOrder.installment &&
176
- preOrder.payment_option?.payment_type !== 'saved_card' &&
177
- installmentOptions.length > 0
178
- ) {
179
- dispatch(
180
- apiEndpoints.setInstallmentOption.initiate(installmentOptions[0].pk)
181
- );
182
- }
183
-
184
- dispatch(
185
- setShippingStepCompleted(
186
- [
187
- preOrder.delivery_option?.delivery_option_type === 'retail_store'
188
- ? true
189
- : preOrder.shipping_address?.pk,
190
- preOrder.billing_address?.pk,
191
- preOrder.shipping_option?.pk,
192
- addresses.length > 0
193
- ].every(Boolean)
194
- )
195
- );
196
-
197
- return result;
198
- };
199
- };
200
-
201
60
  export const contextListMiddleware: Middleware = ({
202
61
  dispatch,
203
62
  getState
204
63
  }: MiddlewareParams) => {
205
64
  return (next) => (action) => {
206
- const { isMobileApp } = getState().root;
65
+ const { isMobileApp, userPhoneNumber } = getState().root;
207
66
  const result: CheckoutResult = next(action);
67
+ const preOrder = result?.payload?.pre_order;
208
68
 
209
69
  if (result?.payload?.context_list) {
210
70
  result.payload.context_list.forEach((context) => {
@@ -323,6 +183,18 @@ export const contextListMiddleware: Middleware = ({
323
183
  if (context.page_context.retail_stores) {
324
184
  dispatch(setRetailStores(context.page_context.retail_stores));
325
185
  }
186
+
187
+ if (context.page_name === 'SendSmsPage' && !preOrder?.phone_number) {
188
+ dispatch(
189
+ checkoutApi.endpoints.sendSms.initiate({
190
+ phone_number: userPhoneNumber ?? preOrder?.user_phone_number
191
+ })
192
+ );
193
+ }
194
+
195
+ if (context.page_name === 'VerifySmsPage') {
196
+ dispatch(setPayOnDeliveryOtpModalActive(true));
197
+ }
326
198
  });
327
199
  }
328
200
 
@@ -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
 
@@ -70,6 +70,7 @@ export interface CheckoutState {
70
70
  };
71
71
  supportedMethods: string;
72
72
  };
73
+ payOnDeliveryOtpModalActive: boolean;
73
74
  }
74
75
 
75
76
  const initialState: CheckoutState = {
@@ -103,7 +104,8 @@ const initialState: CheckoutState = {
103
104
  retailStores: [],
104
105
  attributeBasedShippingOptions: [],
105
106
  selectedShippingOptions: {},
106
- hepsipayAvailability: false
107
+ hepsipayAvailability: false,
108
+ payOnDeliveryOtpModalActive: false
107
109
  };
108
110
 
109
111
  const checkoutSlice = createSlice({
@@ -193,6 +195,9 @@ const checkoutSlice = createSlice({
193
195
  },
194
196
  setWalletPaymentData(state, { payload }) {
195
197
  state.walletPaymentData = payload;
198
+ },
199
+ setPayOnDeliveryOtpModalActive(state, { payload }) {
200
+ state.payOnDeliveryOtpModalActive = payload;
196
201
  }
197
202
  }
198
203
  });
@@ -225,7 +230,8 @@ export const {
225
230
  setAttributeBasedShippingOptions,
226
231
  setSelectedShippingOptions,
227
232
  setHepsipayAvailability,
228
- setWalletPaymentData
233
+ setWalletPaymentData,
234
+ setPayOnDeliveryOtpModalActive
229
235
  } = checkoutSlice.actions;
230
236
 
231
237
  export default checkoutSlice.reducer;
@@ -14,7 +14,8 @@ const initialState = {
14
14
  open: false,
15
15
  title: null,
16
16
  content: null
17
- }
17
+ },
18
+ userPhoneNumber: null
18
19
  };
19
20
 
20
21
  const rootSlice = createSlice({
@@ -45,6 +46,9 @@ const rootSlice = createSlice({
45
46
  state.rootModal.open = false;
46
47
  state.rootModal.title = null;
47
48
  state.rootModal.content = null;
49
+ },
50
+ setUserPhoneNumber(state, { payload }) {
51
+ state.userPhoneNumber = payload;
48
52
  }
49
53
  }
50
54
  });
@@ -55,7 +59,8 @@ export const {
55
59
  toggleMiniBasket,
56
60
  setHighlightedItem,
57
61
  openRootModal,
58
- closeRootModal
62
+ closeRootModal,
63
+ setUserPhoneNumber
59
64
  } = rootSlice.actions;
60
65
 
61
66
  export default rootSlice.reducer;
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
  };
@@ -108,6 +108,7 @@ export interface PreOrder {
108
108
  context_extras?: ExtraField;
109
109
  token?: string;
110
110
  agreement_confirmed?: boolean;
111
+ phone_number?: string;
111
112
  }
112
113
 
113
114
  export type ExtraField = Record<string, any>;
@@ -200,3 +201,11 @@ export interface MiddlewareParams {
200
201
  getState: () => RootState;
201
202
  dispatch: TypedDispatch;
202
203
  }
204
+
205
+ export type SendSmsType = {
206
+ phone_number: string;
207
+ };
208
+
209
+ export type VerifySmsType = {
210
+ verify_code: string;
211
+ };
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,93 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useLocalization } from '../hooks';
3
+ import { Button, Link } from '../components';
4
+ import { ROUTES } from 'routes';
5
+
6
+ export default function PzErrorPage({
7
+ error,
8
+ reset
9
+ }: {
10
+ error: Error & { digest?: string; isServerError?: boolean };
11
+ reset: () => void;
12
+ }) {
13
+ const [isServerError, setIsServerError] = useState(false);
14
+
15
+ useEffect(() => {
16
+ if ('isServerError' in error) {
17
+ setIsServerError(true);
18
+ return;
19
+ }
20
+
21
+ setIsServerError(!!error.digest);
22
+ }, [error]);
23
+
24
+ return isServerError ? (
25
+ <ServerErrorUI />
26
+ ) : (
27
+ <ClientErrorUI error={error} reset={reset} />
28
+ );
29
+ }
30
+
31
+ function ClientErrorUI({
32
+ error,
33
+ reset
34
+ }: {
35
+ error: Error & { digest?: string };
36
+ reset: () => void;
37
+ }) {
38
+ const { t } = useLocalization();
39
+
40
+ const errorMessage = error?.message || 'Unknown error';
41
+
42
+ return (
43
+ <section className="text-center px-6 py-6 my-14 md:px-0 md:m-14">
44
+ <div className="text-4xl font-bold md:text-6xl text-red-500">500</div>
45
+ <h1 className="text-lg md:text-xl mt-4">
46
+ {t('common.client_error.title')}
47
+ </h1>
48
+ <p className="text-lg md:text-xl mt-2">
49
+ {t('common.client_error.description')}
50
+ </p>
51
+
52
+ <div className="mt-4 mx-auto max-w-lg">
53
+ <p className="text-xs text-gray-600 font-mono bg-gray-100 p-3 rounded overflow-auto text-left">
54
+ <span className="font-semibold">Error:</span> {errorMessage}
55
+ </p>
56
+ </div>
57
+
58
+ <div className="mt-6 flex flex-col gap-4 items-center justify-center">
59
+ <Link href={ROUTES.HOME} className="text-lg underline">
60
+ {t('common.client_error.link_text')}
61
+ </Link>
62
+ <Button onClick={reset} className="text-lg">
63
+ {t('common.try_again')}
64
+ </Button>
65
+ </div>
66
+ </section>
67
+ );
68
+ }
69
+
70
+ function ServerErrorUI() {
71
+ const { t } = useLocalization();
72
+
73
+ const reloadPage = () => {
74
+ window.location.reload();
75
+ };
76
+
77
+ return (
78
+ <section className="text-center px-6 my-14 md:px-0 md:m-14">
79
+ <div className="text-7xl font-bold md:text-8xl">500</div>
80
+ <h1 className="text-lg md:text-xl"> {t('common.page_500.title')} </h1>
81
+ <p className="text-lg md:text-xl"> {t('common.page_500.description')} </p>
82
+
83
+ <div className="mt-6 flex flex-col gap-4 items-center justify-center">
84
+ <Link href={ROUTES.HOME} className="text-lg underline">
85
+ {t('common.page_500.link_text')}
86
+ </Link>
87
+ <Button onClick={reloadPage} className="text-lg">
88
+ {t('common.try_again')}
89
+ </Button>
90
+ </div>
91
+ </section>
92
+ );
93
+ }
package/with-pz-config.js CHANGED
@@ -45,7 +45,8 @@ const defaultConfig = {
45
45
  },
46
46
  {
47
47
  key: 'Content-Security-Policy',
48
- value: "frame-ancestors 'self' https://*.akifast.com akifast.com"
48
+ value:
49
+ "frame-ancestors 'self' https://*.akifast.com akifast.com https://*.akinoncloud.com akinoncloud.com"
49
50
  }
50
51
  ]
51
52
  }
@@ -64,7 +65,7 @@ const defaultConfig = {
64
65
  },
65
66
  sentry: {
66
67
  hideSourceMaps: true
67
- }
68
+ } // TODO: This section will be reviewed again in the Sentry 8 update.
68
69
  };
69
70
 
70
71
  const withPzConfig = (