@akinon/next 2.0.0-beta.2 → 2.0.0-beta.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.
Files changed (189) hide show
  1. package/.eslintrc.js +12 -0
  2. package/CHANGELOG.md +377 -7
  3. package/__tests__/next-config.test.ts +83 -0
  4. package/__tests__/tsconfig.json +23 -0
  5. package/api/auth.ts +133 -44
  6. package/api/barcode-search.ts +59 -0
  7. package/api/cache.ts +41 -5
  8. package/api/client.ts +21 -4
  9. package/api/form.ts +85 -0
  10. package/api/image-proxy.ts +75 -0
  11. package/api/product-categories.ts +53 -0
  12. package/api/similar-product-list.ts +63 -0
  13. package/api/similar-products.ts +111 -0
  14. package/api/virtual-try-on.ts +382 -0
  15. package/assets/styles/index.scss +84 -0
  16. package/babel.config.js +6 -0
  17. package/bin/pz-generate-routes.js +115 -0
  18. package/bin/pz-prebuild.js +1 -0
  19. package/bin/pz-predev.js +1 -0
  20. package/bin/pz-run-tests.js +99 -0
  21. package/bin/run-prebuild-tests.js +46 -0
  22. package/components/accordion.tsx +20 -5
  23. package/components/button.tsx +51 -36
  24. package/components/client-root.tsx +138 -2
  25. package/components/file-input.tsx +65 -3
  26. package/components/index.ts +1 -0
  27. package/components/input.tsx +1 -1
  28. package/components/link.tsx +46 -16
  29. package/components/logger-popup.tsx +213 -0
  30. package/components/modal.tsx +32 -16
  31. package/components/plugin-module.tsx +62 -3
  32. package/components/price.tsx +2 -2
  33. package/components/select.tsx +1 -1
  34. package/components/selected-payment-option-view.tsx +21 -0
  35. package/components/theme-editor/blocks/accordion-block.tsx +136 -0
  36. package/components/theme-editor/blocks/block-renderer-registry.tsx +77 -0
  37. package/components/theme-editor/blocks/button-block.tsx +593 -0
  38. package/components/theme-editor/blocks/counter-block.tsx +348 -0
  39. package/components/theme-editor/blocks/divider-block.tsx +20 -0
  40. package/components/theme-editor/blocks/embed-block.tsx +208 -0
  41. package/components/theme-editor/blocks/group-block.tsx +116 -0
  42. package/components/theme-editor/blocks/hotspot-block.tsx +147 -0
  43. package/components/theme-editor/blocks/icon-block.tsx +230 -0
  44. package/components/theme-editor/blocks/image-block.tsx +137 -0
  45. package/components/theme-editor/blocks/image-gallery-block.tsx +269 -0
  46. package/components/theme-editor/blocks/input-block.tsx +123 -0
  47. package/components/theme-editor/blocks/link-block.tsx +216 -0
  48. package/components/theme-editor/blocks/lottie-block.tsx +325 -0
  49. package/components/theme-editor/blocks/map-block.tsx +89 -0
  50. package/components/theme-editor/blocks/slider-block.tsx +595 -0
  51. package/components/theme-editor/blocks/tab-block.tsx +10 -0
  52. package/components/theme-editor/blocks/text-block.tsx +52 -0
  53. package/components/theme-editor/blocks/video-block.tsx +122 -0
  54. package/components/theme-editor/components/action-toolbar.tsx +305 -0
  55. package/components/theme-editor/components/designer-overlay.tsx +74 -0
  56. package/components/theme-editor/components/with-designer-features.tsx +142 -0
  57. package/components/theme-editor/dynamic-font-loader.tsx +79 -0
  58. package/components/theme-editor/hooks/use-designer-features.tsx +100 -0
  59. package/components/theme-editor/hooks/use-external-designer.tsx +95 -0
  60. package/components/theme-editor/hooks/use-native-widget-data.ts +188 -0
  61. package/components/theme-editor/hooks/use-visibility-context.ts +27 -0
  62. package/components/theme-editor/placeholder-registry.ts +31 -0
  63. package/components/theme-editor/sections/before-after-section.tsx +245 -0
  64. package/components/theme-editor/sections/contact-form-section.tsx +563 -0
  65. package/components/theme-editor/sections/countdown-campaign-banner-section.tsx +433 -0
  66. package/components/theme-editor/sections/coupon-banner-section.tsx +710 -0
  67. package/components/theme-editor/sections/divider-section.tsx +62 -0
  68. package/components/theme-editor/sections/featured-product-spotlight-section.tsx +507 -0
  69. package/components/theme-editor/sections/find-in-store-section.tsx +1995 -0
  70. package/components/theme-editor/sections/hover-showcase-section.tsx +326 -0
  71. package/components/theme-editor/sections/image-hotspot-section.tsx +142 -0
  72. package/components/theme-editor/sections/installment-options-section.tsx +1065 -0
  73. package/components/theme-editor/sections/notification-banner-section.tsx +173 -0
  74. package/components/theme-editor/sections/order-tracking-lookup-section.tsx +1379 -0
  75. package/components/theme-editor/sections/posts-slider-section.tsx +472 -0
  76. package/components/theme-editor/sections/pre-order-launch-banner-section.tsx +663 -0
  77. package/components/theme-editor/sections/section-renderer-registry.tsx +89 -0
  78. package/components/theme-editor/sections/section-wrapper.tsx +135 -0
  79. package/components/theme-editor/sections/shipping-threshold-progress-section.tsx +586 -0
  80. package/components/theme-editor/sections/stats-counter-section.tsx +486 -0
  81. package/components/theme-editor/sections/tabs-section.tsx +578 -0
  82. package/components/theme-editor/theme-block.tsx +102 -0
  83. package/components/theme-editor/theme-placeholder-client.tsx +218 -0
  84. package/components/theme-editor/theme-placeholder-wrapper.tsx +732 -0
  85. package/components/theme-editor/theme-placeholder.tsx +288 -0
  86. package/components/theme-editor/theme-section.tsx +1224 -0
  87. package/components/theme-editor/theme-settings-context.tsx +13 -0
  88. package/components/theme-editor/utils/index.ts +792 -0
  89. package/components/theme-editor/utils/iterator-utils.ts +234 -0
  90. package/components/theme-editor/utils/publish-window.ts +86 -0
  91. package/components/theme-editor/utils/visibility-rules.ts +188 -0
  92. package/data/client/account.ts +17 -2
  93. package/data/client/api.ts +2 -0
  94. package/data/client/basket.ts +66 -5
  95. package/data/client/checkout.ts +391 -99
  96. package/data/client/misc.ts +38 -2
  97. package/data/client/product.ts +19 -2
  98. package/data/client/user.ts +16 -8
  99. package/data/server/category.ts +11 -9
  100. package/data/server/flatpage.ts +11 -4
  101. package/data/server/form.ts +15 -4
  102. package/data/server/landingpage.ts +11 -4
  103. package/data/server/list.ts +5 -4
  104. package/data/server/menu.ts +11 -3
  105. package/data/server/product.ts +111 -55
  106. package/data/server/seo.ts +14 -4
  107. package/data/server/special-page.ts +5 -4
  108. package/data/server/widget.ts +90 -5
  109. package/data/urls.ts +16 -5
  110. package/hocs/client/with-segment-defaults.tsx +2 -2
  111. package/hocs/server/with-segment-defaults.tsx +65 -20
  112. package/hooks/index.ts +4 -0
  113. package/hooks/use-localization.ts +24 -10
  114. package/hooks/use-logger-context.tsx +114 -0
  115. package/hooks/use-logger.ts +92 -0
  116. package/hooks/use-loyalty-availability.ts +21 -0
  117. package/hooks/use-payment-options.ts +2 -1
  118. package/hooks/use-pz-params.ts +37 -0
  119. package/hooks/use-router.ts +51 -14
  120. package/hooks/use-sentry-uncaught-errors.ts +24 -0
  121. package/instrumentation/index.ts +10 -1
  122. package/instrumentation/node.ts +2 -20
  123. package/jest.config.js +25 -0
  124. package/lib/cache-handler.mjs +534 -16
  125. package/lib/cache.ts +272 -37
  126. package/localization/index.ts +2 -1
  127. package/localization/provider.tsx +2 -5
  128. package/middlewares/bfcache-headers.ts +18 -0
  129. package/middlewares/checkout-provider.ts +1 -1
  130. package/middlewares/complete-gpay.ts +32 -26
  131. package/middlewares/complete-masterpass.ts +33 -26
  132. package/middlewares/complete-wallet.ts +182 -0
  133. package/middlewares/default.ts +360 -215
  134. package/middlewares/index.ts +10 -2
  135. package/middlewares/locale.ts +34 -11
  136. package/middlewares/masterpass-rest-callback.ts +230 -0
  137. package/middlewares/oauth-login.ts +200 -57
  138. package/middlewares/pretty-url.ts +21 -8
  139. package/middlewares/redirection-payment.ts +32 -26
  140. package/middlewares/saved-card-redirection.ts +33 -26
  141. package/middlewares/three-d-redirection.ts +32 -26
  142. package/middlewares/url-redirection.ts +11 -1
  143. package/middlewares/wallet-complete-redirection.ts +206 -0
  144. package/package.json +25 -10
  145. package/plugins.d.ts +19 -4
  146. package/plugins.js +10 -1
  147. package/redux/actions.ts +47 -0
  148. package/redux/middlewares/checkout.ts +63 -138
  149. package/redux/middlewares/index.ts +14 -10
  150. package/redux/middlewares/pre-order/address.ts +7 -2
  151. package/redux/middlewares/pre-order/attribute-based-shipping-option.ts +7 -1
  152. package/redux/middlewares/pre-order/data-source-shipping-option.ts +7 -1
  153. package/redux/middlewares/pre-order/delivery-option.ts +7 -1
  154. package/redux/middlewares/pre-order/index.ts +16 -10
  155. package/redux/middlewares/pre-order/installment-option.ts +8 -1
  156. package/redux/middlewares/pre-order/payment-option-reset.ts +37 -0
  157. package/redux/middlewares/pre-order/payment-option.ts +7 -1
  158. package/redux/middlewares/pre-order/pre-order-validation.ts +8 -3
  159. package/redux/middlewares/pre-order/redirection.ts +8 -2
  160. package/redux/middlewares/pre-order/set-pre-order.ts +6 -2
  161. package/redux/middlewares/pre-order/shipping-option.ts +7 -1
  162. package/redux/middlewares/pre-order/shipping-step.ts +5 -1
  163. package/redux/reducers/checkout.ts +23 -3
  164. package/redux/reducers/index.ts +11 -3
  165. package/redux/reducers/root.ts +7 -2
  166. package/redux/reducers/widget.ts +80 -0
  167. package/sentry/index.ts +69 -13
  168. package/tailwind/content.js +16 -0
  169. package/types/commerce/account.ts +5 -1
  170. package/types/commerce/checkout.ts +35 -1
  171. package/types/commerce/widget.ts +33 -0
  172. package/types/index.ts +101 -6
  173. package/types/next-auth.d.ts +2 -2
  174. package/types/widget.ts +80 -0
  175. package/utils/app-fetch.ts +7 -2
  176. package/utils/generate-commerce-search-params.ts +3 -2
  177. package/utils/get-checkout-path.ts +3 -0
  178. package/utils/get-root-hostname.ts +28 -0
  179. package/utils/index.ts +64 -10
  180. package/utils/localization.ts +4 -0
  181. package/utils/mobile-3d-iframe.ts +8 -2
  182. package/utils/override-middleware.ts +7 -12
  183. package/utils/pz-segments.ts +92 -0
  184. package/utils/redirect-ignore.ts +35 -0
  185. package/utils/redirect.ts +9 -3
  186. package/utils/redirection-iframe.ts +8 -2
  187. package/utils/widget-styles.ts +107 -0
  188. package/views/error-page.tsx +93 -0
  189. package/with-pz-config.js +13 -6
package/types/index.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { LocaleUrlStrategy } from '../localization';
2
2
  import { PzNextRequest } from '../middlewares';
3
+ import { NextFetchEvent } from 'next/server';
4
+ import { NextURL } from 'next/dist/server/web/next-url';
3
5
  import { Control, FieldError } from 'react-hook-form';
4
6
  import { ReactNode } from 'react';
5
7
  import { UsePaginationType } from '../hooks/use-pagination';
@@ -203,17 +205,33 @@ export interface Settings {
203
205
  useOptimizedTranslations?: boolean;
204
206
  plugins?: Record<string, Record<string, any>>;
205
207
  includedProxyHeaders?: string[];
208
+ commerceRedirectionIgnoreList?: string[];
206
209
  /**
207
210
  * By default, the currency will be reset when the currency is changed.
208
211
  * If you want to keep the basket when the currency is changed, you can set this option to `false`.
209
212
  */
210
213
  resetBasketOnCurrencyChange?: boolean;
214
+ frontendIds?: Record<string, number>;
215
+ usePzSegment?: boolean;
216
+ pzSegments?: {
217
+ separator?: string;
218
+ segments: PzSegmentDefinition[];
219
+ };
211
220
  }
212
221
 
213
222
  export interface CacheOptions {
214
223
  cache?: boolean;
215
224
  expire?: number;
216
225
  useProxy?: boolean;
226
+ compressed?: boolean;
227
+ }
228
+
229
+ export interface SetCookieOptions {
230
+ expires?: number; // days
231
+ path?: string;
232
+ domain?: string;
233
+ secure?: boolean;
234
+ sameSite?: 'strict' | 'lax' | 'none';
217
235
  }
