@wix/headless-stores 0.0.103 → 0.0.105

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,6 +1,6 @@
1
1
  import type { V3Product } from '@wix/auto_sdk_stores_products-v-3';
2
- import React from 'react';
3
2
  import { AsChildChildren } from '@wix/headless-utils/react';
3
+ import React from 'react';
4
4
  import { AsContent } from './types.js';
5
5
  /**
6
6
  * Context for sharing variant options state between components
@@ -885,6 +885,53 @@ export declare const Action: {
885
885
  /** Pre-order action button */
886
886
  readonly PreOrder: React.ForwardRefExoticComponent<ProductActionProps & React.RefAttributes<HTMLButtonElement>>;
887
887
  };
888
+ /**
889
+ * Props for Product Link component
890
+ */
891
+ export interface ProductLinkProps {
892
+ /** Whether to render as a child component */
893
+ asChild?: boolean;
894
+ /** Custom render function when using asChild */
895
+ children?: React.ReactNode;
896
+ /** CSS classes to apply to the link */
897
+ className?: string;
898
+ /** List or collection the product is part of (e.g., "Category: T-Shirts") */
899
+ list?: string;
900
+ /** Position of the product within a list or collection */
901
+ position?: string;
902
+ /** Event origin (e.g., "Search Results", "Related Products") */
903
+ origin?: string;
904
+ }
905
+ /**
906
+ * Product link component that automatically tracks ClickProduct events.
907
+ * Wraps a link to the product page and fires analytics when clicked.
908
+ *
909
+ * @component
910
+ * @example
911
+ * ```tsx
912
+ * // Simple usage
913
+ * <Product.Link className="btn-secondary">
914
+ * View Product
915
+ * </Product.Link>
916
+ *
917
+ * // With tracking parameters
918
+ * <Product.Link
919
+ * list="Category: T-Shirts"
920
+ * position="3"
921
+ * className="btn-secondary"
922
+ * >
923
+ * View Product
924
+ * </Product.Link>
925
+ *
926
+ * // asChild with custom link
927
+ * <Product.Link asChild list="Search Results" position="5">
928
+ * <a href="/custom-url" className="custom-link">
929
+ * <Product.Name />
930
+ * </a>
931
+ * </Product.Link>
932
+ * ```
933
+ */
934
+ export declare const Link: React.ForwardRefExoticComponent<ProductLinkProps & React.RefAttributes<HTMLAnchorElement>>;
888
935
  /**
889
936
  * Props for ProductVariantStock component
890
937
  */
@@ -1,17 +1,19 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { InventoryAvailabilityStatus } from '@wix/auto_sdk_stores_products-v-3';
3
- import React from 'react';
3
+ import { Quantity as QuantityComponent } from '@wix/headless-components/react';
4
4
  import { Commerce } from '@wix/headless-ecom/react';
5
- import { AsChildSlot } from '@wix/headless-utils/react';
6
5
  import { MediaGallery } from '@wix/headless-media/react';
7
- import { Quantity as QuantityComponent } from '@wix/headless-components/react';
6
+ import { AsChildSlot } from '@wix/headless-utils/react';
7
+ import { useService } from '@wix/services-manager-react';
8
+ import React from 'react';
9
+ import { DataComponentTags } from '../data-component-tags.js';
10
+ import { ProductServiceDefinition, SelectedVariantServiceDefinition, } from '../services/index.js';
8
11
  import * as CoreProduct from './core/Product.js';
9
- import * as CoreProductVariantSelector from './core/ProductVariantSelector.js';
10
12
  import * as CoreProductModifiers from './core/ProductModifiers.js';
13
+ import * as CoreProductVariantSelector from './core/ProductVariantSelector.js';
11
14
  import * as CoreSelectedVariant from './core/SelectedVariant.js';
12
15
  import * as Option from './Option.js';
13
16
  import { AsContent } from './types.js';
14
- import { DataComponentTags } from '../data-component-tags.js';
15
17
  const VariantsContext = React.createContext(null);
16
18
  /**
17
19
  * Hook to access variants context
@@ -61,6 +63,7 @@ var TestIds;
61
63
  TestIds["productActionAddToCart"] = "product-action-add-to-cart";
62
64
  TestIds["productActionBuyNow"] = "product-action-buy-now";
63
65
  TestIds["productActionPreOrder"] = "product-action-can-pre-order";
66
+ TestIds["productLink"] = "product-link";
64
67
  TestIds["productQuantity"] = "product-quantity";
65
68
  TestIds["productQuantityDecrement"] = "product-quantity-decrement";
66
69
  TestIds["productQuantityInput"] = "product-quantity-input";
@@ -879,6 +882,19 @@ export const ProductVariantSelectorReset = React.forwardRef((props, ref) => {
879
882
  */
