@magento/peregrine 15.5.2 → 15.6.2-alpha12

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.
@@ -1,15 +1,5 @@
1
- import React, {
2
- createContext,
3
- useContext,
4
- useEffect,
5
- useMemo,
6
- useCallback
7
- } from 'react';
1
+ import React, { createContext, useContext, useMemo, useCallback } from 'react';
8
2
  import { connect } from 'react-redux';
9
- import { useMutation } from '@apollo/client';
10
- import gql from 'graphql-tag';
11
-
12
- import { useAwaitQuery } from '@magento/peregrine/lib/hooks/useAwaitQuery';
13
3
  import actions from '../store/actions/cart/actions';
14
4
  import * as asyncActions from '../store/actions/cart/asyncActions';
15
5
  import bindActionCreators from '../util/bindActionCreators';
@@ -62,9 +52,6 @@ const CartContextProvider = props => {
62
52
  return [derivedCartState, cartApi];
63
53
  }, [cartApi, cartState, derivedDetails]);
64
54
 
65
- const [fetchCartId] = useMutation(CREATE_CART_MUTATION);
66
- const fetchCartDetails = useAwaitQuery(CART_DETAILS_QUERY);
67
-
68
55
  // Storage listener to force a state update if cartId changes from another browser tab.
69
56
  const storageListener = useCallback(() => {
70
57
  const storage = new BrowserPersistence();
@@ -77,14 +64,6 @@ const CartContextProvider = props => {
77
64
 
78
65
  useEventListener(globalThis, 'storage', storageListener);
79
66
 
80
- useEffect(() => {
81
- // cartApi.getCartDetails initializes the cart if there isn't one.
82
- cartApi.getCartDetails({
83
- fetchCartId,
84
- fetchCartDetails
85
- });
86
- }, [cartApi, fetchCartDetails, fetchCartId]);
87
-
88
67
  return (
89
68
  <CartContext.Provider value={contextValue}>
90
69
  {children}
@@ -105,24 +84,3 @@ export default connect(
105
84
  )(CartContextProvider);
106
85
 
107
86
  export const useCartContext = () => useContext(CartContext);
108
-
109
- /**
110
- * We normally do not keep GQL queries in Peregrine. All components should pass
111
- * queries to talons/hooks. This is an exception to the rule because it would
112
- * be unecessarily complex to pass these queries to the context provider.
113
- */
114
- const CREATE_CART_MUTATION = gql`
115
- mutation createCart {
116
- cartId: createEmptyCart
117
- }
118
- `;
119
-
120
- const CART_DETAILS_QUERY = gql`
121
- query checkUserIsAuthed($cartId: String!) {
122
- cart(cart_id: $cartId) {
123
- # The purpose of this query is to check that the user is authorized
124
- # to query on the current cart. Just fetch "id" to keep it small.
125
- id
126
- }
127
- }
128
- `;
@@ -7,7 +7,7 @@ export const GET_WISHLIST_ITEMS = gql`
7
7
  customer {
8
8
  wishlists {
9
9
  id
10
- items_v2(currentPage: $currentPage, pageSize: 10) {
10
+ items_v2(currentPage: $currentPage, pageSize: 20) {
11
11
  items {
12
12
  id
13
13
  # eslint-disable-next-line @graphql-eslint/require-id-when-available
@@ -1,4 +1,3 @@
1
- import { useState } from 'react';
2
1
  import { useQuery } from '@apollo/client';
3
2
  import { useUserContext } from '../../context/user';
4
3
  import mergeOperations from '../../util/shallowMerge';
@@ -16,19 +15,16 @@ export const useCustomerWishlistSkus = (props = {}) => {
16
15
  const operations = mergeOperations(defaultOperations, props.operations);
17
16
  const [{ isSignedIn }] = useUserContext();
18
17
 
19
- const [currentPage, setCurrentPage] = useState(1);
20
-
21
18
  const {
22
19
  client,
23
20
  data: { customerWishlistProducts }
24
21
  } = useQuery(operations.getProductsInWishlistsQuery);
25
22
 
26
23
  useQuery(operations.getWishlistItemsQuery, {
27
- fetchPolicy: 'cache-and-network',
24
+ fetchPolicy: 'cache-first',
28
25
  onCompleted: data => {
29
26
  const itemsToAdd = new Set();
30
27
  const wishlists = data.customer.wishlists;
31
- let shouldFetchMore = false;
32
28
  wishlists.map(wishlist => {
33
29
  const items = wishlist.items_v2.items;
34
30
  items.map(item => {
@@ -37,12 +33,6 @@ export const useCustomerWishlistSkus = (props = {}) => {
37
33
  itemsToAdd.add(sku);
38
34
  }
39
35
  });
40
-
41
- const pageInfo = wishlist.items_v2.page_info;
42
-
43
- if (pageInfo.total_pages > pageInfo.current_page) {
44
- shouldFetchMore = true;
45
- }
46
36
  });
47
37
 
48
38
  if (itemsToAdd.size) {
@@ -56,14 +46,10 @@ export const useCustomerWishlistSkus = (props = {}) => {
56
46
  }
57
47
  });
58
48
  }
59
-
60
- if (shouldFetchMore) {
61
- setCurrentPage(current => ++current);
62
- }
63
49
  },
64
50
  skip: !isSignedIn,
65
51
  variables: {
66
- currentPage
52
+ currentPage: 1
67
53
  }
68
54
  });
69
55
  };
@@ -64,11 +64,11 @@ const scheduleSignOut = store => next => action => {
64
64
  intervals.set(parsedValue, intervalId);
65
65
  }
66
66
  } else if (isSigningOut(action.type)) {
67
- for (const timeoutId of timeouts) {
67
+ for (const [, timeoutId] of timeouts) {
68
68
  clearTimeout(timeoutId);
69
69
  }
70
70
 
71
- for (const intervalId of intervals) {
71
+ for (const [, intervalId] of intervals) {
72
72
  clearInterval(intervalId);
73
73
  }
74
74
 
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useState } from 'react';
2
- import { useMutation, useQuery } from '@apollo/client';
2
+ import { useMutation, useQuery, gql } from '@apollo/client';
3
3
 
4
4
  import mergeOperations from '../../util/shallowMerge';
5
5
  import { useCartContext } from '../../context/cart';
@@ -7,6 +7,8 @@ import defaultOperations from './addToCartDialog.gql';
7
7
  import { useEventingContext } from '../../context/eventing';
8
8
  import { isProductConfigurable } from '@magento/peregrine/lib/util/isProductConfigurable';
9
9
  import { getOutOfStockVariants } from '@magento/peregrine/lib/util/getOutOfStockVariants';
10
+ import { useAwaitQuery } from '@magento/peregrine/lib/hooks/useAwaitQuery';
11
+ import BrowserPersistence from '../../util/simplePersistence';
10
12
 
11
13
  export const useAddToCartDialog = props => {
12
14
  const { item, onClose } = props;
@@ -25,7 +27,51 @@ export const useAddToCartDialog = props => {
25
27
  new Map()
26
28
  );
27
29
 
28
- const [{ cartId }] = useCartContext();
30
+ //const [{ cartId }] = useCartContext();
31
+
32
+ const [cartState, cartApi] = useCartContext();
33
+
34
+ const { cartId } = cartState;
35
+
36
+ // cart creation logic
37
+
38
+ const CREATE_CART_MUTATION = gql`
39
+ mutation createCart {
40
+ cartId: createEmptyCart
41
+ }
42
+ `;
43
+
44
+ const CART_DETAILS_QUERY = gql`
45
+ query checkUserIsAuthed($cartId: String!) {
46
+ cart(cart_id: $cartId) {
47
+ id
48
+ }
49
+ }
50
+ `;
51
+
52
+ const [fetchCartId] = useMutation(CREATE_CART_MUTATION);
53
+
54
+ const fetchCartDetails = useAwaitQuery(CART_DETAILS_QUERY);
55
+
56
+ const ensureCartId = useCallback(async () => {
57
+ let newCartId = cartId;
58
+
59
+ if (!newCartId) {
60
+ await cartApi.getCartDetails({
61
+ fetchCartId,
62
+
63
+ fetchCartDetails
64
+ });
65
+
66
+ newCartId = new BrowserPersistence().getItem('cartId');
67
+
68
+ if (!newCartId) {
69
+ throw new Error('Failed to create a new cart');
70
+ }
71
+ }
72
+
73
+ return newCartId;
74
+ }, [cartId, cartApi, fetchCartId, fetchCartDetails]);
29
75
 
30
76
  const optionCodes = useMemo(() => {
31
77
  const optionCodeMap = new Map();
@@ -112,7 +158,9 @@ export const useAddToCartDialog = props => {
112
158
  fetchPolicy: 'cache-and-network',
113
159
  nextFetchPolicy: 'cache-first',
114
160
  variables: {
115
- configurableOptionValues: selectedOptionsArray,
161
+ configurableOptionValues: selectedOptionsArray.length
162
+ ? selectedOptionsArray
163
+ : null,
116
164
  sku
117
165
  },
118
166
  skip: !sku
@@ -127,6 +175,7 @@ export const useAddToCartDialog = props => {
127
175
  useEffect(() => {
128
176
  if (data) {
129
177
  const product = data.products.items[0];
178
+ console.log('useAddToCartDialog.js - data', data);
130
179
  const {
131
180
  media_gallery: selectedProductMediaGallery,
132
181
  variant: selectedVariant
@@ -181,12 +230,14 @@ export const useAddToCartDialog = props => {
181
230
  );
182
231
 
183
232
  const handleAddToCart = useCallback(async () => {
233
+ //console.log("useAddToCartDialog.js handleAddToCart is called for ",cartId);
184
234
  try {
235
+ const ensuredCartId = await ensureCartId();
185
236
  const quantity = 1;
186
237
 
187
238
  await addProductToCart({
188
239
  variables: {
189
- cartId,
240
+ cartId: ensuredCartId,
190
241
  cartItem: {
191
242
  quantity,
192
243
  selected_options: selectedOptionsArray,
@@ -207,7 +258,7 @@ export const useAddToCartDialog = props => {
207
258
  dispatch({
208
259
  type: 'CART_ADD_ITEM',
209
260
  payload: {
210
- cartId,
261
+ cartId: ensuredCartId,
211
262
  sku: item.product.sku,
212
263
  name: item.product.name,
213
264
  pricing: item.product.price,
@@ -225,7 +276,7 @@ export const useAddToCartDialog = props => {
225
276
  }
226
277
  }, [
227
278
  addProductToCart,
228
- cartId,
279
+ ensureCartId,
229
280
  currentDiscount,
230
281
  currentPrice,
231
282
  dispatch,
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo } from 'react';
2
2
  import { useHistory } from 'react-router-dom';
3
3
 
4
4
  import errorRecord from '@magento/peregrine/lib/util/createErrorRecord';
5
+ import { useCustomerWishlistSkus } from '@magento/peregrine/lib/hooks/useCustomerWishlistSkus/useCustomerWishlistSkus';
5
6
  import { useAppContext } from '@magento/peregrine/lib/context/app';
6
7
 
7
8
  const dismissers = new WeakMap();
@@ -41,6 +42,8 @@ export const useApp = props => {
41
42
  } = props;
42
43
  const history = useHistory();
43
44
 
45
+ useCustomerWishlistSkus();
46
+
44
47
  const reload = useCallback(() => {
45
48
  if (process.env.NODE_ENV !== 'development') {
46
49
  history.go(0);
@@ -202,19 +202,31 @@ export const useProductForm = props => {
202
202
 
203
203
  const outOfStockVariants = useMemo(() => {
204
204
  if (cartItem && configItem) {
205
- const product = cartItem.product;
205
+ const product = configItem;
206
+ const currentSelections = new Map();
207
+
208
+ cartItem.configurable_options.forEach(option => {
209
+ currentSelections.set(String(option.id), option.value_id);
210
+ });
211
+
212
+ optionSelections.forEach((value, key) => {
213
+ currentSelections.set(key, value);
214
+ });
215
+
206
216
  return getOutOfStockVariantsWithInitialSelection(
207
217
  product,
208
218
  configurableOptionCodes,
209
- multipleOptionSelections,
219
+ currentSelections,
210
220
  configItem,
211
221
  isOutOfStockProductDisplayed
212
222
  );
213
223
  }
224
+
225
+ return [];
214
226
  }, [
215
227
  cartItem,
216
228
  configurableOptionCodes,
217
- multipleOptionSelections,
229
+ optionSelections,
218
230
  configItem,
219
231
  isOutOfStockProductDisplayed
220
232
  ]);
@@ -182,10 +182,22 @@ export const useFilterSidebar = props => {
182
182
  }, [handleClose]);
183
183
 
184
184
  const handleReset = useCallback(() => {
185
- //filterApi.clear();
186
- //setIsApplying(true);
187
- history.replace({ search: 'page=1' });
188
- }, [history]);
185
+ filterApi.clear();
186
+
187
+ const params = new URLSearchParams(search);
188
+ const filterKeys = [];
189
+ for (const key of params.keys()) {
190
+ if (key.endsWith('[filter]')) {
191
+ filterKeys.push(key);
192
+ }
193
+ }
194
+ for (const key of filterKeys) {
195
+ params.delete(key);
196
+ }
197
+
198
+ const newSearch = `?${params.toString()}`;
199
+ history.replace({ pathname, search: newSearch });
200
+ }, [filterApi, search, history, pathname]);
189
201
 
190
202
  const handleKeyDownActions = useCallback(
191
203
  event => {
@@ -1,11 +1,13 @@
1
1
  import { useCallback, useState } from 'react';
2
- import { useMutation } from '@apollo/client';
2
+ import { useMutation, gql } from '@apollo/client';
3
3
  import { useHistory } from 'react-router-dom';
4
4
 
5
5
  import { useCartContext } from '../../context/cart';
6
6
  import { useEventingContext } from '../../context/eventing';
7
7
  import resourceUrl from '../../util/makeUrl';
8
8
  import operations from './addToCart.gql';
9
+ import { useAwaitQuery } from '@magento/peregrine/lib/hooks/useAwaitQuery';
10
+ import BrowserPersistence from '../../util/simplePersistence';
9
11
 
10
12
  /**
11
13
  * @param {String} props.item.uid - uid of item
@@ -29,6 +31,20 @@ const UNSUPPORTED_PRODUCT_TYPES = [
29
31
  'DownloadableProduct'
30
32
  ];
31
33
 
34
+ const CREATE_CART_MUTATION = gql`
35
+ mutation createCart {
36
+ cartId: createEmptyCart
37
+ }
38
+ `;
39
+
40
+ const CART_DETAILS_QUERY = gql`
41
+ query checkUserIsAuthed($cartId: String!) {
42
+ cart(cart_id: $cartId) {
43
+ id
44
+ }
45
+ }
46
+ `;
47
+
32
48
  export const useAddToCartButton = props => {
33
49
  const { item, urlSuffix } = props;
34
50
 
@@ -36,6 +52,12 @@ export const useAddToCartButton = props => {
36
52
 
37
53
  const [isLoading, setIsLoading] = useState(false);
38
54
 
55
+ const [cartState, cartApi] = useCartContext();
56
+ const { cartId } = cartState;
57
+
58
+ const [fetchCartId] = useMutation(CREATE_CART_MUTATION);
59
+ const fetchCartDetails = useAwaitQuery(CART_DETAILS_QUERY);
60
+
39
61
  const isInStock = item.stock_status === 'IN_STOCK';
40
62
 
41
63
  const productType = item
@@ -52,21 +74,42 @@ export const useAddToCartButton = props => {
52
74
 
53
75
  const history = useHistory();
54
76
 
55
- const [{ cartId }] = useCartContext();
56
-
57
77
  const [addToCart] = useMutation(operations.ADD_ITEM);
58
78
 
79
+ // helper: ensure we have a valid cartId before adding
80
+ const ensureCartId = useCallback(async () => {
81
+ let newCartId = cartId;
82
+ if (!newCartId) {
83
+ console.log('No cart ID found, creating a new cart...');
84
+ await cartApi.getCartDetails({
85
+ fetchCartId,
86
+ fetchCartDetails
87
+ });
88
+
89
+ newCartId = new BrowserPersistence().getItem('cartId');
90
+
91
+ if (!newCartId) {
92
+ throw new Error('Failed to create a new cart');
93
+ }
94
+ }
95
+ return newCartId;
96
+ }, [cartId, cartApi, fetchCartId, fetchCartDetails]);
97
+
59
98
  const handleAddToCart = useCallback(async () => {
60
99
  try {
61
100
  if (productType === 'SimpleProduct' || productType === 'simple') {
62
101
  setIsLoading(true);
63
102
 
64
103
  const quantity = 1;
104
+ let newCartId;
65
105
 
66
106
  if (item.uid) {
107
+ // ensure cart right before addToCart
108
+ newCartId = await ensureCartId();
109
+
67
110
  await addToCart({
68
111
  variables: {
69
- cartId,
112
+ cartId: newCartId,
70
113
  cartItem: {
71
114
  quantity,
72
115
  entered_options: [
@@ -80,9 +123,12 @@ export const useAddToCartButton = props => {
80
123
  }
81
124
  });
82
125
  } else {
126
+ // ensure cart right before addToCart
127
+ newCartId = await ensureCartId();
128
+
83
129
  await addToCart({
84
130
  variables: {
85
- cartId,
131
+ cartId: newCartId,
86
132
  cartItem: {
87
133
  quantity,
88
134
  sku: item.sku
@@ -94,7 +140,7 @@ export const useAddToCartButton = props => {
94
140
  dispatch({
95
141
  type: 'CART_ADD_ITEM',
96
142
  payload: {
97
- cartId,
143
+ cartId: newCartId,
98
144
  sku: item.sku,
99
145
  name: item.name,
100
146
  pricing: {
@@ -130,7 +176,15 @@ export const useAddToCartButton = props => {
130
176
  } catch (error) {
131
177
  console.error(error);
132
178
  }
133
- }, [productType, addToCart, cartId, item, dispatch, history, urlSuffix]);
179
+ }, [
180
+ productType,
181
+ addToCart,
182
+ item,
183
+ dispatch,
184
+ history,
185
+ urlSuffix,
186
+ ensureCartId
187
+ ]);
134
188
 
135
189
  return {
136
190
  handleAddToCart,
@@ -1,15 +1,11 @@
1
1
  import { useQuery } from '@apollo/client';
2
2
 
3
- import { useCustomerWishlistSkus } from '../../hooks/useCustomerWishlistSkus/useCustomerWishlistSkus';
4
-
5
3
  import mergeOperations from '../../util/shallowMerge';
6
4
  import defaultOperations from './gallery.gql';
7
5
 
8
6
  export const useGallery = (props = {}) => {
9
7
  const operations = mergeOperations(defaultOperations, props.operations);
10
8
 
11
- useCustomerWishlistSkus();
12
-
13
9
  const { data: storeConfigData } = useQuery(operations.getStoreConfigQuery, {
14
10
  fetchPolicy: 'cache-and-network'
15
11
  });
@@ -71,8 +71,8 @@ export const useMagentoRoute = (props = {}) => {
71
71
  });
72
72
 
73
73
  if (isMounted) {
74
- setRouteData(data);
75
74
  fetchedPathname.current = pathname;
75
+ setRouteData(data);
76
76
  }
77
77
  } catch (error) {
78
78
  if (isMounted) {
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useState, useMemo } from 'react';
2
2
  import { useIntl } from 'react-intl';
3
- import { useMutation, useQuery } from '@apollo/client';
3
+ import { useMutation, useQuery, gql } from '@apollo/client';
4
4
  import { useCartContext } from '@magento/peregrine/lib/context/cart';
5
5
  import { useUserContext } from '@magento/peregrine/lib/context/user';
6
6
 
@@ -13,6 +13,8 @@ import mergeOperations from '../../util/shallowMerge';
13
13
  import defaultOperations from './productFullDetail.gql';
14
14
  import { useEventingContext } from '../../context/eventing';
15
15
  import { getOutOfStockVariants } from '@magento/peregrine/lib/util/getOutOfStockVariants';
16
+ import { useAwaitQuery } from '@magento/peregrine/lib/hooks/useAwaitQuery';
17
+ import BrowserPersistence from '../../util/simplePersistence';
16
18
 
17
19
  const INITIAL_OPTION_CODES = new Map();
18
20
  const INITIAL_OPTION_SELECTIONS = new Map();
@@ -257,7 +259,9 @@ export const useProductFullDetail = props => {
257
259
 
258
260
  const isSupportedProductType = isSupported(productType);
259
261
 
260
- const [{ cartId }] = useCartContext();
262
+ //const [{ cartId }] = useCartContext();
263
+ const [cartState, cartApi] = useCartContext();
264
+ const { cartId } = cartState;
261
265
  const [{ isSignedIn }] = useUserContext();
262
266
  const { formatMessage } = useIntl();
263
267
 
@@ -411,6 +415,42 @@ export const useProductFullDetail = props => {
411
415
  return selectedOptions;
412
416
  }, [attributeIdToValuesMap, optionSelections]);
413
417
 
418
+ // Cart creation wiring (same approach as useAddToCartButton.js)
419
+ const CREATE_CART_MUTATION = gql`
420
+ mutation createCart {
421
+ cartId: createEmptyCart
422
+ }
423
+ `;
424
+
425
+ const CART_DETAILS_QUERY = gql`
426
+ query checkUserIsAuthed($cartId: String!) {
427
+ cart(cart_id: $cartId) {
428
+ id
429
+ }
430
+ }
431
+ `;
432
+
433
+ const [fetchCartId] = useMutation(CREATE_CART_MUTATION);
434
+ const fetchCartDetails = useAwaitQuery(CART_DETAILS_QUERY);
435
+
436
+ const ensureCartId = useCallback(async () => {
437
+ let newCartId = cartId;
438
+ if (!newCartId) {
439
+ await cartApi.getCartDetails({
440
+ fetchCartId,
441
+ fetchCartDetails
442
+ });
443
+
444
+ newCartId = new BrowserPersistence().getItem('cartId');
445
+ if (!newCartId) {
446
+ throw new Error('Failed to create a new cart');
447
+ }
448
+ }
449
+ return newCartId;
450
+ }, [cartId, cartApi, fetchCartId, fetchCartDetails]);
451
+
452
+ // Cart Creation ends
453
+
414
454
  const handleAddToCart = useCallback(
415
455
  async formValues => {
416
456
  const { quantity } = formValues;
@@ -435,7 +475,7 @@ export const useProductFullDetail = props => {
435
475
 
436
476
  if (isSupportedProductType) {
437
477
  const variables = {
438
- cartId,
478
+ cartId, // will be replaced by ensured cart id below
439
479
  parentSku: payload.parentSku,
440
480
  product: payload.item,
441
481
  quantity: payload.quantity,
@@ -484,6 +524,10 @@ export const useProductFullDetail = props => {
484
524
  }
485
525
 
486
526
  try {
527
+ //Ensure cart exists *right before* mutation runs
528
+ const ensuredCartId = await ensureCartId();
529
+ variables.cartId = ensuredCartId;
530
+
487
531
  await addProductToCart({ variables });
488
532
 
489
533
  const selectedOptionsLabels =
@@ -498,7 +542,7 @@ export const useProductFullDetail = props => {
498
542
  dispatch({
499
543
  type: 'CART_ADD_ITEM',
500
544
  payload: {
501
- cartId,
545
+ cartId: ensuredCartId,
502
546
  sku: product.sku,
503
547
  name: product.name,
504
548
  pricing: product.price,
@@ -527,7 +571,8 @@ export const useProductFullDetail = props => {
527
571
  product,
528
572
  productPrice,
529
573
  productType,
530
- selectedOptionsArray
574
+ selectedOptionsArray,
575
+ ensureCartId
531
576
  ]
532
577
  );
533
578
 
@@ -98,6 +98,16 @@ export const useSignIn = props => {
98
98
  try {
99
99
  // Get source cart id (guest cart id).
100
100
  const sourceCartId = cartId;
101
+ let hasSourceItems = false;
102
+
103
+ if (sourceCartId !== null) {
104
+ const { data: sourceCartData } = await fetchCartDetails({
105
+ variables: { cartId: sourceCartId },
106
+ fetchPolicy: 'network-only'
107
+ });
108
+
109
+ hasSourceItems = sourceCartData?.cart?.items?.length > 0;
110
+ }
101
111
 
102
112
  // Get recaptchaV3 data for login
103
113
  const recaptchaData = await generateReCaptchaData();
@@ -127,14 +137,30 @@ export const useSignIn = props => {
127
137
  });
128
138
  const destinationCartId = await retrieveCartId();
129
139
 
130
- // Merge the guest cart into the customer cart.
131
- await mergeCarts({
132
- variables: {
133
- destinationCartId,
134
- sourceCartId
135
- }
140
+ const { data: destCartData } = await fetchCartDetails({
141
+ variables: { cartId: destinationCartId },
142
+ fetchPolicy: 'network-only'
136
143
  });
137
144
 
145
+ const hasDestinationItems =
146
+ destCartData?.cart?.items?.length > 0;
147
+
148
+ if (sourceCartId !== null && hasSourceItems) {
149
+ console.log('Merging guest cart into customer cart');
150
+ // Merge the guest cart into the customer cart.
151
+ await mergeCarts({
152
+ variables: {
153
+ destinationCartId,
154
+ sourceCartId
155
+ }
156
+ });
157
+ } else if (!hasSourceItems && !hasDestinationItems) {
158
+ console.log('Both carts empty → clearing local cart');
159
+ // Clear all cart/customer data from cache and redux.
160
+ await apolloClient.clearCacheData(apolloClient, 'cart');
161
+ await removeCart();
162
+ }
163
+
138
164
  // Ensure old stores are updated with any new data.
139
165
 
140
166
  await getUserDetails({ fetchUserDetails });
@@ -150,7 +176,9 @@ export const useSignIn = props => {
150
176
  }
151
177
  });
152
178
 
153
- getCartDetails({ fetchCartId, fetchCartDetails });
179
+ if (sourceCartId !== null && hasSourceItems) {
180
+ getCartDetails({ fetchCartId, fetchCartDetails });
181
+ }
154
182
 
155
183
  if (
156
184
  userOnOrderSuccess &&
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useState } from 'react';
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { useLazyQuery } from '@apollo/client';
3
3
  import mergeOperations from '../../util/shallowMerge';
4
4
  import defaultOperations from './wishlist.gql';
@@ -17,15 +17,17 @@ export const useWishlist = (props = {}) => {
17
17
  const [page, setPage] = useState(1);
18
18
  const [isOpen, setIsOpen] = useState(!isCollapsed);
19
19
  const [isFetchingMore, setIsFetchingMore] = useState(false);
20
+ const hasFetchedRef = useRef(false);
20
21
 
21
22
  const [fetchWishlistItems, queryResult] = useLazyQuery(
22
23
  operations.getCustomerWishlistItems,
23
24
  {
24
- fetchPolicy: 'cache-and-network',
25
+ fetchPolicy: 'cache-first',
25
26
  nextFetchPolicy: 'cache-first',
26
27
  variables: {
27
28
  id,
28
- currentPage: 1
29
+ currentPage: 1,
30
+ pageSize: 20
29
31
  }
30
32
  }
31
33
  );
@@ -38,28 +40,84 @@ export const useWishlist = (props = {}) => {
38
40
  const handleLoadMore = useCallback(async () => {
39
41
  setIsFetchingMore(true);
40
42
  const currentPage = page + 1;
41
- await fetchMore({
42
- variables: {
43
- id,
44
- currentPage
45
- }
46
- });
47
43
 
48
- setPage(currentPage);
49
- setIsFetchingMore(false);
44
+ try {
45
+ await fetchMore({
46
+ variables: {
47
+ id,
48
+ currentPage,
49
+ pageSize: 20
50
+ },
51
+ updateQuery: (prevResult, { fetchMoreResult }) => {
52
+ if (!fetchMoreResult) {
53
+ return prevResult;
54
+ }
55
+
56
+ const prevWishlist = prevResult.customer.wishlist_v2;
57
+ const newWishlist = fetchMoreResult.customer.wishlist_v2;
58
+
59
+ if (prevWishlist.id !== newWishlist.id) {
60
+ return prevResult;
61
+ }
62
+
63
+ const prevItems = prevWishlist.items_v2.items || [];
64
+ const newItems = newWishlist.items_v2.items || [];
65
+
66
+ const existingIds = new Set(prevItems.map(item => item.id));
67
+ const uniqueNewItems = newItems.filter(
68
+ item => !existingIds.has(item.id)
69
+ );
70
+
71
+ return {
72
+ ...prevResult,
73
+ customer: {
74
+ ...prevResult.customer,
75
+ wishlist_v2: {
76
+ ...prevWishlist,
77
+ items_v2: {
78
+ ...prevWishlist.items_v2,
79
+ items: [...prevItems, ...uniqueNewItems]
80
+ }
81
+ }
82
+ }
83
+ };
84
+ }
85
+ });
86
+
87
+ setPage(currentPage);
88
+ } catch (error) {
89
+ console.error('Error loading more wishlist items:', error);
90
+ } finally {
91
+ setIsFetchingMore(false);
92
+ }
50
93
  }, [id, fetchMore, page]);
51
94
 
52
95
  useEffect(() => {
53
- setPage(1);
54
- if (itemsCount >= 1 && isOpen === true && !data) {
96
+ if (itemsCount >= 1 && isOpen === true && !hasFetchedRef.current) {
97
+ hasFetchedRef.current = true;
55
98
  fetchWishlistItems();
56
99
  }
57
- }, [itemsCount, isOpen, fetchWishlistItems, data]);
100
+ }, [itemsCount, isOpen, fetchWishlistItems]);
101
+
102
+ const items = useMemo(() => {
103
+ if (!data || !data.customer || !data.customer.wishlist_v2) {
104
+ return [];
105
+ }
106
+
107
+ const allItems = data.customer.wishlist_v2.items_v2?.items || [];
108
+
109
+ const uniqueItems = [];
110
+ const seenIds = new Set();
111
+
112
+ for (const item of allItems) {
113
+ if (!seenIds.has(item.id)) {
114
+ seenIds.add(item.id);
115
+ uniqueItems.push(item);
116
+ }
117
+ }
58
118
 
59
- const items =
60
- data && data.customer.wishlist_v2.items_v2.items
61
- ? data.customer.wishlist_v2.items_v2.items
62
- : [];
119
+ return uniqueItems;
120
+ }, [data]);
63
121
 
64
122
  return {
65
123
  handleContentToggle,
@@ -1,7 +1,9 @@
1
1
  import { useCallback, useMemo, useState } from 'react';
2
- import { useMutation } from '@apollo/client';
2
+ import { gql, useMutation } from '@apollo/client';
3
3
 
4
4
  import { useCartContext } from '@magento/peregrine/lib/context/cart';
5
+ import { useAwaitQuery } from '@magento/peregrine/lib/hooks/useAwaitQuery';
6
+ import BrowserPersistence from '../../util/simplePersistence';
5
7
  import mergeOperations from '../../util/shallowMerge';
6
8
  import defaultOperations from './wishlistItem.gql';
7
9
  import { useEventingContext } from '../../context/eventing';
@@ -18,6 +20,20 @@ const mergeSupportedProductTypes = (supportedProductTypes = []) => {
18
20
  return newSupportedProductTypes;
19
21
  };
20
22
 
23
+ const CREATE_CART_MUTATION = gql`
24
+ mutation createCart {
25
+ cartId: createEmptyCart
26
+ }
27
+ `;
28
+
29
+ const CART_DETAILS_QUERY = gql`
30
+ query getCart($cartId: String!) {
31
+ cart(cart_id: $cartId) {
32
+ id
33
+ }
34
+ }
35
+ `;
36
+
21
37
  /**
22
38
  * @function
23
39
  *
@@ -61,7 +77,8 @@ export const useWishlistItem = props => {
61
77
  removeProductsFromWishlistMutation
62
78
  } = operations;
63
79
 
64
- const [{ cartId }] = useCartContext();
80
+ //const [{ cartId }] = useCartContext();
81
+ const [{ cartId: existingCartId }, { getCartDetails }] = useCartContext();
65
82
 
66
83
  const [isRemovalInProgress, setIsRemovalInProgress] = useState(false);
67
84
 
@@ -119,18 +136,26 @@ export const useWishlistItem = props => {
119
136
  return item;
120
137
  }, [configurableOptions, selectedConfigurableOptions, sku]);
121
138
 
139
+ // const [
140
+ // addWishlistItemToCart,
141
+ // {
142
+ // error: addWishlistItemToCartError,
143
+ // loading: addWishlistItemToCartLoading
144
+ // }
145
+ // ] = useMutation(addWishlistItemToCartMutation, {
146
+ // variables: {
147
+ // cartId,
148
+ // cartItem
149
+ // }
150
+ // });
151
+
122
152
  const [
123
153
  addWishlistItemToCart,
124
154
  {
125
155
  error: addWishlistItemToCartError,
126
156
  loading: addWishlistItemToCartLoading
127
157
  }
128
- ] = useMutation(addWishlistItemToCartMutation, {
129
- variables: {
130
- cartId,
131
- cartItem
132
- }
133
- });
158
+ ] = useMutation(addWishlistItemToCartMutation);
134
159
 
135
160
  const [removeProductsFromWishlist] = useMutation(
136
161
  removeProductsFromWishlistMutation,
@@ -169,13 +194,41 @@ export const useWishlistItem = props => {
169
194
  }
170
195
  );
171
196
 
197
+ const [fetchCartId] = useMutation(CREATE_CART_MUTATION);
198
+ const fetchCartDetails = useAwaitQuery(CART_DETAILS_QUERY);
199
+
200
+ const ensureCartId = useCallback(async () => {
201
+ let newCartId = existingCartId;
202
+
203
+ if (!newCartId) {
204
+ await getCartDetails({
205
+ fetchCartId,
206
+ fetchCartDetails
207
+ });
208
+
209
+ newCartId = new BrowserPersistence().getItem('cartId');
210
+
211
+ if (!newCartId) {
212
+ throw new Error('Failed to create a new cart');
213
+ }
214
+ }
215
+ return newCartId;
216
+ }, [existingCartId, getCartDetails, fetchCartId, fetchCartDetails]);
217
+
172
218
  const handleAddToCart = useCallback(async () => {
173
219
  if (
174
220
  configurableOptions.length === 0 ||
175
221
  selectedConfigurableOptions.length === configurableOptions.length
176
222
  ) {
177
223
  try {
178
- await addWishlistItemToCart();
224
+ const ensuredCartId = await ensureCartId();
225
+ //await addWishlistItemToCart();
226
+ await addWishlistItemToCart({
227
+ variables: {
228
+ cartId: ensuredCartId,
229
+ cartItem
230
+ }
231
+ });
179
232
 
180
233
  const selectedOptionsLabels =
181
234
  selectedConfigurableOptions?.length > 0
@@ -190,7 +243,7 @@ export const useWishlistItem = props => {
190
243
  dispatch({
191
244
  type: 'CART_ADD_ITEM',
192
245
  payload: {
193
- cartId,
246
+ cartId: ensuredCartId,
194
247
  sku: item.product.sku,
195
248
  name: item.product.name,
196
249
  pricing: item.product.price,
@@ -215,9 +268,11 @@ export const useWishlistItem = props => {
215
268
  }
216
269
  }, [
217
270
  addWishlistItemToCart,
218
- cartId,
271
+ //cartId,
272
+ cartItem,
219
273
  configurableOptions.length,
220
274
  dispatch,
275
+ ensureCartId,
221
276
  item,
222
277
  onOpenAddToCartDialog,
223
278
  selectedConfigurableOptions
@@ -17,12 +17,12 @@ export const GET_CUSTOMER_WISHLIST = gql`
17
17
  `;
18
18
 
19
19
  export const GET_CUSTOMER_WISHLIST_ITEMS = gql`
20
- query getCustomerWishlist($id: ID!, $currentPage: Int) {
20
+ query getCustomerWishlist($id: ID!, $currentPage: Int, $pageSize: Int) {
21
21
  # eslint-disable-next-line @graphql-eslint/require-id-when-available
22
22
  customer {
23
23
  wishlist_v2(id: $id) {
24
24
  id
25
- items_v2(currentPage: $currentPage) {
25
+ items_v2(currentPage: $currentPage, pageSize: $pageSize) {
26
26
  items {
27
27
  id
28
28
  ...WishlistItemFragment
@@ -18,8 +18,6 @@ export const getOutOfStockVariants = (
18
18
  isOutOfStockProductDisplayed
19
19
  ) => {
20
20
  const isConfigurable = isProductConfigurable(product);
21
- const singeOptionSelected =
22
- singleOptionSelection && singleOptionSelection.size === 1;
23
21
  const outOfStockIndexes = [];
24
22
 
25
23
  if (isConfigurable) {
@@ -32,6 +30,14 @@ export const getOutOfStockVariants = (
32
30
  ? variants
33
31
  : variantsIfOutOfStockProductsNotDisplayed;
34
32
 
33
+ if (!variants || variants.length === 0) {
34
+ return [];
35
+ }
36
+
37
+ if (!variants[0] || !variants[0].attributes) {
38
+ return [];
39
+ }
40
+
35
41
  const numberOfVariations = variants[0].attributes.length;
36
42
 
37
43
  // If only one pair of variations, display out of stock variations before option selection
@@ -45,18 +51,14 @@ export const getOutOfStockVariants = (
45
51
  );
46
52
  return outOfStockIndex;
47
53
  } else {
48
- if (singeOptionSelected) {
49
- const optionsSelected =
50
- Array.from(optionSelections.values()).filter(
51
- value => !!value
52
- ).length > 1;
53
- const selectedIndexes = Array.from(
54
- optionSelections.values()
55
- ).flat();
54
+ const selectedIndexes = Array.from(
55
+ optionSelections.values()
56
+ ).filter(value => !!value);
56
57
 
58
+ if (selectedIndexes.length > 0) {
57
59
  const items = findAllMatchingVariants({
58
60
  optionCodes,
59
- singleOptionSelection,
61
+ singleOptionSelection: optionSelections,
60
62
  variants
61
63
  });
62
64
  const outOfStockItemsIndexes = getOutOfStockIndexes(items);
@@ -70,15 +72,14 @@ export const getOutOfStockVariants = (
70
72
  const differentIndexes = indexes.filter(
71
73
  num => !selectedIndexes.includes(num)
72
74
  );
73
- if (sameIndexes.length >= optionCodes.size - 1) {
75
+ if (sameIndexes.length > 0) {
74
76
  outOfStockIndexes.push(differentIndexes);
75
77
  }
76
78
  }
77
79
  // Display all possible out of stock swatches with current selections, when all groups of swatches are selected
78
80
  if (
79
- optionsSelected &&
80
- !selectedIndexes.includes(undefined) &&
81
- selectedIndexes.length === optionCodes.size
81
+ selectedIndexes.length === optionCodes.size &&
82
+ !selectedIndexes.includes(undefined)
82
83
  ) {
83
84
  const selectedIndexesCombinations = getCombinations(
84
85
  selectedIndexes,
@@ -96,7 +97,7 @@ export const getOutOfStockVariants = (
96
97
  )
97
98
  );
98
99
  const curItems = findAllMatchingVariants({
99
- optionCodes: optionCodes,
100
+ optionCodes,
100
101
  singleOptionSelection: curOption,
101
102
  variants: variants
102
103
  });
@@ -107,8 +108,11 @@ export const getOutOfStockVariants = (
107
108
  }
108
109
  return oosIndexes;
109
110
  }
110
- return outOfStockIndexes;
111
+ } else {
112
+ return [];
111
113
  }
114
+
115
+ return outOfStockIndexes;
112
116
  }
113
117
  }
114
118
  return [];
@@ -7,6 +7,8 @@ import { getOutOfStockIndexes } from '@magento/peregrine/lib/util/getOutOfStockI
7
7
  import { createProductVariants } from '@magento/peregrine/lib/util/createProductVariants';
8
8
  import { getCombinations } from '@magento/peregrine/lib/util/getCombinations';
9
9
 
10
+ const OUT_OF_STOCK_CODE = 'OUT_OF_STOCK';
11
+
10
12
  export const getOutOfStockVariantsWithInitialSelection = (
11
13
  product,
12
14
  configurableOptionCodes,
@@ -14,50 +16,100 @@ export const getOutOfStockVariantsWithInitialSelection = (
14
16
  configItem,
15
17
  isOutOfStockProductDisplayed
16
18
  ) => {
17
- if (configItem && product) {
19
+ if (configItem) {
20
+ const selectedIndexes = Array.from(
21
+ multipleOptionSelections.values()
22
+ ).filter(value => !!value);
23
+
18
24
  let variants = product.variants;
19
25
  const variantsIfOutOfStockProductsNotDisplayed = createProductVariants(
20
- configItem
26
+ product
21
27
  );
22
28
  //If out of stock products is set to not displayed, use the variants created
23
29
  variants = isOutOfStockProductDisplayed
24
30
  ? variants
25
31
  : variantsIfOutOfStockProductsNotDisplayed;
26
- if (
27
- multipleOptionSelections &&
28
- multipleOptionSelections.size === configurableOptionCodes.size
29
- ) {
30
- const selectedIndexes = Array.from(
31
- multipleOptionSelections.values()
32
- ).flat();
33
-
34
- const selectedIndexesCombinations = getCombinations(
35
- selectedIndexes,
36
- selectedIndexes.length - 1
32
+
33
+ if (!variants || variants.length === 0) {
34
+ return [];
35
+ }
36
+
37
+ if (!variants[0] || !variants[0].attributes) {
38
+ return [];
39
+ }
40
+
41
+ const numberOfVariations = variants[0].attributes.length;
42
+
43
+ if (numberOfVariations === 1) {
44
+ const outOfStockOptions = variants.filter(
45
+ variant => variant.product.stock_status === OUT_OF_STOCK_CODE
37
46
  );
38
- const oosIndexes = [];
39
- for (const option of selectedIndexesCombinations) {
40
- const curOption = new Map(
41
- [...multipleOptionSelections].filter(
42
- ([key, val]) => (
43
- option.includes(key), option.includes(val)
44
- )
45
- )
46
- );
47
- const curItems = findAllMatchingVariants({
47
+
48
+ const outOfStockIndex = outOfStockOptions.map(option =>
49
+ option.attributes.map(attribute => attribute.value_index)
50
+ );
51
+ return outOfStockIndex;
52
+ } else {
53
+ const outOfStockIndexes = [];
54
+
55
+ if (selectedIndexes.length > 0) {
56
+ const items = findAllMatchingVariants({
48
57
  optionCodes: configurableOptionCodes,
49
- singleOptionSelection: curOption,
50
- variants: variants
58
+ singleOptionSelection: multipleOptionSelections,
59
+ variants
51
60
  });
52
61
 
53
- const outOfStockIndex = getOutOfStockIndexes(curItems)
54
- ?.flat()
55
- .filter(idx => !selectedIndexes.includes(idx));
62
+ const outOfStockItemsIndexes = getOutOfStockIndexes(items);
63
+
64
+ for (const indexes of outOfStockItemsIndexes) {
65
+ const sameIndexes = indexes.filter(num =>
66
+ selectedIndexes.includes(num)
67
+ );
68
+ const differentIndexes = indexes.filter(
69
+ num => !selectedIndexes.includes(num)
70
+ );
56
71
 
57
- oosIndexes.push(outOfStockIndex);
72
+ if (sameIndexes.length > 0) {
73
+ outOfStockIndexes.push(differentIndexes);
74
+ }
75
+ }
76
+
77
+ if (
78
+ selectedIndexes.length === configurableOptionCodes.size &&
79
+ !selectedIndexes.includes(undefined)
80
+ ) {
81
+ const selectedIndexesCombinations = getCombinations(
82
+ selectedIndexes,
83
+ selectedIndexes.length - 1
84
+ );
85
+
86
+ const oosIndexes = [];
87
+ for (const option of selectedIndexesCombinations) {
88
+ const curOption = new Map(
89
+ [...multipleOptionSelections].filter(
90
+ ([key, val]) => (
91
+ option.includes(key), option.includes(val)
92
+ )
93
+ )
94
+ );
95
+ const curItems = findAllMatchingVariants({
96
+ optionCodes: configurableOptionCodes,
97
+ singleOptionSelection: curOption,
98
+ variants: variants
99
+ });
100
+ const outOfStockIndex = getOutOfStockIndexes(curItems)
101
+ ?.flat()
102
+ .filter(idx => !selectedIndexes.includes(idx));
103
+ oosIndexes.push(outOfStockIndex);
104
+ }
105
+ return oosIndexes;
106
+ }
107
+ } else {
108
+ return [];
58
109
  }
59
- return oosIndexes;
110
+
111
+ return outOfStockIndexes;
60
112
  }
61
- return [];
62
113
  }
114
+ return [];
63
115
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magento/peregrine",
3
- "version": "15.5.2",
3
+ "version": "15.6.2-alpha12",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },