@magento/peregrine 12.4.0-beta.1 → 12.5.0-alpha.2

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 (64) hide show
  1. package/lib/Apollo/links/authLink.js +19 -0
  2. package/lib/Apollo/links/errorLink.js +64 -0
  3. package/lib/Apollo/links/gqlCacheLink.js +63 -0
  4. package/lib/Apollo/links/index.js +66 -0
  5. package/lib/Apollo/links/mutationQueueLink.js +5 -0
  6. package/lib/Apollo/links/retryLink.js +17 -0
  7. package/lib/Apollo/links/storeLink.js +22 -0
  8. package/lib/Apollo/magentoGqlCacheLink.js +3 -59
  9. package/lib/PeregrineContextProvider/peregrineContextProvider.js +2 -0
  10. package/lib/context/eventing.js +57 -0
  11. package/lib/hooks/useDelayedTransition.js +1 -1
  12. package/lib/talons/AccountInformationPage/useAccountInformationPage.js +15 -1
  13. package/lib/talons/Adapter/useAdapter.js +23 -200
  14. package/lib/talons/AddToCartDialog/addToCartDialog.gql.js +8 -0
  15. package/lib/talons/AddToCartDialog/useAddToCartDialog.js +46 -2
  16. package/lib/talons/AddressBookPage/useAddressBookPage.js +52 -17
  17. package/lib/talons/AuthModal/useAuthModal.js +12 -2
  18. package/lib/talons/Breadcrumbs/useBreadcrumbs.js +7 -2
  19. package/lib/talons/CartPage/ProductListing/EditModal/__fixtures__/configurableProduct.js +39 -4
  20. package/lib/talons/CartPage/ProductListing/EditModal/useProductForm.js +47 -3
  21. package/lib/talons/CartPage/ProductListing/productListingFragments.gql.js +6 -0
  22. package/lib/talons/CartPage/ProductListing/useProduct.js +49 -3
  23. package/lib/talons/CartPage/useCartPage.js +15 -0
  24. package/lib/talons/CheckoutPage/ItemsReview/itemsReviewFragments.gql.js +12 -14
  25. package/lib/talons/CheckoutPage/OrderConfirmationPage/orderConfirmationPageFragments.gql.js +21 -1
  26. package/lib/talons/CheckoutPage/OrderConfirmationPage/useCreateAccount.js +15 -1
  27. package/lib/talons/CheckoutPage/PaymentInformation/useEditModal.js +10 -1
  28. package/lib/talons/CheckoutPage/PaymentInformation/usePaymentInformation.js +14 -0
  29. package/lib/talons/CheckoutPage/ShippingInformation/AddressForm/useCustomerForm.js +38 -1
  30. package/lib/talons/CheckoutPage/ShippingInformation/AddressForm/useGuestForm.js +15 -1
  31. package/lib/talons/CheckoutPage/ShippingInformation/useShippingInformation.js +13 -0
  32. package/lib/talons/CheckoutPage/ShippingMethod/useShippingMethod.js +28 -6
  33. package/lib/talons/CheckoutPage/checkoutPage.gql.js +3 -0
  34. package/lib/talons/CheckoutPage/useCheckoutPage.js +79 -3
  35. package/lib/talons/Cms/useCmsPage.js +14 -0
  36. package/lib/talons/CreateAccount/useCreateAccount.js +15 -1
  37. package/lib/talons/FilterModal/helpers.js +8 -2
  38. package/lib/talons/FilterModal/useFilterModal.js +1 -0
  39. package/lib/talons/FilterSidebar/useFilterSidebar.js +1 -0
  40. package/lib/talons/Gallery/useAddToCartButton.js +24 -12
  41. package/lib/talons/Gallery/useGalleryItem.js +85 -1
  42. package/lib/talons/Header/storeSwitcher.gql.js +1 -16
  43. package/lib/talons/Header/useAccountMenu.js +21 -2
  44. package/lib/talons/Header/useStoreSwitcher.js +40 -93
  45. package/lib/talons/MagentoRoute/useMagentoRoute.js +7 -7
  46. package/lib/talons/MiniCart/ProductList/productListFragments.gql.js +4 -0
  47. package/lib/talons/MiniCart/useMiniCart.js +46 -3
  48. package/lib/talons/ProductFullDetail/useProductFullDetail.js +37 -9
  49. package/lib/talons/RootComponents/Category/categoryContent.gql.js +2 -0
  50. package/lib/talons/RootComponents/Category/categoryFragments.gql.js +3 -0
  51. package/lib/talons/RootComponents/Category/useCategory.js +4 -1
  52. package/lib/talons/RootComponents/Category/useCategoryContent.js +35 -13
  53. package/lib/talons/RootComponents/Product/productDetailFragment.gql.js +6 -0
  54. package/lib/talons/RootComponents/Product/useProduct.js +27 -0
  55. package/lib/talons/SearchBar/index.js +1 -0
  56. package/lib/talons/SearchBar/useAutocomplete.js +18 -1
  57. package/lib/talons/SearchBar/useSuggestedProduct.js +99 -0
  58. package/lib/talons/SearchPage/searchPage.gql.js +3 -0
  59. package/lib/talons/SearchPage/useSearchPage.js +56 -28
  60. package/lib/talons/SignIn/useSignIn.js +19 -2
  61. package/lib/talons/WishlistPage/useWishlistItem.js +36 -1
  62. package/lib/talons/WishlistPage/wishlistItemFragments.gql.js +3 -0
  63. package/lib/util/makeUrl.js +1 -1
  64. package/package.json +5 -4
