@tapcart/mobile-components 0.14.6 → 0.15.0

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.
@@ -0,0 +1,123 @@
1
+ interface SearchAnalyticsProduct {
2
+ id?: string;
3
+ objectID?: string;
4
+ variants?: Array<{
5
+ id?: string;
6
+ } | undefined | null>;
7
+ }
8
+ interface EmitProductInteractionPayload {
9
+ client: unknown;
10
+ surface: string;
11
+ product: {
12
+ productId?: string;
13
+ variantId?: string;
14
+ objectId?: string;
15
+ position?: number;
16
+ };
17
+ }
18
+ interface OpenProductProps {
19
+ productId?: string;
20
+ variantId?: string;
21
+ [key: string]: unknown;
22
+ }
23
+ interface UseSearchAnalyticsOpenProductParams {
24
+ products?: Array<SearchAnalyticsProduct | undefined | null>;
25
+ searchClient?: unknown;
26
+ emitProductClick?: (payload: EmitProductInteractionPayload) => void;
27
+ action?: (name: string, params?: unknown) => void;
28
+ surface?: string;
29
+ }
30
+ /**
31
+ * Returns a stable `openProduct` handler that opens the product AND emits a
32
+ * search-analytics click, looking up the clicked product's Algolia `objectID`
33
+ * and 1-based position from the current results.
34
+ *
35
+ * Provider-agnostic: `emitProductClick` reads attribution from whatever search
36
+ * client is active, so this same wiring works for every search provider. The
37
+ * click analytics no-op when the grid isn't search-backed (`searchClient`
38
+ * falsy) or the emitter isn't injected, so non-search grids stay clean.
39
+ *
40
+ * Centralizes the wiring shared by every search-backed grid block
41
+ * (CollectionProductGrid, ProductGrid, TabbedProductGrid, the product
42
+ * carousels, ...) so there's one implementation to maintain. The results are
43
+ * held in a ref so the returned callback keeps a stable identity (preserving
44
+ * a memoized ProductCard) while still reading the latest list on click.
45
+ */
46
+ export declare function useSearchAnalyticsOpenProduct({ products, searchClient, emitProductClick, action, surface, }: UseSearchAnalyticsOpenProductParams): (props: OpenProductProps) => void;
47
+ interface AddToCartProps {
48
+ lineItems?: Array<{
49
+ variantId?: string;
50
+ quantity?: number;
51
+ } | undefined | null>;
52
+ [key: string]: unknown;
53
+ }
54
+ interface UseSearchAnalyticsAddToCartParams {
55
+ products?: Array<SearchAnalyticsProduct | undefined | null>;
56
+ searchClient?: unknown;
57
+ emitProductConversion?: (payload: EmitProductInteractionPayload) => void;
58
+ action?: (name: string, params?: unknown) => void | Promise<unknown>;
59
+ surface?: string;
60
+ }
61
+ /**
62
+ * Sibling of `useSearchAnalyticsOpenProduct` for the add-to-cart path. Returns a
63
+ * stable `addToCart` handler that adds the item AND emits a search-analytics
64
+ * `add_to_cart` conversion, attributed to the originating search via the
65
+ * product's Algolia `objectID` + the active client's attribution.
66
+ *
67
+ * Unlike open-product, ProductCard's `addToCart` receives a line-item shape
68
+ * (`{ lineItems: [{ variantId }] }`) with no productId, so the results lookup
69
+ * matches on the added variant. Same ref-based stability as the sibling hook.
70
+ *
71
+ * The conversion is emitted only AFTER the add succeeds — `cart/add` is awaited,
72
+ * so a failed add (rejection) never counts as a conversion. This matches the
73
+ * quick-add drawer's on-success semantics (`useSearchAnalyticsConversionEmit`),
74
+ * which the drawer fires from its own add's `onSuccess`. The attribution target
75
+ * is captured from the results BEFORE the await so a list change mid-add can't
76
+ * misattribute. Awaiting a non-promise `action` (e.g. in tests) resolves
77
+ * immediately and still emits, so the conversion is never silently dropped.
78
+ *
79
+ * No-op (the item is still added) when the grid isn't search-backed
80
+ * (`searchClient` falsy), the emitter isn't injected, or the added variant
81
+ * isn't in the current results — without a matched hit there's no `objectID` to
82
+ * attribute, so we skip the emit rather than send a provider-less event.
83
+ */
84
+ export declare function useSearchAnalyticsAddToCart({ products, searchClient, emitProductConversion, action, surface, }: UseSearchAnalyticsAddToCartParams): (props: AddToCartProps) => Promise<void>;
85
+ /**
86
+ * Emit-only sibling of `useSearchAnalyticsAddToCart` for the quick-add drawer
87
+ * path. Multi-variant products open the quick-add drawer (rendered at app-layout
88
+ * level, OUTSIDE the page's SearchClientProvider), which performs its own
89
+ * `actions.addToCart`. So the drawer can't read the search context or call the
90
+ * combined add-and-emit handler without double-adding. This returns a stable
91
+ * `emitAddToCartConversion(variantId)` the drawer invokes on add success to emit
92
+ * the `add_to_cart` conversion only — attributed via the originating result's
93
+ * Algolia `objectID` + 1-based position, no cart mutation. No-ops when the grid
94
+ * isn't search-backed (`searchClient` falsy), the emitter isn't injected, the
95
+ * variant is missing, or the variant isn't found in the current results.
96
+ */
97
+ export declare function useSearchAnalyticsConversionEmit({ products, searchClient, emitProductConversion, surface, }: UseSearchAnalyticsAddToCartParams): (variantId?: string) => void;
98
+ interface UseSearchAnalyticsImpressionParams {
99
+ searchClient?: unknown;
100
+ emitProductImpression?: (payload: EmitProductInteractionPayload) => void;
101
+ surface?: string;
102
+ /**
103
+ * Whether search-analytics is enabled (the `app-studio-search-analytics`
104
+ * flag). Required to gate setup: `emitProductImpression` is a truthy no-op
105
+ * when the flag is off, so without this the IntersectionObserver would still
106
+ * be constructed per-card on every search-backed grid. Defaults on for
107
+ * backward compatibility with callers that don't pass it yet.
108
+ */
109
+ enabled?: boolean;
110
+ }
111
+ /**
112
+ * Returns an `onImpression(product)` callback for ProductCard to fire when a
113
+ * result first scrolls into view, emitting a search-analytics `impression`
114
+ * (Algolia `viewedObjectIDs` — objectID only, no position). Dedupes so each
115
+ * product emits at most once per session, even across virtualized remounts.
116
+ *
117
+ * Returns `undefined` when the grid isn't search-backed or no emitter is
118
+ * injected, so ProductCard skips setting up an IntersectionObserver entirely on
119
+ * non-search surfaces.
120
+ */
121
+ export declare function useSearchAnalyticsImpression({ searchClient, emitProductImpression, surface, enabled, }: UseSearchAnalyticsImpressionParams): ((product: SearchAnalyticsProduct | undefined | null) => void) | undefined;
122
+ export {};
123
+ //# sourceMappingURL=use-search-analytics-open-product.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-search-analytics-open-product.d.ts","sourceRoot":"","sources":["../../../components/hooks/use-search-analytics-open-product.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,191 @@
1
+ "use client";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ import React from "react";
12
+ import { getIdFromGid } from "../../lib/utils";
13
+ /**
14
+ * Returns a stable `openProduct` handler that opens the product AND emits a
15
+ * search-analytics click, looking up the clicked product's Algolia `objectID`
16
+ * and 1-based position from the current results.
17
+ *
18
+ * Provider-agnostic: `emitProductClick` reads attribution from whatever search
19
+ * client is active, so this same wiring works for every search provider. The
20
+ * click analytics no-op when the grid isn't search-backed (`searchClient`
21
+ * falsy) or the emitter isn't injected, so non-search grids stay clean.
22
+ *
23
+ * Centralizes the wiring shared by every search-backed grid block
24
+ * (CollectionProductGrid, ProductGrid, TabbedProductGrid, the product
25
+ * carousels, ...) so there's one implementation to maintain. The results are
26
+ * held in a ref so the returned callback keeps a stable identity (preserving
27
+ * a memoized ProductCard) while still reading the latest list on click.
28
+ */
29
+ export function useSearchAnalyticsOpenProduct({ products, searchClient, emitProductClick, action, surface = "collection", }) {
30
+ const productsRef = React.useRef(products);
31
+ productsRef.current = products;
32
+ // Hold `action` in a ref and read the latest at fire time so the returned
33
+ // callback stays a stable reference regardless of whether `Tapcart.action`
34
+ // is stable across renders — otherwise an unstable `action` would rebuild it
35
+ // every render and re-render every memoized ProductCard, defeating the whole
36
+ // point of this hook. Mirrors useSearchAnalyticsEmitter.
37
+ const actionRef = React.useRef(action);
38
+ actionRef.current = action;
39
+ return React.useCallback((props) => {
40
+ const action = actionRef.current;
41
+ action === null || action === void 0 ? void 0 : action("trigger/haptic");
42
+ action === null || action === void 0 ? void 0 : action("product/open", props);
43
+ if (!searchClient || !emitProductClick)
44
+ return;
45
+ const list = productsRef.current || [];
46
+ const index = list.findIndex((item) => getIdFromGid(item === null || item === void 0 ? void 0 : item.id) === (props === null || props === void 0 ? void 0 : props.productId));
47
+ const hit = list[index];
48
+ emitProductClick({
49
+ client: searchClient,
50
+ surface,
51
+ product: {
52
+ productId: props === null || props === void 0 ? void 0 : props.productId,
53
+ variantId: props === null || props === void 0 ? void 0 : props.variantId,
54
+ objectId: hit === null || hit === void 0 ? void 0 : hit.objectID,
55
+ position: index >= 0 ? index + 1 : undefined,
56
+ },
57
+ });
58
+ }, [searchClient, emitProductClick, surface]);
59
+ }
60
+ /**
61
+ * Sibling of `useSearchAnalyticsOpenProduct` for the add-to-cart path. Returns a
62
+ * stable `addToCart` handler that adds the item AND emits a search-analytics
63
+ * `add_to_cart` conversion, attributed to the originating search via the
64
+ * product's Algolia `objectID` + the active client's attribution.
65
+ *
66
+ * Unlike open-product, ProductCard's `addToCart` receives a line-item shape
67
+ * (`{ lineItems: [{ variantId }] }`) with no productId, so the results lookup
68
+ * matches on the added variant. Same ref-based stability as the sibling hook.
69
+ *
70
+ * The conversion is emitted only AFTER the add succeeds — `cart/add` is awaited,
71
+ * so a failed add (rejection) never counts as a conversion. This matches the
72
+ * quick-add drawer's on-success semantics (`useSearchAnalyticsConversionEmit`),
73
+ * which the drawer fires from its own add's `onSuccess`. The attribution target
74
+ * is captured from the results BEFORE the await so a list change mid-add can't
75
+ * misattribute. Awaiting a non-promise `action` (e.g. in tests) resolves
76
+ * immediately and still emits, so the conversion is never silently dropped.
77
+ *
78
+ * No-op (the item is still added) when the grid isn't search-backed
79
+ * (`searchClient` falsy), the emitter isn't injected, or the added variant
80
+ * isn't in the current results — without a matched hit there's no `objectID` to
81
+ * attribute, so we skip the emit rather than send a provider-less event.
82
+ */
83
+ export function useSearchAnalyticsAddToCart({ products, searchClient, emitProductConversion, action, surface = "collection", }) {
84
+ const productsRef = React.useRef(products);
85
+ productsRef.current = products;
86
+ const actionRef = React.useRef(action);
87
+ actionRef.current = action;
88
+ return React.useCallback((props) => __awaiter(this, void 0, void 0, function* () {
89
+ var _a, _b;
90
+ const action = actionRef.current;
91
+ action === null || action === void 0 ? void 0 : action("trigger/haptic");
92
+ // Not search-backed (or no emitter): just add — nothing to attribute.
93
+ if (!searchClient || !emitProductConversion) {
94
+ action === null || action === void 0 ? void 0 : action("cart/add", props);
95
+ return;
96
+ }
97
+ // Resolve the attribution target from the CURRENT results up front, so a
98
+ // list change while the add is in flight can't misattribute the emit.
99
+ const variantId = (_b = (_a = props === null || props === void 0 ? void 0 : props.lineItems) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.variantId;
100
+ const list = productsRef.current || [];
101
+ const index = variantId
102
+ ? list.findIndex((item) => { var _a; return (_a = item === null || item === void 0 ? void 0 : item.variants) === null || _a === void 0 ? void 0 : _a.some((v) => getIdFromGid(v === null || v === void 0 ? void 0 : v.id) === variantId); })
103
+ : -1;
104
+ const hit = index >= 0 ? list[index] : undefined;
105
+ // Emit only on a successful add (see JSDoc): a rejected cart/add skips it.
106
+ try {
107
+ yield (action === null || action === void 0 ? void 0 : action("cart/add", props));
108
+ }
109
+ catch (_c) {
110
+ return;
111
+ }
112
+ // No matched result → no objectID to attribute; skip.
113
+ if (!variantId || index < 0)
114
+ return;
115
+ emitProductConversion({
116
+ client: searchClient,
117
+ surface,
118
+ product: {
119
+ productId: getIdFromGid(hit === null || hit === void 0 ? void 0 : hit.id),
120
+ variantId,
121
+ objectId: hit === null || hit === void 0 ? void 0 : hit.objectID,
122
+ position: index + 1,
123
+ },
124
+ });
125
+ }), [searchClient, emitProductConversion, surface]);
126
+ }
127
+ /**
128
+ * Emit-only sibling of `useSearchAnalyticsAddToCart` for the quick-add drawer
129
+ * path. Multi-variant products open the quick-add drawer (rendered at app-layout
130
+ * level, OUTSIDE the page's SearchClientProvider), which performs its own
131
+ * `actions.addToCart`. So the drawer can't read the search context or call the
132
+ * combined add-and-emit handler without double-adding. This returns a stable
133
+ * `emitAddToCartConversion(variantId)` the drawer invokes on add success to emit
134
+ * the `add_to_cart` conversion only — attributed via the originating result's
135
+ * Algolia `objectID` + 1-based position, no cart mutation. No-ops when the grid
136
+ * isn't search-backed (`searchClient` falsy), the emitter isn't injected, the
137
+ * variant is missing, or the variant isn't found in the current results.
138
+ */
139
+ export function useSearchAnalyticsConversionEmit({ products, searchClient, emitProductConversion, surface = "collection", }) {
140
+ const productsRef = React.useRef(products);
141
+ productsRef.current = products;
142
+ return React.useCallback((variantId) => {
143
+ if (!searchClient || !emitProductConversion || !variantId)
144
+ return;
145
+ const list = productsRef.current || [];
146
+ const index = list.findIndex((item) => { var _a; return (_a = item === null || item === void 0 ? void 0 : item.variants) === null || _a === void 0 ? void 0 : _a.some((v) => getIdFromGid(v === null || v === void 0 ? void 0 : v.id) === variantId); });
147
+ if (index < 0)
148
+ return;
149
+ const hit = list[index];
150
+ emitProductConversion({
151
+ client: searchClient,
152
+ surface,
153
+ product: {
154
+ productId: getIdFromGid(hit === null || hit === void 0 ? void 0 : hit.id),
155
+ variantId,
156
+ objectId: hit === null || hit === void 0 ? void 0 : hit.objectID,
157
+ position: index + 1,
158
+ },
159
+ });
160
+ }, [searchClient, emitProductConversion, surface]);
161
+ }
162
+ /**
163
+ * Returns an `onImpression(product)` callback for ProductCard to fire when a
164
+ * result first scrolls into view, emitting a search-analytics `impression`
165
+ * (Algolia `viewedObjectIDs` — objectID only, no position). Dedupes so each
166
+ * product emits at most once per session, even across virtualized remounts.
167
+ *
168
+ * Returns `undefined` when the grid isn't search-backed or no emitter is
169
+ * injected, so ProductCard skips setting up an IntersectionObserver entirely on
170
+ * non-search surfaces.
171
+ */
172
+ export function useSearchAnalyticsImpression({ searchClient, emitProductImpression, surface = "collection", enabled = true, }) {
173
+ const seenRef = React.useRef(new Set());
174
+ return React.useMemo(() => {
175
+ if (!searchClient || !emitProductImpression || !enabled)
176
+ return undefined;
177
+ return (product) => {
178
+ const objectId = product === null || product === void 0 ? void 0 : product.objectID;
179
+ const productId = getIdFromGid(product === null || product === void 0 ? void 0 : product.id);
180
+ const key = objectId || productId;
181
+ if (!key || seenRef.current.has(key))
182
+ return;
183
+ seenRef.current.add(key);
184
+ emitProductImpression({
185
+ client: searchClient,
186
+ surface,
187
+ product: { productId, objectId },
188
+ });
189
+ };
190
+ }, [searchClient, emitProductImpression, surface, enabled]);
191
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=use-search-analytics-open-product.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-search-analytics-open-product.test.d.ts","sourceRoot":"","sources":["../../../components/hooks/use-search-analytics-open-product.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,315 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { renderHook } from "@testing-library/react";
11
+ import { useSearchAnalyticsOpenProduct, useSearchAnalyticsAddToCart, useSearchAnalyticsImpression, } from "./use-search-analytics-open-product";
12
+ const products = [
13
+ { id: "gid://shopify/Product/1", objectID: "obj-1" },
14
+ { id: "gid://shopify/Product/2", objectID: "obj-2" },
15
+ { id: "gid://shopify/Product/3", objectID: "obj-3" },
16
+ ];
17
+ describe("useSearchAnalyticsOpenProduct", () => {
18
+ it("always opens the product (haptic + product/open) even when not search-backed", () => {
19
+ const action = jest.fn();
20
+ const emitProductClick = jest.fn();
21
+ const { result } = renderHook(() => useSearchAnalyticsOpenProduct({
22
+ products,
23
+ searchClient: undefined,
24
+ emitProductClick,
25
+ action,
26
+ }));
27
+ result.current({ productId: "2", variantId: "v2" });
28
+ expect(action).toHaveBeenCalledWith("trigger/haptic");
29
+ expect(action).toHaveBeenCalledWith("product/open", {
30
+ productId: "2",
31
+ variantId: "v2",
32
+ });
33
+ // No search to attribute to -> no click emitted.
34
+ expect(emitProductClick).not.toHaveBeenCalled();
35
+ });
36
+ it("does not throw and opens the product when no emitter is injected", () => {
37
+ const action = jest.fn();
38
+ const { result } = renderHook(() => useSearchAnalyticsOpenProduct({
39
+ products,
40
+ searchClient: {},
41
+ emitProductClick: undefined,
42
+ action,
43
+ }));
44
+ expect(() => result.current({ productId: "1" })).not.toThrow();
45
+ expect(action).toHaveBeenCalledWith("product/open", { productId: "1" });
46
+ });
47
+ it("emits a click with the hit objectID and 1-based position for a search-backed grid", () => {
48
+ const action = jest.fn();
49
+ const emitProductClick = jest.fn();
50
+ const searchClient = { provider: "algolia" };
51
+ const { result } = renderHook(() => useSearchAnalyticsOpenProduct({
52
+ products,
53
+ searchClient,
54
+ emitProductClick,
55
+ action,
56
+ surface: "collection",
57
+ }));
58
+ result.current({ productId: "2", variantId: "v2" });
59
+ expect(emitProductClick).toHaveBeenCalledTimes(1);
60
+ expect(emitProductClick).toHaveBeenCalledWith({
61
+ client: searchClient,
62
+ surface: "collection",
63
+ product: {
64
+ productId: "2",
65
+ variantId: "v2",
66
+ objectId: "obj-2",
67
+ position: 2, // 1-based index of the second product
68
+ },
69
+ });
70
+ });
71
+ it("emits with undefined objectId/position when the clicked product isn't in the list", () => {
72
+ const action = jest.fn();
73
+ const emitProductClick = jest.fn();
74
+ const { result } = renderHook(() => useSearchAnalyticsOpenProduct({
75
+ products,
76
+ searchClient: { provider: "algolia" },
77
+ emitProductClick,
78
+ action,
79
+ }));
80
+ result.current({ productId: "999" });
81
+ expect(emitProductClick).toHaveBeenCalledWith(expect.objectContaining({
82
+ product: expect.objectContaining({
83
+ objectId: undefined,
84
+ position: undefined,
85
+ }),
86
+ }));
87
+ });
88
+ it("keeps a stable callback identity across re-renders (preserves memoized cards)", () => {
89
+ const action = jest.fn();
90
+ const emitProductClick = jest.fn();
91
+ const searchClient = { provider: "algolia" };
92
+ const { result, rerender } = renderHook(({ list }) => useSearchAnalyticsOpenProduct({
93
+ products: list,
94
+ searchClient,
95
+ emitProductClick,
96
+ action,
97
+ }), { initialProps: { list: products } });
98
+ const first = result.current;
99
+ // New products array reference (e.g. next infinite-scroll page) must NOT
100
+ // rebuild the callback, or every ProductCard would re-render.
101
+ rerender({
102
+ list: [...products, { id: "gid://shopify/Product/4", objectID: "obj-4" }],
103
+ });
104
+ expect(result.current).toBe(first);
105
+ // ...but the stable callback still reads the latest list on click.
106
+ result.current({ productId: "4" });
107
+ expect(emitProductClick).toHaveBeenCalledWith(expect.objectContaining({
108
+ product: expect.objectContaining({ objectId: "obj-4", position: 4 }),
109
+ }));
110
+ });
111
+ it("stays stable across an unstable `action` and still fires the latest one", () => {
112
+ const emitProductClick = jest.fn();
113
+ const searchClient = { provider: "algolia" };
114
+ const firstAction = jest.fn();
115
+ const { result, rerender } = renderHook(({ action }) => useSearchAnalyticsOpenProduct({
116
+ products,
117
+ searchClient,
118
+ emitProductClick,
119
+ action,
120
+ }), { initialProps: { action: firstAction } });
121
+ const first = result.current;
122
+ // A new `action` identity (e.g. useTapcart() returning a fresh fn) must NOT
123
+ // rebuild the callback — it's held in a ref.
124
+ const secondAction = jest.fn();
125
+ rerender({ action: secondAction });
126
+ expect(result.current).toBe(first);
127
+ // The stable callback still invokes the *latest* action, not the stale one.
128
+ result.current({ productId: "1" });
129
+ expect(secondAction).toHaveBeenCalledWith("product/open", {
130
+ productId: "1",
131
+ });
132
+ expect(firstAction).not.toHaveBeenCalled();
133
+ });
134
+ });
135
+ const cartProducts = [
136
+ {
137
+ id: "gid://shopify/Product/1",
138
+ objectID: "obj-1",
139
+ variants: [{ id: "gid://shopify/ProductVariant/11" }],
140
+ },
141
+ {
142
+ id: "gid://shopify/Product/2",
143
+ objectID: "obj-2",
144
+ variants: [
145
+ { id: "gid://shopify/ProductVariant/22" },
146
+ { id: "gid://shopify/ProductVariant/23" },
147
+ ],
148
+ },
149
+ ];
150
+ // ProductCard's add-to-cart payload: line items keyed by numeric variant id.
151
+ const addProps = (variantId) => ({
152
+ lineItems: [{ variantId, quantity: 1 }],
153
+ cartAttributes: [],
154
+ });
155
+ describe("useSearchAnalyticsAddToCart", () => {
156
+ it("always adds the item (haptic + cart/add) even when not search-backed", () => __awaiter(void 0, void 0, void 0, function* () {
157
+ const action = jest.fn();
158
+ const emitProductConversion = jest.fn();
159
+ const { result } = renderHook(() => useSearchAnalyticsAddToCart({
160
+ products: cartProducts,
161
+ searchClient: undefined,
162
+ emitProductConversion,
163
+ action,
164
+ }));
165
+ yield result.current(addProps("22"));
166
+ expect(action).toHaveBeenCalledWith("trigger/haptic");
167
+ expect(action).toHaveBeenCalledWith("cart/add", addProps("22"));
168
+ expect(emitProductConversion).not.toHaveBeenCalled();
169
+ }));
170
+ it("emits an add_to_cart conversion (after the add succeeds) with the matched product's objectID + 1-based position", () => __awaiter(void 0, void 0, void 0, function* () {
171
+ const action = jest.fn();
172
+ const emitProductConversion = jest.fn();
173
+ const searchClient = { provider: "algolia" };
174
+ const { result } = renderHook(() => useSearchAnalyticsAddToCart({
175
+ products: cartProducts,
176
+ searchClient,
177
+ emitProductConversion,
178
+ action,
179
+ }));
180
+ // Add the second variant of the second product.
181
+ yield result.current(addProps("23"));
182
+ expect(action).toHaveBeenCalledWith("cart/add", addProps("23"));
183
+ expect(emitProductConversion).toHaveBeenCalledTimes(1);
184
+ expect(emitProductConversion).toHaveBeenCalledWith({
185
+ client: searchClient,
186
+ surface: "collection",
187
+ product: {
188
+ productId: "2",
189
+ variantId: "23",
190
+ objectId: "obj-2",
191
+ position: 2,
192
+ },
193
+ });
194
+ }));
195
+ it("does NOT emit the conversion when the add fails (cart/add rejects)", () => __awaiter(void 0, void 0, void 0, function* () {
196
+ const action = jest.fn((name) => name === "cart/add" ? Promise.reject(new Error("add failed")) : undefined);
197
+ const emitProductConversion = jest.fn();
198
+ const { result } = renderHook(() => useSearchAnalyticsAddToCart({
199
+ products: cartProducts,
200
+ searchClient: { provider: "algolia" },
201
+ emitProductConversion,
202
+ action,
203
+ }));
204
+ yield result.current(addProps("23"));
205
+ expect(action).toHaveBeenCalledWith("cart/add", addProps("23"));
206
+ expect(emitProductConversion).not.toHaveBeenCalled();
207
+ }));
208
+ it("does not emit when the added variant isn't in the current results", () => __awaiter(void 0, void 0, void 0, function* () {
209
+ const action = jest.fn();
210
+ const emitProductConversion = jest.fn();
211
+ const { result } = renderHook(() => useSearchAnalyticsAddToCart({
212
+ products: cartProducts,
213
+ searchClient: { provider: "algolia" },
214
+ emitProductConversion,
215
+ action,
216
+ }));
217
+ yield result.current(addProps("999"));
218
+ expect(action).toHaveBeenCalledWith("cart/add", addProps("999"));
219
+ expect(emitProductConversion).not.toHaveBeenCalled();
220
+ }));
221
+ it("does not throw and still adds when no emitter is injected", () => {
222
+ const action = jest.fn();
223
+ const { result } = renderHook(() => useSearchAnalyticsAddToCart({
224
+ products: cartProducts,
225
+ searchClient: { provider: "algolia" },
226
+ emitProductConversion: undefined,
227
+ action,
228
+ }));
229
+ expect(() => result.current(addProps("11"))).not.toThrow();
230
+ expect(action).toHaveBeenCalledWith("cart/add", addProps("11"));
231
+ });
232
+ it("keeps a stable callback identity across a new products array", () => __awaiter(void 0, void 0, void 0, function* () {
233
+ const action = jest.fn();
234
+ const emitProductConversion = jest.fn();
235
+ const searchClient = { provider: "algolia" };
236
+ const { result, rerender } = renderHook(({ list }) => useSearchAnalyticsAddToCart({
237
+ products: list,
238
+ searchClient,
239
+ emitProductConversion,
240
+ action,
241
+ }), { initialProps: { list: cartProducts } });
242
+ const first = result.current;
243
+ rerender({
244
+ list: [
245
+ ...cartProducts,
246
+ {
247
+ id: "gid://shopify/Product/3",
248
+ objectID: "obj-3",
249
+ variants: [{ id: "gid://shopify/ProductVariant/33" }],
250
+ },
251
+ ],
252
+ });
253
+ expect(result.current).toBe(first);
254
+ // Stable callback still reads the latest list.
255
+ yield result.current(addProps("33"));
256
+ expect(emitProductConversion).toHaveBeenCalledWith(expect.objectContaining({
257
+ product: expect.objectContaining({ objectId: "obj-3", position: 3 }),
258
+ }));
259
+ }));
260
+ });
261
+ describe("useSearchAnalyticsImpression", () => {
262
+ it("returns undefined when not search-backed (ProductCard skips the observer)", () => {
263
+ const { result } = renderHook(() => useSearchAnalyticsImpression({
264
+ searchClient: undefined,
265
+ emitProductImpression: jest.fn(),
266
+ }));
267
+ expect(result.current).toBeUndefined();
268
+ });
269
+ it("returns undefined when no emitter is injected", () => {
270
+ const { result } = renderHook(() => useSearchAnalyticsImpression({
271
+ searchClient: {},
272
+ emitProductImpression: undefined,
273
+ }));
274
+ expect(result.current).toBeUndefined();
275
+ });
276
+ it("returns undefined when disabled, even with a search client and a callable no-op emitter (flag OFF skips the observer)", () => {
277
+ // The real NOOP_EMITTER exposes callable no-ops, not undefined, so a truthy
278
+ // emitter must not be enough to set up the observer — the flag must gate it.
279
+ const { result } = renderHook(() => useSearchAnalyticsImpression({
280
+ searchClient: { provider: "algolia" },
281
+ emitProductImpression: () => { },
282
+ enabled: false,
283
+ }));
284
+ expect(result.current).toBeUndefined();
285
+ });
286
+ it("emits one impression with objectId + productId when a card becomes visible", () => {
287
+ var _a;
288
+ const emitProductImpression = jest.fn();
289
+ const searchClient = { provider: "algolia" };
290
+ const { result } = renderHook(() => useSearchAnalyticsImpression({
291
+ searchClient,
292
+ emitProductImpression,
293
+ surface: "collection",
294
+ }));
295
+ (_a = result.current) === null || _a === void 0 ? void 0 : _a.call(result, { id: "gid://shopify/Product/2", objectID: "obj-2" });
296
+ expect(emitProductImpression).toHaveBeenCalledTimes(1);
297
+ expect(emitProductImpression).toHaveBeenCalledWith({
298
+ client: searchClient,
299
+ surface: "collection",
300
+ product: { productId: "2", objectId: "obj-2" },
301
+ });
302
+ });
303
+ it("dedupes repeat impressions of the same product", () => {
304
+ var _a, _b;
305
+ const emitProductImpression = jest.fn();
306
+ const { result } = renderHook(() => useSearchAnalyticsImpression({
307
+ searchClient: { provider: "algolia" },
308
+ emitProductImpression,
309
+ }));
310
+ const product = { id: "gid://shopify/Product/2", objectID: "obj-2" };
311
+ (_a = result.current) === null || _a === void 0 ? void 0 : _a.call(result, product);
312
+ (_b = result.current) === null || _b === void 0 ? void 0 : _b.call(result, product);
313
+ expect(emitProductImpression).toHaveBeenCalledTimes(1);
314
+ });
315
+ });
package/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export * from "./components/hooks/use-infinite-scroll";
10
10
  export * from "./components/hooks/swr-retry";
11
11
  export * from "./components/hooks/use-infinite-wishlist";
12
12
  export * from "./components/hooks/use-recommendations";
13
+ export * from "./components/hooks/use-search-analytics-open-product";
13
14
  export * from "./components/hooks/use-products";
14
15
  export * from "./components/hooks/use-personalized-cluster-feed";
15
16
  export * from "./components/hooks/use-order-details";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ export * from "./components/hooks/use-infinite-scroll";
10
10
  export * from "./components/hooks/swr-retry";
11
11
  export * from "./components/hooks/use-infinite-wishlist";
12
12
  export * from "./components/hooks/use-recommendations";
13
+ export * from "./components/hooks/use-search-analytics-open-product";
13
14
  export * from "./components/hooks/use-products";
14
15
  export * from "./components/hooks/use-personalized-cluster-feed";
15
16
  export * from "./components/hooks/use-order-details";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tapcart/mobile-components",
3
- "version": "0.14.6",
3
+ "version": "0.15.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "style": "dist/styles.css",