@akinon/next 1.92.0 → 1.93.0-rc.46

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 (54) hide show
  1. package/CHANGELOG.md +1166 -28
  2. package/__tests__/next-config.test.ts +1 -10
  3. package/__tests__/redirect.test.ts +758 -0
  4. package/api/image-proxy.ts +75 -0
  5. package/api/similar-product-list.ts +84 -0
  6. package/api/similar-products.ts +120 -0
  7. package/bin/pz-prebuild.js +0 -1
  8. package/components/accordion.tsx +20 -5
  9. package/components/file-input.tsx +65 -3
  10. package/components/input.tsx +2 -0
  11. package/components/link.tsx +16 -12
  12. package/components/modal.tsx +32 -16
  13. package/components/plugin-module.tsx +35 -3
  14. package/components/selected-payment-option-view.tsx +11 -0
  15. package/data/client/checkout.ts +25 -4
  16. package/data/server/basket.ts +72 -0
  17. package/data/server/category.ts +48 -28
  18. package/data/server/flatpage.ts +16 -12
  19. package/data/server/landingpage.ts +16 -12
  20. package/data/server/list.ts +23 -13
  21. package/data/server/product.ts +66 -39
  22. package/data/server/special-page.ts +16 -12
  23. package/data/urls.ts +7 -2
  24. package/hocs/server/with-segment-defaults.tsx +5 -2
  25. package/hooks/use-localization.ts +2 -3
  26. package/hooks/use-loyalty-availability.ts +21 -0
  27. package/instrumentation/node.ts +15 -13
  28. package/jest.config.js +7 -1
  29. package/lib/cache-handler.mjs +225 -16
  30. package/lib/cache.ts +2 -0
  31. package/middlewares/checkout-provider.ts +1 -1
  32. package/middlewares/complete-gpay.ts +6 -2
  33. package/middlewares/complete-masterpass.ts +7 -2
  34. package/middlewares/default.ts +232 -183
  35. package/middlewares/index.ts +3 -1
  36. package/middlewares/locale.ts +9 -1
  37. package/middlewares/redirection-payment.ts +6 -2
  38. package/middlewares/saved-card-redirection.ts +7 -2
  39. package/middlewares/three-d-redirection.ts +7 -2
  40. package/middlewares/url-redirection.ts +9 -15
  41. package/middlewares/wallet-complete-redirection.ts +203 -0
  42. package/package.json +3 -3
  43. package/plugins.d.ts +10 -0
  44. package/plugins.js +4 -1
  45. package/redux/middlewares/checkout.ts +15 -2
  46. package/redux/reducers/checkout.ts +9 -1
  47. package/sentry/index.ts +54 -17
  48. package/types/commerce/order.ts +1 -0
  49. package/types/index.ts +42 -1
  50. package/utils/app-fetch.ts +7 -2
  51. package/utils/index.ts +34 -10
  52. package/utils/redirect-ignore.ts +35 -0
  53. package/utils/redirect.ts +31 -6
  54. package/with-pz-config.js +1 -5
@@ -0,0 +1,75 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+
3
+ async function proxyImage(imageUrl: string) {
4
+ if (!imageUrl) {
5
+ return NextResponse.json(
6
+ { success: false, error: 'Missing url parameter' },
7
+ { status: 400 }
8
+ );
9
+ }
10
+
11
+ const imageResponse = await fetch(imageUrl);
12
+
13
+ if (!imageResponse.ok) {
14
+ return NextResponse.json(
15
+ { success: false, error: 'Failed to fetch image from URL' },
16
+ { status: 400 }
17
+ );
18
+ }
19
+
20
+ const imageBuffer = await imageResponse.arrayBuffer();
21
+ const base64Image = Buffer.from(imageBuffer).toString('base64');
22
+ const contentType = imageResponse.headers.get('content-type') || 'image/jpeg';
23
+
24
+ return { base64Image, contentType, imageBuffer };
25
+ }
26
+
27
+ export async function GET(request: NextRequest) {
28
+ try {
29
+ const { searchParams } = new URL(request.url);
30
+ const imageUrl = searchParams.get('url');
31
+
32
+ const result = await proxyImage(imageUrl!);
33
+ if (result instanceof NextResponse) {
34
+ return result;
35
+ }
36
+
37
+ return new NextResponse(result.imageBuffer, {
38
+ status: 200,
39
+ headers: {
40
+ 'Content-Type': result.contentType,
41
+ 'Access-Control-Allow-Origin': '*',
42
+ 'Cache-Control': 'public, max-age=3600'
43
+ }
44
+ });
45
+ } catch (error) {
46
+ console.error('Image proxy error:', error);
47
+ return NextResponse.json(
48
+ { success: false, error: 'Internal server error' },
49
+ { status: 500 }
50
+ );
51
+ }
52
+ }
53
+
54
+ export async function POST(request: NextRequest) {
55
+ try {
56
+ const body = await request.json();
57
+ const imageUrl = body.imageUrl;
58
+
59
+ const result = await proxyImage(imageUrl);
60
+ if (result instanceof NextResponse) {
61
+ return result;
62
+ }
63
+
64
+ return NextResponse.json({
65
+ success: true,
66
+ base64Image: `data:${result.contentType};base64,${result.base64Image}`
67
+ });
68
+ } catch (error) {
69
+ console.error('Image proxy POST error:', error);
70
+ return NextResponse.json(
71
+ { success: false, error: 'Internal server error' },
72
+ { status: 500 }
73
+ );
74
+ }
75
+ }
@@ -0,0 +1,84 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { cookies } from 'next/headers';
3
+ import Settings from 'settings';
4
+ import { ServerVariables } from '../utils/server-variables';
5
+
6
+ export async function GET(request: NextRequest) {
7
+ try {
8
+ const { searchParams } = new URL(request.url);
9
+ const dynamicFilter = request.headers.get('x-search-dynamic-filter');
10
+ const dynamicExclude = request.headers.get('x-search-dynamic-exclude');
11
+
12
+ const nextCookies = cookies();
13
+ const currency =
14
+ nextCookies.get('pz-currency')?.value || ServerVariables.currency;
15
+ const locale =
16
+ nextCookies.get('pz-locale')?.value || ServerVariables.locale;
17
+
18
+ if (!dynamicFilter && !dynamicExclude) {
19
+ return NextResponse.json(
20
+ {
21
+ error:
22
+ 'Missing x-search-dynamic-filter or x-search-dynamic-exclude header'
23
+ },
24
+ { status: 400 }
25
+ );
26
+ }
27
+
28
+ if (Settings.commerceUrl === 'default') {
29
+ return NextResponse.json(
30
+ { error: 'Commerce URL is not configured' },
31
+ { status: 500 }
32
+ );
33
+ }
34
+
35
+ const queryString = searchParams.toString();
36
+ const apiUrl = `${Settings.commerceUrl}/list${
37
+ queryString ? `?${queryString}` : ''
38
+ }`;
39
+
40
+ const headers: Record<string, string> = {
41
+ Accept: 'application/json',
42
+ 'Content-Type': 'application/json'
43
+ };
44
+
45
+ if (dynamicFilter) {
46
+ headers['x-search-dynamic-filter'] = dynamicFilter;
47
+ } else if (dynamicExclude) {
48
+ headers['x-search-dynamic-exclude'] = dynamicExclude;
49
+ }
50
+
51
+ if (currency) {
52
+ headers['x-currency'] = currency;
53
+ }
54
+
55
+ if (locale) {
56
+ const currentLocale = Settings.localization.locales.find(
57
+ (l) => l.value === locale
58
+ );
59
+ if (currentLocale) {
60
+ headers['Accept-Language'] = currentLocale.apiValue;
61
+ }
62
+ }
63
+
64
+ const response = await fetch(apiUrl, {
65
+ method: 'GET',
66
+ headers
67
+ });
68
+
69
+ if (!response.ok) {
70
+ return NextResponse.json(
71
+ { error: `API request failed with status: ${response.status}` },
72
+ { status: response.status }
73
+ );
74
+ }
75
+
76
+ const data = await response.json();
77
+ return NextResponse.json(data);
78
+ } catch (error) {
79
+ return NextResponse.json(
80
+ { error: (error as Error).message },
81
+ { status: 500 }
82
+ );
83
+ }
84
+ }
@@ -0,0 +1,120 @@
1
+ import { NextResponse } from 'next/server';
2
+ import Settings from 'settings';
3
+
4
+ const IMAGE_SEARCH_API_URL = Settings.commerceUrl + '/image-search/';
5
+
6
+ const errorResponse = (message: string, status: number) => {
7
+ return NextResponse.json({ error: message }, { status });
8
+ };
9
+
10
+ export async function GET(request: Request) {
11
+ const { searchParams } = new URL(request.url);
12
+ const limit = searchParams.get('limit') || '20';
13
+ const url = searchParams.get('url');
14
+ const excludedProductIds = searchParams.get('excluded_product_ids');
15
+ const text = searchParams.get('text');
16
+
17
+ if (!url) {
18
+ return errorResponse('URL parameter is required', 400);
19
+ }
20
+
21
+ if (Settings.commerceUrl === 'default') {
22
+ return errorResponse('Commerce URL is not configured', 500);
23
+ }
24
+
25
+ const apiParams = new URLSearchParams();
26
+ apiParams.append('limit', limit);
27
+ apiParams.append('url', url);
28
+
29
+ if (excludedProductIds) {
30
+ apiParams.append('excluded_product_ids', excludedProductIds);
31
+ }
32
+
33
+ if (text) {
34
+ apiParams.append('text', text);
35
+ }
36
+
37
+ const apiUrl = `${IMAGE_SEARCH_API_URL}?${apiParams.toString()}`;
38
+
39
+ try {
40
+ const response = await fetch(apiUrl, {
41
+ method: 'GET',
42
+ headers: {
43
+ Accept: 'application/json'
44
+ }
45
+ });
46
+
47
+ if (!response.ok) {
48
+ const errorText = await response.text();
49
+ return errorResponse(
50
+ errorText || `API request failed with status: ${response.status}`,
51
+ response.status
52
+ );
53
+ }
54
+
55
+ const responseText = await response.text();
56
+
57
+ return NextResponse.json(JSON.parse(responseText));
58
+ } catch (error) {
59
+ return errorResponse((error as Error).message, 500);
60
+ }
61
+ }
62
+
63
+ export async function POST(request: Request) {
64
+ const { searchParams } = new URL(request.url);
65
+ const limit = searchParams.get('limit') || '20';
66
+
67
+ if (Settings.commerceUrl === 'default') {
68
+ return errorResponse('Commerce URL is not configured', 500);
69
+ }
70
+
71
+ let requestBody;
72
+ try {
73
+ requestBody = await request.json();
74
+ } catch (error) {
75
+ return errorResponse('Invalid JSON in request body', 400);
76
+ }
77
+
78
+ if (!requestBody.image) {
79
+ return errorResponse('Image data is required in request body', 400);
80
+ }
81
+
82
+ const apiParams = new URLSearchParams();
83
+ apiParams.append('limit', limit);
84
+
85
+ const apiUrl = `${IMAGE_SEARCH_API_URL}?${apiParams.toString()}`;
86
+
87
+ const bodyData: any = { image: requestBody.image };
88
+
89
+ if (requestBody.excluded_product_ids) {
90
+ bodyData.excluded_product_ids = requestBody.excluded_product_ids;
91
+ }
92
+
93
+ if (requestBody.text) {
94
+ bodyData.text = requestBody.text;
95
+ }
96
+
97
+ try {
98
+ const response = await fetch(apiUrl, {
99
+ method: 'POST',
100
+ headers: {
101
+ 'Content-Type': 'application/json',
102
+ Accept: 'application/json'
103
+ },
104
+ body: JSON.stringify(bodyData)
105
+ });
106
+
107
+ if (!response.ok) {
108
+ const errorText = await response.text();
109
+ return errorResponse(
110
+ errorText || `API request failed with status: ${response.status}`,
111
+ response.status
112
+ );
113
+ }
114
+
115
+ const responseData = await response.json();
116
+ return NextResponse.json(responseData);
117
+ } catch (error) {
118
+ return errorResponse((error as Error).message, 500);
119
+ }
120
+ }
@@ -2,7 +2,6 @@
2
2
 