@@ -2,6 +2,7 @@ import { useQuery } from '@apollo/client';
2
2
  import { useCallback, useMemo } from 'react';
3
3
  import { useLocation } from 'react-router-dom';
4
4
  import { useDropdown } from '@magento/peregrine/lib/hooks/useDropdown';
5
+ import { useAwaitQuery } from '@magento/peregrine/lib/hooks/useAwaitQuery';
5
6
  import { BrowserPersistence } from '@magento/peregrine/lib/util';
6
7
  import mergeOperations from '../../util/shallowMerge';
7
8
  import DEFAULT_OPERATIONS from './storeSwitcher.gql';
@@ -13,11 +14,8 @@ const mapAvailableOptions = (config, stores) => {
13
14
 
14
15
  return stores.reduce((map, store) => {
15
16
  const {
16
- category_url_suffix,
17
- store_code: code,
18
17
  default_display_currency_code: currency,
19
18
  locale,
20
- product_url_suffix,
21
19
  secure_base_media_url,
22
20
  store_code: storeCode,
23
21
  store_group_code: storeGroupCode,
@@ -26,14 +24,11 @@ const mapAvailableOptions = (config, stores) => {
26
24
  store_sort_order: sortOrder
27
25
  } = store;
28
26
 
29
- const isCurrent = code === configCode;
27
+ const isCurrent = storeCode === configCode;
30
28
  const option = {
31
- category_url_suffix,
32
- code,
33
29
  currency,
34
30
  isCurrent,
35
31
  locale,
36
- product_url_suffix,
37
32
  secure_base_media_url,
38
33
  sortOrder,
39
34
  storeCode,
@@ -42,14 +37,15 @@ const mapAvailableOptions = (config, stores) => {
42
37
  storeName
43
38
  };
44
39
 
45
- return map.set(code, option);
40
+ return map.set(storeCode, option);
46
41
  }, new Map());
47
42
  };
48
43
 
49
44
  /**
50
45
  * The useStoreSwitcher talon complements the StoreSwitcher component.
51
46
  *
52
- * @param {*} props.getStoreConfig the store switcher data getStoreConfig
47
+ * @param {Array<Object>} [props.availableRoutes] - Hardcoded app routes.
48
+ * @param {Object} [props.operations] - GraphQL operations to be run by the hook.
53
49
  *
54
50
  * @returns {Map} talonProps.availableStores - Details about the available store views.
55
51
  * @returns {String} talonProps.currentStoreName - Name of the current store view.
@@ -62,12 +58,21 @@ const mapAvailableOptions = (config, stores) => {
62
58
 
63
59
  export const useStoreSwitcher = (props = {}) => {
64
60
  const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations);
61
+ const { availableRoutes = [] } = props;
62
+ const internalRoutes = useMemo(() => {
63
+ return availableRoutes.map(path => {
64
+ if (path.exact) {
65
+ return path.pattern;
66
+ }
67
+ });
68
+ }, [availableRoutes]);
69
+
65
70
  const {
66
71
  getStoreConfigData,
67
72
  getRouteData,
68
73
  getAvailableStoresData
69
74
  } = operations;
70
- const { pathname } = useLocation();
75
+ const { pathname, search: searchParams } = useLocation();
71
76
  const {
72
77
  elementRef: storeMenuRef,
73
78
  expanded: storeMenuIsOpen,
@@ -80,10 +85,7 @@ export const useStoreSwitcher = (props = {}) => {
80
85
  nextFetchPolicy: 'cache-first'
81
86
  });
82
87
 
83
- const { data: routeData } = useQuery(getRouteData, {
84
- fetchPolicy: 'cache-first',
85
- variables: { url: pathname }
86
- });
88
+ const fetchRouteData = useAwaitQuery(getRouteData);
87
89
 
88
90
  const { data: availableStoresData } = useQuery(getAvailableStoresData, {
89
91
  fetchPolicy: 'cache-and-network',
@@ -102,18 +104,6 @@ export const useStoreSwitcher = (props = {}) => {
102
104
  }
103
105
  }, [storeConfigData]);
104
106
 
105
- const currentStoreCode = useMemo(() => {
106
- if (storeConfigData) {
107
- return storeConfigData.storeConfig.store_code;
108
- }
109
- }, [storeConfigData]);
110
-
111
- const pageType = useMemo(() => {
112
- if (routeData && routeData.route) {
113
- return routeData.route.type;
114
- }
115
- }, [routeData]);
116
-
117
107
  // availableStores => mapped options or empty map if undefined.
118
108
  const availableStores = useMemo(() => {
119
109
  return (
@@ -146,51 +136,35 @@ export const useStoreSwitcher = (props = {}) => {
146
136
  return groups;
147
137
  }, [availableStores]);
148
138
 
149
- // Get pathname with suffix based on page type
150
139
  const getPathname = useCallback(
151
- storeCode => {
152
- // Use globalThis.location.pathname to get the path with the store view code
153
- // pathname from useLocation() does not include the store view code
154
- const pathname = globalThis.location.pathname;
155
-
156
- if (pageType === 'CATEGORY') {
157
- const currentSuffix =
158
- availableStores.get(currentStoreCode).category_url_suffix ||
159
- '';
160
- const newSuffix =
161
- availableStores.get(storeCode).category_url_suffix || '';
162
-
163
- return currentSuffix
164
- ? pathname.replace(currentSuffix, newSuffix)
165
- : `${pathname}${newSuffix}`;
166
- }
167
- if (pageType === 'PRODUCT') {
168
- const currentSuffix =
169
- availableStores.get(currentStoreCode).product_url_suffix ||
170
- '';
171
- const newSuffix =
172
- availableStores.get(storeCode).product_url_suffix || '';
173
-
174
- return currentSuffix
175
- ? pathname.replace(currentSuffix, newSuffix)
176
- : `${pathname}${newSuffix}`;
140
+ async storeCode => {
141
+ if (pathname === '' || pathname === '/') return '';
142
+ let newPath = '';
143
+ if (internalRoutes.includes(pathname)) {
144
+ newPath = pathname;
145
+ } else {
146
+ const { data: routeData } = await fetchRouteData({
147
+ fetchPolicy: 'no-cache',
148
+ variables: {
149
+ url: pathname
150
+ },
151
+ context: { headers: { store: storeCode } }
152
+ });
153
+ if (routeData.route) {
154
+ newPath = routeData.route.relative_url;
155
+ }
177
156
  }
178
-
179
- // search.html ...etc
180
- return pathname;
157
+ return newPath.startsWith('/') ? newPath.substr(1) : newPath;
181
158
  },
182
- [availableStores, currentStoreCode, pageType]
159
+ [pathname, fetchRouteData, internalRoutes]
183
160
  );
184
161
 
185
162
  const handleSwitchStore = useCallback(
186
163
  // Change store view code and currency to be used in Apollo link request headers
187
- storeCode => {
164
+ async storeCode => {
188
165
  // Do nothing when store view is not present in available stores
189
166
  if (!availableStores.has(storeCode)) return;
190
167
 
191
- const pathName = getPathname(storeCode);
192
- const params = globalThis.location.search || '';
193
-
194
168
  storage.setItem('store_view_code', storeCode);
195
169
  storage.setItem(
196
170
  'store_view_currency',
@@ -200,43 +174,16 @@ export const useStoreSwitcher = (props = {}) => {
200
174
  'store_view_secure_base_media_url',
201
175
  availableStores.get(storeCode).secure_base_media_url
202
176
  );
177
+ const pathName = await getPathname(storeCode);
178
+ const newPath = pathName ? `/${pathName}${searchParams}` : '';
203
179
 
204
- // Handle updating the URL if the store code should be present.
205
- // In this block we use `globalThis.location.assign` to work around the
206
- // static React Router basename, which is changed on initialization.
207
180
  if (process.env.USE_STORE_CODE_IN_URL === 'true') {
208
- // Check to see if we're on a page outside of the homepage
209
- if (pathName !== '' && pathName !== '/') {
210
- const [, pathStoreCode] = pathName.split('/');
211
-
212
- // If the current store code is in the url, replace it with
213
- // the new one.
214
- if (
215
- availableStores.has(pathStoreCode) &&
216
- availableStores.get(pathStoreCode).isCurrent
217
- ) {
218
- const newPath = `${pathName.replace(
219
- `/${pathStoreCode}`,
220
- `/${storeCode}`
221
- )}${params}`;
222
-
223
- globalThis.location.assign(newPath);
224
- } else {
225
- // Otherwise include it and reload.
226
- const newPath = `/${storeCode}${pathName}${params}`;
227
-
228
- globalThis.location.assign(newPath);
229
- }
230
- } else {
231
- globalThis.location.assign(`/${storeCode}`);
232
- }
181
+ globalThis.location.assign(`/${storeCode}${newPath || ''}`);
233
182
  } else {
234
- // Refresh the page to re-trigger the queries once code/currency
235
- // are saved in local storage.
236
- globalThis.location.assign(`${pathName}${params}`);
183
+ globalThis.location.assign(`${newPath || '/'}`);
237
184
  }
238
185
  },
239
- [availableStores, getPathname]
186
+ [availableStores, getPathname, searchParams]
240
187
  );
241
188
 
242
189
  const handleTriggerClick = useCallback(() => {
@@ -109,6 +109,13 @@ export const useMagentoRoute = (props = {}) => {
109
109
  } else if (routeError) {
110
110
  // ERROR
111
111
  routeData = { hasError: true, routeError };
112
+ } else if (empty && fetchedPathname.current === pathname && !loading) {
113
+ // NOT FOUND
114
+ routeData = { isNotFound: true };
115
+ } else if (nextRootComponent) {
116
+ // LOADING with full page shimmer
117
+ showPageLoader = true;
118
+ routeData = { isLoading: true, shimmer: nextRootComponent };
112
119
  } else if (redirect) {
113
120
  // REDIRECT
114
121
  routeData = {
@@ -117,13 +124,6 @@ export const useMagentoRoute = (props = {}) => {
117
124
  ? relative_url
118
125
  : '/' + relative_url
119
126
  };
120
- } else if (empty && fetchedPathname.current === pathname && !loading) {
121
- // NOT FOUND
122
- routeData = { isNotFound: true };
123
- } else if (nextRootComponent) {
124
- // LOADING with full page shimmer
125
- showPageLoader = true;
126
- routeData = { isLoading: true, shimmer: nextRootComponent };
127
127
  } else {
128
128
  // LOADING
129
129
  const isInitialLoad = !isInitialized;
@@ -10,6 +10,7 @@ export const ProductListFragment = gql`
10
10
  product {
11
11
  uid
12
12
  name
13
+ sku
13
14
  url_key
14
15
  thumbnail {
15
16
  url
@@ -36,6 +37,9 @@ export const ProductListFragment = gql`
36
37
  currency
37
38
  value
38
39
  }
40
+ total_item_discount {
41
+ value
42
+ }
39
43
  }
40
44
  quantity
41
45
  # eslint-disable-next-line @graphql-eslint/require-id-when-available
@@ -1,4 +1,4 @@
1
- import { useCallback, useMemo } from 'react';
1
+ import { useCallback, useEffect, useMemo } from 'react';
2
2
  import { useHistory } from 'react-router-dom';
3
3
  import { useQuery, useMutation } from '@apollo/client';
4
4
 
@@ -6,9 +6,11 @@ import { useCartContext } from '../../context/cart';
6
6
  import { deriveErrorMessage } from '../../util/deriveErrorMessage';
7
7
  import mergeOperations from '../../util/shallowMerge';
8
8
  import DEFAULT_OPERATIONS from './miniCart.gql';
9
+ import { useEventingContext } from '../../context/eventing';
9
10
 
10
11
  /**
11
12
  *
13
+ * @param {Boolean} props.isOpen - True if the mini cart is open
12
14
  * @param {Function} props.setIsOpen - Function to toggle the mini cart
13
15
  * @param {DocumentNode} props.operations.miniCartQuery - Query to fetch mini cart data
14
16
  * @param {DocumentNode} props.operations.removeItemMutation - Mutation to remove an item from cart
@@ -27,7 +29,9 @@ import DEFAULT_OPERATIONS from './miniCart.gql';
27
29
  * }
28
30
  */
29
31
  export const useMiniCart = props => {
30
- const { setIsOpen } = props;
32
+ const { isOpen, setIsOpen } = props;
33
+
34
+ const [, { dispatch }] = useEventingContext();
31
35
 
32
36
  const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations);
33
37
  const {
@@ -106,11 +110,38 @@ export const useMiniCart = props => {
106
110
  itemId: id
107
111
  }
108
112
  });
113
+
114
+ const [product] = productList.filter(
115
+ p => (p.uid || p.id) === id
116
+ );
117
+
118
+ const selectedOptionsLabels =
119
+ product.configurable_options?.map(
120
+ ({ option_label, value_label }) => ({
121
+ attribute: option_label,
122
+ value: value_label
123
+ })
124
+ ) || null;
125
+
126
+ dispatch({
127
+ type: 'CART_REMOVE_ITEM',
128
+ payload: {
129
+ cartId,
130
+ sku: product.product.sku,
131
+ name: product.product.name,
132
+ priceTotal: product.prices.price.value,
133
+ currencyCode: product.prices.price.currency,
134
+ discountAmount:
135
+ product.prices.total_item_discount.value,
136
+ selectedOptions: selectedOptionsLabels,
137
+ quantity: product.quantity
138
+ }
139
+ });
109
140
  } catch (e) {
110
141
  // Error is logged by apollo link - no need to double log.
111
142
  }
