@riosst100/pwa-marketplace 3.1.2 → 3.1.4

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 (76) hide show
  1. package/package.json +1 -1
  2. package/src/components/BrandLandingPage/brandLanding.js +0 -5
  3. package/src/components/CheckoutHeader/accountTrigger.js +104 -0
  4. package/src/components/CheckoutHeader/cartTrigger.js +110 -0
  5. package/src/components/CheckoutHeader/cartTrigger.module.css +47 -0
  6. package/src/components/CheckoutHeader/storeSwitcher.js +119 -0
  7. package/src/components/CheckoutHeader/storeSwitcher.module.css +107 -0
  8. package/src/components/CheckoutHeader/switcherItem.js +47 -0
  9. package/src/components/CheckoutHeader/wishlistTrigger.js +23 -0
  10. package/src/components/FilterListContent/filterListPage.js +0 -25
  11. package/src/components/FilterTop/filterTop.js +1 -1
  12. package/src/components/HelpCenter/helpCenter.js +151 -0
  13. package/src/components/HelpCenter/helpcenter.module.css +225 -0
  14. package/src/components/HelpCenter/index.js +1 -0
  15. package/src/components/HelpCenter/questionDetail.js +89 -0
  16. package/src/components/ProductContent/productContent.js +1 -1
  17. package/src/components/RMAPage/components/productItem.css +15 -0
  18. package/src/components/RMAPage/components/productItem.module.css +15 -0
  19. package/src/components/RelatedProducts/index.js +1 -0
  20. package/src/components/RelatedProducts/relatedProducts.js +44 -0
  21. package/src/components/SellerMegaMenu/__tests__/MegaMenu.spec.js +91 -0
  22. package/src/components/SellerMegaMenu/__tests__/MegaMenuItem.spec.js +123 -0
  23. package/src/components/SellerMegaMenu/__tests__/Submenu.spec.js +61 -0
  24. package/src/components/SellerMegaMenu/__tests__/SubmenuColumn.spec.js +50 -0
  25. package/src/components/SellerMegaMenu/__tests__/__snapshots__/MegaMenu.spec.js.snap +114 -0
  26. package/src/components/SellerMegaMenu/__tests__/__snapshots__/MegaMenuItem.spec.js.snap +71 -0
  27. package/src/components/SellerMegaMenu/__tests__/__snapshots__/Submenu.spec.js.snap +59 -0
  28. package/src/components/SellerMegaMenu/__tests__/__snapshots__/SubmenuColumn.spec.js.snap +34 -0
  29. package/src/components/SellerMegaMenu/customSubmenuColumn.js +75 -0
  30. package/src/components/SellerMegaMenu/customSubmenuColumn.module.css +29 -0
  31. package/src/components/SellerMegaMenu/shopByColumn.js +121 -0
  32. package/src/components/SellerProducts/productContent.js +2 -4
  33. package/src/components/SetsData/setsData.js +0 -25
  34. package/src/components/ShopBy/shopBy.js +3 -78
  35. package/src/components/ShopBySets/shopBySets.js +2 -2
  36. package/src/components/ShowMore/showMore.js +3 -49
  37. package/src/overwrites/peregrine/lib/talons/OrderHistoryPage/orderHistoryPage.gql.js +1 -15
  38. package/src/overwrites/peregrine/lib/talons/OrderHistoryPage/orderRow.gql.js +0 -11
  39. package/src/overwrites/peregrine/lib/talons/RelatedProducts/productReview.gql.js +89 -0
  40. package/src/overwrites/peregrine/lib/talons/RelatedProducts/useRelatedProducts.js +833 -0
  41. package/src/overwrites/peregrine/lib/talons/RootComponents/Category/categoryContent.gql.js +0 -16
  42. package/src/overwrites/peregrine/lib/talons/RootComponents/Category/useCategoryContent.js +1 -5
  43. package/src/overwrites/venia-ui/lib/RootComponents/Category/category.js +118 -62
  44. package/src/overwrites/venia-ui/lib/RootComponents/Category/categoryContent.js +3 -50
  45. package/src/overwrites/venia-ui/lib/components/Breadcrumbs/breadcrumbs.js +1 -7
  46. package/src/overwrites/venia-ui/lib/components/CartPage/ProductListingBySeller/EditModal/editModal.js +41 -0
  47. package/src/overwrites/venia-ui/lib/components/CartPage/ProductListingBySeller/EditModal/index.js +1 -0
  48. package/src/overwrites/venia-ui/lib/components/CartPage/ProductListingBySeller/EditModal/productDetail.js +80 -0
  49. package/src/overwrites/venia-ui/lib/components/CartPage/ProductListingBySeller/EditModal/productDetail.module.css +33 -0
  50. package/src/overwrites/venia-ui/lib/components/CartPage/ProductListingBySeller/EditModal/productForm.js +153 -0
  51. package/src/overwrites/venia-ui/lib/components/CartPage/ProductListingBySeller/EditModal/productForm.module.css +52 -0
  52. package/src/overwrites/venia-ui/lib/components/CartPage/ProductListingBySeller/errorMessage.js +31 -0
  53. package/src/overwrites/venia-ui/lib/components/CartPage/ProductListingBySeller/errorMessage.module.css +13 -0
  54. package/src/overwrites/venia-ui/lib/components/CartPage/ProductListingBySeller/quantity.js +40 -0
  55. package/src/overwrites/venia-ui/lib/components/FilterSidebar/filterSidebar.js +21 -92
  56. package/src/overwrites/venia-ui/lib/components/MegaMenu/megaMenuItem.js +6 -2
  57. package/src/overwrites/venia-ui/lib/components/MegaMenu/submenu.js +0 -21
  58. package/src/overwrites/venia-ui/lib/components/OrderHistoryPage/Reviews/reviewModal.js +8 -0
  59. package/src/overwrites/venia-ui/lib/components/OrderHistoryPage/orderHistoryPage.js +3 -3
  60. package/src/overwrites/venia-ui/lib/components/OrderHistoryPage/orderRow.js +4 -85
  61. package/src/talons/HelpCenter/helpCenter.gql.js +93 -0
  62. package/src/talons/HelpCenter/useHelpCenter.js +59 -0
  63. package/src/talons/ProductContent/productContent.gql.js +0 -16
  64. package/src/talons/ProductContent/useProductContent.js +0 -4
  65. package/src/talons/RelatedProducts/relatedProducts.gql.js +209 -0
  66. package/src/talons/RelatedProducts/useRelatedProducts.js +112 -0
  67. package/src/talons/SellerProducts/productContent.gql.js +1 -17
  68. package/src/talons/SellerProducts/useProductContent.js +1 -36
  69. package/src/components/AttributesBlock/attributesBlock.js +0 -54
  70. package/src/components/AttributesBlock/attributesBlock.module.css +0 -28
  71. package/src/components/ShopBy/shopBy copy.js +0 -172
  72. package/src/components/SubCategory/customSubCategory.js +0 -45
  73. package/src/components/SubCategory/customSubCategory.module.css +0 -22
  74. package/src/talons/AttributesBlock/attributesBlock.gql.js +0 -15
  75. package/src/talons/AttributesBlock/useAttributesBlock.js +0 -38
  76. package/src/talons/SubCategory/useCustomSubCategory.js +0 -50