880
883
  export const ProductActionAddToCart = React.forwardRef((props, ref) => {
881
884
  const { asChild, children, className, label, loadingState } = props;
885
+ // Get services for tracking
886
+ const productService = useService(ProductServiceDefinition);
887
+ const variantService = useService(SelectedVariantServiceDefinition);
888
+ // Track after successful add to cart
889
+ const handleSuccess = async () => {
890
+ const choices = variantService.selectedChoices.get();
891
+ const quantity = variantService.selectedQuantity.get();
892
+ const variantString = Object.values(choices).join(' ') || undefined;
893
+ productService.reportAddToCartTrackEvent({
894
+ quantity,
895
+ variant: variantString,
896
+ });
897
+ };
882
898
  return (_jsx(CoreSelectedVariant.Actions, { children: ({ lineItems, canAddToCart, isLoading, addToCart, isPreOrderEnabled, }) => {
883
899
  if (isPreOrderEnabled) {
884
900
  return null;
@@ -886,13 +902,13 @@ export const ProductActionAddToCart = React.forwardRef((props, ref) => {
886
902
  const onClick = addToCart;
887
903
  const disabled = !canAddToCart || isLoading;
888
904
  if (asChild && children) {
889
- return (_jsx(Commerce.Actions.AddToCart, { lineItems: lineItems, asChild: asChild, children: _jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.productActionAddToCart, "data-in-progress": isLoading, customElement: children, customElementProps: {
905
+ return (_jsx(Commerce.Actions.AddToCart, { lineItems: lineItems, asChild: asChild, onSuccess: handleSuccess, children: _jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.productActionAddToCart, "data-in-progress": isLoading, customElement: children, customElementProps: {
890
906
  disabled,
891
907
  isLoading,
892
908
  onClick,
893
909
  } }) }));
894
910
  }
895
- return (_jsx(Commerce.Actions.AddToCart, { ref: ref, label: label, lineItems: lineItems, className: className, disabled: disabled, loadingState: loadingState, "data-testid": TestIds.productActionAddToCart, "data-in-progress": isLoading }));
911
+ return (_jsx(Commerce.Actions.AddToCart, { ref: ref, label: label, lineItems: lineItems, className: className, disabled: disabled, loadingState: loadingState, onSuccess: handleSuccess, "data-testid": TestIds.productActionAddToCart, "data-in-progress": isLoading }));
896
912
  } }));
897
913
  });
898
914
  /**
@@ -923,6 +939,19 @@ export const ProductActionBuyNow = React.forwardRef((props, ref) => {
923
939
  */
924
940
  export const ProductActionPreOrder = React.forwardRef((props, ref) => {
925
941
  const { asChild, children, className, label, loadingState } = props;
942
+ // Get services for tracking
943
+ const productService = useService(ProductServiceDefinition);
944
+ const variantService = useService(SelectedVariantServiceDefinition);
945
+ // Track after successful pre-order
946
+ const handleSuccess = async () => {
947
+ const choices = variantService.selectedChoices.get();
948
+ const quantity = variantService.selectedQuantity.get();
949
+ const variantString = Object.values(choices).join(' ') || undefined;
950
+ productService.reportAddToCartTrackEvent({
951
+ quantity,
952
+ variant: variantString,
953
+ });
954
+ };
926
955
  return (_jsx(CoreSelectedVariant.Actions, { children: ({ lineItems, isLoading, addToCart, isPreOrderEnabled }) => {
927
956
  if (!isPreOrderEnabled) {
928
957
  return null;
@@ -931,13 +960,13 @@ export const ProductActionPreOrder = React.forwardRef((props, ref) => {
931
960
  const onClick = addToCart;
932
961
  const disabled = !canPreOrder;
933
962
  if (asChild && children) {
934
- return (_jsx(Commerce.Actions.AddToCart, { lineItems: lineItems, asChild: asChild, children: _jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.productActionPreOrder, "data-in-progress": isLoading, customElement: children, customElementProps: {
963
+ return (_jsx(Commerce.Actions.AddToCart, { lineItems: lineItems, asChild: asChild, onSuccess: handleSuccess, children: _jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.productActionPreOrder, "data-in-progress": isLoading, customElement: children, customElementProps: {
935
964
  disabled,
936
965
  isLoading,
937
966
  onClick,
938
967
  } }) }));
939
968
  }
940
- return (_jsx(Commerce.Actions.AddToCart, { ref: ref, label: label, lineItems: lineItems, className: className, disabled: disabled, loadingState: loadingState, "data-testid": TestIds.productActionPreOrder, "data-in-progress": isLoading }));
969
+ return (_jsx(Commerce.Actions.AddToCart, { ref: ref, label: label, lineItems: lineItems, className: className, disabled: disabled, loadingState: loadingState, onSuccess: handleSuccess, "data-testid": TestIds.productActionPreOrder, "data-in-progress": isLoading }));
941
970
  } }));
942
971
  });
943
972
  /**
@@ -952,6 +981,57 @@ export const Action = {
952
981
  /** Pre-order action button */
953
982
  PreOrder: ProductActionPreOrder,
954
983
  };
984
+ /**
985
+ * Product link component that automatically tracks ClickProduct events.
986
+ * Wraps a link to the product page and fires analytics when clicked.
987
+ *
988
+ * @component
989
+ * @example
990
+ * ```tsx
991
+ * // Simple usage
992
+ * <Product.Link className="btn-secondary">
993
+ * View Product
994
+ * </Product.Link>
995
+ *
996
+ * // With tracking parameters
997
+ * <Product.Link
998
+ * list="Category: T-Shirts"
999
+ * position="3"
1000
+ * className="btn-secondary"
1001
+ * >
1002
+ * View Product
1003
+ * </Product.Link>
1004
+ *
1005
+ * // asChild with custom link
1006
+ * <Product.Link asChild list="Search Results" position="5">
1007
+ * <a href="/custom-url" className="custom-link">
1008
+ * <Product.Name />
1009
+ * </a>
1010
+ * </Product.Link>
1011
+ * ```
1012
+ */
1013
+ export const Link = React.forwardRef((props, ref) => {
1014
+ const { asChild, children, className, list, position, origin } = props;
1015
+ // Get services for tracking
1016
+ const productService = useService(ProductServiceDefinition);
1017
+ const variantService = useService(SelectedVariantServiceDefinition);
1018
+ return (_jsx(CoreProduct.Slug, { children: ({ slug }) => {
1019
+ const handleClick = () => {
1020
+ const choices = variantService.selectedChoices.get();
1021
+ const variantString = Object.values(choices).join(' ') || undefined;
1022
+ productService.reportClickProductTrackEvent({
1023
+ variant: variantString,
1024
+ list,
1025
+ position,
1026
+ origin,
1027
+ });
1028
+ };
1029
+ if (asChild && children) {
1030
+ return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.productLink, customElement: children, customElementProps: { onClick: handleClick } }));
1031
+ }
1032
+ return (_jsx("a", { ref: ref, href: `/${slug}`, onClick: handleClick, className: className, "data-testid": TestIds.productLink, children: children }));
1033
+ } }));
1034
+ });
955
1035
  /**
956
1036
  * Helper function to determine stock status and label based on availability and can-pre-order settings
957
1037
  */
@@ -1,5 +1,5 @@
1
- import { type Signal } from '@wix/services-definitions/core-services/signals';
2
1
  import * as productsV3 from '@wix/auto_sdk_stores_products-v-3';
2
+ import { type Signal } from '@wix/services-definitions/core-services/signals';
3
3
  /**
4
4
  * API interface for the Product service, providing reactive product data management.
5
5
  * This service handles loading and managing a single product's data, loading state, and errors.
@@ -15,6 +15,22 @@ export interface ProductServiceAPI {
15
15
  error: Signal<string | null>;
16
16
  /** Function to load a product by its slug */
17
17
  loadProduct: (slug: string) => Promise<void>;
18
+ /** Reports a ViewContent analytics event for the current product */
19
+ reportViewContentTrackEvent: () => void;
20
+ /** Reports an AddToCart analytics event for the current product */
21
+ reportAddToCartTrackEvent: (options?: {
22
+ quantity?: number;
23
+ variant?: string;
24
+ origin?: string;
25
+ position?: string;
26
+ }) => void;
27
+ /** Reports a ClickProduct analytics event for the current product */
28
+ reportClickProductTrackEvent: (options?: {
29
+ variant?: string;
30
+ list?: string;
31
+ position?: string;
32
+ origin?: string;
33
+ }) => void;
18
34
  }
19
35
  /**
20
36
  * Service definition for the Product service.
@@ -1,6 +1,7 @@
1
+ import * as productsV3 from '@wix/auto_sdk_stores_products-v-3';
1
2
  import { defineService, implementService } from '@wix/services-definitions';
2
3
  import { SignalsServiceDefinition, } from '@wix/services-definitions/core-services/signals';
3
- import * as productsV3 from '@wix/auto_sdk_stores_products-v-3';
4
+ import { analytics } from '@wix/site';
4
5
  /**
5
6
  * Service definition for the Product service.
6
7
  * This defines the contract that the ProductService must implement.
@@ -61,11 +62,160 @@ export const ProductService = implementService.withConfig()(ProductServiceDefini
61
62
  if (config.productSlug) {
62
63
  loadProduct(config.productSlug);
63
64
  }
65
+ /**
66
+ * Reports a ViewContent analytics event for product page tracking.
67
+ * This function automatically formats and sends a standardized ViewContent event
68
+ * to the Wix Analytics service when a user views a product page. It extracts
69
+ * relevant product information (brand, category, price, SKU) and sends it in
70
+ * the proper format for analytics tracking.
71
+ *
72
+ * @example
73
+ * ```tsx
74
+ * // Track when a product page loads
75
+ * import { useService } from '@wix/services-manager-react';
76
+ * import { ProductServiceDefinition } from '@wix/stores/services';
77
+ *
78
+ * function ProductPage() {
79
+ * const productService = useService(ProductServiceDefinition);
80
+ *
81
+ * useEffect(() => {
82
+ * productService.reportViewContentTrackEvent();
83
+ * }, []);
84
+ *
85
+ * return <div>Product Page</div>;
86
+ * }
87
+ * ```
88
+ */
89
+ const reportViewContentTrackEvent = () => {
90
+ const currentProduct = product.get();
91
+ if (!currentProduct)
92
+ return;
93
+ const eventData = {
94
+ brand: currentProduct.brand?.name ?? undefined,
95
+ category: currentProduct.breadcrumbsInfo?.breadcrumbs?.[0]?.categoryName,
96
+ currency: currentProduct.currency ?? undefined,
97
+ id: currentProduct._id,
98
+ name: currentProduct.name || '',
99
+ price: parseFloat(currentProduct.variantsInfo?.variants?.[0]?.price?.actualPrice
100
+ ?.amount ?? '0'),
101
+ sku: currentProduct.variantsInfo?.variants?.[0]?.sku ?? undefined,
102
+ };
103
+ analytics.trackEvent('ViewContent', eventData);
104
+ };
105
+ /**
106
+ * Reports an AddToCart analytics event for cart tracking.
107
+ * This function automatically formats and sends a standardized AddToCart event
108
+ * to the Wix Analytics service when a user adds a product to their cart. It extracts
109
+ * relevant product information (brand, category, price, SKU, variant, quantity) and sends
110
+ * it in the proper format for analytics tracking.
111
+ *
112
+ * @param options Additional options for the AddToCart event
113
+ * @param options.quantity The quantity being added to cart (defaults to 1)
114
+ * @param options.variant The selected product variant (e.g., "Large", "Green")
115
+ * @param options.origin Event origin (e.g., "Product Page", "Quick View")
116
+ * @param options.position List or collection position (e.g., "Product Gallery", "Search Results")
117
+ *
118
+ * @example
119
+ * ```tsx
120
+ * // Track add to cart from product page
121
+ * import { useService } from '@wix/services-manager-react';
122
+ * import { ProductServiceDefinition } from '@wix/stores/services';
123
+ *
124
+ * function AddToCartButton() {
125
+ * const productService = useService(ProductServiceDefinition);
126
+ *
127
+ * const handleAddToCart = () => {
128
+ * productService.reportAddToCartTrackEvent({
129
+ * quantity: 1,
130
+ * variant: 'Large Blue'
131
+ * });
132
+ * };
133
+ *
134
+ * return <button onClick={handleAddToCart}>Add to Cart</button>;
135
+ * }
136
+ * ```
137
+ */
138
+ const reportAddToCartTrackEvent = (options) => {
139
+ const currentProduct = product.get();
140
+ if (!currentProduct)
141
+ return;
142
+ const eventData = {
143
+ brand: currentProduct.brand?.name ?? undefined,
144
+ category: currentProduct.breadcrumbsInfo?.breadcrumbs?.[0]?.categoryName,
145
+ currency: currentProduct.currency ?? undefined,
146
+ id: currentProduct._id,
147
+ name: currentProduct.name || '',
148
+ price: parseFloat(currentProduct.variantsInfo?.variants?.[0]?.price?.actualPrice
149
+ ?.amount ?? '0'),
150
+ quantity: options?.quantity ?? 1,
151
+ sku: currentProduct.variantsInfo?.variants?.[0]?.sku ?? undefined,
152
+ variant: options?.variant,
153
+ origin: options?.origin,
154
+ position: options?.position,
155
+ };
156
+ analytics.trackEvent('AddToCart', eventData);
157
+ };
158
+ /**
159
+ * Reports a ClickProduct analytics event for product click tracking.
160
+ * This function automatically formats and sends a standardized ClickProduct event
161
+ * to the Wix Analytics service when a user clicks on a product link. It extracts
162
+ * relevant product information (brand, category, price, SKU, variant) and sends
163
+ * it in the proper format for analytics tracking.
164
+ *
165
+ * @param options Additional options for the ClickProduct event
166
+ * @param options.variant The selected product variant (e.g., "Large", "Green")
167
+ * @param options.list List or collection the product is part of (e.g., "Category: T-Shirts")
168
+ * @param options.position Position of the product within a list or collection (e.g., "3")
169
+ * @param options.origin Event origin (e.g., "Search Results", "Related Products")
170
+ *
171
+ * @example
172
+ * ```tsx
173
+ * // Track product click from category page
174
+ * import { useService } from '@wix/services-manager-react';
175
+ * import { ProductServiceDefinition } from '@wix/stores/services';
176
+ *
177
+ * function ProductLink() {
178
+ * const productService = useService(ProductServiceDefinition);
179
+ *
180
+ * const handleClick = () => {
181
+ * productService.reportClickProductTrackEvent({
182
+ * list: 'Category: T-Shirts',
183
+ * position: '3'
184
+ * });
185
+ * };
186
+ *
187
+ * return <a onClick={handleClick}>View Product</a>;
188
+ * }
189
+ * ```
190
+ */
191
+ const reportClickProductTrackEvent = (options) => {
192
+ const currentProduct = product.get();
193
+ if (!currentProduct)
194
+ return;
195
+ const eventData = {
196
+ brand: currentProduct.brand?.name ?? undefined,
197
+ category: currentProduct.breadcrumbsInfo?.breadcrumbs?.[0]?.categoryName,
198
+ currency: currentProduct.currency ?? undefined,
199
+ id: currentProduct._id,
200
+ name: currentProduct.name || '',
201
+ price: parseFloat(currentProduct.variantsInfo?.variants?.[0]?.price?.actualPrice
202
+ ?.amount ?? '0'),
203
+ sku: currentProduct.variantsInfo?.variants?.[0]?.sku ?? undefined,
204
+ variant: options?.variant,
205
+ list: options?.list,
206
+ position: options?.position,
207
+ origin: options?.origin,
208
+ };
209
+ analytics.trackEvent('ClickProduct', eventData);
210
+ };
64
211
  return {
65
212
  product,
66
213
  isLoading,
67
214
  error,
68
215
  loadProduct,
216
+ reportViewContentTrackEvent,
217
+ reportAddToCartTrackEvent,
218
+ reportClickProductTrackEvent,
69
219
  };
70
220
  });
71
221
  /**
@@ -412,14 +412,14 @@ export const ProductListService = implementService.withConfig()(ProductsListServ
412
412
  };
413
413
  });
414
414
  function getMinPrice(aggregationData) {
415
- const minPriceAggregation = aggregationData.find((data) => data.fieldPath === 'actualPriceRange.minValue.amount');
415
+ const minPriceAggregation = aggregationData?.find((data) => data.fieldPath === 'actualPriceRange.minValue.amount');
416
416
  if (minPriceAggregation?.scalar?.value) {
417
417
  return Number(minPriceAggregation.scalar.value) || 0;
418
418
  }
419
419
  return 0;
420
420
  }
421
421
  function getMaxPrice(aggregationData) {
422
- const maxPriceAggregation = aggregationData.find((data) => data.fieldPath === 'actualPriceRange.maxValue.amount');
422
+ const maxPriceAggregation = aggregationData?.find((data) => data.fieldPath === 'actualPriceRange.maxValue.amount');
423
423
  if (maxPriceAggregation?.scalar?.value) {
424
424
  return Number(maxPriceAggregation.scalar.value) || 0;
425
425
  }
@@ -1,6 +1,6 @@
1
1
  import type { V3Product } from '@wix/auto_sdk_stores_products-v-3';
2
- import React from 'react';
3
2
  import { AsChildChildren } from '@wix/headless-utils/react';
3
+ import React from 'react';
4
4
  import { AsContent } from './types.js';
5
5
  /**
6
6
  * Context for sharing variant options state between components
@@ -885,6 +885,53 @@ export declare const Action: {
885
885
  /** Pre-order action button */
886
886
  readonly PreOrder: React.ForwardRefExoticComponent<ProductActionProps & React.RefAttributes<HTMLButtonElement>>;
887
887
  };
888
+ /**
889
+ * Props for Product Link component
890
+ */
891
+ export interface ProductLinkProps {
892
+ /** Whether to render as a child component */
893
+ asChild?: boolean;
894
+ /** Custom render function when using asChild */
895
+ children?: React.ReactNode;
896
+ /** CSS classes to apply to the link */
897
+ className?: string;
898
+ /** List or collection the product is part of (e.g., "Category: T-Shirts") */
899
+ list?: string;
900
+ /** Position of the product within a list or collection */
901
+ position?: string;
902
+ /** Event origin (e.g., "Search Results", "Related Products") */
903
+ origin?: string;
904
+ }
905
+ /**
906
+ * Product link component that automatically tracks ClickProduct events.
907
+ * Wraps a link to the product page and fires analytics when clicked.
908
+ *
909
+ * @component
910
+ * @example
911
+ * ```tsx
912
+ * // Simple usage
913
+ * <Product.Link className="btn-secondary">
914
+ * View Product
915
+ * </Product.Link>
916
+ *
917
+ * // With tracking parameters
918
+ * <Product.Link
919
+ * list="Category: T-Shirts"
920
+ * position="3"
921
+ * className="btn-secondary"
922
+ * >
923
+ * View Product
924
+ * </Product.Link>
925
+ *
926
+ * // asChild with custom link
927
+ * <Product.Link asChild list="Search Results" position="5">
928
+ * <a href="/custom-url" className="custom-link">
929
+ * <Product.Name />
930
+ * </a>
931
+ * </Product.Link>
932
+ * ```
933
+ */
934
+ export declare const Link: React.ForwardRefExoticComponent<ProductLinkProps & React.RefAttributes<HTMLAnchorElement>>;
888
935
  /**
889
936
  * Props for ProductVariantStock component
890
937
  */
@@ -1,17 +1,19 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { InventoryAvailabilityStatus } from '@wix/auto_sdk_stores_products-v-3';
3
- import React from 'react';
3
+ import { Quantity as QuantityComponent } from '@wix/headless-components/react';
4
4
  import { Commerce } from '@wix/headless-ecom/react';
5
- import { AsChildSlot } from '@wix/headless-utils/react';
6
5
  import { MediaGallery } from '@wix/headless-media/react';
7
- import { Quantity as QuantityComponent } from '@wix/headless-components/react';
6
+ import { AsChildSlot } from '@wix/headless-utils/react';
7
+ import { useService } from '@wix/services-manager-react';
8
+ import React from 'react';
9
+ import { DataComponentTags } from '../data-component-tags.js';
10
+ import { ProductServiceDefinition, SelectedVariantServiceDefinition, } from '../services/index.js';
8
11
  import * as CoreProduct from './core/Product.js';
9
- import * as CoreProductVariantSelector from './core/ProductVariantSelector.js';
10
12
  import * as CoreProductModifiers from './core/ProductModifiers.js';
13
+ import * as CoreProductVariantSelector from './core/ProductVariantSelector.js';
11
14
  import * as CoreSelectedVariant from './core/SelectedVariant.js';
12
15
  import * as Option from './Option.js';
13
16
  import { AsContent } from './types.js';
14
- import { DataComponentTags } from '../data-component-tags.js';
15
17
  const VariantsContext = React.createContext(null);
16
18
  /**
17
19
  * Hook to access variants context
@@ -61,6 +63,7 @@ var TestIds;
61
63
  TestIds["productActionAddToCart"] = "product-action-add-to-cart";
62
64
  TestIds["productActionBuyNow"] = "product-action-buy-now";
63
65
  TestIds["productActionPreOrder"] = "product-action-can-pre-order";
66
+ TestIds["productLink"] = "product-link";
64
67
  TestIds["productQuantity"] = "product-quantity";
65
68
  TestIds["productQuantityDecrement"] = "product-quantity-decrement";
66
69
  TestIds["productQuantityInput"] = "product-quantity-input";
@@ -879,6 +882,19 @@ export const ProductVariantSelectorReset = React.forwardRef((props, ref) => {
879
882
  */
880
883
  export const ProductActionAddToCart = React.forwardRef((props, ref) => {
881
884
  const { asChild, children, className, label, loadingState } = props;
885
+ // Get services for tracking
886
+ const productService = useService(ProductServiceDefinition);
887
+ const variantService = useService(SelectedVariantServiceDefinition);
888
+ // Track after successful add to cart
889
+ const handleSuccess = async () => {
890
+ const choices = variantService.selectedChoices.get();
891
+ const quantity = variantService.selectedQuantity.get();
892
+ const variantString = Object.values(choices).join(' ') || undefined;
893
+ productService.reportAddToCartTrackEvent({
894
+ quantity,
895
+ variant: variantString,
896
+ });
897
+ };
882
898
  return (_jsx(CoreSelectedVariant.Actions, { children: ({ lineItems, canAddToCart, isLoading, addToCart, isPreOrderEnabled, }) => {
883
899
  if (isPreOrderEnabled) {
884
900
  return null;
@@ -886,13 +902,13 @@ export const ProductActionAddToCart = React.forwardRef((props, ref) => {
886
902
  const onClick = addToCart;
887
903
  const disabled = !canAddToCart || isLoading;
888
904
  if (asChild && children) {
889
- return (_jsx(Commerce.Actions.AddToCart, { lineItems: lineItems, asChild: asChild, children: _jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.productActionAddToCart, "data-in-progress": isLoading, customElement: children, customElementProps: {
905
+ return (_jsx(Commerce.Actions.AddToCart, { lineItems: lineItems, asChild: asChild, onSuccess: handleSuccess, children: _jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.productActionAddToCart, "data-in-progress": isLoading, customElement: children, customElementProps: {
890
906
  disabled,
891
907
  isLoading,
892
908
  onClick,
893
909
  } }) }));
894
910
  }
895
- return (_jsx(Commerce.Actions.AddToCart, { ref: ref, label: label, lineItems: lineItems, className: className, disabled: disabled, loadingState: loadingState, "data-testid": TestIds.productActionAddToCart, "data-in-progress": isLoading }));
911
+ return (_jsx(Commerce.Actions.AddToCart, { ref: ref, label: label, lineItems: lineItems, className: className, disabled: disabled, loadingState: loadingState, onSuccess: handleSuccess, "data-testid": TestIds.productActionAddToCart, "data-in-progress": isLoading }));
896
912
  } }));
897
913
  });
898
914
  /**
@@ -923,6 +939,19 @@ export const ProductActionBuyNow = React.forwardRef((props, ref) => {
923
939
  */
924
940
  export const ProductActionPreOrder = React.forwardRef((props, ref) => {
925
941
  const { asChild, children, className, label, loadingState } = props;
942
+ // Get services for tracking
943
+ const productService = useService(ProductServiceDefinition);
944
+ const variantService = useService(SelectedVariantServiceDefinition);
945
+ // Track after successful pre-order
946
+ const handleSuccess = async () => {
947
+ const choices = variantService.selectedChoices.get();
948
+ const quantity = variantService.selectedQuantity.get();
949
+ const variantString = Object.values(choices).join(' ') || undefined;
950
+ productService.reportAddToCartTrackEvent({
951
+ quantity,
952
+ variant: variantString,
953
+ });
954
+ };
926
955
  return (_jsx(CoreSelectedVariant.Actions, { children: ({ lineItems, isLoading, addToCart, isPreOrderEnabled }) => {
927
956
  if (!isPreOrderEnabled) {
928
957
  return null;
@@ -931,13 +960,13 @@ export const ProductActionPreOrder = React.forwardRef((props, ref) => {
931
960
  const onClick = addToCart;
932
961
  const disabled = !canPreOrder;
933
962
  if (asChild && children) {
934
- return (_jsx(Commerce.Actions.AddToCart, { lineItems: lineItems, asChild: asChild, children: _jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.productActionPreOrder, "data-in-progress": isLoading, customElement: children, customElementProps: {
963
+ return (_jsx(Commerce.Actions.AddToCart, { lineItems: lineItems, asChild: asChild, onSuccess: handleSuccess, children: _jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.productActionPreOrder, "data-in-progress": isLoading, customElement: children, customElementProps: {
935
964
  disabled,
936
965
  isLoading,
937
966
  onClick,
938
967
  } }) }));
939
968
  }
940
- return (_jsx(Commerce.Actions.AddToCart, { ref: ref, label: label, lineItems: lineItems, className: className, disabled: disabled, loadingState: loadingState, "data-testid": TestIds.productActionPreOrder, "data-in-progress": isLoading }));
969
+ return (_jsx(Commerce.Actions.AddToCart, { ref: ref, label: label, lineItems: lineItems, className: className, disabled: disabled, loadingState: loadingState, onSuccess: handleSuccess, "data-testid": TestIds.productActionPreOrder, "data-in-progress": isLoading }));
941
970
  } }));
942
971
  });
943
972
  /**
@@ -952,6 +981,57 @@ export const Action = {
952
981
  /** Pre-order action button */
953
982
  PreOrder: ProductActionPreOrder,
954
983
  };
984
+ /**
985
+ * Product link component that automatically tracks ClickProduct events.
986
+ * Wraps a link to the product page and fires analytics when clicked.
987
+ *
988
+ * @component
989
+ * @example
990
+ * ```tsx
991
+ * // Simple usage
992
+ * <Product.Link className="btn-secondary">
993
+ * View Product
994
+ * </Product.Link>
995
+ *
996
+ * // With tracking parameters
997
+ * <Product.Link
998
+ * list="Category: T-Shirts"
999
+ * position="3"
1000
+ * className="btn-secondary"
1001
+ * >
1002
+ * View Product
1003
+ * </Product.Link>
1004
+ *
1005
+ * // asChild with custom link
1006
+ * <Product.Link asChild list="Search Results" position="5">
1007
+ * <a href="/custom-url" className="custom-link">
1008
+ * <Product.Name />
1009
+ * </a>
1010
+ * </Product.Link>
1011
+ * ```
1012
+ */
1013
+ export const Link = React.forwardRef((props, ref) => {
1014
+ const { asChild, children, className, list, position, origin } = props;
1015
+ // Get services for tracking
1016
+ const productService = useService(ProductServiceDefinition);
1017
+ const variantService = useService(SelectedVariantServiceDefinition);
1018
+ return (_jsx(CoreProduct.Slug, { children: ({ slug }) => {
1019
+ const handleClick = () => {
1020
+ const choices = variantService.selectedChoices.get();
1021
+ const variantString = Object.values(choices).join(' ') || undefined;
1022
+ productService.reportClickProductTrackEvent({
1023
+ variant: variantString,
1024
+ list,
1025
+ position,
1026
+ origin,
1027
+ });
1028
+ };
1029
+ if (asChild && children) {
1030
+ return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.productLink, customElement: children, customElementProps: { onClick: handleClick } }));
1031
+ }
1032
+ return (_jsx("a", { ref: ref, href: `/${slug}`, onClick: handleClick, className: className, "data-testid": TestIds.productLink, children: children }));
1033
+ } }));
1034
+ });
955
1035
  /**
956
1036
  * Helper function to determine stock status and label based on availability and can-pre-order settings
957
1037
  */
@@ -1,5 +1,5 @@
1
- import { type Signal } from '@wix/services-definitions/core-services/signals';
2
1
  import * as productsV3 from '@wix/auto_sdk_stores_products-v-3';
2
+ import { type Signal } from '@wix/services-definitions/core-services/signals';
3
3
  /**
4
4
  * API interface for the Product service, providing reactive product data management.
5
5
  * This service handles loading and managing a single product's data, loading state, and errors.
@@ -15,6 +15,22 @@ export interface ProductServiceAPI {
15
15
  error: Signal<string | null>;
16
16
  /** Function to load a product by its slug */
17
17
  loadProduct: (slug: string) => Promise<void>;
18
+ /** Reports a ViewContent analytics event for the current product */
19
+ reportViewContentTrackEvent: () => void;
20
+ /** Reports an AddToCart analytics event for the current product */
21
+ reportAddToCartTrackEvent: (options?: {
22
+ quantity?: number;
23
+ variant?: string;
24
+ origin?: string;
25
+ position?: string;
26
+ }) => void;
27
+ /** Reports a ClickProduct analytics event for the current product */
28
+ reportClickProductTrackEvent: (options?: {
29
+ variant?: string;
30
+ list?: string;
31
+ position?: string;
32
+ origin?: string;
33
+ }) => void;
18
34
  }
19
35
  /**
20
36
  * Service definition for the Product service.
@@ -1,6 +1,7 @@
1
+ import * as productsV3 from '@wix/auto_sdk_stores_products-v-3';
1
2
  import { defineService, implementService } from '@wix/services-definitions';
2
3
  import { SignalsServiceDefinition, } from '@wix/services-definitions/core-services/signals';
3
- import * as productsV3 from '@wix/auto_sdk_stores_products-v-3';
4
+ import { analytics } from '@wix/site';
4
5
  /**
5
6
  * Service definition for the Product service.
6
7
  * This defines the contract that the ProductService must implement.
@@ -61,11 +62,160 @@ export const ProductService = implementService.withConfig()(ProductServiceDefini
61
62
  if (config.productSlug) {
62
63
  loadProduct(config.productSlug);
63
64
  }
65
+ /**
66
+ * Reports a ViewContent analytics event for product page tracking.
67
+ * This function automatically formats and sends a standardized ViewContent event
68
+ * to the Wix Analytics service when a user views a product page. It extracts
69
+ * relevant product information (brand, category, price, SKU) and sends it in
70
+ * the proper format for analytics tracking.
71
+ *
72
+ * @example
73
+ * ```tsx
74
+ * // Track when a product page loads
75
+ * import { useService } from '@wix/services-manager-react';
76
+ * import { ProductServiceDefinition } from '@wix/stores/services';
77
+ *
78
+ * function ProductPage() {
79
+ * const productService = useService(ProductServiceDefinition);
80
+ *
81
+ * useEffect(() => {
82
+ * productService.reportViewContentTrackEvent();
83
+ * }, []);
84
+ *
85
+ * return <div>Product Page</div>;
86
+ * }
87
+ * ```
88
+ */
89
+ const reportViewContentTrackEvent = () => {
90
+ const currentProduct = product.get();
91
+ if (!currentProduct)
92
+ return;
93
+ const eventData = {
94
+ brand: currentProduct.brand?.name ?? undefined,
95
+ category: currentProduct.breadcrumbsInfo?.breadcrumbs?.[0]?.categoryName,
96
+ currency: currentProduct.currency ?? undefined,
97
+ id: currentProduct._id,
98
+ name: currentProduct.name || '',
99
+ price: parseFloat(currentProduct.variantsInfo?.variants?.[0]?.price?.actualPrice
100
+ ?.amount ?? '0'),
101
+ sku: currentProduct.variantsInfo?.variants?.[0]?.sku ?? undefined,
102
+ };
103
+ analytics.trackEvent('ViewContent', eventData);
104
+ };
105
+ /**
106
+ * Reports an AddToCart analytics event for cart tracking.
107
+ * This function automatically formats and sends a standardized AddToCart event
108
+ * to the Wix Analytics service when a user adds a product to their cart. It extracts
109
+ * relevant product information (brand, category, price, SKU, variant, quantity) and sends
110
+ * it in the proper format for analytics tracking.
111
+ *
112
+ * @param options Additional options for the AddToCart event
113
+ * @param options.quantity The quantity being added to cart (defaults to 1)
114
+ * @param options.variant The selected product variant (e.g., "Large", "Green")
115
+ * @param options.origin Event origin (e.g., "Product Page", "Quick View")
116
+ * @param options.position List or collection position (e.g., "Product Gallery", "Search Results")
117
+ *
118
+ * @example
119
+ * ```tsx
120
+ * // Track add to cart from product page
121
+ * import { useService } from '@wix/services-manager-react';
122
+ * import { ProductServiceDefinition } from '@wix/stores/services';
123
+ *
124
+ * function AddToCartButton() {
125
+ * const productService = useService(ProductServiceDefinition);
126
+ *
127
+ * const handleAddToCart = () => {
128
+ * productService.reportAddToCartTrackEvent({
129
+ * quantity: 1,
130
+ * variant: 'Large Blue'
131
+ * });
132
+ * };
133
+ *
134
+ * return <button onClick={handleAddToCart}>Add to Cart</button>;
135
+ * }
136
+ * ```
137
+ */
138
+ const reportAddToCartTrackEvent = (options) => {
139
+ const currentProduct = product.get();
140
+ if (!currentProduct)
141
+ return;
142
+ const eventData = {
143
+ brand: currentProduct.brand?.name ?? undefined,
144
+ category: currentProduct.breadcrumbsInfo?.breadcrumbs?.[0]?.categoryName,
145
+ currency: currentProduct.currency ?? undefined,
146
+ id: currentProduct._id,
147
+ name: currentProduct.name || '',
148
+ price: parseFloat(currentProduct.variantsInfo?.variants?.[0]?.price?.actualPrice
149
+ ?.amount ?? '0'),
150
+ quantity: options?.quantity ?? 1,
151
+ sku: currentProduct.variantsInfo?.variants?.[0]?.sku ?? undefined,
152
+ variant: options?.variant,
153
+ origin: options?.origin,
154
+ position: options?.position,
155
+ };
156
+ analytics.trackEvent('AddToCart', eventData);
157
+ };
158
+ /**
159
+ * Reports a ClickProduct analytics event for product click tracking.
160
+ * This function automatically formats and sends a standardized ClickProduct event
161
+ * to the Wix Analytics service when a user clicks on a product link. It extracts
162
+ * relevant product information (brand, category, price, SKU, variant) and sends
163
+ * it in the proper format for analytics tracking.
164
+ *
165
+ * @param options Additional options for the ClickProduct event
166
+ * @param options.variant The selected product variant (e.g., "Large", "Green")
167
+ * @param options.list List or collection the product is part of (e.g., "Category: T-Shirts")
168
+ * @param options.position Position of the product within a list or collection (e.g., "3")
169
+ * @param options.origin Event origin (e.g., "Search Results", "Related Products")
170
+ *
171
+ * @example
172
+ * ```tsx
173
+ * // Track product click from category page
174
+ * import { useService } from '@wix/services-manager-react';
175
+ * import { ProductServiceDefinition } from '@wix/stores/services';
176
+ *
177
+ * function ProductLink() {
178
+ * const productService = useService(ProductServiceDefinition);
179
+ *
180
+ * const handleClick = () => {
181
+ * productService.reportClickProductTrackEvent({
182
+ * list: 'Category: T-Shirts',
183
+ * position: '3'
184
+ * });
185
+ * };
186
+ *
187
+ * return <a onClick={handleClick}>View Product</a>;
188
+ * }
189
+ * ```
190
+ */
191
+ const reportClickProductTrackEvent = (options) => {
192
+ const currentProduct = product.get();
193
+ if (!currentProduct)
194
+ return;
195
+ const eventData = {
196
+ brand: currentProduct.brand?.name ?? undefined,
197
+ category: currentProduct.breadcrumbsInfo?.breadcrumbs?.[0]?.categoryName,
198
+ currency: currentProduct.currency ?? undefined,
199
+ id: currentProduct._id,
200
+ name: currentProduct.name || '',
201
+ price: parseFloat(currentProduct.variantsInfo?.variants?.[0]?.price?.actualPrice
202
+ ?.amount ?? '0'),
203
+ sku: currentProduct.variantsInfo?.variants?.[0]?.sku ?? undefined,
204
+ variant: options?.variant,
205
+ list: options?.list,
206
+ position: options?.position,
207
+ origin: options?.origin,
208
+ };
209
+ analytics.trackEvent('ClickProduct', eventData);
210
+ };
64
211
  return {
65
212
  product,
66
213
  isLoading,
67
214
  error,
68
215
  loadProduct,
216
+ reportViewContentTrackEvent,
217
+ reportAddToCartTrackEvent,
218
+ reportClickProductTrackEvent,
69
219
  };
70
220
  });
71
221
  /**
@@ -412,14 +412,14 @@ export const ProductListService = implementService.withConfig()(ProductsListServ
412
412
  };
413
413
  });
414
414
  function getMinPrice(aggregationData) {
415
- const minPriceAggregation = aggregationData.find((data) => data.fieldPath === 'actualPriceRange.minValue.amount');
415
+ const minPriceAggregation = aggregationData?.find((data) => data.fieldPath === 'actualPriceRange.minValue.amount');
416
416
  if (minPriceAggregation?.scalar?.value) {
417
417
  return Number(minPriceAggregation.scalar.value) || 0;
418
418
  }
419
419
  return 0;
420
420
  }
421
421
  function getMaxPrice(aggregationData) {
422
- const maxPriceAggregation = aggregationData.find((data) => data.fieldPath === 'actualPriceRange.maxValue.amount');
422
+ const maxPriceAggregation = aggregationData?.find((data) => data.fieldPath === 'actualPriceRange.maxValue.amount');
423
423
  if (maxPriceAggregation?.scalar?.value) {
424
424
  return Number(maxPriceAggregation.scalar.value) || 0;
425
425
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/headless-stores",
3
- "version": "0.0.103",
3
+ "version": "0.0.105",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -66,12 +66,13 @@
66
66
  "@wix/auto_sdk_stores_read-only-variants-v-3": "^1.0.23",
67
67
  "@wix/ecom": "^1.0.1461",
68
68
  "@wix/essentials": "^0.1.24",
69
- "@wix/headless-ecom": "0.0.37",
69
+ "@wix/headless-ecom": "0.0.38",
70
70
  "@wix/headless-media": "0.0.18",
71
71
  "@wix/headless-utils": "0.0.8",
72
72
  "@wix/redirects": "^1.0.83",
73
73
  "@wix/services-definitions": "^0.1.4",
74
- "@wix/services-manager-react": "^0.1.26"
74
+ "@wix/services-manager-react": "^0.1.26",
75
+ "@wix/site": "^1.27.0"
75
76
  },
76
77
  "peerDependencies": {
77
78
  "@wix/headless-components": "^0.0.0"
@@ -86,5 +87,5 @@
86
87
  "groupId": "com.wixpress.headless-components"
87
88
  }
88
89
  },
89
- "falconPackageHash": "7c87f1a9f4c48735b5b7097f168cb22756fb75f84ec2f44446c90c45"
90
+ "falconPackageHash": "2937c978ada9e33f794daf2161a4f84ffe9ea93141e59938401efb95"
90
91
  }