112
143
  },
113
- [cartId, removeItem]
144
+ [removeItem, cartId, dispatch, productList]
114
145
  );
115
146
 
116
147
  const handleProceedToCheckout = useCallback(() => {
@@ -128,6 +159,18 @@ export const useMiniCart = props => {
128
159
  [removeItemError]
129
160
  );
130
161
 
162
+ useEffect(() => {
163
+ if (isOpen) {
164
+ dispatch({
165
+ type: 'MINI_CART_VIEW',
166
+ payload: {
167
+ cartId: cartId,
168
+ products: productList
169
+ }
170
+ });
171
+ }
172
+ }, [isOpen, cartId, productList, dispatch]);
173
+
131
174
  return {
132
175
  closeMiniCart,
133
176
  errorMessage: derivedErrorMessage,
@@ -11,6 +11,7 @@ import { isSupportedProductType as isSupported } from '@magento/peregrine/lib/ut
11
11
  import { deriveErrorMessage } from '../../util/deriveErrorMessage';
12
12
  import mergeOperations from '../../util/shallowMerge';
13
13
  import defaultOperations from './productFullDetail.gql';
14
+ import { useEventingContext } from '../../context/eventing';
14
15
 
15
16
  const INITIAL_OPTION_CODES = new Map();
16
17
  const INITIAL_OPTION_SELECTIONS = new Map();
@@ -155,7 +156,7 @@ const getConfigPrice = (product, optionCodes, optionSelections) => {
155
156
  0;
156
157
 
157
158
  if (!isConfigurable || !optionsSelected) {
158
- value = product.price_range?.maximum_price?.final_price;
159
+ value = product.price_range?.maximum_price;
159
160
  } else {
160
161
  const item = findMatchingVariant({
161
162
  optionCodes,
@@ -164,8 +165,8 @@ const getConfigPrice = (product, optionCodes, optionSelections) => {
164
165
  });
165
166
 
166
167
  value = item
167
- ? item.product.price_range?.maximum_price?.final_price
168
- : product.price_range?.maximum_price?.final_price;
168
+ ? item.product.price_range?.maximum_price
169
+ : product.price_range?.maximum_price;
169
170
  }
170
171
 
171
172
  return value;
@@ -229,6 +230,8 @@ export const useProductFullDetail = props => {
229
230
  product
230
231
  } = props;
231
232
 
233
+ const [, { dispatch }] = useEventingContext();
234
+
232
235
  const hasDeprecatedOperationProp = !!(
233
236
  addConfigurableProductToCartMutation || addSimpleProductToCartMutation
234
237
  );
@@ -314,6 +317,11 @@ export const useProductFullDetail = props => {
314
317
  [product, optionCodes, optionSelections]
315
318
  );
316
319
 
320
+ const productPrice = useMemo(
321
+ () => getConfigPrice(product, optionCodes, optionSelections),
322
+ [product, optionCodes, optionSelections]
323
+ );
324
+
317
325
  // The map of ids to values (and their uids)
318
326
  // For example:
319
327
  // { "179" => [{ uid: "abc", value_index: 1 }, { uid: "def", value_index: 2 }]}
@@ -421,6 +429,29 @@ export const useProductFullDetail = props => {
421
429
 
422
430
  try {
423
431
  await addProductToCart({ variables });
432
+
433
+ const selectedOptionsLabels =
434
+ selectedOptionsArray?.map((uid, i) => ({
435
+ attribute: product.configurable_options[i].label,
436
+ value:
437
+ product.configurable_options[i].values.findLast(
438
+ x => x.uid === uid
439
+ )?.label || null
440
+ })) || null;
441
+
442
+ dispatch({
443
+ type: 'CART_ADD_ITEM',
444
+ payload: {
445
+ cartId,
446
+ sku: product.sku,
447
+ name: product.name,
448
+ priceTotal: productPrice.final_price.value,
449
+ currencyCode: productPrice.final_price.currency,
450
+ discountAmount: productPrice.discount.amount_off,
451
+ selectedOptions: selectedOptionsLabels,
452
+ quantity
453
+ }
454
+ });
424
455
  } catch {
425
456
  return;
426
457
  }
@@ -431,11 +462,13 @@ export const useProductFullDetail = props => {
431
462
  addProductToCart,
432
463
  addSimpleProductToCart,
433
464
  cartId,
465
+ dispatch,
434
466
  hasDeprecatedOperationProp,
435
467
  isSupportedProductType,
436
468
  optionCodes,
437
469
  optionSelections,
438
470
  product,
471
+ productPrice,
439
472
  productType,
440
473
  selectedOptionsArray
441
474
  ]
@@ -452,17 +485,12 @@ export const useProductFullDetail = props => {
452
485
  [optionSelections]
453
486
  );
454
487
 
455
- const productPrice = useMemo(
456
- () => getConfigPrice(product, optionCodes, optionSelections),
457
- [product, optionCodes, optionSelections]
458
- );
459
-
460
488
  // Normalization object for product details we need for rendering.
461
489
  const productDetails = {
462
490
  description: product.description,
463
491
  shortDescription: product.short_description,
464
492
  name: product.name,
465
- price: productPrice,
493
+ price: productPrice?.final_price,
466
494
  sku: product.sku
467
495
  };
468
496
 
@@ -27,6 +27,8 @@ export const GET_CATEGORY_CONTENT = gql`
27
27
  uid
28
28
  name
29
29
  description
30
+ url_key
31
+ url_path
30
32
  }
31
33
  }
32
34
  }
@@ -26,6 +26,9 @@ export const ProductsFragment = gql`
26
26
  currency
27
27
  value
28
28
  }
29
+ discount {
30
+ amount_off
31
+ }
29
32
  }
30
33
  }
31
34
  sku
@@ -200,6 +200,8 @@ export const useCategory = props => {
200
200
  }, [currentSort, previousSearch, search, setCurrentPage]);
201
201
 
202
202
  const categoryData = categoryLoading && !data ? null : data;
203
+ const categoryNotFound =
204
+ !categoryLoading && data && data.categories.items.length === 0;
203
205
  const metaDescription =
204
206
  data &&
205
207
  data.categories.items[0] &&
@@ -222,6 +224,7 @@ export const useCategory = props => {
222
224
  metaDescription,
223
225
  pageControl,
224
226
  sortProps,
225
- pageSize
227
+ pageSize,
228
+ categoryNotFound
226
229
  };
227
230
  };
@@ -2,6 +2,7 @@ import { useEffect } from 'react';
2
2
  import { useLazyQuery, useQuery } from '@apollo/client';
3
3
 
4
4
  import mergeOperations from '../../../util/shallowMerge';
5
+ import { useEventingContext } from '../../../context/eventing';
5
6
 
6
7
  import DEFAULT_OPERATIONS from './categoryContent.gql';
7
8
 
@@ -46,14 +47,19 @@ export const useCategoryContent = props => {
46
47
  }
47
48
  );