218
236
 
219
237
  export interface ClientRequestOptions {
@@ -238,22 +256,69 @@ export type ImageOptions = CDNOptions & {
238
256
 
239
257
  export type Translations = { [key: string]: object };
240
258
 
259
+ export interface PzSegmentResolveContext {
260
+ req: PzNextRequest;
261
+ event: NextFetchEvent;
262
+ url: NextURL;
263
+ locale: string;
264
+ currency: string;
265
+ pathname: string;
266
+ }
267
+
268
+ export interface PzSegmentDefinition {
269
+ name: string;
270
+ resolve?: (context: PzSegmentResolveContext) => string;
271
+ }
272
+
273
+ export interface PzSegmentsConfig {
274
+ separator: string;
275
+ segments: PzSegmentDefinition[];
276
+ }
277
+
278
+ // Next.js 16 async page/layout props (for exported page components and generateMetadata)
241
279
  export interface PageProps<T = any> {
242
- params: Promise<T & { locale: string; currency: string }>;
243
- searchParams: Promise<URLSearchParams>;
280
+ params: Promise<T & {
281
+ pz?: string;
282
+ commerce?: string;
283
+ locale?: string;
284
+ currency?: string;
285
+ url?: string;
286
+ [key: string]: any;
287
+ }>;
288
+ searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
244
289
  }
245
290
 
246
291
  export interface LayoutProps<T = any> extends PageProps<T> {
247
292
  children: React.ReactNode;
248
293
  }
249
294
 
250
- export interface RootLayoutProps<
295
+ export interface RootLayoutProps<T = any> extends LayoutProps<T> {
296
+ translations: Translations;
297
+ locale: Locale & {
298
+ isoCode: string;
299
+ };
300
+ }
301
+
302
+ // Search params type compatible with both Next.js resolved searchParams and URLSearchParams
303
+ export type SearchParams = Record<string, string | string[] | undefined> | URLSearchParams;
304
+
305
+ // Resolved (sync) versions for inner components after withSegmentDefaults resolves props
306
+ export interface ResolvedPageProps<T = any> {
307
+ params: T & { locale: string; currency: string };
308
+ searchParams: SearchParams;
309
+ }
310
+
311
+ export interface ResolvedLayoutProps<T = any> extends ResolvedPageProps<T> {
312
+ children: React.ReactNode;
313
+ }
314
+
315
+ export interface ResolvedRootLayoutProps<
251
316
  T = {
252
317
  commerce: string;
253
318
  locale: string;
254
319
  currency: string;
255
320
  }
256
- > extends LayoutProps<T> {
321
+ > extends ResolvedLayoutProps<T> {
257
322
  translations: Translations;
258
323
  locale: Locale & {
259
324
  isoCode: string;
@@ -275,10 +340,19 @@ export interface IconProps extends React.ComponentPropsWithRef<'i'> {
275
340
 
276
341
  export interface ButtonProps
277
342
  extends React.ButtonHTMLAttributes<HTMLButtonElement> {
278
- appearance?: 'filled' | 'outlined' | 'ghost';
343
+ appearance?: 'filled' | 'outlined' | 'ghost' | 'link' | string;
344
+ size?: 'sm' | 'md' | 'lg' | 'xl';
345
+ href?: string;
346
+ target?: '_blank' | '_self' | '_parent' | '_top';
279
347
  }
280
348
 
281
- export type FileInputProps = React.HTMLProps<HTMLInputElement>;
349
+ export interface FileInputProps extends React.HTMLProps<HTMLInputElement> {
350
+ fileClassName?: string;
351
+ fileNameWrapperClassName?: string;
352
+ fileInputClassName?: string;
353
+ onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
354
+ buttonClassName?: string;
355
+ }
282
356
 
283
357
  export interface PriceProps {
284
358
  currencyCode?: string;
@@ -299,15 +373,19 @@ export interface InputProps extends React.HTMLProps<HTMLInputElement> {
299
373
 
300
374
  export interface AccordionProps {
301
375
  isCollapse?: boolean;
376
+ collapseClassName?: string;
302
377
  title?: string;
303
378
  subTitle?: string;
304
379
  icons?: string[];
305
380
  iconSize?: number;
306
381
  iconColor?: string;
307
382
  children?: ReactNode;
383
+ headerClassName?: string;
308
384
  className?: string;
309
385
  titleClassName?: string;
386
+ subTitleClassName?: string;
310
387
  dataTestId?: string;
388
+ contentClassName?: string;
311
389
  }
312
390
 
313
391
  export interface PluginModuleComponentProps {
@@ -332,3 +410,20 @@ export interface PaginationProps {
332
410
  direction?: 'next' | 'prev';
333
411
  isLoading?: boolean;
334
412
  }
413
+
414
+ export interface ModalProps {
415
+ portalId: string;
416
+ children?: React.ReactNode;
417
+ open?: boolean;
418
+ setOpen?: (open: boolean) => void;
419
+ title?: React.ReactNode;
420
+ showCloseButton?: React.ReactNode;
421
+ className?: string;
422
+ overlayClassName?: string;
423
+ headerWrapperClassName?: string;
424
+ titleClassName?: string;
425
+ closeButtonClassName?: string;
426
+ iconName?: string;
427
+ iconSize?: number;
428
+ iconClassName?: string;
429
+ }
@@ -1,8 +1,8 @@
1
- import NextAuth from 'next-auth';
1
+ import 'next-auth';
2
2
 
3
3
  declare module 'next-auth' {
4
4
  interface User {
5
- id?: number;
5
+ id?: string;
6
6
  pk: number;
7
7
  firstName: string;
8
8
  lastName: string;
@@ -0,0 +1,80 @@
1
+ export type WidgetStyle = {
2
+ [breakpoint: string]: Record<string, string>;
3
+ };
4
+
5
+ export enum WidgetDataSourceType {
6
+ COLLECTION = 'collection',
7
+ API = 'api',
8
+ STATIC = 'static'
9
+ }
10
+
11
+ export interface WidgetDataSource {
12
+ type: WidgetDataSourceType;
13
+ id: string;
14
+ widgetSlug?: string;
15
+ details: {
16
+ collection?: {
17
+ pk: number;
18
+ productLimit: number;
19
+ };
20
+ api?: {
21
+ url: string;
22
+ method: string;
23
+ headers: Record<string, string>;
24
+ body: Record<string, string>;
25
+ };
26
+ static?: {
27
+ name: string;
28
+ data: Record<string, any>;
29
+ };
30
+ };
31
+ }
32
+
33
+ export type ImageSrc = {
34
+ [key: string]: {
35
+ src: string;
36
+ dimensions: { width: number; height: number };
37
+ };
38
+ };
39
+
40
+ export type WidgetComponentData = {
41
+ id: string;
42
+ widgetSlug: string;
43
+ type: string;
44
+ tag: string;
45
+ style: WidgetStyle;
46
+ value: {
47
+ text?: string;
48
+ src?: string;
49
+ alt?: string;
50
+ title?: string;
51
+ url?: string;
52
+ type?: string;
53
+ width?: string;
54
+ height?: string;
55
+ autoplay?: boolean;
56
+ controls?: boolean;
57
+ muted?: boolean;
58
+ loop?: boolean;
59
+ [key: string]: any;
60
+ };
61
+ parentId: string | null;
62
+ iteratingNode: string;
63
+ iteratorValue: string;
64
+ iteratingValue: string;
65
+ selectedDataSourceId: string;
66
+ dataSources: WidgetDataSource[];
67
+ imageSrc?: ImageSrc;
68
+ href?: string;
69
+ target?: string;
70
+ slider?: {
71
+ items?: number;
72
+ slidesToSlide?: number;
73
+ autoPlay?: boolean;
74
+ autoPlaySpeed?: number;
75
+ infinite?: boolean;
76
+ showDots?: boolean;
77
+ arrows?: boolean;
78
+ itemsToShow?: number;
79
+ };
80
+ };
@@ -2,6 +2,7 @@ import Settings from 'settings';
2
2
  import logger from '../utils/log';
3
3
  import { headers, cookies } from 'next/headers';
4
4
  import { ServerVariables } from './server-variables';
5
+ import { notFound } from 'next/navigation';
5
6
 
6
7
  export enum FetchResponseType {
7
8
  JSON = 'json',
@@ -43,12 +44,12 @@ const appFetch = async <T>({
43
44
  const requestURL = `${decodeURIComponent(commerceUrl)}${url}`;
44
45
 
45
46
  init.headers = {
47
+ cookie: nextCookies.toString(),
46
48
  ...(init.headers ?? {}),
47
49
  ...(ServerVariables.globalHeaders ?? {}),
48
50
  'Accept-Language': currentLocale.apiValue,
49
51
  'x-currency': currency,
50
- 'x-forwarded-for': ip,
51
- cookie: nextCookies.toString()
52
+ 'x-forwarded-for': ip
52
53
  };
53
54
 
54
55
  init.next = {
@@ -75,6 +76,10 @@ const appFetch = async <T>({
75
76
  }
76
77
  }
77
78
 
79
+ if (status === 422) {
80
+ notFound();
81
+ }
82
+
78
83
  return response;
79
84
  };
80
85
 
@@ -1,13 +1,14 @@
1
+ import { SearchParams } from '../types';
1
2
  import logger from './log';
2
3
 
3
4
  export const generateCommerceSearchParams = (
4
- searchParams?: URLSearchParams
5
+ searchParams?: SearchParams
5
6
  ) => {
6
7
  if (!searchParams) {
7
8
  return null;
8
9
  }
9
10
 
10
- const urlSerchParams = new URLSearchParams(searchParams);
11
+ const urlSerchParams = new URLSearchParams(searchParams as any);
11
12
 
12
13
  Object.entries(searchParams).forEach(([key, value]) => {
13
14
  if (typeof value === 'object') {
@@ -0,0 +1,3 @@
1
+ export const getCheckoutPath = (isPostCheckout: boolean): string => {
2
+ return isPostCheckout ? '/orders/post-checkout/' : '/orders/checkout/';
3
+ };
@@ -0,0 +1,28 @@
1
+ import Settings from 'settings';
2
+
3
+ export default function getRootHostname(
4
+ url: string | undefined
5
+ ): string | null {
6
+ if (!url) return null;
7
+
8
+ try {
9
+ const urlObj = new URL(url);
10
+ const hostname = urlObj.hostname;
11
+ const parts = hostname.split('.');
12
+
13
+ const firstPart = parts[0];
14
+
15
+ const isLocale = Settings.localization.locales.some(
16
+ (locale) => locale.value === firstPart
17
+ );
18
+
19
+ if (isLocale) {
20
+ const hostnameAfterLocale = parts.slice(1).join('.');
21
+ return `.${hostnameAfterLocale}`;
22
+ }
23
+
24
+ return `.${hostname}`;
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
package/utils/index.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  import settings from 'settings';
2
2
  import { LocaleUrlStrategy } from '../localization';
3
- import { CDNOptions, ClientRequestOptions } from '../types';
3
+ import { CDNOptions, ClientRequestOptions, SetCookieOptions } from '../types';
4
+ import getRootHostname from './get-root-hostname';
4
5
 
5
6
  export * from './get-currency';
6
7
  export * from './menu-generator';
7
8
  export * from './generate-commerce-search-params';
8
9
  export * from './get-currency-label';
10
+ export * from './pz-segments';
11
+ export * from './get-checkout-path';
9
12
 
10
13
  export function getCookie(name: string) {
11
14
  if (typeof document === 'undefined') {
@@ -20,14 +23,40 @@ export function getCookie(name: string) {
20
23
  }
21
24
  }
22
25
 
23
- export function setCookie(name: string, val: string) {
24
- const date = new Date();
25
- const value = val;
26
+ export function setCookie(
27
+ name: string,
28
+ value: string,
29
+ options: SetCookieOptions = {}
30
+ ) {
31
+ const cookieParts = [`${name}=${value}`];
32
+
33
+ if (options.expires) {
34
+ const date = new Date();
35
+ date.setTime(date.getTime() + options.expires * 24 * 60 * 60 * 1000);
36
+ cookieParts.push(`expires=${date.toUTCString()}`);
37
+ }
38
+
39
+ cookieParts.push(`path=${options.path ?? '/'}`);
40
+
41
+ if (options.secure) {
42
+ cookieParts.push('secure');
43
+ }
44
+
45
+ if (options.sameSite) {
46
+ cookieParts.push(`sameSite=${options.sameSite}`);
47
+ }
26
48
 
27
- date.setTime(date.getTime() + 7 * 24 * 60 * 60 * 1000);
49
+ const domain =
50
+ options.domain ??
51
+ (settings.localization.localeUrlStrategy === LocaleUrlStrategy.Subdomain
52
+ ? getRootHostname(document.location.href)
53
+ : null);
28
54
 
29
- document.cookie =
30
- name + '=' + value + '; expires=' + date.toUTCString() + '; path=/';
55
+ if (domain) {
56
+ cookieParts.push(`domain=${domain}`);
57
+ }
58
+
59
+ document.cookie = cookieParts.join('; ');
31
60
  }
32
61
 
33
62
  export function removeCookie(name: string) {
@@ -155,8 +184,9 @@ export function buildCDNUrl(url: string, config?: CDNOptions) {
155
184
  export const urlLocaleMatcherRegex = new RegExp(
156
185
  `^/(${settings.localization.locales
157
186
  .filter((l) =>
158
- settings.localization.localeUrlStrategy !==
159
- LocaleUrlStrategy.ShowAllLocales
187
+ ![LocaleUrlStrategy.ShowAllLocales, LocaleUrlStrategy.Subdomain].includes(
188
+ settings.localization.localeUrlStrategy
189
+ )
160
190
  ? l.value !== settings.localization.defaultLocaleValue
161
191
  : l
162
192
  )
@@ -165,7 +195,14 @@ export const urlLocaleMatcherRegex = new RegExp(
165
195
  );
166
196
 
167
197
  export const getPosError = () => {
168
- const error = JSON.parse(getCookie('pz-pos-error') ?? '{}');
198
+ const cookieValue = getCookie('pz-pos-error');
199
+ let decoded: string;
200
+ try {
201
+ decoded = cookieValue ? decodeURIComponent(cookieValue) : '{}';
202
+ } catch {
203
+ decoded = cookieValue ?? '{}';
204
+ }
205
+ const error = JSON.parse(decoded);
169
206
 
170
207
  // delete 'pz-pos-error' cookie when refreshing or closing page
171
208
  window.addEventListener('beforeunload', () => {
@@ -175,6 +212,23 @@ export const getPosError = () => {
175
212
  return error;
176
213
  };
177
214
 
215
+ export const checkPaymentWillRedirect = (response: {
216
+ context_list?: Array<{
217
+ page_name: string;
218
+ page_context?: { context_data?: { redirect_url?: string } };
219
+ }>;
220
+ redirect_url?: string;
221
+ errors?: unknown;
222
+ }): boolean => {
223
+ if (!response) return false;
224
+
225
+ const hasThankYouPage = response.context_list?.some(
226
+ (c) => c.page_name === 'ThankYouPage'
227
+ );
228
+
229
+ return Boolean(hasThankYouPage || response.redirect_url);
230
+ };
231
+
178
232
  export const urlSchemes = [
179
233
  'http',
180
234
  'tel:',
@@ -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
  }
@@ -1,8 +1,14 @@
1
1
  const iframeURLChange = (iframe, callback) => {
2
2
  iframe.addEventListener('load', () => {
3
3
  setTimeout(() => {
4
- if (iframe?.contentWindow?.location) {
5
- callback(iframe.contentWindow.location);
4
+ try {
5
+ if (iframe?.contentWindow?.location) {
6
+ const iframeLocation = iframe.contentWindow.location;
7
+
8
+ callback(iframeLocation);
9
+ }
10
+ } catch (error) {
11
+ // Expected: browser blocks cross-origin iframe access for security
6
12
  }
7
13
  }, 0);
8
14
  });
@@ -10,22 +10,17 @@ export enum MiddlewareNames {
10
10
  DataSourceShippingOptionMiddleware = 'dataSourceShippingOptionMiddleware',
11
11
  AttributeBasedShippingOptionMiddleware = 'attributeBasedShippingOptionMiddleware',
12
12
  PaymentOptionMiddleware = 'paymentOptionMiddleware',
13
+ PaymentOptionResetMiddleware = 'paymentOptionResetMiddleware',
13
14
  InstallmentOptionMiddleware = 'installmentOptionMiddleware',
14
15
  ShippingStepMiddleware = 'shippingStepMiddleware'
15
16
  }
16
17
 
17
18
  export const overrideMiddleware = (
18
- middlewares: Middleware[],
19
- updates: { name: MiddlewareNames; middleware: Middleware }[]
20
- ): void => {
21
- const updatesMap = new Map(
22
- updates.map(({ name, middleware }) => [name, middleware])
23
- );
24
-
25
- middlewares.forEach((mw, index) => {
26
- const updatedMiddleware = updatesMap.get(mw.name as MiddlewareNames);
27
- if (updatedMiddleware) {
28
- middlewares[index] = updatedMiddleware;
29
- }
19
+ originalMiddlewares: Middleware[],
20
+ overrides: Record<string, Middleware>
21
+ ): Middleware[] => {
22
+ return originalMiddlewares.map((middleware) => {
23
+ const middlewareKey = middleware.name || middleware.toString();
24
+ return overrides[middlewareKey] || middleware;
30
25
  });
31
26
  };
@@ -0,0 +1,92 @@
1
+ import type { PzSegmentDefinition, PzSegmentsConfig } from '../types';
2
+
3
+ export function isLegacyMode(settings: any): boolean {
4
+ return !settings.usePzSegment;
5
+ }
6
+
7
+ const DEFAULT_SEPARATOR = '--';
8
+
9
+ const DEFAULT_SEGMENTS: PzSegmentDefinition[] = [
10
+ { name: 'locale' },
11
+ { name: 'currency' },
12
+ { name: 'url' }
13
+ ];
14
+
15
+ export function getPzSegmentsConfig(settings: any): PzSegmentsConfig {
16
+ if (settings.pzSegments) {
17
+ const customSegments = (settings.pzSegments.segments ?? []).filter(
18
+ (seg: PzSegmentDefinition) =>
19
+ !DEFAULT_SEGMENTS.some((d) => d.name === seg.name)
20
+ );
21
+
22
+ return {
23
+ separator: settings.pzSegments.separator ?? DEFAULT_SEPARATOR,
24
+ segments: [...DEFAULT_SEGMENTS, ...customSegments]
25
+ };
26
+ }
27
+
28
+ return {
29
+ separator: DEFAULT_SEPARATOR,
30
+ segments: DEFAULT_SEGMENTS
31
+ };
32
+ }
33
+
34
+ export function encodePzValue(
35
+ values: Record<string, string>,
36
+ config: PzSegmentsConfig
37
+ ): string {
38
+ return config.segments
39
+ .map((seg) => values[seg.name] ?? '')
40
+ .join(config.separator);
41
+ }
42
+
43
+ export function decodePzValue(
44
+ pzValue: string,
45
+ config: PzSegmentsConfig
46
+ ): Record<string, string> {
47
+ const parts = pzValue.split(config.separator);
48
+ const result: Record<string, string> = {};
49
+
50
+ config.segments.forEach((seg, index) => {
51
+ result[seg.name] = parts[index] ?? '';
52
+ });
53
+
54
+ return result;
55
+ }
56
+
57
+ export function getBuiltInSegments(
58
+ parsed: Record<string, string>,
59
+ settings: any
60
+ ): { locale: string; currency: string; url: string } {
61
+ const { defaultLocaleValue, defaultCurrencyCode } = settings.localization;
62
+
63
+ return {
64
+ locale: parsed.locale || defaultLocaleValue,
65
+ currency: parsed.currency || defaultCurrencyCode,
66
+ url: parsed.url ? decodeURIComponent(parsed.url) : ''
67
+ };
68
+ }
69
+
70
+ export function parsePzParams(
71
+ params: { pz?: string; locale?: string; currency?: string; url?: string },
72
+ settings: any
73
+ ): { locale: string; currency: string; url: string; [key: string]: string } {
74
+ if (isLegacyMode(settings)) {
75
+ return {
76
+ locale:
77
+ params.locale ?? settings.localization.defaultLocaleValue,
78
+ currency:
79
+ params.currency ?? settings.localization.defaultCurrencyCode,
80
+ url: params.url ? decodeURIComponent(params.url) : ''
81
+ };
82
+ }
83
+
84
+ const config = getPzSegmentsConfig(settings);
85
+ const parsed = decodePzValue(params.pz ?? '', config);
86
+ const builtIn = getBuiltInSegments(parsed, settings);
87
+
88
+ return {
89
+ ...parsed,
90
+ ...builtIn
91
+ };
92
+ }
@@ -0,0 +1,35 @@
1
+ import settings from 'settings';
2
+ import { getUrlPathWithLocale } from './localization';
3
+
4
+ type IgnorePath = string | RegExp;
5
+
6
+ const defaultIgnoreList: string[] = [];
7
+
8
+ const extraIgnores: IgnorePath[] = Array.isArray(
9
+ settings.commerceRedirectionIgnoreList
10
+ )
11
+ ? settings.commerceRedirectionIgnoreList.map((path) => {
12
+ if (path === '/users/reset') {
13
+ return /^\/users\/reset\/[^/]+\/[^/]+\/$/;
14
+ }
15
+ return path;
16
+ })
17
+ : [];
18
+
19
+ export function shouldIgnoreRedirect(
20
+ pathname: string,
21
+ locale: string
22
+ ): boolean {
23
+ if (!pathname) return false;
24
+
25
+ const rawIgnoreList: IgnorePath[] = [...defaultIgnoreList, ...extraIgnores];
26
+
27
+ return rawIgnoreList.some((ignorePath) => {
28
+ if (ignorePath instanceof RegExp) {
29
+ return ignorePath.test(pathname);
30
+ }
31
+
32
+ const localized = getUrlPathWithLocale(ignorePath, locale);
33
+ return localized === pathname;
34
+ });
35
+ }
package/utils/redirect.ts CHANGED
@@ -3,21 +3,27 @@ import Settings from 'settings';
3
3
  import { headers } from 'next/headers';
4
4
  import { ServerVariables } from '@akinon/next/utils/server-variables';
5
5
  import { getUrlPathWithLocale } from '@akinon/next/utils/localization';
6
+ import { urlLocaleMatcherRegex } from '@akinon/next/utils';
6
7
 
7
8
  export const redirect = async (path: string, type?: RedirectType) => {
8
9
  const nextHeaders = await headers();
9
10
  const pageUrl = new URL(
10
- nextHeaders.get('pz-url') ?? process.env.NEXT_PUBLIC_URL
11
+ nextHeaders.get('pz-url') ?? process.env.NEXT_PUBLIC_URL ?? ''
11
12
  );
12
13
 
13
14
  const currentLocale = Settings.localization.locales.find(
14
15
  (locale) => locale.value === ServerVariables.locale
15
16
  );
16
17
 
17
- const callbackUrl = pageUrl.pathname;
18
+ const searchParams = new URLSearchParams(pageUrl.search);
19
+
20
+ const callbackUrl =
21
+ pageUrl.pathname.replace(urlLocaleMatcherRegex, '') +
22
+ (searchParams.toString() ? `?${searchParams.toString()}` : '');
23
+
18
24
  const redirectUrlWithLocale = getUrlPathWithLocale(
19
25
  path,
20
- currentLocale.localePath ?? currentLocale.value
26
+ currentLocale?.value
21
27
  );
22
28
 
23
29
  const redirectUrl = `${redirectUrlWithLocale}?callbackUrl=${callbackUrl}`;