@tapcart/mobile-components 0.12.9 → 0.12.11

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 +1 @@
1
- {"version":3,"file":"use-block-conditional-rendering.d.ts","sourceRoot":"","sources":["../../../components/hooks/use-block-conditional-rendering.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,YAAY,EAGb,MAAM,kBAAkB,CAAA;AAezB,eAAO,MAAM,4BAA4B,WAC/B;IACN,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,eAAe,EAAE,GAAG,CAAA;IACpB,iBAAiB,EAAE,GAAG,CAAA;CACvB,UACO,YAAY,6BACO,GAAG;;;CA4Q/B,CAAA"}
1
+ {"version":3,"file":"use-block-conditional-rendering.d.ts","sourceRoot":"","sources":["../../../components/hooks/use-block-conditional-rendering.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,YAAY,EAGb,MAAM,kBAAkB,CAAA;AAezB,eAAO,MAAM,4BAA4B,WAC/B;IACN,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,eAAe,EAAE,GAAG,CAAA;IACpB,iBAAiB,EAAE,GAAG,CAAA;CACvB,UACO,YAAY,6BACO,GAAG;;;CAiT/B,CAAA"}
@@ -29,11 +29,7 @@ export const useBlockConditionalRendering = (_props, _block, mobileComponentOver
29
29
  const { id: deviceId = "" } = deviceVariables || {};
30
30
  const productId = (_f = searchParams === null || searchParams === void 0 ? void 0 : searchParams.get("productId")) !== null && _f !== void 0 ? _f : undefined;
31
31
  const productHandle = (_g = searchParams === null || searchParams === void 0 ? void 0 : searchParams.get("productHandle")) !== null && _g !== void 0 ? _g : undefined;
32
- const location = {
33
- country,
34
- language,
35
- deviceId,
36
- };
32
+ const location = useMemo(() => ({ country, language, deviceId }), [country, language, deviceId]);
37
33
  const productMetafieldsQuery = useMemo(() => {
38
34
  var _a;
39
35
  if (!isConditionalsEnabled) {
@@ -67,7 +63,7 @@ export const useBlockConditionalRendering = (_props, _block, mobileComponentOver
67
63
  ]);
68
64
  const shouldFetchProduct = isConditionalsEnabled && (productId || productHandle);
69
65
  const useProducts = (mobileComponentOverrides === null || mobileComponentOverrides === void 0 ? void 0 : mobileComponentOverrides.useProducts) || mclUseProducts;
70
- const useProductsArguments = (mobileComponentOverrides === null || mobileComponentOverrides === void 0 ? void 0 : mobileComponentOverrides.useProducts)
66
+ const useProductsArguments = useMemo(() => (mobileComponentOverrides === null || mobileComponentOverrides === void 0 ? void 0 : mobileComponentOverrides.useProducts)
71
67
  ? {
72
68
  productIds: productId ? [gidFromId(productId)] : [],
73
69
  productHandles: productHandle ? [productHandle] : [],
@@ -82,13 +78,20 @@ export const useBlockConditionalRendering = (_props, _block, mobileComponentOver
82
78
  language,
83
79
  country,
84
80
  },
85
- };
86
- const useProductsSkipArguments = (mobileComponentOverrides === null || mobileComponentOverrides === void 0 ? void 0 : mobileComponentOverrides.useProducts)
87
- ? {
88
- skip: true,
89
- }
90
- : null;
91
- const { products, error: useProductsError, isLoading: isProductsLoading, } = useProducts(shouldFetchProduct ? useProductsArguments : useProductsSkipArguments);
81
+ }, [
82
+ mobileComponentOverrides === null || mobileComponentOverrides === void 0 ? void 0 : mobileComponentOverrides.useProducts,
83
+ productId,
84
+ productHandle,
85
+ apiUrl,
86
+ language,
87
+ country,
88
+ productMetafieldsQuery,
89
+ ]);
90
+ const useProductsSkipArguments = useMemo(() => (mobileComponentOverrides === null || mobileComponentOverrides === void 0 ? void 0 : mobileComponentOverrides.useProducts)
91
+ ? { skip: true }
92
+ : null, [mobileComponentOverrides === null || mobileComponentOverrides === void 0 ? void 0 : mobileComponentOverrides.useProducts]);
93
+ const useProductsInput = useMemo(() => shouldFetchProduct ? useProductsArguments : useProductsSkipArguments, [shouldFetchProduct, useProductsArguments, useProductsSkipArguments]);
94
+ const { products, error: useProductsError, isLoading: isProductsLoading, } = useProducts(useProductsInput);
92
95
  const blockCollectionMetafields = useMemo(() => {
93
96
  var _a;
94
97
  if (!isConditionalsEnabled) {
@@ -144,20 +147,30 @@ export const useBlockConditionalRendering = (_props, _block, mobileComponentOver
144
147
  const collectionHandle = (_j = searchParams === null || searchParams === void 0 ? void 0 : searchParams.get("collectionHandle")) !== null && _j !== void 0 ? _j : undefined;
145
148
  const shouldFetchCollection = isConditionalsEnabled && (collectionId || collectionHandle);
146
149
  const useCollection = (mobileComponentOverrides === null || mobileComponentOverrides === void 0 ? void 0 : mobileComponentOverrides.useCollection) || mclUseCollection;
147
- const { specificCollection = {}, error: useCollectionError, loading: isCollectionLoading, } = useCollection({
150
+ const useCollectionArguments = useMemo(() => ({
148
151
  appId,
149
152
  apiUrl,
150
153
  collectionId: shouldFetchCollection ? collectionId : undefined,
151
154
  collectionHandle: shouldFetchCollection ? collectionHandle : undefined,
152
155
  language: location === null || location === void 0 ? void 0 : location.language,
153
156
  metafields: shouldFetchCollection ? memoizedMetafields : undefined,
154
- });
157
+ }), [
158
+ appId,
159
+ apiUrl,
160
+ shouldFetchCollection,
161
+ collectionId,
162
+ collectionHandle,
163
+ location === null || location === void 0 ? void 0 : location.language,
164
+ memoizedMetafields,
165
+ ]);
166
+ const { specificCollection = {}, error: useCollectionError, loading: isCollectionLoading, } = useCollection(useCollectionArguments);
155
167
  const isLoading = isProductsLoading || isCollectionLoading;
156
168
  if (useProductsError || useCollectionError) {
157
169
  console.error("Unable to load products in conditional block rendering hook: ", useProductsError !== null && useProductsError !== void 0 ? useProductsError : useCollectionError);
158
170
  }
171
+ const disabledResult = useMemo(() => ({ shouldShow: true, isLoading: false }), []);
159
172
  if (!isConditionalsEnabled) {
160
- return { shouldShow: true, isLoading: false };
173
+ return disabledResult;
161
174
  }
162
175
  let shouldShow = true;
163
176
  const blockHasTags = countNumberOfTagsInState(blockState) > 0;
@@ -200,5 +213,5 @@ export const useBlockConditionalRendering = (_props, _block, mobileComponentOver
200
213
  console.error("Error evaluating block visibility conditions:", e);
201
214
  shouldShow = true; // Fail-safe to show block
202
215
  }
203
- return { shouldShow, isLoading };
216
+ return useMemo(() => ({ shouldShow, isLoading }), [shouldShow, isLoading]);
204
217
  };
@@ -1 +1 @@
1
- {"version":3,"file":"use-infinite-scroll.d.ts","sourceRoot":"","sources":["../../../components/hooks/use-infinite-scroll.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,uBAAuB,EAAmB,MAAM,iBAAiB,CAAA;AAE1E,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAE5D,UAAU,QAAQ;IAChB,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,GAAG,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,cAAc,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAE,CAAA;CACxC;AAED,UAAU,sBAAsB;IAE9B,YAAY,CAAC,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAA;IAGxC,WAAW,CAAC,EAAE,QAAQ,CAAA;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAGpC,SAAS,CAAC,EAAE,UAAU,GAAG,YAAY,CAAA;IACrC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,CAAA;IAChD,YAAY,CAAC,EAAE,CACb,SAAS,EAAE,MAAM,EACjB,gBAAgB,EAAE,GAAG,GAAG,IAAI,EAC5B,GAAG,IAAI,EAAE,GAAG,EAAE,KACX,GAAG,CAAA;IACR,eAAe,CAAC,EAAE,OAAO,CAAA;CAC1B;AAED,UAAU,uBAAuB;IAC/B,IAAI,EAAE,QAAQ,EAAE,GAAG,SAAS,CAAA;IAC5B,KAAK,EAAE,GAAG,CAAA;IACV,oBAAoB,EAAE,OAAO,CAAA;IAC7B,aAAa,EAAE,OAAO,GAAG,SAAS,CAAA;IAClC,OAAO,EAAE,OAAO,CAAA;IAChB,aAAa,EAAE,OAAO,CAAA;IACtB,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,GAAG,SAAS,KAAK,IAAI,CAAA;IAChD,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,oBAAoB,EAAE,GAAG,EAAE,CAAA;IAC3B,SAAS,EAAE,OAAO,CAAA;IAClB,YAAY,EAAE,OAAO,CAAA;CACtB;AAED,eAAO,MAAM,sCAAsC,iBACnC,uBAAuB,OAsBtC,CAAA;AAED,QAAA,MAAM,YAAY,WAAY,MAAM,WAGnC,CAAA;AAED,QAAA,MAAM,iBAAiB,wLAgBpB,sBAAsB,KAAG,uBAwR3B,CAAA;AAED,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,CAAA"}
1
+ {"version":3,"file":"use-infinite-scroll.d.ts","sourceRoot":"","sources":["../../../components/hooks/use-infinite-scroll.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,uBAAuB,EAAmB,MAAM,iBAAiB,CAAA;AAE1E,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAE5D,UAAU,QAAQ;IAChB,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,GAAG,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,cAAc,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAE,CAAA;CACxC;AAED,UAAU,sBAAsB;IAE9B,YAAY,CAAC,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAA;IAGxC,WAAW,CAAC,EAAE,QAAQ,CAAA;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAGpC,SAAS,CAAC,EAAE,UAAU,GAAG,YAAY,CAAA;IACrC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,CAAA;IAChD,YAAY,CAAC,EAAE,CACb,SAAS,EAAE,MAAM,EACjB,gBAAgB,EAAE,GAAG,GAAG,IAAI,EAC5B,GAAG,IAAI,EAAE,GAAG,EAAE,KACX,GAAG,CAAA;IACR,eAAe,CAAC,EAAE,OAAO,CAAA;CAC1B;AAED,UAAU,uBAAuB;IAC/B,IAAI,EAAE,QAAQ,EAAE,GAAG,SAAS,CAAA;IAC5B,KAAK,EAAE,GAAG,CAAA;IACV,oBAAoB,EAAE,OAAO,CAAA;IAC7B,aAAa,EAAE,OAAO,GAAG,SAAS,CAAA;IAClC,OAAO,EAAE,OAAO,CAAA;IAChB,aAAa,EAAE,OAAO,CAAA;IACtB,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,GAAG,SAAS,KAAK,IAAI,CAAA;IAChD,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,oBAAoB,EAAE,GAAG,EAAE,CAAA;IAC3B,SAAS,EAAE,OAAO,CAAA;IAClB,YAAY,EAAE,OAAO,CAAA;CACtB;AAED,eAAO,MAAM,sCAAsC,iBACnC,uBAAuB,OAsBtC,CAAA;AAED,QAAA,MAAM,YAAY,WAAY,MAAM,WAGnC,CAAA;AAED,QAAA,MAAM,iBAAiB,wLAgBpB,sBAAsB,KAAG,uBAqS3B,CAAA;AAED,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,CAAA"}
@@ -49,7 +49,7 @@ initialData, queryVariables: queryVariableProps,
49
49
  // Common props
50
50
  direction = "vertical", productLimit = Infinity, threshold = 0.01, interval = 33, // ~2 frames
51
51
  customFetcher, customGetKey, shouldSkipFetch: shouldSkipFetchProp, }) => {
52
- var _a, _b, _c, _d, _e;
52
+ var _a, _b, _c, _d, _e, _f;
53
53
  const searchParams = useSearchParams();
54
54
  const productCount = useRef(0);
55
55
  const isFirstRender = useRef(true);
@@ -102,7 +102,7 @@ customFetcher, customGetKey, shouldSkipFetch: shouldSkipFetchProp, }) => {
102
102
  return Object.assign(Object.assign({}, queryVariables), { cursorBlob: previousPageData.pageData.cursorBlob });
103
103
  };
104
104
  const apiFetcher = (body) => __awaiter(void 0, void 0, void 0, function* () {
105
- var _f, _g;
105
+ var _g, _h;
106
106
  if (!(initialData === null || initialData === void 0 ? void 0 : initialData.apiURL)) {
107
107
  throw new Error("initialData.apiURL is required for API mode");
108
108
  }
@@ -111,7 +111,7 @@ customFetcher, customGetKey, shouldSkipFetch: shouldSkipFetchProp, }) => {
111
111
  body: JSON.stringify(body),
112
112
  });
113
113
  const data = yield res.json();
114
- productCount.current += (_g = (_f = data === null || data === void 0 ? void 0 : data.products) === null || _f === void 0 ? void 0 : _f.length) !== null && _g !== void 0 ? _g : 0;
114
+ productCount.current += (_h = (_g = data === null || data === void 0 ? void 0 : data.products) === null || _g === void 0 ? void 0 : _g.length) !== null && _h !== void 0 ? _h : 0;
115
115
  return data;
116
116
  });
117
117
  // Return null from getKey to conditionally skip fetching
@@ -134,20 +134,23 @@ customFetcher, customGetKey, shouldSkipFetch: shouldSkipFetchProp, }) => {
134
134
  };
135
135
  const fetcher = usingSearchClient
136
136
  ? (...args) => __awaiter(void 0, void 0, void 0, function* () {
137
- var _h, _j;
137
+ var _j, _k;
138
138
  if (!searchClientFetcher)
139
139
  return null;
140
140
  const result = yield searchClientFetcher(...args);
141
- productCount.current += (_j = (_h = result === null || result === void 0 ? void 0 : result.products) === null || _h === void 0 ? void 0 : _h.length) !== null && _j !== void 0 ? _j : 0;
141
+ productCount.current += (_k = (_j = result === null || result === void 0 ? void 0 : result.products) === null || _j === void 0 ? void 0 : _j.length) !== null && _k !== void 0 ? _k : 0;
142
142
  return result;
143
143
  })
144
144
  : (...args) => __awaiter(void 0, void 0, void 0, function* () {
145
145
  const effectiveFetcher = customFetcher || apiFetcher;
146
146
  return effectiveFetcher(...args);
147
147
  });
148
+ // Check if caller provided pre-fetched products to display while SWR fetches
149
+ const hasInitialProducts = Boolean((_a = initialData === null || initialData === void 0 ? void 0 : initialData.products) === null || _a === void 0 ? void 0 : _a.length);
148
150
  const { data, error, size, setSize, isLoading, isValidating, mutate, } = useSWRInfinite(getKey, fetcher, {
149
151
  revalidateFirstPage: false,
150
152
  initialSize: 1,
153
+ keepPreviousData: true,
151
154
  });
152
155
  // Detect when params change and force cache invalidation
153
156
  useEffect(() => {
@@ -166,15 +169,19 @@ customFetcher, customGetKey, shouldSkipFetch: shouldSkipFetchProp, }) => {
166
169
  // Skip reset on first render, but force it for any subsequent transitions to valid params
167
170
  if (!isFirstRender.current && hasValidCurrentParams) {
168
171
  productCount.current = 0;
169
- mutate(undefined, { revalidate: true });
172
+ mutate();
170
173
  }
171
174
  }
172
175
  isFirstRender.current = false;
173
176
  }, [currentCollectionId, currentCollectionHandle, currentSearchQuery, mutate]);