48
49
 
49
- const { data: categoryData } = useQuery(getCategoryContentQuery, {
50
- fetchPolicy: 'cache-and-network',
51
- nextFetchPolicy: 'cache-first',
52
- skip: !categoryId,
53
- variables: {
54
- id: categoryId
50
+ const { data: categoryData, loading: categoryLoading } = useQuery(
51
+ getCategoryContentQuery,
52
+ {
53
+ fetchPolicy: 'cache-and-network',
54
+ nextFetchPolicy: 'cache-first',
55
+ skip: !categoryId,
56
+ variables: {
57
+ id: categoryId
58
+ }
55
59
  }
56
- });
60
+ );
61
+
62
+ const [, { dispatch }] = useEventingContext();
57
63
 
58
64
  useEffect(() => {
59
65
  if (categoryId) {
@@ -85,16 +91,32 @@ export const useCategoryContent = props => {
85
91
  ? data.products.page_info.total_pages
86
92
  : null;
87
93
  const totalCount = data ? data.products.total_count : null;
88
- const categoryName = categoryData
89
- ? categoryData.categories.items[0].name
90
- : null;
91
- const categoryDescription = categoryData
92
- ? categoryData.categories.items[0].description
93
- : null;
94
+ const categoryName =
95
+ categoryData && categoryData.categories.items.length
96
+ ? categoryData.categories.items[0].name
97
+ : null;
98
+ const categoryDescription =
99
+ categoryData && categoryData.categories.items.length
100
+ ? categoryData.categories.items[0].description
101
+ : null;
94
102
  const availableSortMethods = sortData
95
103
  ? sortData.products.sort_fields.options
96
104
  : null;
97
105
 
106
+ useEffect(() => {
107
+ if (!categoryLoading && categoryData.categories.items.length > 0) {
108
+ dispatch({
109
+ type: 'CATEGORY_PAGE_VIEW',
110
+ payload: {
111
+ id: categoryData.categories.items[0].uid,
112
+ name: categoryData.categories.items[0].name,
113
+ url_key: categoryData.categories.items[0].url_key,
114
+ url_path: categoryData.categories.items[0].url_path
115
+ }
116
+ });
117
+ }
118
+ }, [categoryData, dispatch, categoryLoading]);
119
+
98
120
  return {
99
121
  availableSortMethods,
100
122
  categoryName,
@@ -42,6 +42,9 @@ export const ProductDetailsFragment = gql`
42
42
  currency
43
43
  value
44
44
  }
45
+ discount {
46
+ amount_off
47
+ }
45
48
  }
46
49
  }
47
50
  sku
@@ -136,6 +139,9 @@ export const ProductDetailsFragment = gql`
136
139
  currency
137
140
  value
138
141
  }
142
+ discount {
143
+ amount_off
144
+ }
139
145
  }
140
146
  }
141
147
  custom_attributes {
@@ -5,6 +5,7 @@ import { useAppContext } from '@magento/peregrine/lib/context/app';
5
5
 
6
6
  import mergeOperations from '../../../util/shallowMerge';
7
7
  import DEFAULT_OPERATIONS from './product.gql';
8
+ import { useEventingContext } from '../../../context/eventing';
8
9
 
9
10
  /**
10
11
  * A [React Hook]{@link https://reactjs.org/docs/hooks-intro.html} that
@@ -76,11 +77,37 @@ export const useProduct = props => {
76
77
  return mapProduct(product);
77
78
  }, [data, mapProduct, urlKey]);
78
79
 
80
+ const [, { dispatch }] = useEventingContext();
81
+
79
82
  // Update the page indicator if the GraphQL query is in flight.
80
83
  useEffect(() => {
81
84
  setPageLoading(isBackgroundLoading);
82
85
  }, [isBackgroundLoading, setPageLoading]);
83
86
 
87
+ useEffect(() => {
88
+ if (!error && !loading && product) {
89
+ dispatch({
90
+ type: 'PRODUCT_PAGE_VIEW',
91
+ payload: {
92
+ id: product.id,
93
+ name: product.name,
94
+ sku: product.sku,
95
+ currency_code:
96
+ product?.price_range?.maximum_price?.final_price
97
+ ?.currency,
98
+ price_range: {
99
+ maximum_price: {
100
+ final_price:
101
+ product?.price_range?.maximum_price?.final_price
102
+ ?.value
103
+ }
104
+ },
105
+ url_key: product.url_key
106
+ }
107
+ });
108
+ }
109
+ }, [error, loading, product, dispatch]);
110
+
84
111
  return {
85
112
  error,
86
113
  loading,
@@ -3,3 +3,4 @@ export { useSearchBar } from './useSearchBar';
3
3
  export { useSearchField } from './useSearchField';
4
4
  export { useSuggestedCategory } from './useSuggestedCategory';
5
5
  export { useSuggestions } from './useSuggestions';
6
+ export { useSuggestedProduct } from './useSuggestedProduct';