@akinon/next 1.58.0 → 1.59.0-rc.0

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 (55) hide show
  1. package/CHANGELOG.md +652 -0
  2. package/api/client.ts +23 -2
  3. package/assets/styles/index.css +49 -0
  4. package/assets/styles/index.css.map +1 -0
  5. package/assets/styles/index.scss +50 -26
  6. package/bin/pz-generate-translations.js +41 -0
  7. package/bin/pz-prebuild.js +1 -0
  8. package/bin/pz-predev.js +1 -0
  9. package/components/file-input.tsx +8 -0
  10. package/components/index.ts +1 -0
  11. package/components/input.tsx +21 -7
  12. package/components/link.tsx +17 -13
  13. package/components/price.tsx +11 -4
  14. package/components/pz-root.tsx +15 -3
  15. package/data/client/api.ts +1 -1
  16. package/data/client/b2b.ts +35 -2
  17. package/data/client/basket.ts +6 -5
  18. package/data/client/checkout.ts +37 -0
  19. package/data/client/user.ts +3 -2
  20. package/data/server/category.ts +43 -19
  21. package/data/server/flatpage.ts +29 -7
  22. package/data/server/form.ts +29 -11
  23. package/data/server/landingpage.ts +26 -7
  24. package/data/server/list.ts +16 -6
  25. package/data/server/menu.ts +15 -2
  26. package/data/server/product.ts +33 -13
  27. package/data/server/seo.ts +17 -24
  28. package/data/server/special-page.ts +15 -5
  29. package/data/server/widget.ts +14 -7
  30. package/data/urls.ts +8 -1
  31. package/hocs/server/with-segment-defaults.tsx +4 -1
  32. package/hooks/index.ts +2 -1
  33. package/hooks/use-message-listener.ts +24 -0
  34. package/hooks/use-pagination.ts +2 -2
  35. package/lib/cache-handler.mjs +33 -0
  36. package/lib/cache.ts +8 -6
  37. package/middlewares/default.ts +46 -2
  38. package/middlewares/pretty-url.ts +11 -1
  39. package/middlewares/url-redirection.ts +4 -0
  40. package/package.json +4 -3
  41. package/plugins.d.ts +1 -0
  42. package/redux/middlewares/checkout.ts +71 -11
  43. package/redux/reducers/checkout.ts +23 -3
  44. package/routes/pretty-url.tsx +192 -0
  45. package/types/commerce/address.ts +1 -1
  46. package/types/commerce/b2b.ts +12 -2
  47. package/types/commerce/checkout.ts +30 -0
  48. package/types/commerce/order.ts +1 -0
  49. package/types/index.ts +17 -2
  50. package/utils/app-fetch.ts +16 -8
  51. package/utils/generate-commerce-search-params.ts +3 -1
  52. package/utils/index.ts +27 -6
  53. package/utils/redirection-iframe.ts +85 -0
  54. package/utils/server-translation.ts +11 -1
  55. package/with-pz-config.js +13 -2
@@ -2,7 +2,6 @@ import settings from 'settings';
2
2
  import { LayoutProps, PageProps, RootLayoutProps } from '../../types';
3
3
  import { redirect } from 'next/navigation';
4
4
  import { ServerVariables } from '../../utils/server-variables';
5
- import { getTranslations } from '../../utils/server-translation';
6
5
  import { ROUTES } from 'routes';
7
6
  import logger from '../../utils/log';
8
7
 
@@ -50,7 +49,11 @@ const addRootLayoutProps = async (componentProps: RootLayoutProps) => {
50
49
  return redirect(ROUTES.HOME);
51
50
  }
52
51
 
52
+ const { getTranslations } = settings.useOptimizedTranslations
53
+ ? require('translations')
54
+ : require('../../utils/server-translation');
53
55
  const translations = await getTranslations(params.locale);
56
+
54
57
  componentProps.translations = translations;
55
58
 