3
3
  const runScript = require('./run-script');
4
4
 
5
- runScript('pz-run-tests.js');
6
5
  runScript('pz-install-theme.js');
7
6
  runScript('pz-pre-check-dist.js');
8
7
  runScript('pz-generate-translations.js');
@@ -7,15 +7,19 @@ import { AccordionProps } from '../types';
7
7
 
8
8
  export const Accordion = ({
9
9
  isCollapse = false,
10
+ collapseClassName,
10
11
  title,
11
12
  subTitle,
12
13
  icons = ['chevron-up', 'chevron-down'],
13
14
  iconSize = 16,
14
15
  iconColor = 'fill-[#000000]',
15
16
  children,
17
+ headerClassName,
16
18
  className,
17
19
  titleClassName,
18
- dataTestId
20
+ subTitleClassName,
21
+ dataTestId,
22
+ contentClassName
19
23
  }: AccordionProps) => {
20
24
  const [collapse, setCollapse] = useState(isCollapse);
21
25
 
@@ -27,15 +31,22 @@ export const Accordion = ({
27
31
  )}
28
32
  >
29
33
  <div
30
- className="flex items-center justify-between cursor-pointer"
34
+ className={twMerge(
35
+ 'flex items-center justify-between cursor-pointer',
36
+ headerClassName
37
+ )}
31
38
  onClick={() => setCollapse(!collapse)}
32
39
  data-testid={dataTestId}
33
40
  >
34
- <div className="flex flex-col">
41
+ <div className={twMerge('flex flex-col', contentClassName)}>
35
42
  {title && (
36
43
  <h3 className={twMerge('text-sm', titleClassName)}>{title}</h3>
37
44
  )}
38
- {subTitle && <h4 className="text-xs text-gray-700">{subTitle}</h4>}
45
+ {subTitle && (
46
+ <h4 className={twMerge('text-xs text-gray-700', subTitleClassName)}>
47
+ {subTitle}
48
+ </h4>
49
+ )}
39
50
  </div>
40
51
 
41
52
  {icons && (
@@ -46,7 +57,11 @@ export const Accordion = ({
46
57
  />
47
58
  )}
48
59
  </div>
49
- {collapse && <div className="mt-3 text-sm">{children}</div>}
60
+ {collapse && (
61
+ <div className={twMerge('mt-3 text-sm', collapseClassName)}>
62
+ {children}
63
+ </div>
64
+ )}
50
65
  </div>
51
66
  );
52
67
  };
@@ -1,8 +1,70 @@
1
+ import { useState } from 'react';
1
2
  import { forwardRef } from 'react';
2
- import { FileInputProps } from '../types/index';
3
+ import { useLocalization } from '@akinon/next/hooks';
4
+ import { twMerge } from 'tailwind-merge';
5
+ import { FileInputProps } from '../types';
3
6
 
4
7
  export const FileInput = forwardRef<HTMLInputElement, FileInputProps>(
5
- function fileInput(props, ref) {
6
- return <input type="file" {...props} ref={ref} />;
8
+ function FileInput(
9
+ {
10
+ buttonClassName,
11
+ onChange,
12
+ fileClassName,
13
+ fileNameWrapperClassName,
14
+ fileInputClassName,
15
+ ...props
16
+ },
17
+ ref
18
+ ) {
19
+ const { t } = useLocalization();
20
+ const [fileNames, setFileNames] = useState<string[]>([]);
21
+
22
+ const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
23
+ const files = Array.from(event.target.files || []);
24
+ setFileNames(files.map((file) => file.name));
25
+
26
+ if (onChange) {
27
+ onChange(event);
28
+ }
29
+ };
30
+
31
+ return (
32
+ <div className="relative">
33
+ <input
34
+ type="file"
35
+ {...props}
36
+ ref={ref}
37
+ className={twMerge(
38
+ 'absolute inset-0 w-full h-full opacity-0 cursor-pointer',
39
+ fileInputClassName
40
+ )}
41
+ onChange={handleFileChange}
42
+ />
43
+ <button
44
+ type="button"
45
+ className={twMerge(
46
+ 'bg-primary text-white py-2 px-4 text-sm',
47
+ buttonClassName
48
+ )}
49
+ >
50
+ {t('common.file_input.select_file')}
51
+ </button>
52
+ <div
53
+ className={twMerge('mt-1 text-gray-500', fileNameWrapperClassName)}
54
+ >
55
+ {fileNames.length > 0 ? (
56
+ <ul className={twMerge('list-disc pl-4 text-xs', fileClassName)}>
57
+ {fileNames.map((name, index) => (
58
+ <li key={index}>{name}</li>
59
+ ))}
60
+ </ul>
61
+ ) : (
62
+ <span className={twMerge('text-xs', fileClassName)}>
63
+ {t('common.file_input.no_file')}
64
+ </span>
65
+ )}
66
+ </div>
67
+ </div>
68
+ );
7
69
  }
8
70
  );
@@ -1,6 +1,8 @@
1
1
  import clsx from 'clsx';
2
2
  import { forwardRef, FocusEvent, useState, Ref } from 'react';
3
3
  import { Controller } from 'react-hook-form';
4
+
5
+ // @ts-ignore
4
6
  import { PatternFormat, PatternFormatProps } from 'react-number-format';
5
7
  import { InputProps } from '../types';
6
8
  import { twMerge } from 'tailwind-merge';
@@ -10,7 +10,9 @@ type LinkProps = Omit<
10
10
  React.AnchorHTMLAttributes<HTMLAnchorElement>,
11
11
  keyof NextLinkProps
12
12
  > &
13
- NextLinkProps;
13
+ NextLinkProps & {
14
+ href: string;
15
+ };
14
16
 
15
17
  export const Link = ({ children, href, ...rest }: LinkProps) => {
16
18
  const { locale, defaultLocaleValue, localeUrlStrategy } = useLocalization();
@@ -26,19 +28,21 @@ export const Link = ({ children, href, ...rest }: LinkProps) => {
26
28
  return href;
27
29
  }
28
30
 
29
- const pathnameWithoutLocale = href.replace(urlLocaleMatcherRegex, '');
30
- const hrefWithLocale = `/${locale}${pathnameWithoutLocale}`;
31
-
32
- if (localeUrlStrategy === LocaleUrlStrategy.ShowAllLocales) {
33
- return hrefWithLocale;
34
- } else if (
35
- localeUrlStrategy === LocaleUrlStrategy.HideDefaultLocale &&
36
- locale !== defaultLocaleValue
37
- ) {
38
- return hrefWithLocale;
31
+ if (typeof href === 'string' && !href.startsWith('http')) {
32
+ const pathnameWithoutLocale = href.replace(urlLocaleMatcherRegex, '');
33
+ const hrefWithLocale = `/${locale}${pathnameWithoutLocale}`;
34
+
35
+ if (localeUrlStrategy === LocaleUrlStrategy.ShowAllLocales) {
36
+ return hrefWithLocale;
37
+ } else if (
38
+ localeUrlStrategy === LocaleUrlStrategy.HideDefaultLocale &&
39
+ locale !== defaultLocaleValue
40
+ ) {
41
+ return hrefWithLocale;
42
+ }
39
43
  }
40
44
 
41
- return href || '#';
45
+ return href;
42
46
  }, [href, defaultLocaleValue, locale, localeUrlStrategy]);
43
47
 
44
48
  return (
@@ -4,16 +4,7 @@ import { ReactPortal } from './react-portal';
4
4
  import { Icon } from './icon';
5
5
  import { twMerge } from 'tailwind-merge';
6
6
  import { useEffect } from 'react';
7
-
8
- export interface ModalProps {
9
- portalId: string;
10
- children?: React.ReactNode;
11
- open?: boolean;
12
- setOpen?: (open: boolean) => void;
13
- title?: React.ReactNode;
14
- showCloseButton?: React.ReactNode;
15
- className?: string;
16
- }
7
+ import { ModalProps } from '../types';
17
8
 
18
9
  export const Modal = (props: ModalProps) => {
19
10
  const {
@@ -23,7 +14,14 @@ export const Modal = (props: ModalProps) => {
23
14
  setOpen,
24
15
  title = '',
25
16
  showCloseButton = true,
26
- className
17
+ className,
18
+ overlayClassName,
19
+ headerWrapperClassName,
20
+ titleClassName,
21
+ closeButtonClassName,
22
+ iconName = 'close',
23
+ iconSize = 16,
24
+ iconClassName
27
25
  } = props;
28
26
 
29
27
  useEffect(() => {
@@ -38,7 +36,12 @@ export const Modal = (props: ModalProps) => {
38
36
 
39
37
  return (
40
38
  <ReactPortal wrapperId={portalId}>
41
- <div className="fixed top-0 left-0 w-screen h-screen bg-primary bg-opacity-60 z-50" />
39
+ <div
40
+ className={twMerge(
41
+ 'fixed top-0 left-0 w-screen h-screen bg-primary bg-opacity-60 z-50',
42
+ overlayClassName
43
+ )}
44
+ />
42
45
  <section
43
46
  className={twMerge(
44
47
  'fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-50 bg-white',
@@ -46,15 +49,28 @@ export const Modal = (props: ModalProps) => {
46
49
  )}
47
50
  >
48
51
  {(showCloseButton || title) && (
49
- <div className="flex px-6 py-4 border-b border-gray-400">
50
- {title && <h3 className="text-lg font-light">{title}</h3>}
52
+ <div
53
+ className={twMerge(
54
+ 'flex px-6 py-4 border-b border-gray-400',
55
+ headerWrapperClassName
56
+ )}
57
+ >
58
+ {title && (
59
+ <h3 className={twMerge('text-lg font-light', titleClassName)}>
60
+ {title}
61
+ </h3>
62
+ )}
51
63
  {showCloseButton && (
52
64
  <button
53
65
  type="button"
54
66
  onClick={() => setOpen(false)}
55
- className="ml-auto"
67
+ className={twMerge('ml-auto', closeButtonClassName)}
56
68
  >
57
- <Icon name="close" size={16} />
69
+ <Icon
70
+ name={iconName}
71
+ size={iconSize}
72
+ className={iconClassName}
73
+ />
58
74
  </button>
59
75
  )}
60
76
  </div>
@@ -20,7 +20,10 @@ enum Plugin {
20
20
  B2B = 'pz-b2b',
21
21
  Akifast = 'pz-akifast',
22
22
  MultiBasket = 'pz-multi-basket',
23
- SavedCard = 'pz-saved-card'
23
+ SavedCard = 'pz-saved-card',
24
+ Hepsipay = 'pz-hepsipay',
25
+ FlowPayment = 'pz-flow-payment',
26
+ SimilarProducts = 'pz-similar-products'
24
27
  }
25
28
 
26
29
  export enum Component {
@@ -45,7 +48,16 @@ export enum Component {
45
48
  AkifastQuickLoginButton = 'QuickLoginButton',
46
49
  AkifastCheckoutButton = 'CheckoutButton',
47
50
  MultiBasket = 'MultiBasket',
48
- SavedCard = 'SavedCardOption'
51
+ SavedCard = 'SavedCardOption',
52
+ Hepsipay = 'Hepsipay',
53
+ FlowPayment = 'FlowPayment',
54
+ SimilarProductsModal = 'SimilarProductsModal',
55
+ SimilarProductsFilterSidebar = 'SimilarProductsFilterSidebar',
56
+ SimilarProductsResultsGrid = 'SimilarProductsResultsGrid',
57
+ SimilarProductsPlugin = 'SimilarProductsPlugin',
58
+ ProductImageSearchFeature = 'ProductImageSearchFeature',
59
+ ImageSearchButton = 'ImageSearchButton',
60
+ HeaderImageSearchFeature = 'HeaderImageSearchFeature'
49
61
  }
50
62
 
51
63
  const PluginComponents = new Map([
@@ -78,7 +90,21 @@ const PluginComponents = new Map([
78
90
  [Component.AkifastQuickLoginButton, Component.AkifastCheckoutButton]
79
91
  ],
80
92
  [Plugin.MultiBasket, [Component.MultiBasket]],
81
- [Plugin.SavedCard, [Component.SavedCard]]
93
+ [Plugin.SavedCard, [Component.SavedCard]],
94
+ [Plugin.Hepsipay, [Component.Hepsipay]],
95
+ [Plugin.FlowPayment, [Component.FlowPayment]],
96
+ [
97
+ Plugin.SimilarProducts,
98
+ [
99
+ Component.SimilarProductsModal,
100
+ Component.SimilarProductsFilterSidebar,
101
+ Component.SimilarProductsResultsGrid,
102
+ Component.SimilarProductsPlugin,
103
+ Component.ProductImageSearchFeature,
104
+ Component.ImageSearchButton,
105
+ Component.HeaderImageSearchFeature
106
+ ]
107
+ ]
82
108
  ]);
83
109
 
84
110
  const getPlugin = (component: Component) => {
@@ -143,6 +169,12 @@ export default function PluginModule({
143
169
  promise = import(`${'@akinon/pz-multi-basket'}`);
144
170
  } else if (plugin === Plugin.SavedCard) {
145
171
  promise = import(`${'@akinon/pz-saved-card'}`);
172
+ } else if (plugin === Plugin.Hepsipay) {
173
+ promise = import(`${'@akinon/pz-hepsipay'}`);
174
+ } else if (plugin === Plugin.FlowPayment) {
175
+ promise = import(`${'@akinon/pz-flow-payment'}`);
176
+ } else if (plugin === Plugin.SimilarProducts) {
177
+ promise = import(`${'@akinon/pz-similar-products'}`);
146
178
  }
147
179
  } catch (error) {
148
180
  logger.error(error);
@@ -62,6 +62,17 @@ export default function SelectedPaymentOptionView({
62
62
  : fallbackView;
63
63
  }
64
64
 
65
+ if (
66
+ payment_option?.payment_type === 'wallet' &&
67
+ wallet_method === 'checkout_flow'
68
+ ) {
69
+ const mod = await import('@akinon/pz-flow-payment');
70
+
71
+ return typeof mod?.default === 'function'
72
+ ? mod.default
73
+ : fallbackView;
74
+ }
75
+
65
76
  const view = paymentTypeToView[payment_option?.payment_type] || null;
66
77
 
67
78
  if (view) {
@@ -35,8 +35,10 @@ import {
35
35
 
36
36
  interface CheckoutResponse {
37
37
  pre_order?: PreOrder;
38
- errors: {
39
- non_field_errors: string;
38
+ errors?: {
39
+ non_field_errors?: string;
40
+ sample_products?: string[];
41
+ [key: string]: string | string[] | undefined;
40
42
  };
41
43
  context_list?: CheckoutContext[];
42
44
  template_name?: string;
@@ -181,7 +183,10 @@ export const checkoutApi = api.injectEndpoints({
181
183
  endpoints: (build) => ({
182
184
  fetchCheckout: build.query<CheckoutResponse, void>({
183
185
  query: () => ({
184
- url: buildClientRequestUrl(checkout.fetchCheckout, {})
186
+ url: buildClientRequestUrl(
187
+ `${checkout.fetchCheckout}?page=IndexPage`,
188
+ {}
189
+ )
185
190
  }),
186
191
  providesTags: ['Checkout']
187
192
  }),
@@ -867,6 +872,21 @@ export const checkoutApi = api.injectEndpoints({
867
872
  method: 'POST',
868
873
  body
869
874
  })
875
+ }),
876
+ saveSampleProducts: build.mutation<CheckoutResponse, number[] | undefined>({
877
+ query: (products = []) => {
878
+ const formData = new FormData();
879
+
880
+ products.forEach((product) => {
881
+ formData.append('sample_products', String(product));
882
+ });
883
+
884
+ return {
885
+ url: buildClientRequestUrl(checkout.saveSampleProducts),
886
+ method: 'POST',
887
+ body: formData
888
+ };
889
+ }
870
890
  })
871
891
  }),
872
892
  overrideExisting: false
@@ -914,5 +934,6 @@ export const {
914
934
  useSetWalletCompletePageMutation,
915
935
  useSendSmsMutation,
916
936
  useVerifySmsMutation,
917
- useResetCheckoutStateQuery
937
+ useResetCheckoutStateQuery,
938
+ useSaveSampleProductsMutation
918
939
  } = checkoutApi;