174
- const isLoadingInitialData = !data && !error;
177
+ // When we have pre-fetched products, don't report as "loading initial data"
178
+ // so the grid renders immediately with those products
179
+ const isLoadingInitialData = !data && !error && !hasInitialProducts;
175
180
  const isLoadingMore = isLoadingInitialData ||
176
181
  (size > 0 && data && typeof data[size - 1] === "undefined");
177
- const isEmpty = ((_c = (_b = (_a = data === null || data === void 0 ? void 0 : data[0]) === null || _a === void 0 ? void 0 : _a.products) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0) === 0;
182
+ const isEmpty = data
183
+ ? ((_d = (_c = (_b = data === null || data === void 0 ? void 0 : data[0]) === null || _b === void 0 ? void 0 : _b.products) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0) === 0
184
+ : !hasInitialProducts;
178
185
  let isEndPointer;
179
186
  if (!data) {
180
187
  isEndPointer = true;
@@ -183,7 +190,7 @@ customFetcher, customGetKey, shouldSkipFetch: shouldSkipFetchProp, }) => {
183
190
  isEndPointer = !(searchClient === null || searchClient === void 0 ? void 0 : searchClient.getHasMore());
184
191
  }
185
192
  else {
186
- isEndPointer = !((_e = (_d = data[data.length - 1]) === null || _d === void 0 ? void 0 : _d.pageData) === null || _e === void 0 ? void 0 : _e.cursorBlob);
193
+ isEndPointer = !((_f = (_e = data[data.length - 1]) === null || _e === void 0 ? void 0 : _e.pageData) === null || _f === void 0 ? void 0 : _f.cursorBlob);
187
194
  }
188
195
  const isReachingEnd = isEmpty || isEndPointer;
189
196
  const isRefreshing = isValidating && data && data.length === size;
@@ -214,15 +221,20 @@ customFetcher, customGetKey, shouldSkipFetch: shouldSkipFetchProp, }) => {
214
221
  return;
215
222
  const unsubscribeAllChanges = searchClient.onSearchStateChange(() => {
216
223
  productCount.current = 0;
217
- mutate(undefined, { revalidate: true });
224
+ mutate();
218
225
  });
219
226
  return unsubscribeAllChanges;
220
227
  }, [searchClient, mutate, usingSearchClient]);
