@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
@@ -5,6 +5,7 @@ import {
5
5
  useMutation,
6
6
  useQuery
7
7
  } from '@apollo/client';
8
+ import { useEventingContext } from '../../context/eventing';
8
9
 
9
10
  import { useUserContext } from '../../context/user';
10
11
  import { useCartContext } from '../../context/cart';
@@ -142,7 +143,7 @@ export const useCheckoutPage = (props = {}) => {
142
143
  });
143
144
 
144
145
  const cartItems = useMemo(() => {
145
- return (checkoutData && checkoutData.cart.items) || [];
146
+ return (checkoutData && checkoutData?.cart?.items) || [];
146
147
  }, [checkoutData]);
147
148
 
148
149
  /**
@@ -240,6 +241,8 @@ export const useCheckoutPage = (props = {}) => {
240
241
  setIsPlacingOrder(true);
241
242
  }, [cartId, getOrderDetails]);
242
243
 
244
+ const [, { dispatch }] = useEventingContext();
245
+
243
246
  // Go back to checkout if shopper logs in
244
247
  useEffect(() => {
245
248
  if (isSignedIn) {
@@ -290,10 +293,83 @@ export const useCheckoutPage = (props = {}) => {
290
293
  isPlacingOrder
291
294
  ]);
292
295
 
296
+ useEffect(() => {
297
+ if (
298
+ checkoutStep === CHECKOUT_STEP.SHIPPING_ADDRESS &&
299
+ cartItems.length
300
+ ) {
301
+ dispatch({
302
+ type: 'CHECKOUT_PAGE_VIEW',
303
+ payload: {
304
+ cart_id: cartId,
305
+ products: cartItems
306
+ }
307
+ });
308
+ } else if (reviewOrderButtonClicked) {
309
+ dispatch({
310
+ type: 'CHECKOUT_REVIEW_BUTTON_CLICKED',
311
+ payload: {
312
+ cart_id: cartId
313
+ }
314
+ });
315
+ } else if (
316
+ placeOrderButtonClicked &&
317
+ orderDetailsData &&
318
+ orderDetailsData.cart
319
+ ) {
320
+ const shipping =
321
+ orderDetailsData.cart?.shipping_addresses &&
322
+ orderDetailsData.cart.shipping_addresses.reduce(
323
+ (result, item) => {
324
+ return [
325
+ ...result,
326
+ {
327
+ ...item.selected_shipping_method
328
+ }
329
+ ];
330
+ },
331
+ []
332
+ );
333
+ const eventPayload = {
334
+ cart_id: cartId,
335
+ amount: orderDetailsData.cart.prices,
336
+ shipping: shipping,
337
+ payment: orderDetailsData.cart.selected_payment_method,
338
+ products: orderDetailsData.cart.items
339
+ };
340
+ if (isPlacingOrder) {
341
+ dispatch({
342
+ type: 'CHECKOUT_PLACE_ORDER_BUTTON_CLICKED',
343
+ payload: eventPayload
344
+ });
345
+ } else if (placeOrderData && orderDetailsData?.cart.id === cartId) {
346
+ dispatch({
347
+ type: 'ORDER_CONFIRMATION_PAGE_VIEW',
348
+ payload: {
349
+ order_number:
350
+ placeOrderData.placeOrder.order.order_number,
351
+ ...eventPayload
352
+ }
353
+ });
354
+ }
355
+ }
356
+ }, [
357
+ placeOrderButtonClicked,
358
+ cartId,
359
+ checkoutStep,
360
+ orderDetailsData,
361
+ cartItems,
362
+ isLoading,
363
+ dispatch,
364
+ placeOrderData,
365
+ isPlacingOrder,
366
+ reviewOrderButtonClicked
367
+ ]);
368
+
293
369
  return {
294
370
  activeContent,
295
371
  availablePaymentMethods: checkoutData
296
- ? checkoutData.cart.available_payment_methods
372
+ ? checkoutData?.cart?.available_payment_methods
297
373
  : null,
298
374
  cartItems,
299
375
  checkoutStep,
@@ -302,7 +378,7 @@ export const useCheckoutPage = (props = {}) => {
302
378
  guestSignInUsername,
303
379
  handlePlaceOrder,
304
380
  hasError: !!checkoutError,
305
- isCartEmpty: !(checkoutData && checkoutData.cart.total_quantity),
381
+ isCartEmpty: !(checkoutData && checkoutData?.cart?.total_quantity),
306
382
  isGuestCheckout: !isSignedIn,
307
383
  isLoading,
308
384
  isUpdating,
@@ -3,6 +3,7 @@ import { useQuery } from '@apollo/client';
3
3
 
4
4
  import mergeOperations from '../../util/shallowMerge';
5
5
  import { useAppContext } from '../../context/app';
6
+ import { useEventingContext } from '../../context/eventing';
6
7
 
7
8
  import DEFAULT_OPERATIONS from './cmsPage.gql';
8
9
 
@@ -19,6 +20,7 @@ export const useCmsPage = props => {
19
20
 
20
21
  const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations);
21
22
  const { getCMSPageQuery } = operations;
23
+ const [, { dispatch }] = useEventingContext();
22
24
 
23
25
  const { loading, data } = useQuery(getCMSPageQuery, {
24
26
  variables: {
@@ -51,6 +53,18 @@ export const useCmsPage = props => {
51
53
 
52
54
  const cmsPage = data ? data.cmsPage : null;
53
55
 
56
+ useEffect(() => {
57
+ if (!loading && cmsPage) {
58
+ dispatch({
59
+ type: 'CMS_PAGE_VIEW',
60
+ payload: {
61
+ url_key: cmsPage.url_key,
62
+ title: cmsPage.title
63
+ }
64
+ });
65
+ }
66
+ }, [loading, cmsPage, dispatch]);
67
+
54
68
  return {
55
69
  cmsPage,
56
70
  shouldShowLoadingIndicator
@@ -9,6 +9,7 @@ import { retrieveCartId } from '../../store/actions/cart';
9
9
  import { useGoogleReCaptcha } from '../../hooks/useGoogleReCaptcha';
10
10
 
11
11
  import DEFAULT_OPERATIONS from './createAccount.gql';
12
+ import { useEventingContext } from '../../context/eventing';
12
13
 
13
14
  /**
14
15
  * Returns props necessary to render CreateAccount component. In particular this
@@ -49,6 +50,8 @@ export const useCreateAccount = props => {
49
50
  { getUserDetails, setToken }
50
51
  ] = useUserContext();
51
52
 
53
+ const [, { dispatch }] = useEventingContext();
54
+
52
55
  const [fetchCartId] = useMutation(createCartMutation);
53
56
 
54
57
  const [mergeCarts] = useMutation(mergeCartsMutation);
@@ -104,6 +107,16 @@ export const useCreateAccount = props => {
104
107
  ...recaptchaDataForCreateAccount
105
108
  });
106
109
 
110
+ dispatch({
111
+ type: 'USER_CREATE_ACCOUNT',
112
+ payload: {
113
+ email: formValues.customer.email,
114
+ firstName: formValues.customer.firstname,
115
+ lastName: formValues.customer.lastname,
116
+ isSubscribed: !!formValues.subscribe
117
+ }
118
+ });
119
+
107
120
  // Get reCaptchaV3 Data for signIn mutation
108
121
  const recaptchaDataForSignIn = await generateReCaptchaData();
109
122
 
@@ -169,7 +182,8 @@ export const useCreateAccount = props => {
169
182
  fetchUserDetails,
170
183
  getCartDetails,
171
184
  fetchCartDetails,
172
- onSubmit
185
+ onSubmit,
186
+ dispatch
173
187
  ]
174
188
  );
175
189
 
@@ -111,10 +111,16 @@ export const getFiltersFromSearch = initialValue => {
111
111
  export const sortFiltersArray = initialArray => {
112
112
  return initialArray.sort((a, b) => {
113
113
  // Place Category filter first
114
- if (a['attribute_code'] === 'category_id') {
114
+ if (
115
+ a['attribute_code'] === 'category_id' ||
116
+ a['attribute_code'] === 'category_uid'
117
+ ) {
115
118
  return -1;
116
119
  }
117
- if (b['attribute_code'] === 'category_id') {
120
+ if (
121
+ b['attribute_code'] === 'category_id' ||
122
+ b['attribute_code'] === 'category_uid'
123
+ ) {
118
124
  return 1;
119
125
  }
120
126
 
@@ -55,6 +55,7 @@ export const useFilterModal = props => {
55
55
  // Disable category filtering when not on a search page.
56
56
  if (pathname !== '/search.html') {
57
57
  disabled.add('category_id');
58
+ disabled.add('category_uid');
58
59
  }
59
60
 
60
61
  return disabled;
@@ -45,6 +45,7 @@ export const useFilterSidebar = props => {
45
45
  // Disable category filtering when not on a search page.
46
46
  if (pathname !== '/search.html') {
47
47
  disabled.add('category_id');
48
+ disabled.add('category_uid');
48
49
  }
49
50
 
50
51
  return disabled;
@@ -3,6 +3,7 @@ import { useMutation } from '@apollo/client';
3
3
  import { useHistory } from 'react-router-dom';
4
4
 
5
5
  import { useCartContext } from '../../context/cart';
6
+ import { useEventingContext } from '../../context/eventing';
6
7
  import operations from './addToCart.gql';
7
8
 
8
9
  /**
@@ -30,6 +31,8 @@ const UNSUPPORTED_PRODUCT_TYPES = [
30
31
  export const useAddToCartButton = props => {
31
32
  const { item, urlSuffix } = props;
32
33
 
34
+ const [, { dispatch }] = useEventingContext();
35
+
33
36
  const [isLoading, setIsLoading] = useState(false);
34
37
 
35
38
  const isInStock = item.stock_status === 'IN_STOCK';
@@ -51,11 +54,13 @@ export const useAddToCartButton = props => {
51
54
  if (productType === 'SimpleProduct') {
52
55
  setIsLoading(true);
53
56
 
57
+ const quantity = 1;
58
+
54
59
  await addToCart({
55
60
  variables: {
56
61
  cartId,
57
62
  cartItem: {
58
- quantity: 1,
63
+ quantity,
59
64
  entered_options: [
60
65
  {
61
66
  uid: item.uid,
@@ -67,6 +72,23 @@ export const useAddToCartButton = props => {
67
72
  }
68
73
  });
69
74
 
75
+ dispatch({
76
+ type: 'CART_ADD_ITEM',
77
+ payload: {
78
+ cartId,
79
+ sku: item.sku,
80
+ name: item.name,
81
+ priceTotal:
82
+ item.price_range.maximum_price.final_price.value,
83
+ currencyCode:
84
+ item.price_range.maximum_price.final_price.currency,
85
+ discountAmount:
86
+ item.price_range.maximum_price.discount.amount_off,
87
+ selectedOptions: null,
88
+ quantity
89
+ }
90
+ });
91
+
70
92
  setIsLoading(false);
71
93
  } else if (productType === 'ConfigurableProduct') {
72
94
  history.push(`${item.url_key}${urlSuffix || ''}`);
@@ -76,17 +98,7 @@ export const useAddToCartButton = props => {
76
98
  } catch (error) {
77
99
  console.error(error);
78
100
  }
79
- }, [
80
- addToCart,
81
- cartId,
82
- history,
83
- item.sku,
84
- item.url_key,
85
- productType,
86
- item.uid,
87
- item.name,
88
- urlSuffix
89
- ]);
101
+ }, [productType, addToCart, cartId, item, dispatch, history, urlSuffix]);
90
102
 
91
103
  return {
92
104
  handleAddToCart,
@@ -1,8 +1,86 @@
1
1
  import { isSupportedProductType as isSupported } from '@magento/peregrine/lib/util/isSupportedProductType';
2
+ import { useEventingContext } from '@magento/peregrine/lib/context/eventing';
3
+ import { useCallback, useEffect, useRef } from 'react';
4
+ import useIntersectionObserver from '@magento/peregrine/lib/hooks/useIntersectionObserver';
2
5
 
3
6
  export const useGalleryItem = (props = {}) => {
7
+ const [, { dispatch }] = useEventingContext();
8
+ const intersectionObserver = useIntersectionObserver();
4
9
  const { item, storeConfig } = props;
5
10
 
11
+ const finalPrice = item?.price_range?.maximum_price?.final_price?.value;
12
+ const discountAmount =
13
+ item?.price_range?.maximum_price?.discount?.amount_off;
14
+ const currencyCode =
15
+ item?.price_range?.maximum_price?.final_price?.currency;
16
+
17
+ const handleLinkClick = useCallback(() => {
18
+ dispatch({
19
+ type: 'PRODUCT_CLICK',
20
+ payload: {
21
+ name: item.name,
22
+ sku: item.sku,
23
+ priceTotal: finalPrice,
24
+ discountAmount,
25
+ currencyCode,
26
+ selectedOptions: null
27
+ }
28
+ });
29
+ }, [currencyCode, discountAmount, dispatch, finalPrice, item]);
30
+
31
+ const itemRef = useRef(null);
32
+ const contextRef = useRef({
33
+ dispatched: false,
34
+ timeOutId: null
35
+ });
36
+ useEffect(() => {
37
+ if (
38
+ typeof intersectionObserver === 'undefined' ||
39
+ !item ||
40
+ contextRef.current.dispatched
41
+ ) {
42
+ return;
43
+ }
44
+ const htmlElement = itemRef.current;
45
+ const onIntersection = entries => {
46
+ if (entries[0].isIntersecting) {
47
+ contextRef.current.timeOutId = setTimeout(() => {
48
+ observer.unobserve(htmlElement);
49
+ dispatch({
50
+ type: 'PRODUCT_IMPRESSION',
51
+ payload: {
52
+ name: item.name,
53
+ sku: item.sku,
54
+ priceTotal: finalPrice,
55
+ discountAmount,
56
+ currencyCode,
57
+ selectedOptions: null
58
+ }
59
+ });
60
+ contextRef.current.dispatched = true;
61
+ }, 500);
62
+ } else {
63
+ clearTimeout(contextRef.current.timeOutId);
64
+ }
65
+ };
66
+ const observer = new intersectionObserver(onIntersection, {
67
+ threshold: 0.9
68
+ });
69
+ observer.observe(htmlElement);
70
+ return () => {
71
+ if (htmlElement) {
72
+ observer.unobserve(htmlElement);
73
+ }
74
+ };
75
+ }, [
76
+ currencyCode,
77
+ discountAmount,
78
+ dispatch,
79
+ finalPrice,
80
+ intersectionObserver,
81
+ item
82
+ ]);
83
+
6
84
  const productType = item ? item.__typename : null;
7
85
 
8
86
  const isSupportedProductType = isSupported(productType);
@@ -18,5 +96,11 @@ export const useGalleryItem = (props = {}) => {
18
96
  }
19
97
  : null;
20
98
 
21
- return { ...props, wishlistButtonProps, isSupportedProductType };
99
+ return {
100
+ ...props,
101
+ itemRef,
102
+ handleLinkClick,
103
+ wishlistButtonProps,
104
+ isSupportedProductType
105
+ };
22
106
  };
@@ -14,20 +14,7 @@ export const GET_STORE_CONFIG_DATA = gql`
14
14
  export const GET_ROUTE_DATA = gql`
15
15
  query getRouteData($url: String!) {
16
16
  route(url: $url) {
17
- type
18
- # eslint-disable-next-line @graphql-eslint/require-id-when-available
19
- ... on CmsPage {
20
- identifier
21
- }
22
- # eslint-disable-next-line @graphql-eslint/require-id-when-available
23
- ... on ProductInterface {
24
- uid
25
- __typename
26
- }
27
- # eslint-disable-next-line @graphql-eslint/require-id-when-available
28
- ... on CategoryInterface {
29
- uid
30
- }
17
+ relative_url
31
18
  }
32
19
  }
33
20
  `;
@@ -36,10 +23,8 @@ export const GET_AVAILABLE_STORES_DATA = gql`
36
23
  query getAvailableStoresData {
37
24
  # eslint-disable-next-line @graphql-eslint/require-id-when-available
38
25
  availableStores {
39
- category_url_suffix
40
26
  default_display_currency_code
41
27
  locale
42
- product_url_suffix
43
28
  secure_base_media_url
44
29
  store_code
45
30
  store_group_code
@@ -6,6 +6,7 @@ import mergeOperations from '../../util/shallowMerge';
6
6
  import { useUserContext } from '../../context/user';
7
7
 
8
8
  import DEFAULT_OPERATIONS from './accountMenu.gql';
9
+ import { useEventingContext } from '../../context/eventing';
9
10
 
10
11
  /**
11
12
  * The useAccountMenu talon complements the AccountMenu component.
@@ -37,7 +38,12 @@ export const useAccountMenu = props => {
37
38
  const history = useHistory();
38
39
  const location = useLocation();
39
40
  const [revokeToken] = useMutation(signOutMutation);
40
- const [{ isSignedIn: isUserSignedIn }, { signOut }] = useUserContext();
41
+ const [
42
+ { isSignedIn: isUserSignedIn, currentUser },
43
+ { signOut }
44
+ ] = useUserContext();
45
+
46
+ const [, { dispatch }] = useEventingContext();
41
47
 
42
48
  const handleSignOut = useCallback(async () => {
43
49
  setView('SIGNIN');
@@ -46,11 +52,24 @@ export const useAccountMenu = props => {
46
52
  // Delete cart/user data from the redux store.
47
53
  await signOut({ revokeToken });
48
54
 
55
+ dispatch({
56
+ type: 'USER_SIGN_OUT',
57
+ payload: {
58
+ ...currentUser
59
+ }
60
+ });
49
61
  // Refresh the page as a way to say "re-initialize". An alternative
50
62
  // would be to call apolloClient.resetStore() but that would require
51
63
  // a large refactor.
52
64
  history.go(0);
53
- }, [history, revokeToken, setAccountMenuIsOpen, signOut]);
65
+ }, [
66
+ history,
67
+ revokeToken,
68
+ setAccountMenuIsOpen,
69
+ signOut,
70
+ currentUser,
71
+ dispatch
72
+ ]);
54
73
 
55
74
  const handleForgotPassword = useCallback(() => {
56
75
  setView('FORGOT_PASSWORD');