@@ -0,0 +1,833 @@
1
+ import { useCallback, useState, useMemo } from 'react';
2
+ import { useToasts } from '@magento/peregrine/lib';
3
+ import { useIntl } from 'react-intl';
4
+ import { useMutation, useQuery } from '@apollo/client';
5
+ import { useCartContext } from '@magento/peregrine/lib/context/cart';
6
+ import { useUserContext } from '@magento/peregrine/lib/context/user';
7
+
8
+ import { useHistory } from 'react-router-dom';
9
+
10
+ import { appendOptionsToPayload } from '@magento/peregrine/lib/util/appendOptionsToPayload';
11
+ import { findMatchingVariant } from '@magento/peregrine/lib/util/findMatchingProductVariant';
12
+ import { isProductConfigurable } from '@magento/peregrine/lib/util/isProductConfigurable';
13
+ import { isSupportedProductType as isSupported } from '@magento/peregrine/lib/util/isSupportedProductType';
14
+ import { deriveErrorMessage } from '@magento/peregrine/lib/util/deriveErrorMessage';
15
+ import mergeOperations from '@magento/peregrine/lib/util/shallowMerge';
16
+ import defaultOperations from '@magento/peregrine/lib/talons/ProductFullDetail/productFullDetail.gql';
17
+ import productReviewOperations from '@riosst100/pwa-marketplace/src/overwrites/peregrine/lib/talons/RelatedProducts/productReview.gql';
18
+ import { useEventingContext } from '@magento/peregrine/lib/context/eventing';
19
+ import { getOutOfStockVariants } from '@magento/peregrine/lib/util/getOutOfStockVariants';
20
+
21
+ const INITIAL_OPTION_CODES = new Map();
22
+ const INITIAL_OPTION_SELECTIONS = new Map();
23
+ const OUT_OF_STOCK_CODE = 'OUT_OF_STOCK';
24
+ const IN_STOCK_CODE = 'IN_STOCK';
25
+
26
+ const deriveOptionCodesFromProduct = product => {
27
+ // If this is a simple product it has no option codes.
28
+ if (!isProductConfigurable(product)) {
29
+ return INITIAL_OPTION_CODES;
30
+ }
31
+
32
+ // Initialize optionCodes based on the options of the product.
33
+ const initialOptionCodes = new Map();
34
+ for (const {
35
+ attribute_id,
36
+ attribute_code
37
+ } of product.configurable_options) {
38
+ initialOptionCodes.set(attribute_id, attribute_code);
39
+ }
40
+
41
+ return initialOptionCodes;
42
+ };
43
+
44
+ // Similar to deriving the initial codes for each option.
45
+ const deriveOptionSelectionsFromProduct = product => {
46
+ if (!isProductConfigurable(product)) {
47
+ return INITIAL_OPTION_SELECTIONS;
48
+ }
49
+
50
+ const initialOptionSelections = new Map();
51
+ for (const { attribute_id } of product.configurable_options) {
52
+ initialOptionSelections.set(attribute_id, undefined);
53
+ }
54
+
55
+ return initialOptionSelections;
56
+ };
57
+
58
+ const getIsMissingOptions = (product, optionSelections) => {
59
+ // Non-configurable products can't be missing options.
60
+ if (!isProductConfigurable(product)) {
61
+ return false;
62
+ }
63
+
64
+ // Configurable products are missing options if we have fewer
65
+ // option selections than the product has options.
66
+ const { configurable_options } = product;
67
+ const numProductOptions = configurable_options.length;
68
+ const numProductSelections = Array.from(optionSelections.values()).filter(
69
+ value => !!value
70
+ ).length;
71
+
72
+ return numProductSelections < numProductOptions;
73
+ };
74
+
75
+ const getIsOutOfStock = (product, optionCodes, optionSelections) => {
76
+ const { stock_status, variants } = product;
77
+ const isConfigurable = isProductConfigurable(product);
78
+ const optionsSelected =
79
+ Array.from(optionSelections.values()).filter(value => !!value).length >
80
+ 0;
81
+
82
+ if (isConfigurable && optionsSelected) {
83
+ const item = findMatchingVariant({
84
+ optionCodes,
85
+ optionSelections,
86
+ variants
87
+ });
88
+ const stockStatus = item?.product?.stock_status;
89
+
90
+ return stockStatus === OUT_OF_STOCK_CODE || !stockStatus;
91
+ }
92
+ return stock_status === OUT_OF_STOCK_CODE;
93
+ };
94
+ const getIsAllOutOfStock = product => {
95
+ const { stock_status, variants } = product;
96
+ const isConfigurable = isProductConfigurable(product);
97
+
98
+ if (isConfigurable) {
99
+ const inStockItem = variants.find(item => {
100
+ return item.product.stock_status === IN_STOCK_CODE;
101
+ });
102
+ return !inStockItem;
103
+ }
104
+
105
+ return stock_status === OUT_OF_STOCK_CODE;
106
+ };
107
+
108
+ const getMediaGalleryEntries = (product, optionCodes, optionSelections) => {
109
+ let value = [];
110
+
111
+ const { media_gallery_entries, variants } = product;
112
+ const isConfigurable = isProductConfigurable(product);
113
+
114
+ let variantGallery = variants && variants.length ? variants.flatMap(variant => variant.product.media_gallery_entries || []) : [];
115
+ if (variantGallery) {
116
+ // variantGallery = variantGallery.sort((a, b) => a.uid - b.uid);
117
+ }
118
+
119
+ value = isConfigurable && variants && variants.length > 0
120
+ ? [
121
+ ...media_gallery_entries,
122
+ ...variantGallery
123
+ ]
124
+ : media_gallery_entries;
125
+
126
+ return value;
127
+ };
128
+
129
+ const getSelectedMedia = (product, optionCodes, optionSelections) => {
130
+ let valueSelected = [];
131
+
132
+ const { media_gallery_entries, variants } = product;
133
+ const isConfigurable = isProductConfigurable(product);
134
+
135
+ // Selections are initialized to "code => undefined". Once we select a value, like color, the selections change. This filters out unselected options.
136
+ const optionsSelected =
137
+ Array.from(optionSelections.values()).filter(value => !!value).length >
138
+ 0;
139
+
140
+ if (isConfigurable && optionsSelected) {
141
+ // If any of the possible variants matches the selection add that
142
+ // variant's image to the media gallery. NOTE: This _can_, and does,
143
+ // include variants such as size. If Magento is configured to display
144
+ // an image for a size attribute, it will render that image.
145
+ const item = findMatchingVariant({
146
+ optionCodes,
147
+ optionSelections,
148
+ variants
149
+ });
150
+
151
+ valueSelected = item
152
+ ? item.product.media_gallery_entries
153
+ : media_gallery_entries;
154
+ }
155
+
156
+ return valueSelected;
157
+ };
158
+
159
+ // We only want to display breadcrumbs for one category on a PDP even if a
160
+ // product has multiple related categories. This function filters and selects
161
+ // one category id for that purpose.
162
+ const getBreadcrumbCategoryId = categories => {
163
+ // Exit if there are no categories for this product.
164
+ if (!categories || !categories.length) {
165
+ return;
166
+ }
167
+ const breadcrumbSet = new Set();
168
+ categories.forEach(({ breadcrumbs }) => {
169
+ // breadcrumbs can be `null`...
170
+ (breadcrumbs || []).forEach(({ category_id }) =>
171
+ breadcrumbSet.add(category_id)
172
+ );
173
+ });
174
+
175
+ // Until we can get the single canonical breadcrumb path to a product we
176
+ // will just return the first category id of the potential leaf categories.
177
+ const leafCategory = categories.find(
178
+ category => !breadcrumbSet.has(category.uid)
179
+ );
180
+
181
+ // If we couldn't find a leaf category then just use the first category
182
+ // in the list for this product.
183
+ return leafCategory.uid || categories[0].uid;
184
+ };
185
+
186
+ const getConfigPrice = (product, optionCodes, optionSelections) => {
187
+ let value;
188
+
189
+ const { variants } = product;
190
+ const isConfigurable = isProductConfigurable(product);
191
+
192
+ const optionsSelected =
193
+ Array.from(optionSelections.values()).filter(value => !!value).length >
194
+ 0;
195
+
196
+ if (!isConfigurable) {
197
+ value = product.price_range?.maximum_price;
198
+ } else {
199
+ const item = findMatchingVariant({
200
+ optionCodes,
201
+ optionSelections,
202
+ variants
203
+ });
204
+
205
+ const minPrice = product.price_range?.minimum_price.final_price.value;
206
+ const maxPrice = product.price_range?.maximum_price.final_price.value;
207
+
208
+ let val = minPrice;
209
+ if (minPrice != maxPrice) {
210
+ val = minPrice + ' - ' + maxPrice;
211
+ }
212
+
213
+ const priceRange = {
214
+ final_price: {
215
+ value: val,
216
+ currency: product.price_range?.minimum_price.final_price.currency
217
+ }
218
+ };
219
+
220
+ value = optionsSelected && item
221
+ ? item.product.price_range?.maximum_price
222
+ : priceRange;
223
+ }
224
+
225
+ return value;
226
+ };
227
+
228
+ const getConfigName = (product, optionCodes, optionSelections) => {
229
+ let value;
230
+
231
+ const { variants } = product;
232
+ const isConfigurable = isProductConfigurable(product);
233
+
234
+ const optionsSelected =
235
+ Array.from(optionSelections.values()).filter(value => !!value).length >
236
+ 0;
237
+
238
+ if (!isConfigurable) {
239
+ value = product.name;
240
+ } else {
241
+ const item = findMatchingVariant({
242
+ optionCodes,
243
+ optionSelections,
244
+ variants
245
+ });
246
+
247
+ value = optionsSelected && item
248
+ ? item.product.name
249
+ : product.name;
250
+ }
251
+
252
+ return value;
253
+ };
254
+
255
+ const getConfigSku = (product, optionCodes, optionSelections) => {
256
+ let value;
257
+
258
+ const { variants } = product;
259
+ const isConfigurable = isProductConfigurable(product);
260
+
261
+ const optionsSelected =
262
+ Array.from(optionSelections.values()).filter(value => !!value).length >
263
+ 0;
264
+
265
+ if (!isConfigurable) {
266
+ value = product.name;
267
+ } else {
268
+ const item = findMatchingVariant({
269
+ optionCodes,
270
+ optionSelections,
271
+ variants
272
+ });
273
+
274
+ value = optionsSelected && item
275
+ ? item.product.sku
276
+ : product.sku;
277
+ }
278
+
279
+ return value;
280
+ };
281
+
282
+ const attributeLabelCompare = (attribute1, attribute2) => {
283
+ const label1 = attribute1['attribute_metadata']['label'].toLowerCase();
284
+ const label2 = attribute2['attribute_metadata']['label'].toLowerCase();
285
+ if (label1 < label2) return -1;
286
+ else if (label1 > label2) return 1;
287
+ else return 0;
288
+ };
289
+
290
+ const getCustomAttributes = (product, optionCodes, optionSelections) => {
291
+ const { custom_attributes, variants } = product;
292
+ const isConfigurable = isProductConfigurable(product);
293
+ const optionsSelected =
294
+ Array.from(optionSelections.values()).filter(value => !!value).length >
295
+ 0;
296
+
297
+ if (isConfigurable && optionsSelected) {
298
+ const item = findMatchingVariant({
299
+ optionCodes,
300
+ optionSelections,
301
+ variants
302
+ });
303
+
304
+ return item && item.product
305
+ ? [...item.product.custom_attributes].sort(attributeLabelCompare)
306
+ : [];
307
+ }
308
+
309
+ return custom_attributes
310
+ ? [...custom_attributes].sort(attributeLabelCompare)
311
+ : [];
312
+ };
313
+
314
+ /**
315
+ * @param {GraphQLDocument} props.addConfigurableProductToCartMutation - configurable product mutation
316
+ * @param {GraphQLDocument} props.addSimpleProductToCartMutation - configurable product mutation
317
+ * @param {Object.<string, GraphQLDocument>} props.operations - collection of operation overrides merged into defaults
318
+ * @param {Object} props.product - the product, see RootComponents/Product
319
+ *
320
+ * @returns {{
321
+ * breadcrumbCategoryId: string|undefined,
322
+ * errorMessage: string|undefined,
323
+ * handleAddToCart: func,
324
+ * handleSelectionChange: func,
325
+ * handleSetQuantity: func,
326
+ * isAddToCartDisabled: boolean,
327
+ * isSupportedProductType: boolean,
328
+ * mediaGalleryEntries: array,
329
+ * productDetails: object,
330
+ * quantity: number
331
+ * }}
332
+ */
333
+ export const useRelatedProducts = props => {
334
+ const {
335
+ addConfigurableProductToCartMutation,
336
+ addSimpleProductToCartMutation,
337
+ product,
338
+ isPreview
339
+ } = props;
340
+
341
+ const history = useHistory();
342
+
343
+ const handleToastAction = useCallback(
344
+ (removeToast) => {
345
+ history.push('/cart');
346
+ removeToast();
347
+ },
348
+ [history]
349
+ );
350
+
351
+ const [, { addToast }] = useToasts();
352
+
353
+ const { data: productReviewData, loading: loadingProductReview, error: errorProductReview, refetch: refetchProductReviews } = useQuery(
354
+ productReviewOperations.getProductReviews,
355
+ {
356
+ variables: { url_key: product?.url_key },
357
+ skip: !product?.url_key,
358
+ fetchPolicy: 'network-only'
359
+ }
360
+ );
361
+
362
+ // Query for ratings metadata
363
+ const { data: ratingsMetadataData, loading: loadingRatingsMetadata } = useQuery(
364
+ productReviewOperations.getProductReviewRatingsMetadata,
365
+ { fetchPolicy: 'network-only' }
366
+ );
367
+
368
+ // Mutation for creating review (allow partial data with errors)
369
+ const [createProductReview, { loading: loadingCreateReview, error: errorCreateReview }] = useMutation(
370
+ productReviewOperations.createProductReview,
371
+ { errorPolicy: 'all' }
372
+ );
373
+
374
+ // Handler for submitting review with robust toast handling
375
+ const handleSubmitReview = async (formValues) => {
376
+ try {
377
+ const result = await createProductReview({
378
+ variables: { input: formValues },
379
+ errorPolicy: 'all'
380
+ });
381
+
382
+ const gqlErrors = result?.errors || [];
383
+ const review = result?.data?.createProductReview?.review;
384
+
385
+ if (gqlErrors.length > 0 || !review) {
386
+ const message = gqlErrors[0]?.message || 'Failed to submit review!';
387
+ addToast({ type: 'error', message });
388
+ return { success: false, error: gqlErrors };
389
+ }
390
+
391
+ await refetchProductReviews();
392
+ addToast({ type: 'success', message: 'Review submitted successfully!' });
393
+ return { success: true };
394
+ } catch (e) {
395
+ addToast({ type: 'error', message: e?.message || 'Failed to submit review!' });
396
+ return { success: false, error: e };
397
+ }
398
+ };
399
+
400
+ const [, { dispatch }] = useEventingContext();
401
+
402
+ const hasDeprecatedOperationProp = !!(
403
+ addConfigurableProductToCartMutation || addSimpleProductToCartMutation
404
+ );
405
+ const operations = mergeOperations(defaultOperations, props.operations);
406
+
407
+ const productType = product.__typename;
408
+
409
+ const isSupportedProductType = isSupported(productType);
410
+
411
+ const [{ cartId }] = useCartContext();
412
+ const [{ isSignedIn, currentUser }] = useUserContext();
413
+ const { formatMessage } = useIntl();
414
+
415
+ const { data: storeConfigData } = useQuery(
416
+ operations.getWishlistConfigQuery,
417
+ {
418
+ fetchPolicy: 'cache-and-network'
419
+ }
420
+ );
421
+
422
+ const [
423
+ addConfigurableProductToCart,
424
+ {
425
+ error: errorAddingConfigurableProduct,
426
+ loading: isAddConfigurableLoading
427
+ }
428
+ ] = useMutation(
429
+ addConfigurableProductToCartMutation ||
430
+ operations.addConfigurableProductToCartMutation
431
+ );
432
+
433
+ const [
434
+ addSimpleProductToCart,
435
+ { error: errorAddingSimpleProduct, loading: isAddSimpleLoading }
436
+ ] = useMutation(
437
+ addSimpleProductToCartMutation ||
438
+ operations.addSimpleProductToCartMutation
439
+ );
440
+
441
+ const [
442
+ addProductToCart,
443
+ {
444
+ data: addToCartResponseData,
445
+ error: errorAddingProductToCart,
446
+ loading: isAddProductLoading
447
+ }
448
+ ] = useMutation(operations.addProductToCartMutation);
449
+
450
+ const breadcrumbCategoryId = useMemo(
451
+ () => getBreadcrumbCategoryId(product.categories),
452
+ [product.categories]
453
+ );
454
+
455
+ const derivedOptionSelections = useMemo(
456
+ () => deriveOptionSelectionsFromProduct(product),
457
+ [product]
458
+ );
459
+
460
+ const [optionSelections, setOptionSelections] = useState(
461
+ derivedOptionSelections
462
+ );
463
+
464
+ const [singleOptionSelection, setSingleOptionSelection] = useState();
465
+
466
+ const derivedOptionCodes = useMemo(
467
+ () => deriveOptionCodesFromProduct(product),
468
+ [product]
469
+ );
470
+ const [optionCodes] = useState(derivedOptionCodes);
471
+
472
+ const isMissingOptions = useMemo(
473
+ () => getIsMissingOptions(product, optionSelections),
474
+ [product, optionSelections]
475
+ );
476
+
477
+ const isOutOfStock = useMemo(
478
+ () => getIsOutOfStock(product, optionCodes, optionSelections),
479
+ [product, optionCodes, optionSelections]
480
+ );
481
+
482
+ // Check if display out of stock products option is selected in the Admin Dashboard
483
+ const isOutOfStockProductDisplayed = useMemo(() => {
484
+ let totalVariants = 1;
485
+ const isConfigurable = isProductConfigurable(product);
486
+ if (product.configurable_options && isConfigurable) {
487
+ for (const option of product.configurable_options) {
488
+ const length = option.values.length;
489
+ totalVariants = totalVariants * length;
490
+ }
491
+ return product.variants.length === totalVariants;
492
+ }
493
+ }, [product]);
494
+
495
+ const isEverythingOutOfStock = useMemo(() => getIsAllOutOfStock(product), [
496
+ product
497
+ ]);
498
+
499
+ const outOfStockVariants = useMemo(
500
+ () =>
501
+ getOutOfStockVariants(
502
+ product,
503
+ optionCodes,
504
+ singleOptionSelection,
505
+ optionSelections,
506
+ isOutOfStockProductDisplayed
507
+ ),
508
+ [
509
+ product,
510
+ optionCodes,
511
+ singleOptionSelection,
512
+ optionSelections,
513
+ isOutOfStockProductDisplayed
514
+ ]
515
+ );
516
+
517
+ const mediaGalleryEntries = useMemo(
518
+ () => getMediaGalleryEntries(product, optionCodes, optionSelections),
519
+ [product, optionCodes, optionSelections]
520
+ );
521
+
522
+ const selectedMedia = useMemo(
523
+ () => getSelectedMedia(product, optionCodes, optionSelections),
524
+ [product, optionCodes, optionSelections]
525
+ );
526
+
527
+ const customAttributes = useMemo(
528
+ () => getCustomAttributes(product, optionCodes, optionSelections),
529
+ [product, optionCodes, optionSelections]
530
+ );
531
+
532
+ const productPrice = useMemo(
533
+ () => getConfigPrice(product, optionCodes, optionSelections),
534
+ [product, optionCodes, optionSelections]
535
+ );
536
+
537
+ const productName = useMemo(
538
+ () => getConfigName(product, optionCodes, optionSelections),
539
+ [product, optionCodes, optionSelections]
540
+ );
541
+
542
+ const productSku = useMemo(
543
+ () => getConfigSku(product, optionCodes, optionSelections),
544
+ [product, optionCodes, optionSelections]
545
+ );
546
+
547
+ // The map of ids to values (and their uids)
548
+ // For example:
549
+ // { "179" => [{ uid: "abc", value_index: 1 }, { uid: "def", value_index: 2 }]}
550
+ const attributeIdToValuesMap = useMemo(() => {
551
+ const map = new Map();
552
+ // For simple items, this will be an empty map.
553
+ const options = product.configurable_options || [];
554
+ for (const { attribute_id, values } of options) {
555
+ map.set(attribute_id, values);
556
+ }
557
+ return map;
558
+ }, [product.configurable_options]);
559
+
560
+ // An array of selected option uids. Useful for passing to mutations.
561
+ // For example:
562
+ // ["abc", "def"]
563
+ const selectedOptionsArray = useMemo(() => {
564
+ const selectedOptions = [];
565
+
566
+ optionSelections.forEach((value, key) => {
567
+ const values = attributeIdToValuesMap.get(key);
568
+
569
+ const selectedValue = values?.find(
570
+ item => item.value_index === value
571
+ );
572
+
573
+ if (selectedValue) {
574
+ selectedOptions.push(selectedValue.uid);
575
+ }
576
+ });
577
+ return selectedOptions;
578
+ }, [attributeIdToValuesMap, optionSelections]);
579
+
580
+ const handleAddToCart = useCallback(
581
+ async formValues => {
582
+ const { quantity, preorder } = formValues;
583
+
584
+ /*
585
+ @deprecated in favor of general addProductsToCart mutation. Will support until the next MAJOR.
586
+ */
587
+ if (hasDeprecatedOperationProp) {
588
+ const payload = {
589
+ item: product,
590
+ productType,
591
+ quantity
592
+ };
593
+
594
+ if (isProductConfigurable(product)) {
595
+ appendOptionsToPayload(
596
+ payload,
597
+ optionSelections,
598
+ optionCodes
599
+ );
600
+ }
601
+
602
+ if (isSupportedProductType) {
603
+ const variables = {
604
+ cartId,
605
+ parentSku: payload.parentSku,
606
+ product: payload.item,
607
+ quantity: payload.quantity,
608
+ sku: payload.item.sku
609
+ };
610
+ // Use the proper mutation for the type.
611
+ if (productType === 'SimpleProduct') {
612
+ try {
613
+ await addSimpleProductToCart({
614
+ variables
615
+ });
616
+ } catch {
617
+ return;
618
+ }
619
+ } else if (productType === 'ConfigurableProduct') {
620
+ try {
621
+ await addConfigurableProductToCart({
622
+ variables
623
+ });
624
+ } catch {
625
+ return;
626
+ }
627
+ }
628
+ } else {
629
+ console.error(
630
+ 'Unsupported product type. Cannot add to cart.'
631
+ );
632
+ }
633
+ } else {
634
+ const variables = {
635
+ cartId,
636
+ product: {
637
+ sku: product.sku,
638
+ quantity
639
+ }
640
+ };
641
+
642
+ if (preorder && Object.keys(preorder).length) {
643
+ variables.product.preorder = preorder;
644
+ }
645
+
646
+ if (selectedOptionsArray.length) {
647
+ variables.product.selected_options = selectedOptionsArray;
648
+ }
649
+
650
+ try {
651
+ await addProductToCart({ variables });
652
+
653
+ const selectedOptionsLabels =
654
+ selectedOptionsArray?.map((uid, i) => ({
655
+ attribute: product.configurable_options[i].label,
656
+ value:
657
+ product.configurable_options[i].values.findLast(
658
+ x => x.uid === uid
659
+ )?.label || null
660
+ })) || null;
661
+
662
+ dispatch({
663
+ type: 'CART_ADD_ITEM',
664
+ payload: {
665
+ cartId,
666
+ sku: product.sku,
667
+ name: product.name,
668
+ pricing: product.price,
669
+ priceTotal: productPrice.final_price.value,
670
+ currencyCode: productPrice.final_price.currency,
671
+ discountAmount: productPrice.discount.amount_off,
672
+ selectedOptions: selectedOptionsLabels,
673
+ quantity
674
+ }
675
+ });
676
+ } catch {
677
+ return;
678
+ }
679
+ }
680
+ },
681
+ [
682
+ addConfigurableProductToCart,
683
+ addProductToCart,
684
+ addSimpleProductToCart,
685
+ cartId,
686
+ dispatch,
687
+ hasDeprecatedOperationProp,
688
+ isSupportedProductType,
689
+ optionCodes,
690
+ optionSelections,
691
+ product,
692
+ productPrice,
693
+ productType,
694
+ selectedOptionsArray
695
+ ]
696
+ );
697
+
698
+ const handleSelectionChange = useCallback(
699
+ (optionId, selection) => {
700
+ // We must create a new Map here so that React knows that the value
701
+ // of optionSelections has changed.
702
+ const nextOptionSelections = new Map([...optionSelections]);
703
+ nextOptionSelections.set(optionId, selection);
704
+ setOptionSelections(nextOptionSelections);
705
+ // Create a new Map to keep track of single selections with key as String
706
+ const nextSingleOptionSelection = new Map();
707
+ nextSingleOptionSelection.set(optionId, selection);
708
+ setSingleOptionSelection(nextSingleOptionSelection);
709
+ },
710
+ [optionSelections]
711
+ );
712
+
713
+ // Normalization object for product details we need for rendering.
714
+ const productDetails = {
715
+ description: product.description,
716
+ shortDescription: product.short_description,
717
+ name: productName,
718
+ price: productPrice?.final_price,
719
+ price_range: product?.price_range,
720
+ sku: productSku,
721
+ publish_status: product.publish_status,
722
+ custom_table_metadata: product.custom_table_metadata,
723
+ term_and_conditions: product.term_and_conditions,
724
+ link_to_other_stores: product.link_to_other_stores,
725
+ shipping_policy: product.shipping_policy,
726
+ return_policy: product.return_policy,
727
+ preorder: product.preorder,
728
+ auction_data: product.auction_data
729
+ };
730
+
731
+ const sellerDetails = {
732
+ name: product.seller?.name,
733
+ url_key: product.seller?.url_key,
734
+ city: product.seller?.city,
735
+ country: product.seller?.country
736
+ };
737
+
738
+ const derivedErrorMessage = useMemo(
739
+ () =>
740
+ deriveErrorMessage([
741
+ errorAddingSimpleProduct,
742
+ errorAddingConfigurableProduct,
743
+ errorAddingProductToCart,
744
+ ...(addToCartResponseData?.addProductsToCart?.user_errors || [])
745
+ ]),
746
+ [
747
+ errorAddingConfigurableProduct,
748
+ errorAddingProductToCart,
749
+ errorAddingSimpleProduct,
750
+ addToCartResponseData
751
+ ]
752
+ );
753
+
754
+ const wishlistItemOptions = useMemo(() => {
755
+ const options = {
756
+ quantity: 1,
757
+ sku: product.sku
758
+ };
759
+
760
+ if (productType === 'ConfigurableProduct') {
761
+ options.selected_options = selectedOptionsArray;
762
+ }
763
+
764
+ return options;
765
+ }, [product, productType, selectedOptionsArray]);
766
+
767
+ const wishlistButtonProps = {
768
+ buttonText: isSelected =>
769
+ isSelected
770
+ ? formatMessage({
771
+ id: 'wishlistButton.addedText',
772
+ defaultMessage: 'Added to Favorites'
773
+ })
774
+ : formatMessage({
775
+ id: 'wishlistButton.addText',
776
+ defaultMessage: 'Add to Favorites'
777
+ }),
778
+ item: wishlistItemOptions,
779
+ storeConfig: storeConfigData ? storeConfigData.storeConfig : {}
780
+ };
781
+
782
+ const defaultNickname = useMemo(() => {
783
+ if (!currentUser) return '';
784
+ const first = currentUser.firstname || '';
785
+ const last = currentUser.lastname || '';
786
+ const full = `${first} ${last}`.trim();
787
+ if (full) return full;
788
+ const email = currentUser.email || '';
789
+ return email ? email.split('@')[0] : '';
790
+ }, [currentUser]);
791
+
792
+ return {
793
+ addToCartResponseData,
794
+ errorAddingProductToCart,
795
+ isAddProductLoading,
796
+ breadcrumbCategoryId,
797
+ errorMessage: derivedErrorMessage,
798
+ handleAddToCart,
799
+ handleSelectionChange,
800
+ isOutOfStock,
801
+ isEverythingOutOfStock,
802
+ outOfStockVariants,
803
+ isAddToCartDisabled:
804
+ isPreview || isOutOfStock ||
805
+ isEverythingOutOfStock ||
806
+ isMissingOptions ||
807
+ isAddConfigurableLoading ||
808
+ isAddSimpleLoading ||
809
+ isAddProductLoading,
810
+ isSupportedProductType,
811
+ mediaGalleryEntries,
812
+ selectedMedia,
813
+ shouldShowWishlistButton:
814
+ isSignedIn &&
815
+ storeConfigData &&
816
+ !!storeConfigData.storeConfig.magento_wishlist_general_is_enabled,
817
+ productDetails,
818
+ customAttributes,
819
+ wishlistButtonProps,
820
+ wishlistItemOptions,
821
+ sellerDetails,
822
+ productReviewData,
823
+ loadingProductReview,
824
+ errorProductReview,
825
+ ratingsMetadataData,
826
+ loadingRatingsMetadata,
827
+ handleSubmitReview,
828
+ loadingCreateReview,
829
+ errorCreateReview,
830
+ defaultNickname,
831
+ handleToastAction
832
+ };
833
+ };