@akinon/projectzero 1.66.0 → 1.68.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,13 @@
1
1
  # @akinon/projectzero
2
2
 
3
+ ## 1.68.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 009d42c: ZERO-2903: build cli commands
8
+
9
+ ## 1.67.0
10
+
3
11
  ## 1.66.0
4
12
 
5
13
  ## 1.65.0
@@ -63,4 +63,5 @@ public/worker-*.js.map
63
63
  next.config.original.js
64
64
  next.config.wizardcopy.js
65
65
 
66
+ certificates
66
67
  public/locales/*/index.json
@@ -1,5 +1,79 @@
1
1
  # projectzeronext
2
2
 
3
+ ## 1.68.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 907813c: ZERO-2934: add payment-gateway/<gateway> redirect in middleware
8
+ - 714e0b4: ZERO-2759: update pz-click-collect peer dependencies
9
+ - c873740: ZERO-2903: add types
10
+ - 034b813: ZERO-2903: create saved card plugin
11
+
12
+ ### Patch Changes
13
+
14
+ - Updated dependencies [907813c]
15
+ - Updated dependencies [82cf1e5]
16
+ - Updated dependencies [b92001c]
17
+ - Updated dependencies [714e0b4]
18
+ - Updated dependencies [c873740]
19
+ - Updated dependencies [d899cc7]
20
+ - Updated dependencies [63597bc]
21
+ - Updated dependencies [fa4c716]
22
+ - Updated dependencies [57d1657]
23
+ - Updated dependencies [2eba2a8]
24
+ - Updated dependencies [3be7462]
25
+ - Updated dependencies [8fb37c4]
26
+ - Updated dependencies [034b813]
27
+ - Updated dependencies [ce25dac]
28
+ - Updated dependencies [48d508f]
29
+ - @akinon/pz-tabby-extension@1.68.0
30
+ - @akinon/next@1.68.0
31
+ - @akinon/pz-saved-card@1.68.0
32
+ - @akinon/pz-click-collect@1.68.0
33
+ - @akinon/pz-akifast@1.68.0
34
+ - @akinon/pz-b2b@1.68.0
35
+ - @akinon/pz-basket-gift-pack@1.68.0
36
+ - @akinon/pz-bkm@1.68.0
37
+ - @akinon/pz-checkout-gift-pack@1.68.0
38
+ - @akinon/pz-credit-payment@1.68.0
39
+ - @akinon/pz-gpay@1.68.0
40
+ - @akinon/pz-masterpass@1.68.0
41
+ - @akinon/pz-one-click-checkout@1.68.0
42
+ - @akinon/pz-otp@1.68.0
43
+ - @akinon/pz-pay-on-delivery@1.68.0
44
+
45
+ ## 1.67.0
46
+
47
+ ### Minor Changes
48
+
49
+ - a1a99f0: ZERO-2887: Add LoaderSpinner to Filters component
50
+ - bc2b411: ZERO-2825: Add attribute-based shipping options to checkout page
51
+ - 3d2212a: ZERO-2745: Add multi basket support
52
+ - 38a634e: ZERO-2893: Refactor category filter handling and URL parameters
53
+ - 9e25a64: ZERO-2835: Update category page layout with breadcrumb
54
+
55
+ ### Patch Changes
56
+
57
+ - Updated dependencies [bc2b411]
58
+ - Updated dependencies [3d2212a]
59
+ - Updated dependencies [8f47cca]
60
+ - Updated dependencies [9e25a64]
61
+ - @akinon/next@1.67.0
62
+ - @akinon/pz-akifast@1.67.0
63
+ - @akinon/pz-b2b@1.67.0
64
+ - @akinon/pz-basket-gift-pack@1.67.0
65
+ - @akinon/pz-bkm@1.67.0
66
+ - @akinon/pz-checkout-gift-pack@1.67.0
67
+ - @akinon/pz-click-collect@1.67.0
68
+ - @akinon/pz-credit-payment@1.67.0
69
+ - @akinon/pz-gpay@1.67.0
70
+ - @akinon/pz-masterpass@1.67.0
71
+ - @akinon/pz-one-click-checkout@1.67.0
72
+ - @akinon/pz-otp@1.67.0
73
+ - @akinon/pz-pay-on-delivery@1.67.0
74
+ - @akinon/pz-saved-card@1.67.0
75
+ - @akinon/pz-tabby-extension@1.67.0
76
+
3
77
  ## 1.66.0
4
78
 
5
79
  ### Minor Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projectzeronext",
3
- "version": "1.66.0",
3
+ "version": "1.68.0",
4
4
  "private": true,
5
5
  "license": "MIT",
6
6
  "scripts": {
@@ -22,21 +22,21 @@
22
22
  "prestart": "pz-prestart"
23
23
  },
24
24
  "dependencies": {
25
- "@akinon/next": "1.66.0",
26
- "@akinon/pz-akifast": "1.66.0",
27
- "@akinon/pz-b2b": "1.66.0",
28
- "@akinon/pz-basket-gift-pack": "1.66.0",
29
- "@akinon/pz-bkm": "1.66.0",
30
- "@akinon/pz-checkout-gift-pack": "1.66.0",
31
- "@akinon/pz-click-collect": "1.66.0",
32
- "@akinon/pz-credit-payment": "1.66.0",
33
- "@akinon/pz-gpay": "1.66.0",
34
- "@akinon/pz-masterpass": "1.66.0",
35
- "@akinon/pz-one-click-checkout": "1.66.0",
36
- "@akinon/pz-otp": "1.66.0",
37
- "@akinon/pz-pay-on-delivery": "1.66.0",
38
- "@akinon/pz-saved-card": "1.66.0",
39
- "@akinon/pz-tabby-extension": "1.66.0",
25
+ "@akinon/next": "1.68.0",
26
+ "@akinon/pz-akifast": "1.68.0",
27
+ "@akinon/pz-b2b": "1.68.0",
28
+ "@akinon/pz-basket-gift-pack": "1.68.0",
29
+ "@akinon/pz-bkm": "1.68.0",
30
+ "@akinon/pz-checkout-gift-pack": "1.68.0",
31
+ "@akinon/pz-click-collect": "1.68.0",
32
+ "@akinon/pz-credit-payment": "1.68.0",
33
+ "@akinon/pz-gpay": "1.68.0",
34
+ "@akinon/pz-masterpass": "1.68.0",
35
+ "@akinon/pz-one-click-checkout": "1.68.0",
36
+ "@akinon/pz-otp": "1.68.0",
37
+ "@akinon/pz-pay-on-delivery": "1.68.0",
38
+ "@akinon/pz-saved-card": "1.68.0",
39
+ "@akinon/pz-tabby-extension": "1.68.0",
40
40
  "@hookform/resolvers": "2.9.0",
41
41
  "@next/third-parties": "14.1.0",
42
42
  "@react-google-maps/api": "2.17.1",
@@ -60,7 +60,7 @@
60
60
  "yup": "0.32.11"
61
61
  },
62
62
  "devDependencies": {
63
- "@akinon/eslint-plugin-projectzero": "1.66.0",
63
+ "@akinon/eslint-plugin-projectzero": "1.68.0",
64
64
  "@semantic-release/changelog": "6.0.2",
65
65
  "@semantic-release/exec": "6.0.3",
66
66
  "@semantic-release/git": "10.0.1",
@@ -4,11 +4,14 @@ import { PageProps } from '@akinon/next/types';
4
4
  import CategoryLayout from '@theme/views/category/layout';
5
5
 
6
6
  async function Page({ params, searchParams }: PageProps<{ pk: number }>) {
7
- const { data } = await getCategoryData({ pk: params.pk, searchParams });
7
+ const { data, breadcrumbData } = await getCategoryData({
8
+ pk: params.pk,
9
+ searchParams
10
+ });
8
11
 
9
12
  return (
10
13
  <>
11
- <CategoryLayout data={data} />
14
+ <CategoryLayout data={data} breadcrumbData={breadcrumbData} />
12
15
  </>
13
16
  );
14
17
  }
@@ -41,6 +41,7 @@ export interface CreditCardForm {
41
41
  card_cvv: string;
42
42
  installment: number;
43
43
  agreement: boolean;
44
+ save?: boolean;
44
45
  }
45
46
 
46
47
  export interface PriceProps {
@@ -5,6 +5,7 @@ import { Icon, Link } from '@theme/components';
5
5
  import { ROUTES } from '@theme/routes';
6
6
  import { useLocalization } from '@akinon/next/hooks';
7
7
  import { BreadcrumbResultType } from '@akinon/next/types';
8
+ import { capitalize } from '@akinon/next/utils';
8
9
 
9
10
  export interface BreadcrumbProps {
10
11
  breadcrumbList?: BreadcrumbResultType[];
@@ -26,7 +27,9 @@ export default function Breadcrumb(props: BreadcrumbProps) {
26
27
  <div className="flex items-center gap-3 text-xs leading-4 text-gray-950">
27
28
  {list.map((item, index) => (
28
29
  <Fragment key={index}>
29
- <Link href={item.url}>{item.text}</Link>
30
+ <Link href={item.url}>
31
+ {capitalize(item.text.toLocaleLowerCase())}
32
+ </Link>
30
33
  {index !== list.length - 1 && <Icon name="chevron-end" size={8} />}
31
34
  </Fragment>
32
35
  ))}
@@ -22,17 +22,28 @@ const CategoryActiveFilters = () => {
22
22
  const handleRemoveFilter = ({ facet, choice }) => {
23
23
  if (facet.widget_type === WIDGET_TYPE.category) {
24
24
  dispatch(removeCategoryFacet({ facet, choice }));
25
- return;
25
+ } else {
26
+ dispatch(toggleFacet({ facet, choice }));
26
27
  }
27
28
 
28
- dispatch(toggleFacet({ facet, choice }));
29
+ const urlSearchParams = new URLSearchParams(window.location.search);
30
+ urlSearchParams.delete(facet.search_key);
31
+ router.replace(pathname + '?' + urlSearchParams.toString());
29
32
  };
30
33
 
31
34
  const url = useMemo(() => {
32
- const facetSearchParams =
33
- convertFacetSearchParams(selectedFacets).toString();
35
+ const facetSearchParams = convertFacetSearchParams(selectedFacets);
36
+ const urlSearchParams = new URLSearchParams(searchParams.toString());
34
37
 
35
- const urlSearchParams = new URLSearchParams(facetSearchParams);
38
+ for (const key of Array.from(urlSearchParams.keys())) {
39
+ if (facetSearchParams.has(key)) {
40
+ urlSearchParams.delete(key);
41
+ }
42
+ }
43
+
44
+ for (const [key, value] of Array.from(facetSearchParams.entries())) {
45
+ urlSearchParams.append(key, value);
46
+ }
36
47
 
37
48
  const searchText = searchParams.get('search_text');
38
49
  const page = searchParams.get('page');
@@ -53,7 +64,6 @@ const CategoryActiveFilters = () => {
53
64
 
54
65
  useEffect(() => {
55
66
  router.push(url);
56
-
57
67
  // eslint-disable-next-line react-hooks/exhaustive-deps
58
68
  }, [url]);
59
69
 
@@ -1,12 +1,14 @@
1
1
  import clsx from 'clsx';
2
2
  import { useAppDispatch } from '@akinon/next/redux/hooks';
3
3
  import { Facet, FacetChoice } from '@akinon/next/types';
4
- import { Accordion, Radio, Checkbox } from '../../../components';
4
+ import { Accordion, Radio, Checkbox, LoaderSpinner } from '../../../components';
5
5
  import { WIDGET_TYPE } from '../../../types';
6
6
  import { SizeFilter } from './size-filter';
7
7
  import { toggleFacet } from '@theme/redux/reducers/category';
8
8
  import { commonProductAttributes } from '@theme/settings';
9
9
  import { useRouter } from '@akinon/next/hooks';
10
+ import { usePathname } from 'next/navigation';
11
+ import { useState } from 'react';
10
12
 
11
13
  const COMPONENT_TYPES = {
12
14
  [WIDGET_TYPE.category]: Radio,
@@ -19,6 +21,8 @@ const sizeKey = commonProductAttributes.find(
19
21
 
20
22
  interface Props {
21
23
  facet: Facet;
24
+ isPending: boolean;
25
+ startTransition: (callback: () => void) => void;
22
26
  }
23
27
 
24
28
  const sortByPredefinedOrder = (
@@ -78,16 +82,33 @@ const getComponentByWidgetType = (widgetType: string, facetKey: string) => {
78
82
  return COMPONENT_TYPES[widgetType] || COMPONENT_TYPES[WIDGET_TYPE.category];
79
83
  };
80
84
 
81
- export const FilterItem = ({ facet }: Props) => {
85
+ export const FilterItem = ({ facet, isPending, startTransition }: Props) => {
82
86
  const dispatch = useAppDispatch();
83
87
  const router = useRouter();
84
-
85
- const handleSelectFilter = (choice: FacetChoice) => {
86
- if (facet.key === 'category_ids') {
87
- router.push(choice.url);
88
- } else {
89
- dispatch(toggleFacet({ facet, choice }));
90
- }
88
+ const pathname = usePathname();
89
+
90
+ const [pendingChoice, setPendingChoice] = useState<string | null>(null);
91
+
92
+ const handleSelectFilter = ({
93
+ facet,
94
+ choice
95
+ }: {
96
+ facet: Facet;
97
+ choice: FacetChoice;
98
+ }) => {
99
+ setPendingChoice(choice.label);
100
+ startTransition(() => {
101
+ if (facet.key === 'category_ids') {
102
+ router.push(choice.url);
103
+ } else {
104
+ dispatch(toggleFacet({ facet, choice }));
105
+ }
106
+ setPendingChoice(null);
107
+
108
+ const urlSearchParams = new URLSearchParams(window.location.search);
109
+ urlSearchParams.delete(facet.search_key);
110
+ router.replace(pathname + '?' + urlSearchParams.toString());
111
+ });
91
112
  };
92
113
 
93
114
  const Component = getComponentByWidgetType(facet.widget_type, facet.key);
@@ -107,23 +128,34 @@ export const FilterItem = ({ facet }: Props) => {
107
128
  })}
108
129
  >
109
130
  {choices.map((choice, index) => (
110
- <Component
111
- key={choice.label}
112
- data={choice}
113
- name={facet.key}
114
- onChange={() => facet.key !== sizeKey && handleSelectFilter(choice)}
115
- onClick={() => facet.key === sizeKey && handleSelectFilter(choice)}
116
- checked={choice.is_selected}
117
- data-testid={`${choice.label.trim()}`}
118
- >
119
- {choice.label} (
120
- <span
121
- data-testid={`filter-count-${facet.name.toLowerCase()}-${index}`}
131
+ <div key={choice.label} className="relative">
132
+ <Component
133
+ key={choice.label}
134
+ data={choice}
135
+ name={facet.key}
136
+ onChange={() => handleSelectFilter({ facet, choice })}
137
+ onClick={() =>
138
+ facet.key === sizeKey && handleSelectFilter({ facet, choice })
139
+ }
140
+ checked={choice.is_selected}
141
+ data-testid={`${choice.label.trim()}`}
142
+ disabled={isPending}
122
143
  >
123
- {choice.quantity}
124
- </span>
125
- )
126
- </Component>
144
+ {choice.label} (
145
+ <span
146
+ data-testid={`filter-count-${facet.name.toLowerCase()}-${index}`}
147
+ >
148
+ {choice.quantity}
149
+ </span>
150
+ )
151
+ </Component>
152
+
153
+ {isPending && pendingChoice === choice.label && (
154
+ <div className="absolute inset-0 flex items-center justify-center z-50">
155
+ <LoaderSpinner />
156
+ </div>
157
+ )}
158
+ </div>
127
159
  ))}
128
160
  </div>
129
161
  </Accordion>
@@ -1,13 +1,12 @@
1
1
  'use client';
2
2
 
3
3
  import clsx from 'clsx';
4
-
5
4
  import { Button, Icon } from '@theme/components';
6
5
  import { useLocalization } from '@akinon/next/hooks';
7
6
  import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
8
7
  import { resetSelectedFacets } from '@theme/redux/reducers/category';
9
8
  import CategoryActiveFilters from '@theme/views/category/category-active-filters';
10
- import { useMemo } from 'react';
9
+ import { useMemo, useState, useTransition } from 'react';
11
10
  import { FilterItem } from './filter-item';
12
11
 
13
12
  interface Props {
@@ -21,12 +20,11 @@ export const Filters = (props: Props) => {
21
20
  const { t } = useLocalization();
22
21
  const { isMenuOpen, setIsMenuOpen } = props;
23
22
 
23
+ const [isPending, startTransition] = useTransition();
24
+
24
25
  const haveFilter = useMemo(() => {
25
- return (
26
- facets.filter(
27
- (facet) =>
28
- facet.data.choices.filter((choice) => choice.is_selected).length > 0
29
- ).length > 0
26
+ return facets.some((facet) =>
27
+ facet.data.choices.some((choice) => choice.is_selected)
30
28
  );
31
29
  }, [facets]);
32
30
 
@@ -51,12 +49,22 @@ export const Filters = (props: Props) => {
51
49
  <span className="text-sm">1 {t('category.filters.results')}</span>
52
50
  <span>{t('category.filters.ready_to_wear')}</span>
53
51
  </div>
52
+
54
53
  {facets.map((facet) => {
55
- return <FilterItem key={facet.key} facet={facet} />;
54
+ return (
55
+ <FilterItem
56
+ key={facet.key}
57
+ facet={facet}
58
+ isPending={isPending}
59
+ startTransition={startTransition}
60
+ />
61
+ );
56
62
  })}
63
+
57
64
  <div className="lg:hidden">
58
65
  <CategoryActiveFilters />
59
66
  </div>
67
+
60
68
  {haveFilter && (
61
69
  <div className="lg:hidden">
62
70
  <Button
@@ -1,15 +1,17 @@
1
1
  import clsx from 'clsx';
2
- import { GetCategoryResponse } from '@akinon/next/types';
2
+ import { BreadcrumbResultType, GetCategoryResponse } from '@akinon/next/types';
3
3
  import Breadcrumb from '@theme/views/breadcrumb';
4
4
  import { CategoryBanner } from '@theme/views/category/category-banner';
5
5
  import ListPage from '@theme/views/category/category-info';
6
6
 
7
7
  export default async function Layout({
8
8
  data,
9
- children
9
+ children,
10
+ breadcrumbData
10
11
  }: {
11
12
  data: GetCategoryResponse;
12
13
  children?: React.ReactNode;
14
+ breadcrumbData?: BreadcrumbResultType[];
13
15
  }) {
14
16
  return (
15
17
  <>
@@ -28,7 +30,7 @@ export default async function Layout({
28
30
  'lg:absolute lg:inset-x-0 z-10 container lg:my-4 mx-auto'
29
31
  )}
30
32
  >
31
- <Breadcrumb />
33
+ <Breadcrumb breadcrumbList={breadcrumbData} />
32
34
  </div>
33
35
  <CategoryBanner {...data.category?.attributes?.category_banner} />
34
36
  </div>
@@ -5,9 +5,12 @@ import { useSetPaymentOptionMutation } from '@akinon/next/data/client/checkout';
5
5
  import { CheckoutPaymentOption } from '@akinon/next/types';
6
6
  import { Radio } from '@theme/components';
7
7
  import { usePaymentOptions } from '@akinon/next/hooks/use-payment-options';
8
+ import { useMemo } from 'react';
8
9
 
9
10
  const PaymentOptionButtons = () => {
10
- const { preOrder } = useAppSelector((state: RootState) => state.checkout);
11
+ const { preOrder, attributeBasedShippingOptions } = useAppSelector(
12
+ (state: RootState) => state.checkout
13
+ );
11
14
  const [setPaymentOption] = useSetPaymentOptionMutation();
12
15
  const { filteredPaymentOptions } = usePaymentOptions();
13
16
 
@@ -22,10 +25,23 @@ const PaymentOptionButtons = () => {
22
25
  });
23
26
  };
24
27
 
28
+ const displayedPaymentOptions = useMemo(() => {
29
+ if (
30
+ attributeBasedShippingOptions &&
31
+ Object.keys(attributeBasedShippingOptions).length > 0
32
+ ) {
33
+ return filteredPaymentOptions.filter(
34
+ (option) => option.slug.toLowerCase() !== 'pay-on-delivery'
35
+ );
36
+ }
37
+
38
+ return filteredPaymentOptions;
39
+ }, [filteredPaymentOptions, attributeBasedShippingOptions]);
40
+
25
41
  return (
26
42
  <>
27
43
  <div className="w-full space-y-4 px-4 flex flex-col mb-8 md:hidden">
28
- {filteredPaymentOptions.map((option) => (
44
+ {displayedPaymentOptions.map((option) => (
29
45
  <label
30
46
  key={`payment-option-${option.pk}`}
31
47
  className="border px-4 py-3 mt-3 flex h-12"
@@ -47,7 +63,7 @@ const PaymentOptionButtons = () => {
47
63
  </div>
48
64
 
49
65
  <div className="hidden md:flex">
50
- {filteredPaymentOptions.map((option) => (
66
+ {displayedPaymentOptions.map((option) => (
51
67
  <button
52
68
  key={`payment-option-${option.pk}`}
53
69
  onClick={() => onClickHandler(option)}
@@ -1,36 +1,84 @@
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
1
2
  import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
2
- import { setCurrentStep } from '@akinon/next/redux/reducers/checkout';
3
+ import {
4
+ setCurrentStep,
5
+ setSelectedShippingOptions
6
+ } from '@akinon/next/redux/reducers/checkout';
3
7
  import { RootState } from '@theme/redux/store';
4
8
  import {
5
9
  useSetShippingOptionMutation,
10
+ useSetAttributeBasedShippingOptionsMutation,
6
11
  useSetDataSourceShippingOptionsMutation
7
12
  } from '@akinon/next/data/client/checkout';
8
13
  import { Price, Button, Radio } from '@theme/components';
9
14
  import { CheckoutStep } from '@akinon/next/types';
10
15
  import { useLocalization } from '@akinon/next/hooks';
11
- import { useEffect, useState } from 'react';
12
16
 
13
- const ShippingOptions = () => {
17
+ const ShippingOptions: React.FC = () => {
14
18
  const { t } = useLocalization();
19
+ const dispatch = useAppDispatch();
15
20
  const {
16
21
  steps,
17
22
  shippingOptions,
23
+ attributeBasedShippingOptions,
18
24
  dataSourceShippingOptions,
19
25
  preOrder,
20
- addressList
26
+ addressList,
27
+ selectedShippingOptions
21
28
  } = useAppSelector((state: RootState) => state.checkout);
22
- const { shipping_option, shipping_address, data_source_shipping_options } =
23
- preOrder ?? {};
29
+ const {
30
+ shipping_option,
31
+ shipping_address,
32
+ attribute_based_shipping_options,
33
+ data_source_shipping_options
34
+ } = preOrder ?? {};
35
+
36
+ const [setShippingOption] = useSetShippingOptionMutation();
37
+ const [setAttributeBasedShippingOptions] =
38
+ useSetAttributeBasedShippingOptionsMutation();
39
+ const [setDataSourceShippingOption] =
40
+ useSetDataSourceShippingOptionsMutation();
41
+
42
+ const prevAttributeBasedOptionsRef = useRef(attributeBasedShippingOptions);
24
43
 
25
44
  const [selectedPks, setSelectedPks] = useState<
26
45
  { dataSourcePk: number; optionPk: number }[] | null
27
46
  >(null);
28
47
 
29
- const [setShippingOption] = useSetShippingOptionMutation();
30
- const [setDataSourceShippingOption] =
31
- useSetDataSourceShippingOptionsMutation();
48
+ const initializeSelectedOptions = useCallback(() => {
49
+ if (attribute_based_shipping_options) {
50
+ const newSelected = { ...selectedShippingOptions };
51
+ let hasChanges = false;
32
52
 
33
- const dispatch = useAppDispatch();
53
+ Object.entries(attributeBasedShippingOptions).forEach(
54
+ ([color, options]) => {
55
+ if (
56
+ !newSelected[color] ||
57
+ !options.some((opt) => opt.pk === newSelected[color])
58
+ ) {
59
+ newSelected[color] = options[0].pk;
60
+ hasChanges = true;
61
+ }
62
+ }
63
+ );
64
+
65
+ if (hasChanges) {
66
+ dispatch(setSelectedShippingOptions(newSelected));
67
+ setAttributeBasedShippingOptions(newSelected);
68
+ }
69
+ } else if (shippingOptions.length > 0 && !shipping_option) {
70
+ setShippingOption(shippingOptions[0].pk);
71
+ }
72
+ // eslint-disable-next-line react-hooks/exhaustive-deps
73
+ }, [
74
+ attributeBasedShippingOptions,
75
+ selectedShippingOptions,
76
+ setAttributeBasedShippingOptions,
77
+ attribute_based_shipping_options,
78
+ shippingOptions,
79
+ shipping_option,
80
+ setShippingOption
81
+ ]);
34
82
 
35
83
  useEffect(() => {
36
84
  if (!data_source_shipping_options) return;
@@ -43,6 +91,49 @@ const ShippingOptions = () => {
43
91
  setSelectedPks(initialSelectedPks);
44
92
  }, [data_source_shipping_options]);
45
93
 
94
+ useEffect(() => {
95
+ if (
96
+ JSON.stringify(prevAttributeBasedOptionsRef.current) !==
97
+ JSON.stringify(attributeBasedShippingOptions) ||
98
+ Object.keys(selectedShippingOptions).length === 0 ||
99
+ (!shipping_option && shippingOptions.length > 0)
100
+ ) {
101
+ initializeSelectedOptions();
102
+ prevAttributeBasedOptionsRef.current = attributeBasedShippingOptions;
103
+ }
104
+ }, [
105
+ attributeBasedShippingOptions,
106
+ selectedShippingOptions,
107
+ initializeSelectedOptions,
108
+ shipping_option,
109
+ shippingOptions
110
+ ]);
111
+
112
+ const handleAttributeBasedOptionChange = useCallback(
113
+ (color: string, newPk: number) => {
114
+ const updatedOptions = { ...selectedShippingOptions, [color]: newPk };
115
+ dispatch(setSelectedShippingOptions(updatedOptions));
116
+ setAttributeBasedShippingOptions(updatedOptions);
117
+ },
118
+ // eslint-disable-next-line react-hooks/exhaustive-deps
119
+ [selectedShippingOptions, setAttributeBasedShippingOptions]
120
+ );
121
+
122
+ if (addressList.length < 1) {
123
+ return (
124
+ <div className="w-full lg:w-2/5">
125
+ <div className="border-b border-gray-400 px-8 py-4">
126
+ <h2 className="text-2xl">{t('checkout.address.shipping.title')}</h2>
127
+ </div>
128
+ <div className="py-4 px-8">
129
+ <p className="text-xs">
130
+ {t('checkout.address.shipping.select_address_to_continue')}
131
+ </p>
132
+ </div>
133
+ </div>
134
+ );
135
+ }
136
+
46
137
  const updateData = (dataSourcePk: number, newPk: number) => {
47
138
  const updatedSelectedPks = selectedPks?.map((item) =>
48
139
  item.dataSourcePk === dataSourcePk ? { ...item, optionPk: newPk } : item
@@ -106,58 +197,79 @@ const ShippingOptions = () => {
106
197
  </span>
107
198
  </div>
108
199
  ))}
109
- <Button
110
- className="mt-2 w-full"
111
- disabled={!steps.shipping.completed}
112
- onClick={() => dispatch(setCurrentStep(CheckoutStep.Payment))}
113
- data-testid="checkout-shipping-save"
114
- >
115
- {t('checkout.address.shipping.button')}
116
- </Button>
117
200
  </>
118
201
  )}
119
- {dataSourceShippingOptions && dataSourceShippingOptions.length > 0 && (
120
- <>
121
- {dataSourceShippingOptions.map((option) => (
122
- <div key={option.pk}>
123
- <h3 className="text-lg font-bold">{option?.name}</h3>
124
- {option.data_source_shipping_options.map((opt) => (
202
+
203
+ {attributeBasedShippingOptions &&
204
+ Object.keys(attributeBasedShippingOptions).length > 0 &&
205
+ Object.entries(attributeBasedShippingOptions).map(
206
+ ([color, options]) => (
207
+ <div key={color}>
208
+ <h3 className="text-lg font-bold">{color}</h3>
209
+ {options.map((option) => (
125
210
  <div
126
- key={opt.pk}
211
+ key={option.pk}
127
212
  className="py-4 border-t border-gray-400 flex justify-between"
128
213
  >
129
214
  <Radio
130
- name={`data-source-shipping-${option.pk}`}
131
- checked={
132
- selectedPks?.some(
133
- (item) => item.optionPk === opt.pk
134
- ) || false
135
- }
136
- onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
137
- handleRadioChange(e, option.pk)
215
+ name={`attribute-based-shipping-${color}`}
216
+ checked={selectedShippingOptions[color] === option.pk}
217
+ onChange={() =>
218
+ handleAttributeBasedOptionChange(color, option.pk)
138
219
  }
139
- value={opt.pk}
140
- data-testid={`checkout-data-source-shipping-${opt.pk}`}
220
+ data-testid={`checkout-attribute-based-shipping-${option.pk}`}
141
221
  >
142
- {opt?.shipping_option_name}
222
+ {`${option.shipping_option_name}`}
143
223
  </Radio>
144
224
  <span className="text-xs">
145
- <Price value={opt?.shipping_amount} />
225
+ <Price value={option.shipping_amount} />
146
226
  </span>
147
227
  </div>
148
228
  ))}
149
229
  </div>
150
- ))}
151
- <Button
152
- className="mt-2 w-full"
153
- disabled={!steps.shipping.completed}
154
- onClick={() => dispatch(setCurrentStep(CheckoutStep.Payment))}
155
- data-testid="checkout-shipping-save"
156
- >
157
- {t('checkout.address.shipping.button')}
158
- </Button>
159
- </>
160
- )}
230
+ )
231
+ )}
232
+
233
+ {dataSourceShippingOptions &&
234
+ dataSourceShippingOptions.length > 0 &&
235
+ dataSourceShippingOptions.map((option) => (
236
+ <div key={option.pk}>
237
+ <h3 className="text-lg font-bold">{option?.name}</h3>
238
+ {option.data_source_shipping_options.map((opt) => (
239
+ <div
240
+ key={opt.pk}
241
+ className="py-4 border-t border-gray-400 flex justify-between"
242
+ >
243
+ <Radio
244
+ name={`data-source-shipping-${option.pk}`}
245
+ checked={
246
+ selectedPks?.some((item) => item.optionPk === opt.pk) ||
247
+ false
248
+ }
249
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
250
+ handleRadioChange(e, option.pk)
251
+ }
252
+ value={opt.pk}
253
+ data-testid={`checkout-data-source-shipping-${opt.pk}`}
254
+ >
255
+ {opt?.shipping_option_name}
256
+ </Radio>
257
+ <span className="text-xs">
258
+ <Price value={opt?.shipping_amount} />
259
+ </span>
260
+ </div>
261
+ ))}
262
+ </div>
263
+ ))}
264
+
265
+ <Button
266
+ className="mt-2 w-full"
267
+ disabled={!steps.shipping.completed}
268
+ onClick={() => dispatch(setCurrentStep(CheckoutStep.Payment))}
269
+ data-testid="checkout-shipping-save"
270
+ >
271
+ {t('checkout.address.shipping.button')}
272
+ </Button>
161
273
  </div>
162
274
  )}
163
275
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akinon/projectzero",
3
- "version": "1.66.0",
3
+ "version": "1.68.0",
4
4
  "private": false,
5
5
  "description": "CLI tool to manage your Project Zero Next project",
6
6
  "bin": {