56
59
  const locale = settings.localization.locales.find(
package/hooks/index.ts CHANGED
@@ -8,4 +8,5 @@ export * from './use-media-query';
8
8
  export * from './use-on-click-outside';
9
9
  export * from './use-mobile-iframe-handler';
10
10
  export * from './use-payment-options';
11
- export * from './use-pagination';
11
+ export * from './use-pagination';
12
+ export * from './use-message-listener';
@@ -0,0 +1,24 @@
1
+ import { useEffect } from 'react';
2
+
3
+ export const useMessageListener = () => {
4
+ useEffect(() => {
5
+ const handleMessage = (event: MessageEvent) => {
6
+ if (event.origin !== window.location.origin) {
7
+ return;
8
+ }
9
+
10
+ if (event.data && typeof event.data === 'string') {
11
+ const messageData = JSON.parse(event.data);
12
+ if (messageData?.url) {
13
+ window.location.href = messageData.url;
14
+ }
15
+ }
16
+ };
17
+
18
+ window.addEventListener('message', handleMessage);
19
+
20
+ return () => {
21
+ window.removeEventListener('message', handleMessage);
22
+ };
23
+ }, []);
24
+ };
@@ -116,7 +116,7 @@ export default function usePagination(
116
116
  urlSearchParams.set('page', (Number(state.page) - 1).toString());
117
117
  return `${pathname}?${urlSearchParams.toString()}`;
118
118
  }
119
- return '#';
119
+ return null;
120
120
  }, [state.page, pathname, urlSearchParams]);
121
121
 
122
122
  const next = useMemo(() => {
@@ -124,7 +124,7 @@ export default function usePagination(
124
124
  urlSearchParams.set('page', (Number(state.page) + 1).toString());
125
125
  return `${pathname}?${urlSearchParams.toString()}`;
126
126
  }
127
- return '#';
127
+ return null;
128
128
  }, [state.page, state.last, pathname, urlSearchParams]);
129
129
 