221
228
  const products = useMemo(() => {
222
- return data
223
- ? data === null || data === void 0 ? void 0 : data.flatMap((page) => page === null || page === void 0 ? void 0 : page.products).slice(0, productLimit)
224
- : [];
225
- }, [data, productLimit]);
229
+ if (data) {
230
+ return data.flatMap((page) => page === null || page === void 0 ? void 0 : page.products).slice(0, productLimit);
231
+ }
232
+ // Use pre-fetched products while SWR fetches the real paginated data
233
+ if (hasInitialProducts) {
234
+ return initialData.products;
235
+ }
236
+ return [];
237
+ }, [data, productLimit, hasInitialProducts, initialData === null || initialData === void 0 ? void 0 : initialData.products]);
226
238
  // Collect all integration responses per page when using search client
227
239
  const integrationResponses = useMemo(() => {
228
240
  if (!usingSearchClient)
@@ -1,16 +1,40 @@
1
+ export type WishlistItemRef = {
2
+ productId: string;
3
+ variantId?: string;
4
+ };
1
5
  type QueryVariables = {
2
6
  language?: string;
3
7
  country?: string;
4
8
  appId?: string;
5
9
  collectionId?: string;
6
10
  };
11
+ /** Preferred: item-level refs. Legacy: parallel arrays (converted to initialItems internally). */
7
12
  type UseInFiniteProductsOptions = {
8
- initialProductIds: string[];
13
+ /** Item-level refs (productId + optional variantId). Use this for multi-variant support. */
14
+ initialItems?: WishlistItemRef[];
15
+ /**
16
+ * @deprecated Use initialItems instead. If initialItems is not provided, these are converted to initialItems (with variantIds by position).
17
+ */
18
+ initialProductIds?: string[];
19
+ /**
20
+ * @deprecated Use initialItems instead. When using with initialProductIds, same-length array by position.
21
+ */
22
+ variantIds?: string[];
9
23
  chunkSize: number;
10
24
  apiUrl: string;
11
25
  shouldMock: boolean;
12
26
  queryVariables?: QueryVariables;
13
- variantIds?: string[];
27
+ };
28
+ /** Return type of useInfiniteWishlist. Do not remove fields — forked blocks may depend on them. */
29
+ export type UseInfiniteWishlistResult = {
30
+ products: Product[];
31
+ isLoading: boolean;
32
+ isLoadingMore: boolean;
33
+ error: Error | null;
34
+ hasMore: boolean;
35
+ loadMore: () => Promise<void>;
36
+ loadMoreRef: (node?: Element | null) => void;
37
+ reset: () => void;
14
38
  };
15
39
  type ProductVariant = {
16
40
  id: string;
@@ -49,15 +73,6 @@ type Product = {
49
73
  height: number;
50
74
  };
51
75
  };
52
- export declare function useInfiniteWishlist({ initialProductIds, chunkSize, apiUrl, shouldMock, queryVariables, variantIds, }: UseInFiniteProductsOptions): {
53
- products: Product[];
54
- isLoading: boolean;
55
- isLoadingMore: boolean;
56
- error: Error | null;
57
- hasMore: boolean;
58
- loadMore: () => Promise<void>;
59
- loadMoreRef: (node?: Element | null | undefined) => void;
60
- reset: () => void;
61
- };
76
+ export declare function useInfiniteWishlist(options: UseInFiniteProductsOptions): UseInfiniteWishlistResult;
62
77
  export {};
63
78
  //# sourceMappingURL=use-infinite-wishlist.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-infinite-wishlist.d.ts","sourceRoot":"","sources":["../../../components/hooks/use-infinite-wishlist.ts"],"names":[],"mappings":"AAOA,KAAK,cAAc,GAAG;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,KAAK,0BAA0B,GAAG;IAChC,iBAAiB,EAAE,MAAM,EAAE,CAAA;IAC3B,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,OAAO,CAAA;IACnB,cAAc,CAAC,EAAE,cAAc,CAAA;IAC/B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;CACtB,CAAA;AAED,KAAK,cAAc,GAAG;IACpB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE;QACL,MAAM,EAAE,MAAM,CAAA;KACf,CAAA;IACD,cAAc,EAAE;QACd,MAAM,EAAE,MAAM,CAAA;KACf,CAAA;IACD,gBAAgB,EAAE,OAAO,CAAA;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,eAAe,EAAE,KAAK,CAAC;QACrB,IAAI,EAAE,MAAM,CAAA;QACZ,KAAK,EAAE,MAAM,CAAA;KACd,CAAC,CAAA;IACF,KAAK,CAAC,EAAE;QACN,GAAG,EAAE,MAAM,CAAA;QACX,OAAO,EAAE,MAAM,CAAA;QACf,KAAK,EAAE,MAAM,CAAA;QACb,MAAM,EAAE,MAAM,CAAA;KACf,CAAA;CACF,CAAA;AAED,KAAK,OAAO,GAAG;IACb,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,gBAAgB,EAAE,OAAO,CAAA;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,cAAc,EAAE,CAAA;IAC1B,aAAa,CAAC,EAAE;QACd,GAAG,EAAE,MAAM,CAAA;QACX,OAAO,EAAE,MAAM,CAAA;QACf,KAAK,EAAE,MAAM,CAAA;QACb,MAAM,EAAE,MAAM,CAAA;KACf,CAAA;CACF,CAAA;AAKD,wBAAgB,mBAAmB,CAAC,EAClC,iBAAiB,EACjB,SAAS,EACT,MAAM,EACN,UAAkB,EAClB,cAAmB,EACnB,UAAe,GAChB,EAAE,0BAA0B;;;;;;;;;EAsO5B"}
1
+ {"version":3,"file":"use-infinite-wishlist.d.ts","sourceRoot":"","sources":["../../../components/hooks/use-infinite-wishlist.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,eAAe,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAEvE,KAAK,cAAc,GAAG;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,kGAAkG;AAClG,KAAK,0BAA0B,GAAG;IAChC,4FAA4F;IAC5F,YAAY,CAAC,EAAE,eAAe,EAAE,CAAA;IAChC;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC5B;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACrB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,OAAO,CAAA;IACnB,cAAc,CAAC,EAAE,cAAc,CAAA;CAChC,CAAA;AAED,mGAAmG;AACnG,MAAM,MAAM,yBAAyB,GAAG;IACtC,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,SAAS,EAAE,OAAO,CAAA;IAClB,aAAa,EAAE,OAAO,CAAA;IACtB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7B,WAAW,EAAE,CAAC,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,KAAK,IAAI,CAAA;IAC5C,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB,CAAA;AAED,KAAK,cAAc,GAAG;IACpB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE;QACL,MAAM,EAAE,MAAM,CAAA;KACf,CAAA;IACD,cAAc,EAAE;QACd,MAAM,EAAE,MAAM,CAAA;KACf,CAAA;IACD,gBAAgB,EAAE,OAAO,CAAA;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,eAAe,EAAE,KAAK,CAAC;QACrB,IAAI,EAAE,MAAM,CAAA;QACZ,KAAK,EAAE,MAAM,CAAA;KACd,CAAC,CAAA;IACF,KAAK,CAAC,EAAE;QACN,GAAG,EAAE,MAAM,CAAA;QACX,OAAO,EAAE,MAAM,CAAA;QACf,KAAK,EAAE,MAAM,CAAA;QACb,MAAM,EAAE,MAAM,CAAA;KACf,CAAA;CACF,CAAA;AAED,KAAK,OAAO,GAAG;IACb,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,gBAAgB,EAAE,OAAO,CAAA;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,cAAc,EAAE,CAAA;IAC1B,aAAa,CAAC,EAAE;QACd,GAAG,EAAE,MAAM,CAAA;QACX,OAAO,EAAE,MAAM,CAAA;QACf,KAAK,EAAE,MAAM,CAAA;QACb,MAAM,EAAE,MAAM,CAAA;KACf,CAAA;CACF,CAAA;AAgDD,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,0BAA0B,GAClC,yBAAyB,CAmW3B"}
@@ -8,20 +8,53 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
9
  });
10
10
  };
