@akinon/next 1.10.0 → 1.12.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @akinon/next
2
2
 
3
+ ## 1.12.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ZERO-2314: Add endpoints for B2B Basket
8
+ - ZERO-2340: Add LazyComponent
9
+ - ZERO-2366: Add email parameter to addStockAlert query
10
+ - ZERO-2361: Add BKM Express to plugin module system
11
+ - ZERO-2365: Add dependency control script
12
+ - ZERO-2341: Add ReactPortal component
13
+
14
+ ## 1.11.0
15
+
16
+ ### Minor Changes
17
+
18
+ - ZERO-2355: Add LoaderSpinner component
19
+ - ZERO-2305: Add endpoints for B2B Basket
20
+ - ZERO-2319: Show 3D & redirection payment errors
21
+ - ZERO-2353: Add Icon component
22
+ - ZERO-2357: Add Radio component
23
+ - ZERO-2307: Prevent multiple mutation calls
24
+ - ZERO-2240: Add endpoints and types for dynamic forms
25
+
3
26
  ## 1.10.0
4
27
 
5
28
  ### Minor Changes
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const semver = require('semver');
6
+
7
+ function checkDir() {
8
+ let currentDir = __dirname;
9
+
10
+ while (currentDir !== path.resolve(currentDir, '..')) {
11
+ if (
12
+ fs.existsSync(
13
+ path.join(currentDir, 'node_modules/@akinon/next/package.json')
14
+ )
15
+ ) {
16
+ return currentDir;
17
+ }
18
+ currentDir = path.resolve(currentDir, '..');
19
+ }
20
+
21
+ return path.resolve(__dirname, '../../../../');
22
+ }
23
+
24
+ const BASE_DIR = checkDir();
25
+
26
+ try {
27
+ const akinonNextPackagePath = fs.existsSync(
28
+ path.join(BASE_DIR, 'packages/akinon-next/package.json')
29
+ )
30
+ ? path.join(BASE_DIR, 'packages/akinon-next/package.json')
31
+ : path.join(BASE_DIR, 'node_modules/@akinon/next/package.json');
32
+
33
+ let akinonNextPackage = JSON.parse(
34
+ fs.readFileSync(akinonNextPackagePath, 'utf8')
35
+ );
36
+
37
+ const projectZeroPwaPackagePath = path.join(BASE_DIR, 'package.json');
38
+
39
+ const projectZeroPwaPackage = JSON.parse(
40
+ fs.readFileSync(projectZeroPwaPackagePath, 'utf8')
41
+ );
42
+
43
+ const { peerDependencies } = akinonNextPackage;
44
+
45
+ let hasErrors = false;
46
+
47
+ let errorMessages = [];
48
+
49
+ for (const dependency in peerDependencies) {
50
+ const requiredVersion = peerDependencies[dependency];
51
+
52
+ const installedVersion =
53
+ projectZeroPwaPackage.dependencies[dependency] ||
54
+ projectZeroPwaPackage.devDependencies[dependency];
55
+
56
+ if (!installedVersion) {
57
+ errorMessages.push(
58
+ `Dependency ${dependency} is missing in projectzeropwa.`
59
+ );
60
+ hasErrors = true;
61
+ } else {
62
+ const requiredSemver = semver.coerce(requiredVersion);
63
+
64
+ const installedSemver = semver.coerce(installedVersion);
65
+
66
+ if (!requiredSemver || !installedSemver) {
67
+ errorMessages.push(
68
+ `Invalid semver for ${dependency}: required ${requiredVersion}, found ${installedVersion}`
69
+ );
70
+ hasErrors = true;
71
+ } else if (
72
+ requiredSemver.major !== installedSemver.major ||
73
+ requiredSemver.minor !== installedSemver.minor
74
+ ) {
75
+ errorMessages.push(
76
+ `Version mismatch for ${dependency}: expected ${requiredVersion} or higher but found ${installedVersion}`
77
+ );
78
+ hasErrors = true;
79
+ }
80
+ }
81
+ }
82
+
83
+ if (hasErrors) {
84
+ console.error(
85
+ '\x1b[31mDependency errors found:\n' +
86
+ errorMessages.join('\n') +
87
+ '\x1b[0m'
88
+ );
89
+ process.exit(1);
90
+ }
91
+
92
+ console.log(
93
+ 'All dependencies are installed and compatible in projectzeropwa.'
94
+ );
95
+ } catch (error) {
96
+ console.error('\x1b[31mError:', error.message + '\x1b[0m');
97
+ process.exit(1);
98
+ }
@@ -15,7 +15,7 @@ try {
15
15
  let installCmd = [];
16
16
 
17
17
  availablePlugins
18
- .filter((p) => plugins.includes(p))
18
+ .filter((p) => plugins?.includes(p))
19
19
  .forEach((name) => {
20
20
  installCmd.push(`git+https://bitbucket.org/akinonteam/${name}`);
21
21
  });
@@ -2,15 +2,31 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const spawn = require('cross-spawn');
6
+
7
+ function findBaseDir() {
8
+ const insideNodeModules = __dirname.includes('node_modules');
9
+
10
+ if (insideNodeModules) {
11
+ return path.resolve(__dirname, '../../../../');
12
+ } else {
13
+ return path.resolve(__dirname, '../../../apps/projectzeropwa');
14
+ }
15
+ }
16
+
17
+ const BASE_DIR = findBaseDir();
5
18
 
6
- const BASE_DIR =
7
- path.resolve(__dirname, '../../../apps/projectzeropwa') || process.cwd();
8
19
  const getFullPath = (relativePath) => path.join(BASE_DIR, relativePath);
9
20
 
10
- const theme = require(getFullPath('src/theme.js'));
21
+ let theme;
22
+ try {
23
+ theme = require(getFullPath('src/theme.js'));
24
+ } catch (error) {
25
+ console.error('Error loading theme.js:', error.message);
26
+ process.exit(1);
27
+ }
11
28
 
12
29
  try {
13
- const spawn = require('cross-spawn');
14
30
  const tsConfigPath = getFullPath('tsconfig.json');
15
31
 
16
32
  if (!fs.existsSync(tsConfigPath)) {
@@ -31,11 +47,12 @@ try {
31
47
 
32
48
  fs.writeFileSync(tsConfigPath, newContent);
33
49
 
34
- if (fs.existsSync(getFullPath('../../turbo.json'))) {
50
+ if (fs.existsSync(getFullPath('turbo.json'))) {
35
51
  spawn.sync('turbo', ['clean'], {
36
52
  stdio: 'inherit'
37
53
  });
38
54
  }
39
55
  } catch (error) {
40
- console.error('Error:', error.message);
56
+ console.error('Error modifying tsconfig.json:', error.message);
57
+ process.exit(1);
41
58
  }
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const spawn = require('cross-spawn');
3
+ const runScript = require('./run-script');
4
4
 
5
- spawn.sync('pz-install-plugins', [], { stdio: 'inherit' });
5
+ runScript('pz-check-dependencies.js');
6
+ runScript('pz-install-plugins.js');
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const spawn = require('cross-spawn');
4
-
5
- spawn.sync('pz-install-theme', [], { stdio: 'inherit' });
3
+ const runScript = require('./run-script');
4
+ runScript('pz-install-theme.js');
package/bin/pz-predev.js CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const spawn = require('cross-spawn');
4
-
5
- spawn.sync('pz-install-theme', [], { stdio: 'inherit' });
3
+ const runScript = require('./run-script');
4
+ runScript('pz-install-theme.js');
@@ -0,0 +1,44 @@
1
+ const spawn = require('cross-spawn');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ function runScript(scriptName) {
6
+ let currentPath;
7
+
8
+ const monorepoPath = path.join(
9
+ process.cwd(),
10
+ '../../packages/akinon-next/bin',
11
+ scriptName
12
+ );
13
+
14
+ const standardRepoPath = path.join(
15
+ process.cwd(),
16
+ 'node_modules/@akinon/next/bin',
17
+ scriptName
18
+ );
19
+
20
+ if (fs.existsSync(monorepoPath)) {
21
+ currentPath = monorepoPath;
22
+ } else if (fs.existsSync(standardRepoPath)) {
23
+ currentPath = standardRepoPath;
24
+ } else {
25
+ console.error(`Unable to find the ${scriptName} file`);
26
+ return;
27
+ }
28
+
29
+ const result = spawn.sync('node', [currentPath], { stdio: 'inherit' });
30
+
31
+ if (result.error) {
32
+ console.error(`Error executing ${scriptName}:`, result.error);
33
+ process.exit(1);
34
+ }
35
+
36
+ if (result.status !== 0) {
37
+ console.error(
38
+ `Script ${scriptName} failed with exit code ${result.status}`
39
+ );
40
+ process.exit(result.status);
41
+ }
42
+ }
43
+
44
+ module.exports = runScript;
@@ -0,0 +1,18 @@
1
+ import { IconProps } from '../types/index';
2
+ import clsx from 'clsx';
3
+
4
+ export const Icon = (props: IconProps) => {
5
+ const { name, size, className, ...rest } = props;
6
+
7
+ return (
8
+ <i
9
+ className={clsx(`flex pz-icon-${name}`, className)}
10
+ {...rest}
11
+ style={
12
+ size && {
13
+ fontSize: `${size}px`
14
+ }
15
+ }
16
+ />
17
+ );
18
+ };
@@ -0,0 +1 @@
1
+ export * from './react-portal';
@@ -0,0 +1,33 @@
1
+ 'use client';
2
+
3
+ import clsx from 'clsx';
4
+ import { useEffect, useState } from 'react';
5
+ import { useInView } from 'react-intersection-observer';
6
+
7
+ interface LazyComponentProps {
8
+ children: React.ReactNode;
9
+ className?: string;
10
+ }
11
+
12
+ export default function LazyComponent({
13
+ children,
14
+ className
15
+ }: LazyComponentProps) {
16
+ const [isInView, setIsInView] = useState(false);
17
+
18
+ const { ref, inView } = useInView({
19
+ threshold: 0
20
+ });
21
+
22
+ useEffect(() => {
23
+ if (inView) {
24
+ setIsInView(true);
25
+ }
26
+ }, [inView]);
27
+
28
+ return (
29
+ <div ref={ref} className={clsx(className)}>
30
+ {isInView ? <>{children}</> : null}
31
+ </div>
32
+ );
33
+ }
@@ -0,0 +1,23 @@
1
+ import { twMerge } from 'tailwind-merge';
2
+
3
+ type LoaderSpinnerProps = {
4
+ className?: string;
5
+ borderType?: 'solid' | 'dotted' | 'dashed';
6
+ };
7
+
8
+ export const LoaderSpinner: React.FC<LoaderSpinnerProps> = ({
9
+ borderType = 'solid',
10
+ className
11
+ }) => {
12
+ return (
13
+ <div className="w-full h-full flex justify-center items-center">
14
+ <div
15
+ className={twMerge(
16
+ 'w-8 h-8 border-2 border-primary border-t-transparent rounded-full animate-spin',
17
+ `border-${borderType}`,
18
+ className
19
+ )}
20
+ />
21
+ </div>
22
+ );
23
+ };
@@ -10,7 +10,8 @@ enum Plugin {
10
10
  PayOnDelivery = 'pz-pay-on-delivery',
11
11
  CheckoutGiftPack = 'pz-checkout-gift-pack',
12
12
  GPay = 'pz-gpay',
13
- Otp = 'pz-otp'
13
+ Otp = 'pz-otp',
14
+ BKMExpress = 'pz-bkm'
14
15
  }
15
16
 
16
17
  export enum Component {
@@ -20,7 +21,8 @@ export enum Component {
20
21
  PayOnDelivery = 'PayOnDelivery',
21
22
  CheckoutGiftPack = 'CheckoutGiftPack',
22
23
  GPay = 'GPayOption',
23
- Otp = 'Otp'
24
+ Otp = 'Otp',
25
+ BKMExpress = 'BKMOption'
24
26
  }
25
27
 
26
28
  const PluginComponents = new Map([
@@ -30,7 +32,8 @@ const PluginComponents = new Map([
30
32
  [Plugin.PayOnDelivery, [Component.PayOnDelivery]],
31
33
  [Plugin.CheckoutGiftPack, [Component.CheckoutGiftPack]],
32
34
  [Plugin.GPay, [Component.GPay]],
33
- [Plugin.Otp, [Component.Otp]]
35
+ [Plugin.Otp, [Component.Otp]],
36
+ [Plugin.BKMExpress, [Component.BKMExpress]]
34
37
  ]);
35
38
 
36
39
  const getPlugin = (component: Component) => {
@@ -75,6 +78,8 @@ export default function PluginModule({
75
78
  promise = import(`${'pz-gpay'}`);
76
79
  } else if (plugin === Plugin.Otp) {
77
80
  promise = import(`${'pz-otp'}`);
81
+ } else if (plugin === Plugin.BKMExpress) {
82
+ promise = import(`${'pz-bkm'}`);
78
83
  }
79
84
  } catch (error) {
80
85
  logger.error(error);
@@ -0,0 +1,18 @@
1
+ import { forwardRef } from 'react';
2
+ import { RadioProps } from '../types/index';
3
+ import { twMerge } from 'tailwind-merge';
4
+
5
+ const Radio = forwardRef<HTMLInputElement, RadioProps>((props, ref) => {
6
+ const { children, ...rest } = props;
7
+
8
+ return (
9
+ <label className={twMerge('flex items-center text-xs', props.className)}>
10
+ <input type="radio" {...rest} ref={ref} className="w-4 h-4" />
11
+ {children && <span className="text-xs ml-2">{children}</span>}
12
+ </label>
13
+ );
14
+ });
15
+
16
+ Radio.displayName = 'Radio';
17
+
18
+ export { Radio };
@@ -0,0 +1,45 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+
4
+ function createWrapperAndAppendToBody(wrapperId: string) {
5
+ const wrapperElement = document.createElement('div');
6
+ wrapperElement.setAttribute('id', wrapperId);
7
+ document.body.appendChild(wrapperElement);
8
+ return wrapperElement;
9
+ }
10
+
11
+ type Props = {
12
+ children: React.ReactNode;
13
+ wrapperId: string;
14
+ };
15
+
16
+ export const ReactPortal: React.FC<Props> = ({
17
+ children,
18
+ wrapperId = 'react-portal-wrapper'
19
+ }) => {
20
+ const [wrapperElement, setWrapperElement] = useState<HTMLElement | null>(
21
+ null
22
+ );
23
+
24
+ useEffect(() => {
25
+ let element = document.getElementById(wrapperId) as HTMLElement;
26
+ let modalCreated = false;
27
+
28
+ if (!element) {
29
+ modalCreated = true;
30
+ element = createWrapperAndAppendToBody(wrapperId);
31
+ }
32
+ setWrapperElement(element);
33
+
34
+ return () => {
35
+ // delete the programatically created element if it was created
36
+ if (modalCreated && element.parentNode) {
37
+ element.parentNode.removeChild(element);
38
+ }
39
+ };
40
+ }, [wrapperId]);
41
+
42
+ if (wrapperElement === null) return null;
43
+
44
+ return createPortal(children, wrapperElement);
45
+ };
@@ -54,6 +54,9 @@ export default function SelectedPaymentOptionView() {
54
54
  // else if (payment_option.payment_type === 'gpay') {
55
55
  // promise = import(`views/checkout/steps/payment/options/gpay`);
56
56
  // }
57
+ // else if (payment_option.payment_type === 'bkm_express') {
58
+ // promise = import(`views/checkout/steps/payment/options/bkm`);
59
+ // }
57
60
  } catch (error) {}
58
61
 
59
62
  return promise
@@ -1,10 +1,42 @@
1
- import { createApi, fetchBaseQuery, retry } from '@reduxjs/toolkit/query/react';
1
+ import {
2
+ createApi,
3
+ fetchBaseQuery,
4
+ retry,
5
+ BaseQueryFn,
6
+ FetchBaseQueryError,
7
+ FetchBaseQueryMeta,
8
+ FetchArgs,
9
+ BaseQueryApi
10
+ } from '@reduxjs/toolkit/query/react';
2
11
  import settings from 'settings';
3
12
  import { getCookie } from '../../utils';
13
+ import { RootState } from '@theme/redux/store';
4
14
 
5
- export const api = createApi({
6
- reducerPath: 'api',
7
- baseQuery: retry(fetchBaseQuery({
15
+ interface CustomBaseQueryApi extends BaseQueryApi {
16
+ getState: () => RootState;
17
+ }
18
+
19
+ const customBaseQuery: BaseQueryFn<
20
+ string | FetchArgs,
21
+ unknown,
22
+ FetchBaseQueryError,
23
+ {},
24
+ FetchBaseQueryMeta
25
+ > = async (args, api: CustomBaseQueryApi, extraOptions) => {
26
+ const mutations = Object.entries(api.getState()?.api?.mutations ?? {}).map(
27
+ (x) => x[1]
28
+ );
29
+
30
+ if (
31
+ api.type === 'mutation' &&
32
+ mutations.filter(
33
+ (m) => m.status === 'pending' && m.endpointName === api.endpoint
34
+ ).length > 1
35
+ ) {
36
+ api.abort('Mutation already in progress.');
37
+ }
38
+
39
+ const baseQuery = fetchBaseQuery({
8
40
  prepareHeaders: async (headers) => {
9
41
  const csrfCookie = getCookie('csrftoken');
10
42
  const activeLocale = getCookie('pz-locale');
@@ -19,22 +51,32 @@ export const api = createApi({
19
51
  if (csrfCookie) {
20
52
  headers.set('x-csrftoken', `${csrfCookie}`);
21
53
  }
22
-
23
54
  return headers;
24
55
  },
25
56
  credentials: 'include'
26
- }), {
27
- maxRetries: 3
28
- }),
57
+ });
58
+ try {
59
+ const result = await baseQuery(args, api, extraOptions);
60
+ return result;
61
+ } catch (error) {
62
+ return { error };
63
+ }
64
+ };
65
+
66
+ export const api = createApi({
67
+ reducerPath: 'api',
68
+ baseQuery: retry(customBaseQuery, { maxRetries: 3 }),
29
69
  tagTypes: [
30
70
  'Basket',
71
+ 'BasketB2b',
72
+ 'DraftsB2b',
31
73
  'Product',
32
74
  'Checkout',
33
75
  'Favorite',
34
76
  'Addresses',
35
77
  'Profile'
36
78
  ],
37
- endpoints: () => ({}) // it must be a function to be able to use `injectEndpoints`
79
+ endpoints: () => ({})
38
80
  });
39
81
 
40
82
  export const {
@@ -1,17 +1,28 @@
1
1
  import { buildClientRequestUrl } from '../../utils';
2
2
  import { api } from './api';
3
3
  import { b2b } from '../urls';
4
- import { Basket, BasketParams, BasketResponse, Division, GetResponse } from '../../types';
4
+ import {
5
+ Basket,
6
+ BasketParams,
7
+ BasketResponse,
8
+ Division,
9
+ DraftResponse,
10
+ GetResponse,
11
+ LoadBasketParams,
12
+ SaveBasketParams,
13
+ UpdateProductParams,
14
+ DeleteProductParams,
15
+ CreateQuotationParams
16
+ } from '../../types';
5
17
 
6
18
  const b2bApi = api.injectEndpoints({
7
19
  endpoints: (build) => ({
8
- getBasket: build.query<Basket, void>({
20
+ getBasketB2b: build.query<BasketResponse, void>({
9
21
  query: () =>
10
22
  buildClientRequestUrl(b2b.basket, {
11
23
  contentType: 'application/json'
12
24
  }),
13
- transformResponse: (response: { basket: Basket }) => response.basket,
14
- providesTags: ['Basket']
25
+ providesTags: ['BasketB2b']
15
26
  }),
16
27
  getDivisions: build.query<GetResponse<Division>, void>({
17
28
  query: () => buildClientRequestUrl(b2b.divisions)
@@ -25,8 +36,71 @@ const b2bApi = api.injectEndpoints({
25
36
  body
26
37
  })
27
38
  }),
39
+ saveBasket: build.mutation<BasketResponse, SaveBasketParams>({
40
+ query: (body) => ({
41
+ url: buildClientRequestUrl(b2b.saveBasket, {
42
+ contentType: 'application/json'
43
+ }),
44
+ method: 'POST',
45
+ body
46
+ }),
47
+ invalidatesTags: ['BasketB2b', 'DraftsB2b']
48
+ }),
49
+ getDrafts: build.query<DraftResponse[], void>({
50
+ query: () => buildClientRequestUrl(b2b.draftBaskets),
51
+ providesTags: ['DraftsB2b']
52
+ }),
53
+ loadBasket: build.mutation<string, number>({
54
+ query: (id) => ({
55
+ url: buildClientRequestUrl(b2b.loadBasket(id), {
56
+ contentType: 'application/json'
57
+ }),
58
+ method: 'POST'
59
+ }),
60
+ invalidatesTags: ['BasketB2b']
61
+ }),
62
+ updateProduct: build.mutation<Basket, UpdateProductParams>({
63
+ query: (body) => ({
64
+ url: buildClientRequestUrl(b2b.basket, {
65
+ contentType: 'application/json'
66
+ }),
67
+ method: 'PUT',
68
+ body
69
+ }),
70
+ invalidatesTags: ['BasketB2b']
71
+ }),
72
+ deleteProduct: build.mutation<Basket, DeleteProductParams>({
73
+ query: (body) => ({
74
+ url: buildClientRequestUrl(b2b.basket, {
75
+ contentType: 'application/json'
76
+ }),
77
+ method: 'DELETE',
78
+ body
79
+ }),
80
+ invalidatesTags: ['BasketB2b']
81
+ }),
82
+ createQuotation: build.mutation<BasketResponse, CreateQuotationParams>({
83
+ query: (body) => ({
84
+ url: buildClientRequestUrl(b2b.myQuotations, {
85
+ contentType: 'application/json'
86
+ }),
87
+ method: 'POST',
88
+ body
89
+ }),
90
+ invalidatesTags: ['BasketB2b', 'DraftsB2b']
91
+ }),
28
92
  }),
29
93
  overrideExisting: true
30
94
  });
31
95
 
32
- export const { useGetBasketQuery, useLazyGetDivisionsQuery, useAddToBasketMutation } = b2bApi;
96
+ export const {
97
+ useGetBasketB2bQuery,
98
+ useLazyGetDivisionsQuery,
99
+ useAddToBasketMutation,
100
+ useSaveBasketMutation,
101
+ useGetDraftsQuery,
102
+ useLoadBasketMutation,
103
+ useUpdateProductMutation,
104
+ useDeleteProductMutation,
105
+ useCreateQuotationMutation
106
+ } = b2bApi;
@@ -1,7 +1,7 @@
1
1
  import { FavoriteItem } from '../../types';
2
2
  import { buildClientRequestUrl } from '../../utils';
3
3
  import { api } from './api';
4
- import { URLS, wishlist } from '../urls';
4
+ import { wishlist } from '../urls';
5
5
 
6
6
  export type AddProductRequest = {
7
7
  product: number;
@@ -28,6 +28,16 @@ interface RemoteFavoriteResponse {
28
28
  success: boolean;
29
29
  }
30
30
 
31
+ interface AddStockAlertRequest {
32
+ productPk: number;
33
+ email?: string;
34
+ }
35
+
36
+ interface AddStockAlertResponse {
37
+ pk: number;
38
+ product: number;
39
+ }
40
+
31
41
  export const wishlistApi = api.injectEndpoints({
32
42
  endpoints: (build) => ({
33
43
  getFavorites: build.query<GetFavoritesResponse, GetFavoritesParams>({
@@ -54,16 +64,15 @@ export const wishlistApi = api.injectEndpoints({
54
64
  }),
55
65
  invalidatesTags: ['Favorite']
56
66
  }),
57
- // TODO: Add stock alert response type
58
- addStockAlert: build.mutation<any, number>({
59
- query: (productPk: number) => ({
67
+ addStockAlert: build.mutation<AddStockAlertResponse, AddStockAlertRequest>({
68
+ query: ({ productPk, email }) => ({
60
69
  url: buildClientRequestUrl(wishlist.addStockAlert, {
61
- useFormData: true,
62
- contentType: 'application/json'
70
+ useFormData: true
63
71
  }),
64
72
  method: 'POST',
65
73
  body: {
66
- product: productPk
74
+ product: productPk,
75
+ ...(email ? { email } : {})
67
76
  }
68
77
  })
69
78
  })
@@ -0,0 +1,22 @@
1
+ import { Cache, CacheKey } from "../../lib/cache";
2
+ import { FormType } from "../../types/commerce/form";
3
+
4
+ import appFetch from "../../utils/app-fetch";
5
+ import { form } from "../urls";
6
+
7
+ const getFormDataHandler = (pk: number) => {
8
+ return async function () {
9
+ const data = await appFetch<FormType>(form.getForm(pk), {
10
+ headers: {
11
+ Accept: 'application/json',
12
+ 'Content-Type': 'application/json'
13
+ }
14
+ });
15
+
16
+ return data;
17
+ };
18
+ };
19
+
20
+ export const getFormData = ({ pk }: { pk: number }) => {
21
+ return Cache.wrap(CacheKey.Form(pk), getFormDataHandler(pk));
22
+ };
@@ -7,3 +7,4 @@ export * from './widget';
7
7
  export * from './seo';
8
8
  export * from './menu';
9
9
  export * from './landingpage';
10
+ export * from './form';
package/data/urls.ts CHANGED
@@ -169,6 +169,10 @@ export const widgets = {
169
169
  getWidget: (slug: string) => `/widgets/${slug}/`
170
170
  };
171
171
 
172
+ export const form = {
173
+ getForm: (pk: number) => `/forms/${pk}/generate/`,
174
+ };
175
+
172
176
  const URLS = {
173
177
  account,
174
178
  address,
@@ -180,7 +184,8 @@ const URLS = {
180
184
  product,
181
185
  wishlist,
182
186
  user,
183
- widgets
187
+ widgets,
188
+ form
184
189
  };
185
190
 
186
191
  const UrlProxyHandler = {
package/lib/cache.ts CHANGED
@@ -51,7 +51,8 @@ export const CacheKey = {
51
51
  Menu: (depth: number, parent?: string) =>
52
52
  `menu_${depth}${parent ? `_${parent}` : ''}`,
53
53
  Seo: (url: string) => `seo_${url}`,
54
- RootSeo: 'root_seo'
54
+ RootSeo: 'root_seo',
55
+ Form: (pk: number) => `form_${pk}`
55
56
  };
56
57
 
57
58
  export class Cache {
@@ -4,7 +4,6 @@ import Settings from 'settings';
4
4
  import logger from '../utils/log';
5
5
  import { PzNextRequest } from '.';
6
6
  import { getUrlPathWithLocale } from '../utils/localization';
7
- import { urlLocaleMatcherRegex } from '../utils';
8
7
 
9
8
  const streamToString = async (stream: ReadableStream<Uint8Array> | null) => {
10
9
  if (stream) {
@@ -33,10 +32,6 @@ const withRedirectionPayment =
33
32
  (middleware: NextMiddleware) =>
34
33
  async (req: PzNextRequest, event: NextFetchEvent) => {
35
34
  const url = req.nextUrl.clone();
36
- const pathnameWithoutLocale = url.pathname.replace(
37
- urlLocaleMatcherRegex,
38
- ''
39
- );
40
35
  const searchParams = new URLSearchParams(url.search);
41
36
  const ip = req.headers.get('x-forwarded-for') ?? '';
42
37
 
@@ -70,12 +65,34 @@ const withRedirectionPayment =
70
65
 
71
66
  const response = await request.json();
72
67
 
73
- const { context_list: contextList } = response;
68
+ const { context_list: contextList, errors } = response;
74
69
  const redirectionContext = contextList?.find(
75
70
  (context) => context.page_context?.redirect_url
76
71
  );
77
72
  const redirectUrl = redirectionContext?.page_context?.redirect_url;
78
73
 
74
+ if (errors && Object.keys(errors).length) {
75
+ logger.error('Error while completing redirection payment', {
76
+ middleware: 'redirection-payment',
77
+ errors,
78
+ requestHeaders,
79
+ ip
80
+ });
81
+
82
+ return NextResponse.redirect(
83
+ `${url.origin}${getUrlPathWithLocale(
84
+ '/orders/checkout/',
85
+ req.cookies.get('pz-locale')?.value
86
+ )}`,
87
+ {
88
+ status: 303,
89
+ headers: {
90
+ 'Set-Cookie': `pz-pos-error=${JSON.stringify(errors)}; path=/;`
91
+ }
92
+ }
93
+ );
94
+ }
95
+
79
96
  logger.info('Order success page context list', {
80
97
  middleware: 'redirection-payment',
81
98
  contextList,
@@ -64,12 +64,34 @@ const withThreeDRedirection =
64
64
 
65
65
  const response = await request.json();
66
66
 
67
- const { context_list: contextList } = response;
67
+ const { context_list: contextList, errors } = response;
68
68
  const redirectionContext = contextList?.find(
69
69
  (context) => context.page_context?.redirect_url
70
70
  );
71
71
  const redirectUrl = redirectionContext?.page_context?.redirect_url;
72
72
 
73
+ if (errors && Object.keys(errors).length) {
74
+ logger.error('Error while completing 3D payment', {
75
+ middleware: 'three-d-redirection',
76
+ errors,
77
+ requestHeaders,
78
+ ip
79
+ });
80
+
81
+ return NextResponse.redirect(
82
+ `${url.origin}${getUrlPathWithLocale(
83
+ '/orders/checkout/',
84
+ req.cookies.get('pz-locale')?.value
85
+ )}`,
86
+ {
87
+ status: 303,
88
+ headers: {
89
+ 'Set-Cookie': `pz-pos-error=${JSON.stringify(errors)}; path=/;`
90
+ }
91
+ }
92
+ );
93
+ }
94
+
73
95
  logger.info('Order success page context list', {
74
96
  middleware: 'three-d-redirection',
75
97
  contextList,
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.10.0",
4
+ "version": "1.12.0",
5
5
  "private": false,
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -18,7 +18,8 @@
18
18
  "cross-spawn": "7.0.3",
19
19
  "react-redux": "8.1.3",
20
20
  "react-string-replace": "1.1.1",
21
- "redis": "4.5.1"
21
+ "redis": "4.5.1",
22
+ "semver": "7.5.4"
22
23
  },
23
24
  "devDependencies": {
24
25
  "@types/react-redux": "7.1.30"
@@ -22,7 +22,11 @@ export type Division = {
22
22
  extra_data: object;
23
23
  };
24
24
 
25
- interface ProductB2b extends Product {
25
+ export interface ProductB2b extends Product {
26
+ sku: string;
27
+ name: string;
28
+ price: string;
29
+ base_code: string;
26
30
  asorti: string;
27
31
  category: string;
28
32
  currency: string;
@@ -41,21 +45,73 @@ export type BasketResponse = {
41
45
  total_amount: string;
42
46
  price: string;
43
47
  quantity: number;
44
- divisions: Division[];
48
+ divisions: BasketItemDivision[];
45
49
  product: ProductB2b;
50
+ product_remote_id: number;
46
51
  }
47
52
  ];
48
53
  };
49
54
 
55
+ export interface QuotationErrorType {
56
+ data: {
57
+ non_field_errors: string[];
58
+ };
59
+ }
60
+
61
+ export interface BasketItemDivision {
62
+ id: number;
63
+ name: string;
64
+ erp_code: string;
65
+ quantity: number;
66
+ }
67
+
68
+ export type BasketItemType = {
69
+ total_amount: string;
70
+ price: string;
71
+ quantity: number;
72
+ divisions: BasketItemDivision[];
73
+ product: ProductB2b;
74
+ product_remote_id: number;
75
+ }
76
+
50
77
  export type BasketParams = {
51
78
  division: string;
52
79
  product_remote_id: string;
53
80
  quantity: string;
54
81
  };
55
82
 
83
+ export type SaveBasketParams = {
84
+ name: string;
85
+ };
86
+
87
+ export type CreateQuotationParams = {
88
+ name: string;
89
+ };
90
+
91
+ export type UpdateProductParams = {
92
+ product_remote_id: number;
93
+ division: number;
94
+ quantity: number
95
+ };
96
+
97
+ export type DeleteProductParams = {
98
+ product_remote_id: number;
99
+ };
100
+
101
+ export type LoadBasketParams = {
102
+ id: number;
103
+ };
104
+
56
105
  export interface GetResponse<T> {
57
106
  count: number;
58
107
  next: null;
59
108
  previous: null;
60
109
  results: T[];
61
110
  }
111
+
112
+ export type DraftResponse = {
113
+ id: number;
114
+ name: string;
115
+ total_amount: number;
116
+ total_quantity: number;
117
+ };
@@ -0,0 +1,66 @@
1
+ import { InputHTMLAttributes, HTMLAttributes } from 'react';
2
+
3
+ export type Validator = {
4
+ regex: {
5
+ regex: string;
6
+ message: string;
7
+ inverse_match: boolean;
8
+ };
9
+ max_length: number;
10
+ required: boolean;
11
+ min_length: number;
12
+ max_value: number;
13
+ min_value: number;
14
+ };
15
+
16
+ export type FormField = {
17
+ chosen: boolean;
18
+ input_type: string;
19
+ id: string;
20
+ key: string;
21
+ validators: Validator;
22
+ label: string;
23
+ order: number;
24
+ class?: string;
25
+ attributes?: object | null;
26
+ labelClass?: string;
27
+ wrapperClass?: string;
28
+ placeholder?: string;
29
+ choices?: string[];
30
+ [key: string]: any;
31
+ };
32
+
33
+ export type Schema = FormField[];
34
+
35
+ export type FormType = {
36
+ pk: number;
37
+ schema: Schema;
38
+ template: string;
39
+ is_active: boolean;
40
+ url: string;
41
+ name: string;
42
+ pretty_url: any;
43
+ created_date: string;
44
+ modified_date: string;
45
+ formprettyurl_set: any[];
46
+ translations: null | any;
47
+ };
48
+
49
+ export type FieldPropertiesType = {
50
+ key?: string;
51
+ className?: string | Record<string, boolean>;
52
+ attributes?: InputHTMLAttributes<object> | HTMLAttributes<object>;
53
+ labelClassName?: string;
54
+ wrapperClassName?: string;
55
+ };
56
+
57
+ export type AllFieldClassesType = {
58
+ className?: string | undefined;
59
+ labelClassName?: string | undefined;
60
+ wrapperClassName?: string | undefined;
61
+ };
62
+
63
+ export type FormPropertiesType = {
64
+ actionUrl: string;
65
+ className?: string;
66
+ };
@@ -9,3 +9,4 @@ export * from './widget';
9
9
  export * from './flatpage';
10
10
  export * from './order';
11
11
  export * from './b2b';
12
+ export * from './form';
package/types/index.ts CHANGED
@@ -222,7 +222,15 @@ export interface RootLayoutProps<
222
222
  };
223
223
  }
224
224
 
225
+ export type RadioProps = React.HTMLProps<HTMLInputElement>;
226
+
225
227
  export interface AuthError {
226
228
  type: string;
227
229
  data?: any;
228
230
  }
231
+
232
+ export interface IconProps extends React.ComponentPropsWithRef<'i'> {
233
+ name: string;
234
+ size?: number;
235
+ className?: string;
236
+ }
package/utils/index.ts CHANGED
@@ -32,7 +32,7 @@ export function setCookie(name: string, val: string) {
32
32
  export function removeCookie(name: string) {
33
33
  const date = 'Thu, 01 Jan 1970 00:00:00 UTC';
34
34
 
35
- document.cookie = name + '=' + '; expires=' + date + '; path=/';
35
+ document.cookie = `${name}=; expires=${date}; path=/;`;
36
36
  }
37
37
 
38
38
  /**
@@ -137,3 +137,14 @@ export const urlLocaleMatcherRegex = new RegExp(
137
137
  .map((l) => l.value)
138
138
  .join('|')})`
139
139
  );
140
+
141
+ export const getPosError = () => {
142
+ const error = JSON.parse(getCookie('pz-pos-error') ?? '{}');
143
+
144
+ // delete 'pz-pos-error' cookie when refreshing or closing page
145
+ window.addEventListener('beforeunload', () => {
146
+ removeCookie('pz-pos-error');
147
+ });
148
+
149
+ return error;
150
+ };