130
130
  return {
@@ -0,0 +1,33 @@
1
+ import { CacheHandler } from '@neshca/cache-handler';
2
+ import createLruHandler from '@neshca/cache-handler/local-lru';
3
+ import createRedisHandler from '@neshca/cache-handler/redis-stack';
4
+ import { createClient } from 'redis';
5
+
6
+ CacheHandler.onCreation(async () => {
7
+ const redisUrl = `redis://${process.env.CACHE_HOST}:${
8
+ process.env.CACHE_PORT
9
+ }/${process.env.CACHE_BUCKET ?? '0'}`;
10
+
11
+ const client = createClient({
12
+ url: redisUrl
13
+ });
14
+
15
+ client.on('error', (error) => {
16
+ console.error('Redis client error', { redisUrl, error });
17
+ });
18
+
19
+ await client.connect();
20
+
21
+ const redisHandler = await createRedisHandler({
22
+ client,
23
+ timeoutMs: 5000
24
+ });
25
+
26
+ const localHandler = createLruHandler();
27
+
28
+ return {
29
+ handlers: [redisHandler, localHandler]
30
+ };
31
+ });
32
+
33
+ export default CacheHandler;
package/lib/cache.ts CHANGED
@@ -3,7 +3,6 @@ import { RedisClientType } from 'redis';
3
3
  import Settings from 'settings';
4
4
  import { CacheOptions } from '../types';
5
5
  import logger from '../utils/log';
6
- import { ServerVariables } from '../utils/server-variables';
7
6
 
8
7
  const hashCacheKey = (object?: Record<string, string>) => {
9
8
  if (!object) {
@@ -59,10 +58,8 @@ export const CacheKey = {
59
58
  export class Cache {
60
59
  static PROXY_URL = `${process.env.NEXT_PUBLIC_URL}/api/cache`;
61
60
 
62
- static formatKey(key: string) {
63
- return encodeURIComponent(
64
- `${Settings.commerceUrl}_${ServerVariables.locale}_${key}`
65
- );
61
+ static formatKey(key: string, locale: string) {
62
+ return encodeURIComponent(`${Settings.commerceUrl}_${locale}_${key}`);
66
63
  }
67
64
 
68
65
  static clientPool: Pool<RedisClientType> = createPool(
@@ -155,9 +152,14 @@ export class Cache {
155
152
 
156
153
  static async wrap<T = any>(
157
154
  key: string,
155
+ locale: string,
158
156
  handler: () => Promise<T>,
159
157
  options?: CacheOptions
160
158
  ): Promise<T> {
159
+ if (Settings.usePrettyUrlRoute) {
160
+ return await handler();
161
+ }
162
+
161
163
  const requiredVariables = [
162
164
  process.env.CACHE_HOST,
163
165
  process.env.CACHE_PORT,
@@ -174,7 +176,7 @@ export class Cache {
174
176
  };
175
177
 
176
178
  const _options = Object.assign(defaultOptions, options);
177
- const formattedKey = Cache.formatKey(key);
179
+ const formattedKey = Cache.formatKey(key, locale);
178
180
 
179
181
  logger.debug('Cache wrap', { key, formattedKey, _options });
180
182
 
@@ -88,14 +88,44 @@ const withPzDefault =
88
88
  req.nextUrl.pathname.includes('/orders/hooks/') ||
89
89
  req.nextUrl.pathname.includes('/orders/checkout-with-token/')
90
90
  ) {
91
- return NextResponse.rewrite(
91
+ const segment = url.searchParams.get('segment');
92
+ const currency = url.searchParams.get('currency')?.toLowerCase();
93
+
94
+ const headers = {};
95
+
96
+ if (segment) {
97
+ headers['X-Segment-Id'] = segment;
98
+ }
99
+
100
+ if (currency) {
101
+ headers['x-currency'] = currency;
102
+ }
103
+
104
+ const response = NextResponse.rewrite(
92
105
  new URL(
93
106
  `${Settings.commerceUrl}${req.nextUrl.pathname.replace(
94
107
  urlLocaleMatcherRegex,
95
108
  ''
96
109
  )}`
97
- )
110
+ ),
111
+ {
112
+ headers
113
+ }
98
114
  );
115
+
116
+ if (segment) {
117
+ response.cookies.set('pz-segment', segment);
118
+ }
119
+
120
+ if (currency) {
121
+ response.cookies.set('pz-currency', currency, {
122
+ sameSite: 'none',
123
+ secure: true,
124
+ expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7) // 7 days
125
+ });
126
+ }
127
+
128
+ return response;
99
129
  }
100
130
 
101
131
  if (req.nextUrl.pathname.startsWith('/orders/redirection/')) {
@@ -238,6 +268,20 @@ const withPzDefault =
238
268
  );
239
269
  }
240
270
 
271
+ if (
272
+ Settings.usePrettyUrlRoute &&
273
+ url.searchParams.toString().length > 0 &&
274
+ !Object.entries(ROUTES).find(([, value]) =>
275
+ new RegExp(`^${value}/?$`).test(
276
+ pathnameWithoutLocale
277
+ )
278
+ )
279
+ ) {
280
+ url.pathname =
281
+ url.pathname +
282
+ `searchparams|${url.searchParams.toString()}`;
283
+ }
284
+
241
285
  Settings.rewrites.forEach((rewrite) => {
242
286
  url.pathname = url.pathname.replace(
243
287
  rewrite.source,
@@ -43,9 +43,14 @@ const resolvePrettyUrlHandler =
43
43
  return prettyUrlResult;
44
44
  };
45
45
 
46
- const resolvePrettyUrl = async (pathname: string, ip: string | null) => {
46
+ const resolvePrettyUrl = async (
47
+ pathname: string,
48
+ locale: string,
49
+ ip: string | null
50
+ ) => {
47
51
  return Cache.wrap(
48
52
  CacheKey.PrettyUrl(pathname),
53
+ locale,
49
54
  resolvePrettyUrlHandler(pathname, ip),
50
55
  {
51
56
  useProxy: true
@@ -56,6 +61,10 @@ const resolvePrettyUrl = async (pathname: string, ip: string | null) => {
56
61
  const withPrettyUrl =
57
62
  (middleware: NextMiddleware) =>
58
63
  async (req: PzNextRequest, event: NextFetchEvent) => {
64
+ if (Settings.usePrettyUrlRoute) {
65
+ return middleware(req, event);
66
+ }
67
+
59
68
  const url = req.nextUrl.clone();
60
69
  const matchedLanguagePrefix = url.pathname.match(
61
70
  urlLocaleMatcherRegex
@@ -84,6 +93,7 @@ const withPrettyUrl =
84
93
  )
85
94
  ? url.pathname
86
95
  : prettyUrlPathname,
96
+ matchedLanguagePrefix,
87
97
  ip
88
98
  );
89
99
 
@@ -11,6 +11,10 @@ import { ROUTES } from 'routes';
11
11
  const withUrlRedirection =
12
12
  (middleware: NextMiddleware) =>
13
13
  async (req: PzNextRequest, event: NextFetchEvent) => {
14
+ if (settings.usePrettyUrlRoute) {
15
+ return middleware(req, event);
16
+ }
17
+
14
18
  const url = req.nextUrl.clone();
15
19
  const ip = req.headers.get('x-forwarded-for') ?? '';
16
20
  const pathnameWithoutLocale = url.pathname.replace(
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.58.0",
4
+ "version": "1.59.0-rc.0",
5
5
  "private": false,
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -20,16 +20,17 @@
20
20
  "@opentelemetry/sdk-trace-node": "1.19.0",
21
21
  "@opentelemetry/semantic-conventions": "1.19.0",
22
22
  "@reduxjs/toolkit": "1.9.7",
23
+ "@neshca/cache-handler": "1.5.1",
23
24
  "cross-spawn": "7.0.3",
24
25
  "generic-pool": "3.9.0",
25
26
  "react-redux": "8.1.3",
26
27
  "react-string-replace": "1.1.1",
27
- "redis": "4.5.1",
28
+ "redis": "4.6.13",
28
29
  "semver": "7.6.2",
29
30
  "set-cookie-parser": "2.6.0"
30
31
  },
31
32
  "devDependencies": {
32
- "@akinon/eslint-plugin-projectzero": "1.58.0",
33
+ "@akinon/eslint-plugin-projectzero": "1.59.0-rc.0",
33
34
  "@types/react-redux": "7.1.30",
34
35
  "@types/set-cookie-parser": "2.4.7",
35
36
  "@typescript-eslint/eslint-plugin": "6.7.4",
package/plugins.d.ts CHANGED
@@ -21,4 +21,5 @@ declare module '@akinon/pz-otp' {
21
21
 
22
22
  declare module '@akinon/pz-otp/src/redux/reducer' {
23
23
  export const showPopup: any;
24
+ export const hidePopup: any;
24
25
  }
@@ -16,8 +16,10 @@ import {
16
16
  setPreOrder,
17
17
  setRetailStores,
18
18
  setShippingOptions,
19
+ setDataSourceShippingOptions,
19
20
  setShippingStepCompleted,
20
- setCreditPaymentOptions
21
+ setCreditPaymentOptions,
22
+ setAttributeBasedShippingOptions
21
23
  } from '../../redux/reducers/checkout';
22
24
  import { RootState, TypedDispatch } from 'redux/store';
23
25
  import { checkoutApi } from '../../data/client/checkout';
@@ -26,6 +28,7 @@ import { getCookie, setCookie } from '../../utils';
26
28
  import settings from 'settings';
27
29
  import { LocaleUrlStrategy } from '../../localization';
28
30
  import { showMobile3dIframe } from '../../utils/mobile-3d-iframe';
31
+ import { showRedirectionIframe } from '../../utils/redirection-iframe';
29
32
 
30
33
  interface CheckoutResult {
31
34
  payload: {
@@ -73,8 +76,10 @@ export const preOrderMiddleware: Middleware = ({
73
76
  deliveryOptions,
74
77
  addressList: addresses,
75
78
  shippingOptions,
79
+ dataSourceShippingOptions,
76
80
  paymentOptions,
77
- installmentOptions
81
+ installmentOptions,
82
+ attributeBasedShippingOptions
78
83
  } = getState().checkout;
79
84
  const { endpoints: apiEndpoints } = checkoutApi;
80
85
 
@@ -107,7 +112,8 @@ export const preOrderMiddleware: Middleware = ({
107
112
  if (
108
113
  (!preOrder.shipping_address || !preOrder.billing_address) &&
109
114
  addresses.length > 0 &&
110
- preOrder.delivery_option?.delivery_option_type === 'customer'
115
+ (!preOrder.delivery_option ||
116
+ preOrder.delivery_option.delivery_option_type === 'customer')
111
117
  ) {
112
118
  dispatch(
113
119
  apiEndpoints.setAddresses.initiate({
@@ -125,6 +131,40 @@ export const preOrderMiddleware: Middleware = ({
125
131
  dispatch(apiEndpoints.setShippingOption.initiate(shippingOptions[0].pk));
126
132
  }
127
133
 
134
+ if (
135
+ dataSourceShippingOptions.length > 0 &&
136
+ !preOrder.data_source_shipping_options
137
+ ) {
138
+ const selectedDataSourceShippingOptionsPks =
139
+ dataSourceShippingOptions.map(
140
+ (opt) => opt.data_source_shipping_options[0].pk
141
+ );
142
+
143
+ dispatch(
144
+ apiEndpoints.setDataSourceShippingOptions.initiate(
145
+ selectedDataSourceShippingOptionsPks
146
+ )
147
+ );
148
+ }
149
+
150
+ if (
151
+ Object.keys(attributeBasedShippingOptions).length > 0 &&
152
+ !preOrder.attribute_based_shipping_options
153
+ ) {
154
+ const initialSelectedOptions: Record<string, number> = Object.fromEntries(
155
+ Object.entries(attributeBasedShippingOptions).map(([key, options]) => [
156
+ key,
157
+ options[0].pk
158
+ ])
159
+ );
160
+
161
+ dispatch(
162
+ apiEndpoints.setAttributeBasedShippingOptions.initiate(
163
+ initialSelectedOptions
164
+ )
165
+ );
166
+ }
167
+
128
168
  if (!preOrder.payment_option && paymentOptions.length > 0) {
129
169
  dispatch(apiEndpoints.setPaymentOption.initiate(paymentOptions[0].pk));
130
170
  }
@@ -163,6 +203,7 @@ export const contextListMiddleware: Middleware = ({
163
203
  if (result?.payload?.context_list) {
164
204
  result.payload.context_list.forEach((context) => {
165
205
  const redirectUrl = context.page_context.redirect_url;
206
+ const isIframe = context.page_context.is_frame ?? false;
166
207
 
167
208
  if (redirectUrl) {
168
209
  const currentLocale = getCookie('pz-locale');
@@ -181,16 +222,19 @@ export const contextListMiddleware: Middleware = ({
181
222
  }
182
223
 
183
224
  const urlObj = new URL(url, window.location.origin);
184
- urlObj.searchParams.set('t', new Date().getTime().toString());
185
-
186
- if (
187
- (isMobileApp ||
188
- /iPad|iPhone|iPod|Android/i.test(navigator.userAgent)) &&
225
+ const isMobileDevice =
226
+ isMobileApp ||
227
+ /iPad|iPhone|iPod|Android/i.test(navigator.userAgent);
228
+ const isIframePaymentOptionExcluded =
189
229
  !settings.checkout?.iframeExcludedPaymentOptions?.includes(
190
230
  result.payload?.pre_order?.payment_option?.slug
191
- )
192
- ) {
231
+ );
232
+ urlObj.searchParams.set('t', new Date().getTime().toString());
233
+
234
+ if (isMobileDevice && !isIframePaymentOptionExcluded) {
193
235
  showMobile3dIframe(urlObj.toString());
236
+ } else if (isIframe && !isIframePaymentOptionExcluded) {
237
+ showRedirectionIframe(urlObj.toString());
194
238
  } else {
195
239
  window.location.href = urlObj.toString();
196
240
  }
@@ -220,12 +264,28 @@ export const contextListMiddleware: Middleware = ({
220
264
  dispatch(setShippingOptions(context.page_context.shipping_options));
221
265
  }
222
266
 
267
+ if (context.page_context.data_sources) {
268
+ dispatch(
269
+ setDataSourceShippingOptions(context.page_context.data_sources)
270
+ );
271
+ }
272
+
273
+ if (context.page_context.attribute_based_shipping_options) {
274
+ dispatch(
275
+ setAttributeBasedShippingOptions(
276
+ context.page_context.attribute_based_shipping_options
277
+ )
278
+ );
279
+ }
280
+
223
281
  if (context.page_context.payment_options) {
224
282
  dispatch(setPaymentOptions(context.page_context.payment_options));
225
283
  }
226
284
 
227
285
  if (context.page_context.credit_payment_options) {
228
- dispatch(setCreditPaymentOptions(context.page_context.credit_payment_options));
286
+ dispatch(
287
+ setCreditPaymentOptions(context.page_context.credit_payment_options)
288
+ );
229
289
  }
230
290
 
231
291
  if (context.page_context.payment_choices) {
@@ -14,7 +14,9 @@ import {
14
14
  CheckoutCreditPaymentOption,
15
15
  PreOrder,
16
16
  RetailStore,
17
- ShippingOption
17
+ ShippingOption,
18
+ DataSource,
19
+ AttributeBasedShippingOption
18
20
  } from '../../types';
19
21
 
20
22
  export interface CheckoutState {
@@ -36,6 +38,7 @@ export interface CheckoutState {
36
38
  addressList: Address[];
37
39
  deliveryOptions: DeliveryOption[];
38
40
  shippingOptions: ShippingOption[];
41
+ dataSourceShippingOptions: DataSource[];
39
42
  paymentOptions: PaymentOption[];
40
43
  creditPaymentOptions: CheckoutCreditPaymentOption[];
41
44
  selectedCreditPaymentPk: number;
@@ -46,6 +49,8 @@ export interface CheckoutState {
46
49
  selectedBankAccountPk: number;
47
50
  loyaltyBalance?: string;
48
51
  retailStores: RetailStore[];
52
+ attributeBasedShippingOptions: AttributeBasedShippingOption[];
53
+ selectedShippingOptions: Record<string, number>;
49
54
  }
50
55
 
51
56
  const initialState: CheckoutState = {
@@ -67,6 +72,7 @@ const initialState: CheckoutState = {
67
72
  addressList: [],
68
73
  deliveryOptions: [],
69
74
  shippingOptions: [],
75
+ dataSourceShippingOptions: [],
70
76
  paymentOptions: [],
71
77
  creditPaymentOptions: [],
72
78
  selectedCreditPaymentPk: null,
@@ -75,7 +81,9 @@ const initialState: CheckoutState = {
75
81
  installmentOptions: [],
76
82
  bankAccounts: [],
77
83
  selectedBankAccountPk: null,
78
- retailStores: []
84
+ retailStores: [],
85
+ attributeBasedShippingOptions: [],
86
+ selectedShippingOptions: {}
79
87
  };
80
88
 
81
89
  const checkoutSlice = createSlice({
@@ -121,6 +129,9 @@ const checkoutSlice = createSlice({
121
129
  setShippingOptions(state, { payload }) {
122
130
  state.shippingOptions = payload;
123
131
  },
132
+ setDataSourceShippingOptions(state, { payload }) {
133
+ state.dataSourceShippingOptions = payload;
134
+ },
124
135
  setPaymentOptions(state, { payload }) {
125
136
  state.paymentOptions = payload;
126
137
  },
@@ -150,6 +161,12 @@ const checkoutSlice = createSlice({
150
161
  },
151
162
  setRetailStores(state, { payload }) {
152
163
  state.retailStores = payload;
164
+ },
165
+ setAttributeBasedShippingOptions(state, { payload }) {
166
+ state.attributeBasedShippingOptions = payload;
167
+ },
168
+ setSelectedShippingOptions: (state, { payload }) => {
169
+ state.selectedShippingOptions = payload;
153
170
  }
154
171
  }
155
172
  });
@@ -168,6 +185,7 @@ export const {
168
185
  setAddressList,
169
186
  setDeliveryOptions,
170
187
  setShippingOptions,
188
+ setDataSourceShippingOptions,
171
189
  setPaymentOptions,
172
190
  setCreditPaymentOptions,
173
191
  setSelectedCreditPaymentPk,
@@ -177,7 +195,9 @@ export const {
177
195
  setBankAccounts,
178
196
  setSelectedBankAccountPk,
179
197
  setLoyaltyBalance,
180
- setRetailStores
198
+ setRetailStores,
199
+ setAttributeBasedShippingOptions,
200
+ setSelectedShippingOptions
181
201
  } = checkoutSlice.actions;
182
202
 
183
203
  export default checkoutSlice.reducer;