11
- import { useState, useEffect, useCallback, useRef } from "react";
11
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
12
12
  import { useInView } from "react-intersection-observer";
13
- import { getProductGidsFromIds } from "../../lib/utils";
13
+ import { getProductGidsFromIds, getVariantGidsFromIds } from "../../lib/utils";
14
14
  import { useCollection } from "./use-collection";
15
15
  const extractVariantId = (fullId) => fullId.split("/").pop() || "";
16
- export function useInfiniteWishlist({ initialProductIds, chunkSize, apiUrl, shouldMock = false, queryVariables = {}, variantIds = [], }) {
16
+ const ensureVariantGID = (id) => getVariantGidsFromIds([id])[0];
17
+ const toItemKey = (item) => item.variantId
18
+ ? ensureVariantGID(item.variantId)
19
+ : getProductGidsFromIds([item.productId])[0];
20
+ /** Unique key for a product card: variant GID when narrowed to one variant, else product GID. */
21
+ const getProductOrVariantKey = (p) => { var _a; return ((_a = p === null || p === void 0 ? void 0 : p.variants) === null || _a === void 0 ? void 0 : _a.length) === 1 ? p.variants[0].id : p.id; };
22
+ const buildIdSnapshot = (ids) => ({
23
+ ids,
24
+ key: ids.join("\0"),
25
+ set: new Set(ids),
26
+ indexMap: new Map(ids.map((id, i) => [id, i])),
27
+ });
28
+ /** Normalize options: prefer initialItems; fall back to legacy initialProductIds + variantIds. */
29
+ function normalizeInitialItems(options) {
30
+ var _a, _b;
31
+ if (options.initialItems != null && Array.isArray(options.initialItems)) {
32
+ return options.initialItems;
33
+ }
34
+ const ids = (_a = options.initialProductIds) !== null && _a !== void 0 ? _a : [];
35
+ const variantIds = (_b = options.variantIds) !== null && _b !== void 0 ? _b : [];
36
+ return ids.map((productId, i) => ({
37
+ productId,
38
+ variantId: variantIds[i],
39
+ }));
40
+ }
41
+ export function useInfiniteWishlist(options) {
17
42
  var _a;
43
+ const { chunkSize, apiUrl, shouldMock = false, queryVariables = {} } = options;
44
+ const initialItems = normalizeInitialItems(options);
18
45
  const isInitialMount = useRef(true);
19
- const [allProductIds, setAllProductIds] = useState(initialProductIds);
46
+ const itemKeys = initialItems.map(toItemKey);
47
+ const prevSnapshotRef = useRef(buildIdSnapshot(itemKeys));
48
+ const loadedIndexRef = useRef(0);
49
+ const [allItemKeys, setAllItemKeys] = useState(itemKeys);
20
50
  const [loadedIndex, setLoadedIndex] = useState(0);
21
51
  const [products, setProducts] = useState([]);
22
52
  const [isLoading, setIsLoading] = useState(false);
23
53
  const [error, setError] = useState(null);
24
54
  const loadingRef = useRef(false);
55
+ // Kept in sync via a dedicated effect so loadMore callbacks can detect
56
+ // whether the wishlist changed while an async fetch was in-flight.
57
+ const allItemKeysRef = useRef(allItemKeys);
25
58
  //Fetching collections are necessary only for the mock data. Since we pass the collectionId to the API, it will return the products for the collection.
26
59
  const { collections } = useCollection({
27
60
  apiUrl: apiUrl,
@@ -32,16 +65,83 @@ export function useInfiniteWishlist({ initialProductIds, chunkSize, apiUrl, shou
32
65
  limit: 1,
33
66
  });
34
67
  const collectionToFetchID = (_a = collections[0]) === null || _a === void 0 ? void 0 : _a.id;
68
+ // Keep loadedIndexRef in sync so the delta-detection effect below can read
69
+ // the current value without adding loadedIndex as a dependency (which would
70
+ // cause the effect to re-run on every pagination step).
71
+ useEffect(() => {
72
+ loadedIndexRef.current = loadedIndex;
73
+ }, [loadedIndex]);
74
+ // Keep allItemKeysRef in sync so in-flight loadMore callbacks can bail out
75
+ // when the wishlist changes before their fetch completes.
76
+ useEffect(() => {
77
+ allItemKeysRef.current = allItemKeys;
78
+ }, [allItemKeys]);
79
+ // Stable key so the delta effect runs only when list content changes, not on every
80
+ // new initialItems reference (avoids CPU spike from repeated effect runs after removal).
81
+ const itemKeysKey = useMemo(() => initialItems.map(toItemKey).join("\0"), [initialItems]);
35
82
  useEffect(() => {
83
+ var _a;
84
+ const nextKeys = initialItems.map(toItemKey);
36
85
  if (isInitialMount.current) {
37
86
  isInitialMount.current = false;
87
+ prevSnapshotRef.current = buildIdSnapshot(nextKeys);
88
+ return;
38
89
  }
39
- else {
40
- setAllProductIds(initialProductIds);
41
- setLoadedIndex(0);
42
- setProducts([]);
90
+ // Webbridge data has no stable refs (JSON deserialization always produces
91
+ // new objects). Build a snapshot for the incoming item keys so all
92
+ // comparisons use O(1) Set/Map lookups rather than repeated array walks.
93
+ const prev = prevSnapshotRef.current;
94
+ const next = buildIdSnapshot(nextKeys);
95
+ prevSnapshotRef.current = next;
96
+ // O(1) same-content check via precomputed key — avoids walking the array
97
+ // on every variables/updated notification (cart, customer, etc.).
98
+ if (next.key === prev.key)
99
+ return;
100
+ const isPureRemoval = next.ids.length < prev.ids.length &&
101
+ next.ids.every((id) => prev.set.has(id)); // O(1) per lookup
102
+ const currentLoadedIndex = loadedIndexRef.current;
103
+ const isPureAddition = next.ids.length > prev.ids.length &&
104
+ prev.ids.every((id) => next.set.has(id));
105
+ if (isPureRemoval) {
106
+ // Filter removed items out of the current list without resetting,
107
+ // so the grid never goes to skeleton for a simple item removal.
108
+ const removedKeys = new Set(prev.ids.filter((k) => !next.set.has(k)));
109
+ setProducts((curr) => curr.filter((p) => !removedKeys.has(getProductOrVariantKey(p))));
110
+ setAllItemKeys(next.ids);
111
+ // Recalculate loadedIndex: count surviving keys that were already in the
112
+ // loaded range of the old array. O(1) per lookup via prev.indexMap.
113
+ const newLoadedIndex = next.ids.filter((id) => { var _a; return ((_a = prev.indexMap.get(id)) !== null && _a !== void 0 ? _a : Infinity) < currentLoadedIndex; }).length;
114
+ setLoadedIndex(newLoadedIndex);
115
+ return;
43
116
  }
44
- }, [initialProductIds]);
117
+ if (isPureAddition) {
118
+ // Walk next.ids from the start to find the new contiguous loaded prefix.
119
+ // We stop at the first ID that either (a) wasn't fetched yet in the old
120
+ // list or (b) is one of the newly added items. Everything from that point
121
+ // onwards will be picked up by the next loadMore.
122
+ let newLoadedIndex = 0;
123
+ for (const id of next.ids) {
124
+ if (prev.set.has(id) &&
125
+ ((_a = prev.indexMap.get(id)) !== null && _a !== void 0 ? _a : Infinity) < currentLoadedIndex) {
126
+ newLoadedIndex++;
127
+ }
128
+ else {
129
+ break;
130
+ }
131
+ }
132
+ setAllItemKeys(next.ids);
133
+ setLoadedIndex(newLoadedIndex);
134
+ // products are unchanged — scroll position preserved, no skeleton.
135
+ // The inView effect re-evaluates after these state updates and will
136
+ // auto-trigger loadMore if the sentinel is already in view.
137
+ return;
138
+ }
139
+ setAllItemKeys(next.ids);
140
+ setLoadedIndex(0);
141
+ setProducts([]);
142
+ // Intentionally depend on itemKeysKey (not initialItems) so we only run when list content changes, not on every new array ref
143
+ // eslint-disable-next-line react-hooks/exhaustive-deps
144
+ }, [itemKeysKey]);
45
145
  const handleFetchError = (err, context) => {
46
146
  const error = err instanceof Error ? err : new Error(`Failed to ${context}`);
47
147
  setError(error);
@@ -84,14 +184,13 @@ export function useInfiniteWishlist({ initialProductIds, chunkSize, apiUrl, shou
84
184
  queryVariables === null || queryVariables === void 0 ? void 0 : queryVariables.country,
85
185
  queryVariables === null || queryVariables === void 0 ? void 0 : queryVariables.appId,
86
186
  ]);
87
- const fetchProducts = useCallback((ids) => __awaiter(this, void 0, void 0, function* () {
88
- if (!ids.length)
187
+ const fetchProducts = useCallback((chunkItemKeys, chunkProductGids) => __awaiter(this, void 0, void 0, function* () {
188
+ if (!chunkItemKeys.length)
89
189
  return [];
90
190
  if (shouldMock)
91
191
  return fetchMockedProducts();
92
192
  const queryParams = new URLSearchParams();
93
- queryParams.set("ids", getProductGidsFromIds(ids).join(","));
94
- // Add country and language parameters for correct currency/pricing
193
+ queryParams.set("ids", chunkProductGids.join(","));
95
194
  if (queryVariables === null || queryVariables === void 0 ? void 0 : queryVariables.country) {
96
195
  queryParams.set("country", queryVariables.country);
97
196
  }
@@ -104,36 +203,47 @@ export function useInfiniteWishlist({ initialProductIds, chunkSize, apiUrl, shou
104
203
  if (!response.ok) {
105
204
  throw new Error(`HTTP error: ${response.status}`);
106
205
  }
107
- const products = yield response.json();
108
- if (variantIds.length > 0) {
109
- return variantIds
110
- .map((variantId) => {
111
- var _a;
206
+ const products = (yield response.json());
207
+ // One card per item key, in chunkItemKeys order. Variant keys get
208
+ // product narrowed to that variant; product keys get product as-is.
209
+ const keyVariantId = (k) => { var _a; return (_a = k.split("/").pop()) !== null && _a !== void 0 ? _a : k; };
210
+ return chunkItemKeys
211
+ .map((key) => {
212
+ var _a;
213
+ const isVariantKey = key.includes("ProductVariant");
214
+ if (isVariantKey) {
215
+ const keyId = keyVariantId(key);
112
216
  const product = products.find((p) => {
113
217
  var _a;
114
- return (_a = p.variants) === null || _a === void 0 ? void 0 : _a.some((v) => extractVariantId(v.id) === variantId);
218
+ return (_a = p.variants) === null || _a === void 0 ? void 0 : _a.some((v) => v.id === key || keyVariantId(v.id) === keyId);
115
219
  });
116
220
  if (!product)
117
221
  return null;
118
- const matchingVariant = (_a = product.variants) === null || _a === void 0 ? void 0 : _a.find((v) => extractVariantId(v.id) === variantId);
222
+ const matchingVariant = (_a = product.variants) === null || _a === void 0 ? void 0 : _a.find((v) => v.id === key || keyVariantId(v.id) === keyId);
119
223
  if (!matchingVariant)
120
224
  return null;
121
225
  return Object.assign(Object.assign({}, product), { variants: [matchingVariant] });
122
- })
123
- .filter((p) => p !== null);
124
- }
125
- return products;
226
+ }
227
+ const product = products.find((p) => p.id === key);
228
+ return product !== null && product !== void 0 ? product : null;
229
+ })
230
+ .filter((p) => p !== null);
126
231
  }
127
232
  catch (err) {
128
233
  handleFetchError(err, "fetch products");
129
234
  return [];
130
235
  }
131
- }), [apiUrl, shouldMock, fetchMockedProducts, variantIds, queryVariables]);
236
+ }), [apiUrl, shouldMock, fetchMockedProducts, queryVariables]);
237
+ const itemKeyToProductGid = useMemo(() => new Map(initialItems.map((item) => [
238
+ toItemKey(item),
239
+ getProductGidsFromIds([item.productId])[0],
240
+ ])), [initialItems]);
132
241
  const loadMore = useCallback(() => __awaiter(this, void 0, void 0, function* () {
133
242
  if (loadingRef.current ||
134
- (!shouldMock && !(allProductIds.length - loadedIndex > 0))) {
243
+ (!shouldMock && !(allItemKeys.length - loadedIndex > 0))) {
135
244
  return;
136
245
  }
246
+ const capturedAllItemKeys = allItemKeysRef.current;
137
247
  loadingRef.current = true;
138
248
  setIsLoading(true);
139
249
  try {
@@ -145,13 +255,25 @@ export function useInfiniteWishlist({ initialProductIds, chunkSize, apiUrl, shou
145
255
  }
146
256
  }
147
257
  else {
148
- const remainingItems = allProductIds.length - loadedIndex;
258
+ const remainingItems = allItemKeys.length - loadedIndex;
149
259
  const loadCount = Math.min(remainingItems, chunkSize);
150
- const nextChunkIds = allProductIds.slice(loadedIndex, loadedIndex + loadCount);
151
- if (nextChunkIds.length === 0)
260
+ const chunkItemKeys = allItemKeys.slice(loadedIndex, loadedIndex + loadCount);
261
+ if (chunkItemKeys.length === 0)
152
262
  return;
153
- const newProducts = yield fetchProducts(nextChunkIds);
154
- setProducts((prev) => [...prev, ...newProducts]);
263
+ const chunkProductGids = Array.from(new Set(chunkItemKeys.map((k) => itemKeyToProductGid.get(k))));
264
+ const newProducts = yield fetchProducts(chunkItemKeys, chunkProductGids);
265
+ if (allItemKeysRef.current !== capturedAllItemKeys)
266
+ return;
267
+ setProducts((prev) => {
268
+ const merged = new Map(prev.map((p) => [getProductOrVariantKey(p), p]));
269
+ for (let i = 0; i < newProducts.length; i++) {
270
+ merged.set(chunkItemKeys[i], newProducts[i]);
271
+ }
272
+ return capturedAllItemKeys.flatMap((key) => {
273
+ const p = merged.get(key);
274
+ return p ? [p] : [];
275
+ });
276
+ });
155
277
  setLoadedIndex((prev) => prev + loadCount);
156
278
  }
157
279
  }
@@ -164,11 +286,12 @@ export function useInfiniteWishlist({ initialProductIds, chunkSize, apiUrl, shou
164
286
  }
165
287
  }), [
166
288
  shouldMock,
167
- allProductIds,
289
+ allItemKeys,
168
290
  loadedIndex,
169
291
  chunkSize,
170
292
  fetchProducts,
171
293
  fetchMockedProducts,
294
+ itemKeyToProductGid,
172
295
  ]);
173
296
  useEffect(() => {
174
297
  if (shouldMock && collectionToFetchID && products.length === 0) {
@@ -182,16 +305,14 @@ export function useInfiniteWishlist({ initialProductIds, chunkSize, apiUrl, shou
182
305
  useEffect(() => {
183
306
  if (inView &&
184
307
  !loadingRef.current &&
185
- (shouldMock
186
- ? products.length < 2
187
- : allProductIds.length - loadedIndex > 0)) {
308
+ (shouldMock ? products.length < 2 : allItemKeys.length - loadedIndex > 0)) {
188
309
  loadMore();
189
310
  }
190
311
  }, [
191
312
  inView,
192
313
  shouldMock,
193
314
  products.length,
194
- allProductIds.length,
315
+ allItemKeys.length,
195
316
  loadedIndex,
196
317
  loadMore,
197
318
  ]);
@@ -202,7 +323,7 @@ export function useInfiniteWishlist({ initialProductIds, chunkSize, apiUrl, shou
202
323
  error,
203
324
  hasMore: shouldMock
204
325
  ? products.length < 2
205
- : allProductIds.length - loadedIndex > 0,
326
+ : allItemKeys.length - loadedIndex > 0,
206
327
  loadMore,
207
328
  loadMoreRef,
208
329
  reset: () => {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=use-infinite-wishlist.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-infinite-wishlist.test.d.ts","sourceRoot":"","sources":["../../../components/hooks/use-infinite-wishlist.test.ts"],"names":[],"mappings":""}