@nuskin/react-search-sdk 1.0.0 → 1.0.1-code-cleanup.1

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.
package/dist/index.mjs CHANGED
@@ -120,21 +120,41 @@ async function fetchSearchPanelCopy(_options = {}) {
120
120
 
121
121
  // src/copy/loadSearchPanelCopy.ts
122
122
  var copyByLanguage = /* @__PURE__ */ new Map();
123
+ var loadedByLanguage = /* @__PURE__ */ new Map();
124
+ function cacheKey(language) {
125
+ return (language ?? "").toLowerCase();
126
+ }
123
127
  function loadSearchPanelCopy(language) {
124
- const key = (language ?? "").toLowerCase();
128
+ const key = cacheKey(language);
125
129
  const cached = copyByLanguage.get(key);
126
130
  if (cached) return cached;
127
131
  const pending = fetchSearchPanelCopy(language ? { } : {});
128
132
  copyByLanguage.set(key, pending);
129
- pending.catch(() => copyByLanguage.delete(key));
133
+ pending.then(
134
+ (copy) => {
135
+ if (copyByLanguage.get(key) === pending) {
136
+ loadedByLanguage.set(key, copy);
137
+ }
138
+ },
139
+ () => {
140
+ if (copyByLanguage.get(key) === pending) {
141
+ copyByLanguage.delete(key);
142
+ }
143
+ }
144
+ );
130
145
  return pending;
131
146
  }
147
+ function peekSearchPanelCopy(language) {
148
+ return loadedByLanguage.get(cacheKey(language));
149
+ }
132
150
 
133
151
  // src/hooks/useSearchPanelCopy.ts
134
152
  function useSearchPanelCopy({
135
153
  language
136
154
  }) {
137
- const [copy, setCopy] = useState(void 0);
155
+ const [copy, setCopy] = useState(
156
+ () => peekSearchPanelCopy(language)
157
+ );
138
158
  useEffect(() => {
139
159
  let isStale = false;
140
160
  loadSearchPanelCopy(language).then((next) => {
@@ -152,21 +172,7 @@ function useSearchPanelCopy({
152
172
  return copy;
153
173
  }
154
174
 
155
- // src/services/SearchApiService.ts
156
- function canUseStorage(storage) {
157
- return Boolean(storage) && typeof storage.getItem === "function";
158
- }
159
- function getSessionStorage(providedStorage) {
160
- if (providedStorage) return providedStorage;
161
- if (typeof window !== "undefined") {
162
- try {
163
- return window.sessionStorage;
164
- } catch {
165
- return null;
166
- }
167
- }
168
- return null;
169
- }
175
+ // src/services/localeParts.ts
170
176
  function normalizeLocaleParts({
171
177
  country,
172
178
  language,
@@ -181,6 +187,102 @@ function normalizeLocaleParts({
181
187
  locale: normalizedLocale
182
188
  };
183
189
  }
190
+ function toCatalogLocale(localeParts) {
191
+ const language = String(localeParts.language ?? "en").toLowerCase();
192
+ const country = String(localeParts.country ?? "US").toUpperCase();
193
+ return `${language}_${country}`;
194
+ }
195
+
196
+ // src/services/catalogRequest.ts
197
+ function resolveApiGatewayPath(apiGatewayPath, apiName = STOREFRONT_SHOPPING_CONTEXT) {
198
+ const apiGateway = String(apiGatewayPath ?? "").trim();
199
+ if (!apiGateway) return "";
200
+ return apiGateway.replace(/\{api_name\}/gi, apiName).replace(/\/$/, "");
201
+ }
202
+ function resolveCatalogApiBaseUrl(marketConfig) {
203
+ return resolveApiGatewayPath(marketConfig?.api_gateway_path);
204
+ }
205
+ function resolveCatalogStoreId(marketConfig) {
206
+ return String(marketConfig?.store_id ?? "").trim();
207
+ }
208
+ function buildCatalogSuggestionsUrl(apiBaseUrl) {
209
+ const normalizedBase = String(apiBaseUrl ?? "").trim().replace(/\/$/, "");
210
+ if (!normalizedBase) return "";
211
+ const suggestionsPath = normalizedBase.endsWith(
212
+ `/${STOREFRONT_SHOPPING_CONTEXT}`
213
+ ) ? "/catalogs/search/suggestions" : CATALOG_SUGGESTIONS_PATH;
214
+ return `${normalizedBase}${suggestionsPath}`;
215
+ }
216
+ function buildSuggestionsRequestUrl({
217
+ suggestionsUrl,
218
+ searchTerm,
219
+ storeId,
220
+ localeParts
221
+ }) {
222
+ const queryParams = new URLSearchParams({
223
+ searchTerm,
224
+ page: String(DEFAULT_SUGGESTION_PAGE),
225
+ size: String(DEFAULT_SUGGESTION_SIZE),
226
+ locale: toCatalogLocale(localeParts),
227
+ storeId
228
+ });
229
+ return `${suggestionsUrl}?${queryParams.toString()}`;
230
+ }
231
+ async function resolveCatalogRequestConfig({
232
+ locationLike,
233
+ country,
234
+ getMarketConfig
235
+ }) {
236
+ const marketConfig = getMarketConfig ? await getMarketConfig({ locationLike, country }) ?? null : null;
237
+ return {
238
+ suggestionsUrl: buildCatalogSuggestionsUrl(
239
+ resolveCatalogApiBaseUrl(marketConfig)
240
+ ),
241
+ storeId: resolveCatalogStoreId(marketConfig)
242
+ };
243
+ }
244
+ function resolveCustomerTypeHeaders(getCustomerType) {
245
+ const customerType = getCustomerType?.()?.trim();
246
+ return customerType ? { "X-Customer-Type": customerType } : void 0;
247
+ }
248
+ var NO_RESULTS_ERROR_CODE = "SFOSER002";
249
+ async function isNoResultsResponse(response) {
250
+ if (response.status !== 404) return false;
251
+ try {
252
+ const body = await response.json();
253
+ return body?.code === NO_RESULTS_ERROR_CODE;
254
+ } catch {
255
+ return false;
256
+ }
257
+ }
258
+ async function fetchJson(fetchImpl, requestUrl, requestHeaders = {}) {
259
+ const response = await fetchImpl(requestUrl, {
260
+ method: "GET",
261
+ credentials: "include",
262
+ headers: { Accept: "application/json", ...requestHeaders }
263
+ });
264
+ if (!response.ok) {
265
+ if (await isNoResultsResponse(response)) return null;
266
+ throw new Error(`Request failed with status ${response.status}`);
267
+ }
268
+ return response.json();
269
+ }
270
+
271
+ // src/services/recentSearches.ts
272
+ function canUseStorage(storage) {
273
+ return Boolean(storage) && typeof storage.getItem === "function";
274
+ }
275
+ function getSessionStorage(providedStorage) {
276
+ if (providedStorage) return providedStorage;
277
+ if (typeof window !== "undefined") {
278
+ try {
279
+ return window.sessionStorage;
280
+ } catch {
281
+ return null;
282
+ }
283
+ }
284
+ return null;
285
+ }
184
286
  function getRecentHeaderSearchesStorageKey(localeParts) {
185
287
  const { country, language, locale } = normalizeLocaleParts(localeParts);
186
288
  return `${RECENT_SEARCHES_PREFIX}:${locale}:${country}:${language}`;
@@ -236,63 +338,8 @@ function clearRecentHeaderSearches(localeParts, storage) {
236
338
  } catch {
237
339
  }
238
340
  }
239
- function getBrowserLocation(providedLocation) {
240
- if (providedLocation) return providedLocation;
241
- if (typeof window !== "undefined" && window.location)
242
- return window.location;
243
- return null;
244
- }
245
- function resolveApiGatewayPath(apiGatewayPath, apiName = STOREFRONT_SHOPPING_CONTEXT) {
246
- const apiGateway = String(apiGatewayPath ?? "").trim();
247
- if (!apiGateway) return "";
248
- return apiGateway.replace(/\{api_name\}/gi, apiName).replace(/\/$/, "");
249
- }
250
- function resolveCatalogApiBaseUrl(marketConfig) {
251
- return resolveApiGatewayPath(marketConfig?.api_gateway_path);
252
- }
253
- function resolveCatalogStoreId(marketConfig) {
254
- return String(marketConfig?.store_id ?? "").trim();
255
- }
256
- function buildCatalogSuggestionsUrl(apiBaseUrl) {
257
- const normalizedBase = String(apiBaseUrl ?? "").trim().replace(/\/$/, "");
258
- if (!normalizedBase) return "";
259
- const suggestionsPath = normalizedBase.endsWith(
260
- `/${STOREFRONT_SHOPPING_CONTEXT}`
261
- ) ? "/catalogs/search/suggestions" : CATALOG_SUGGESTIONS_PATH;
262
- return `${normalizedBase}${suggestionsPath}`;
263
- }
264
- function resolveSuggestionsUrl(suggestionsApi, locationLike) {
265
- const pathOrUrl = String(suggestionsApi ?? "").trim();
266
- if (!pathOrUrl) return "";
267
- if (pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")) {
268
- return pathOrUrl;
269
- }
270
- const normalizedPath = pathOrUrl.startsWith("/") ? pathOrUrl : `/${pathOrUrl}`;
271
- const origin = String(locationLike?.origin ?? "").trim();
272
- return origin ? `${origin}${normalizedPath}` : normalizedPath;
273
- }
274
- async function resolveCatalogRequestConfig({
275
- suggestionsApi,
276
- storeIdOption,
277
- locationLike,
278
- country,
279
- getMarketConfig
280
- }) {
281
- const explicitStoreId = storeIdOption !== void 0 && storeIdOption !== null && String(storeIdOption).trim() !== "" ? String(storeIdOption).trim() : "";
282
- const marketConfig = getMarketConfig ? await getMarketConfig({ locationLike, country }) ?? null : null;
283
- const configuredApiBase = resolveCatalogApiBaseUrl(marketConfig);
284
- const configuredStoreId = resolveCatalogStoreId(marketConfig);
285
- const explicitSuggestionsApi = String(suggestionsApi ?? "").trim();
286
- return {
287
- suggestionsUrl: explicitSuggestionsApi ? resolveSuggestionsUrl(explicitSuggestionsApi, locationLike) : buildCatalogSuggestionsUrl(configuredApiBase),
288
- storeId: explicitStoreId || configuredStoreId
289
- };
290
- }
291
- function toCatalogLocale(localeParts) {
292
- const language = String(localeParts.language ?? "en").toLowerCase();
293
- const country = String(localeParts.country ?? "US").toUpperCase();
294
- return `${language}_${country}`;
295
- }
341
+
342
+ // src/services/searchPaths.ts
296
343
  function buildSearchResultsPath(query, localeParts) {
297
344
  const trimmedQuery = String(query ?? "").trim();
298
345
  const hyphenatedQuery = trimmedQuery.replace(/\s+/g, "-");
@@ -362,50 +409,52 @@ function resolveOptionHref(option, localeParts) {
362
409
  }
363
410
  return "";
364
411
  }
365
- function resolveCustomerTypeHeaders(getCustomerType) {
366
- const customerType = getCustomerType?.()?.trim();
367
- return customerType ? { "X-Customer-Type": customerType } : void 0;
412
+
413
+ // src/services/suggestionsPayload.ts
414
+ function emptySuggestions() {
415
+ return { suggestions: [], didYouMean: [], totalResultsCount: 0 };
368
416
  }
369
- var NO_RESULTS_ERROR_CODE = "SFOSER002";
370
- async function isNoResultsResponse(response) {
371
- if (response.status !== 404) return false;
372
- try {
373
- const body = await response.json();
374
- return body?.code === NO_RESULTS_ERROR_CODE;
375
- } catch {
376
- return false;
417
+ function resolveTotalResultsCount(payload, fallback) {
418
+ if (typeof payload?.totalResultsCount === "number") {
419
+ return payload.totalResultsCount;
377
420
  }
421
+ const pageableTotal = Number(
422
+ payload?.pageableInfo?.totalCount ?? Number.NaN
423
+ );
424
+ return Number.isNaN(pageableTotal) ? fallback : pageableTotal;
378
425
  }
379
- async function fetchJsonFromCandidates(fetchImpl, requestUrls, requestHeaders = {}) {
380
- let lastError;
381
- for (const requestUrl of requestUrls) {
382
- try {
383
- const response = await fetchImpl(requestUrl, {
384
- method: "GET",
385
- credentials: "include",
386
- headers: { Accept: "application/json", ...requestHeaders }
387
- });
388
- if (!response.ok) {
389
- if (await isNoResultsResponse(response)) return null;
390
- lastError = new Error(
391
- `Request failed with status ${response.status}`
392
- );
393
- continue;
394
- }
395
- return await response.json();
396
- } catch (error) {
397
- lastError = error;
398
- }
399
- }
400
- throw lastError ?? new Error("Unable to fetch header search data");
426
+ function normalizeSuggestionsPayload(payload) {
427
+ const suggestions = Array.isArray(payload?.suggestions) ? payload.suggestions : [];
428
+ const products = Array.isArray(payload?.product) ? payload.product : null;
429
+ const rawDidYouMean = payload?.didYouMean ?? payload?.did_you_mean;
430
+ const didYouMean = Array.isArray(rawDidYouMean) ? rawDidYouMean : [];
431
+ return {
432
+ ...payload ?? {},
433
+ suggestions,
434
+ didYouMean,
435
+ totalResultsCount: resolveTotalResultsCount(
436
+ payload,
437
+ products?.length ?? suggestions.length
438
+ )
439
+ };
440
+ }
441
+
442
+ // src/services/SearchApiService.ts
443
+ function getBrowserLocation(providedLocation) {
444
+ if (providedLocation) return providedLocation;
445
+ if (typeof window !== "undefined" && window.location)
446
+ return window.location;
447
+ return null;
448
+ }
449
+ function resolveFetch(fetchImplOption) {
450
+ if (fetchImplOption !== void 0) return fetchImplOption;
451
+ return typeof fetch === "function" ? fetch.bind(globalThis) : null;
401
452
  }
402
453
  function createSearchService(options = {}) {
403
454
  const {
404
455
  fetchImpl: fetchImplOption,
405
456
  location: providedLocation,
406
457
  storage: providedStorage,
407
- suggestionsApi,
408
- storeId: storeIdOption,
409
458
  country,
410
459
  language,
411
460
  locale,
@@ -413,7 +462,7 @@ function createSearchService(options = {}) {
413
462
  getCustomerType,
414
463
  minimumSearchLength = MINIMUM_SEARCH_CHAR_LENGTH
415
464
  } = options;
416
- const fetchImpl = fetchImplOption !== void 0 ? fetchImplOption : typeof fetch === "function" ? fetch.bind(globalThis) : null;
465
+ const fetchImpl = resolveFetch(fetchImplOption);
417
466
  const locationLike = getBrowserLocation(providedLocation);
418
467
  const storage = getSessionStorage(providedStorage);
419
468
  const localeParts = normalizeLocaleParts({ country, language, locale });
@@ -421,8 +470,6 @@ function createSearchService(options = {}) {
421
470
  function loadCatalogRequestConfig() {
422
471
  if (!catalogRequestConfigPromise) {
423
472
  catalogRequestConfigPromise = resolveCatalogRequestConfig({
424
- suggestionsApi,
425
- storeIdOption,
426
473
  locationLike,
427
474
  country: localeParts.country,
428
475
  getMarketConfig
@@ -441,47 +488,21 @@ function createSearchService(options = {}) {
441
488
  async fetchSuggestions(searchTerm) {
442
489
  const trimmedSearchTerm = String(searchTerm ?? "").trim();
443
490
  if (!trimmedSearchTerm || trimmedSearchTerm.length < minimumSearchLength || !fetchImpl) {
444
- return {
445
- suggestions: [],
446
- didYouMean: [],
447
- totalResultsCount: 0
448
- };
491
+ return emptySuggestions();
449
492
  }
450
493
  const { suggestionsUrl, storeId } = await loadCatalogRequestConfig();
451
- if (!suggestionsUrl || !storeId) {
452
- return {
453
- suggestions: [],
454
- didYouMean: [],
455
- totalResultsCount: 0
456
- };
457
- }
458
- const queryParams = new URLSearchParams({
459
- searchTerm: trimmedSearchTerm,
460
- page: String(DEFAULT_SUGGESTION_PAGE),
461
- size: String(DEFAULT_SUGGESTION_SIZE),
462
- locale: toCatalogLocale(localeParts),
463
- storeId: String(storeId)
464
- });
465
- const requestUrls = [`${suggestionsUrl}?${queryParams.toString()}`];
466
- const responsePayload = await fetchJsonFromCandidates(
494
+ if (!suggestionsUrl || !storeId) return emptySuggestions();
495
+ const responsePayload = await fetchJson(
467
496
  fetchImpl,
468
- requestUrls,
497
+ buildSuggestionsRequestUrl({
498
+ suggestionsUrl,
499
+ searchTerm: trimmedSearchTerm,
500
+ storeId,
501
+ localeParts
502
+ }),
469
503
  resolveCustomerTypeHeaders(getCustomerType)
470
504
  );
471
- const suggestionsField = Array.isArray(responsePayload?.suggestions) ? responsePayload?.suggestions : [];
472
- const productField = Array.isArray(responsePayload?.product) ? responsePayload?.product : null;
473
- const rawDidYouMean = responsePayload?.didYouMean ?? responsePayload?.did_you_mean;
474
- const didYouMeanField = Array.isArray(rawDidYouMean) ? rawDidYouMean : [];
475
- const pageableTotal = Number(
476
- responsePayload?.pageableInfo?.totalCount ?? Number.NaN
477
- );
478
- const totalResultsCount = typeof responsePayload?.totalResultsCount === "number" ? responsePayload.totalResultsCount : !Number.isNaN(pageableTotal) ? pageableTotal : productField?.length ?? suggestionsField.length;
479
- return {
480
- ...responsePayload ?? {},
481
- suggestions: suggestionsField,
482
- didYouMean: didYouMeanField,
483
- totalResultsCount
484
- };
505
+ return normalizeSuggestionsPayload(responsePayload);
485
506
  },
486
507
  submitProductSearch(query, option) {
487
508
  const trimmedQuery = String(query ?? "").trim();
@@ -566,7 +587,90 @@ function getProductEnvironment() {
566
587
  }
567
588
  }
568
589
 
569
- // src/utils/catalogHelpers.ts
590
+ // src/utils/nuiSearchGate.ts
591
+ var NUI_SEARCH_FLAG = "enableNuiSearch";
592
+ function parseFlagValue(raw) {
593
+ const value = raw?.trim().toLowerCase();
594
+ if (value === "true" || value === "1") return true;
595
+ if (value === "false" || value === "0") return false;
596
+ return void 0;
597
+ }
598
+ function readCookieFlag(cookie) {
599
+ for (const part of cookie.split(";")) {
600
+ const separator = part.indexOf("=");
601
+ if (separator < 0) continue;
602
+ if (part.slice(0, separator).trim() !== NUI_SEARCH_FLAG) continue;
603
+ try {
604
+ return parseFlagValue(
605
+ decodeURIComponent(part.slice(separator + 1))
606
+ );
607
+ } catch {
608
+ return void 0;
609
+ }
610
+ }
611
+ return void 0;
612
+ }
613
+ function resolveNuiSearchFlag(env = {}) {
614
+ const inBrowser = typeof window !== "undefined" && typeof document !== "undefined";
615
+ const search = env.search ?? (inBrowser ? window.location.search : "");
616
+ const cookie = env.cookie ?? (inBrowser ? document.cookie : "");
617
+ return parseFlagValue(new URLSearchParams(search).get(NUI_SEARCH_FLAG)) ?? readCookieFlag(cookie);
618
+ }
619
+ function isNuiSearchEnabled({
620
+ isAuthenticated,
621
+ flag,
622
+ allowGuests = false
623
+ }) {
624
+ return flag === true && (isAuthenticated === true || allowGuests);
625
+ }
626
+
627
+ // src/hooks/useNuiAgentGate.ts
628
+ function useNuiAgentGate({
629
+ hasAgent,
630
+ isOpen,
631
+ isAuthenticated,
632
+ allowGuestAgent
633
+ }) {
634
+ const [flag, setFlag] = useState(void 0);
635
+ useEffect(() => {
636
+ if (isOpen) setFlag(resolveNuiSearchFlag());
637
+ }, [isOpen]);
638
+ return hasAgent && isNuiSearchEnabled({
639
+ isAuthenticated,
640
+ flag,
641
+ allowGuests: allowGuestAgent
642
+ });
643
+ }
644
+ function uniqueRecentSearches(queries) {
645
+ const seen = /* @__PURE__ */ new Set();
646
+ return queries.filter((query) => {
647
+ const key = query.trim().toLowerCase();
648
+ if (!key || seen.has(key)) return false;
649
+ seen.add(key);
650
+ return true;
651
+ });
652
+ }
653
+ function useRecentSearches({
654
+ service,
655
+ isOpen,
656
+ refreshKey
657
+ }) {
658
+ const [recentSearches, setRecentSearches] = useState([]);
659
+ useEffect(() => {
660
+ if (isOpen) {
661
+ setRecentSearches(
662
+ uniqueRecentSearches(service.getRecentSearches())
663
+ );
664
+ }
665
+ }, [isOpen, service, refreshKey]);
666
+ const clearRecentSearches = useCallback(() => {
667
+ service.clearRecentSearches();
668
+ setRecentSearches([]);
669
+ }, [service]);
670
+ return { recentSearches, clearRecentSearches };
671
+ }
672
+
673
+ // src/utils/catalogStatus.ts
570
674
  var CART_DISABLED_PRODUCT_STATUSES = /* @__PURE__ */ new Set([
571
675
  "discontinued",
572
676
  "preview",
@@ -614,6 +718,8 @@ function isSuggestionDisabled(suggestionOption) {
614
718
  !suggestionOption || suggestionOption["out_of_stock"] || suggestionOption["outOfStock"]
615
719
  );
616
720
  }
721
+
722
+ // src/utils/catalogHelpers.ts
617
723
  function getImageFromProperty(properties = {}) {
618
724
  let image = String(properties["primaryimage"] ?? "").trim() || String(properties["imageURL"] ?? "").trim();
619
725
  if (!image) {
@@ -851,6 +957,46 @@ function normalizeSuggestionsResponse(response) {
851
957
  didYouMean: normalizeDidYouMean(r.didYouMean)
852
958
  };
853
959
  }
960
+ function resolveTotalResultsCount2(response, fallback) {
961
+ if (typeof response.totalResultsCount === "number") {
962
+ return response.totalResultsCount;
963
+ }
964
+ if (typeof response.pageableInfo?.totalCount === "number") {
965
+ return response.pageableInfo.totalCount;
966
+ }
967
+ return fallback;
968
+ }
969
+ var FETCH_FAILED = {
970
+ suggestions: [],
971
+ didYouMean: [],
972
+ hasMoreResults: false,
973
+ emptyFetched: true,
974
+ failed: true
975
+ };
976
+ function toSettledResults(response) {
977
+ const { suggestions: allSuggestions, didYouMean } = normalizeSuggestionsResponse(response);
978
+ if (didYouMean.length > 0) {
979
+ return {
980
+ suggestions: [],
981
+ didYouMean,
982
+ hasMoreResults: false,
983
+ emptyFetched: false,
984
+ failed: false
985
+ };
986
+ }
987
+ const total = Math.max(
988
+ resolveTotalResultsCount2(response, allSuggestions.length),
989
+ allSuggestions.length
990
+ );
991
+ const suggestions = allSuggestions.slice(0, MAX_VISIBLE_SUGGESTIONS);
992
+ return {
993
+ suggestions,
994
+ didYouMean,
995
+ hasMoreResults: total > MAX_VISIBLE_SUGGESTIONS,
996
+ emptyFetched: suggestions.length === 0,
997
+ failed: false
998
+ };
999
+ }
854
1000
  function useSearchSuggestions({
855
1001
  service,
856
1002
  enabled = true
@@ -862,7 +1008,6 @@ function useSearchSuggestions({
862
1008
  const [emptyFetched, setEmptyFetched] = useState(false);
863
1009
  const [suggestionFetchFailed, setSuggestionFetchFailed] = useState(false);
864
1010
  const [hasMoreResults, setHasMoreResults] = useState(false);
865
- const [totalResultsCount, setTotalResultsCount] = useState(0);
866
1011
  const [settledQuery, setSettledQuery] = useState("");
867
1012
  const fetchDebounced = useRef(null);
868
1013
  const latestRequestId = useRef(0);
@@ -872,7 +1017,6 @@ function useSearchSuggestions({
872
1017
  setEmptyFetched(false);
873
1018
  setSuggestionFetchFailed(false);
874
1019
  setHasMoreResults(false);
875
- setTotalResultsCount(0);
876
1020
  setSettledQuery("");
877
1021
  setLoading(false);
878
1022
  }, []);
@@ -890,6 +1034,14 @@ function useSearchSuggestions({
890
1034
  resetSuggestionsState();
891
1035
  setQuery("");
892
1036
  }, [resetSuggestionsState]);
1037
+ const settle = useCallback((results, term) => {
1038
+ setDidYouMean(results.didYouMean);
1039
+ setSuggestions(results.suggestions);
1040
+ setHasMoreResults(results.hasMoreResults);
1041
+ setSuggestionFetchFailed(results.failed);
1042
+ setSettledQuery(term);
1043
+ setEmptyFetched(results.emptyFetched);
1044
+ }, []);
893
1045
  const runFetch = useCallback(
894
1046
  async (nextQuery) => {
895
1047
  const trimmedQuery = nextQuery.trim();
@@ -901,51 +1053,20 @@ function useSearchSuggestions({
901
1053
  setLoading(true);
902
1054
  setEmptyFetched(false);
903
1055
  setSuggestionFetchFailed(false);
1056
+ let results;
904
1057
  try {
905
- const fetchedSuggestions = await service.fetchSuggestions(trimmedQuery);
906
- if (requestId !== latestRequestId.current) {
907
- return;
908
- }
909
- const {
910
- suggestions: allSuggestions,
911
- didYouMean: suggestedTerms
912
- } = normalizeSuggestionsResponse(fetchedSuggestions);
913
- const visibleSuggestions = allSuggestions.slice(
914
- 0,
915
- MAX_VISIBLE_SUGGESTIONS
1058
+ results = toSettledResults(
1059
+ await service.fetchSuggestions(trimmedQuery)
916
1060
  );
917
- const hasDidYouMean = suggestedTerms.length > 0;
918
- const normalizedTotal = typeof fetchedSuggestions.totalResultsCount === "number" ? fetchedSuggestions.totalResultsCount : typeof fetchedSuggestions.pageableInfo?.totalCount === "number" ? fetchedSuggestions.pageableInfo.totalCount : allSuggestions.length;
919
- const effectiveTotal = hasDidYouMean ? suggestedTerms.length : Math.max(normalizedTotal, allSuggestions.length);
920
- setDidYouMean(suggestedTerms);
921
- setSuggestions(hasDidYouMean ? [] : visibleSuggestions);
922
- setHasMoreResults(
923
- !hasDidYouMean && effectiveTotal > MAX_VISIBLE_SUGGESTIONS
924
- );
925
- setTotalResultsCount(effectiveTotal);
926
- setSuggestionFetchFailed(false);
927
- setSettledQuery(trimmedQuery);
928
- setEmptyFetched(
929
- hasDidYouMean ? false : visibleSuggestions.length === 0 && suggestedTerms.length === 0
930
- );
931
- } catch {
932
- if (requestId !== latestRequestId.current) {
933
- return;
934
- }
935
- setSuggestions([]);
936
- setDidYouMean([]);
937
- setEmptyFetched(true);
938
- setSuggestionFetchFailed(true);
939
- setHasMoreResults(false);
940
- setTotalResultsCount(0);
941
- setSettledQuery(trimmedQuery);
942
- } finally {
943
- if (requestId === latestRequestId.current) {
944
- setLoading(false);
945
- }
1061
+ } catch (error) {
1062
+ console.warn("[search] Could not load suggestions:", error);
1063
+ results = FETCH_FAILED;
946
1064
  }
1065
+ if (requestId !== latestRequestId.current) return;
1066
+ settle(results, trimmedQuery);
1067
+ setLoading(false);
947
1068
  },
948
- [enabled, service, resetSuggestionsState]
1069
+ [enabled, service, resetSuggestionsState, settle]
949
1070
  );
950
1071
  useEffect(() => {
951
1072
  if (!enabled) {
@@ -969,15 +1090,12 @@ function useSearchSuggestions({
969
1090
  clearQuery,
970
1091
  emptyFetched,
971
1092
  hasMoreResults,
972
- hasQuery: Boolean(query.trim()),
973
1093
  didYouMean,
974
1094
  loading,
975
1095
  query,
976
1096
  settledQuery,
977
- setQuery,
978
1097
  suggestionFetchFailed,
979
1098
  suggestions,
980
- totalResultsCount,
981
1099
  updateQuery
982
1100
  };
983
1101
  }
@@ -1052,43 +1170,6 @@ function trackProductSearch(input) {
1052
1170
  });
1053
1171
  }
1054
1172
 
1055
- // src/utils/nuiSearchGate.ts
1056
- var NUI_SEARCH_FLAG = "enableNuiSearch";
1057
- function parseFlagValue(raw) {
1058
- const value = raw?.trim().toLowerCase();
1059
- if (value === "true" || value === "1") return true;
1060
- if (value === "false" || value === "0") return false;
1061
- return void 0;
1062
- }
1063
- function readCookieFlag(cookie) {
1064
- for (const part of cookie.split(";")) {
1065
- const separator = part.indexOf("=");
1066
- if (separator < 0) continue;
1067
- if (part.slice(0, separator).trim() !== NUI_SEARCH_FLAG) continue;
1068
- try {
1069
- return parseFlagValue(
1070
- decodeURIComponent(part.slice(separator + 1))
1071
- );
1072
- } catch {
1073
- return void 0;
1074
- }
1075
- }
1076
- return void 0;
1077
- }
1078
- function resolveNuiSearchFlag(env = {}) {
1079
- const inBrowser = typeof window !== "undefined" && typeof document !== "undefined";
1080
- const search = env.search ?? (inBrowser ? window.location.search : "");
1081
- const cookie = env.cookie ?? (inBrowser ? document.cookie : "");
1082
- return parseFlagValue(new URLSearchParams(search).get(NUI_SEARCH_FLAG)) ?? readCookieFlag(cookie);
1083
- }
1084
- function isNuiSearchEnabled({
1085
- isAuthenticated,
1086
- flag,
1087
- allowGuests = false
1088
- }) {
1089
- return flag === true && (isAuthenticated === true || allowGuests);
1090
- }
1091
-
1092
1173
  // src/utils/searchRouting.ts
1093
1174
  function decideSearchRoute({
1094
1175
  enabled,
@@ -1101,6 +1182,96 @@ function decideSearchRoute({
1101
1182
  return snapshot.suggestions.length > 0 ? "search" : "handoff";
1102
1183
  }
1103
1184
 
1185
+ // src/hooks/useSubmitRouting.ts
1186
+ var SUBMIT_WAIT_TIMEOUT_MS = 2e3;
1187
+ function useSubmitRouting({
1188
+ enabled,
1189
+ minimumSearchLength,
1190
+ search,
1191
+ gtmContext,
1192
+ pendingTerm,
1193
+ setPendingTerm,
1194
+ onSearch,
1195
+ onHandoff
1196
+ }) {
1197
+ const {
1198
+ didYouMean,
1199
+ loading,
1200
+ settledQuery,
1201
+ suggestionFetchFailed,
1202
+ suggestions
1203
+ } = search;
1204
+ const decide = useCallback(
1205
+ (term) => decideSearchRoute({
1206
+ enabled,
1207
+ snapshot: {
1208
+ query: term,
1209
+ loading,
1210
+ settledQuery,
1211
+ suggestions,
1212
+ suggestionFetchFailed
1213
+ }
1214
+ }),
1215
+ [enabled, loading, settledQuery, suggestions, suggestionFetchFailed]
1216
+ );
1217
+ const act = useCallback(
1218
+ (term, route) => {
1219
+ setPendingTerm(null);
1220
+ if (route === "handoff") {
1221
+ trackChatEntryPoint(gtmContext ?? {});
1222
+ onHandoff({
1223
+ query: term,
1224
+ didYouMean: didYouMean.map((item) => item.value)
1225
+ });
1226
+ return;
1227
+ }
1228
+ onSearch(term);
1229
+ },
1230
+ [didYouMean, gtmContext, onHandoff, onSearch, setPendingTerm]
1231
+ );
1232
+ const submit = useCallback(
1233
+ (value) => {
1234
+ const term = value.trim();
1235
+ if (!term) return;
1236
+ if (term.length < minimumSearchLength) {
1237
+ onSearch(term);
1238
+ return;
1239
+ }
1240
+ const route = decide(term);
1241
+ if (route === "wait") {
1242
+ setPendingTerm(term);
1243
+ return;
1244
+ }
1245
+ act(term, route);
1246
+ },
1247
+ [act, decide, minimumSearchLength, onSearch, setPendingTerm]
1248
+ );
1249
+ const latest = useRef({ act, decide });
1250
+ useEffect(() => {
1251
+ latest.current = { act, decide };
1252
+ });
1253
+ useEffect(() => {
1254
+ if (pendingTerm === null) return void 0;
1255
+ const route = latest.current.decide(pendingTerm);
1256
+ if (route !== "wait") {
1257
+ latest.current.act(pendingTerm, route);
1258
+ return void 0;
1259
+ }
1260
+ const timer = setTimeout(
1261
+ () => latest.current.act(pendingTerm, "search"),
1262
+ SUBMIT_WAIT_TIMEOUT_MS
1263
+ );
1264
+ return () => clearTimeout(timer);
1265
+ }, [
1266
+ pendingTerm,
1267
+ loading,
1268
+ settledQuery,
1269
+ suggestions,
1270
+ suggestionFetchFailed
1271
+ ]);
1272
+ return submit;
1273
+ }
1274
+
1104
1275
  // src/utils/cachedProductClient.ts
1105
1276
  function withProductCache(client) {
1106
1277
  const lookups = /* @__PURE__ */ new Map();
@@ -1168,7 +1339,8 @@ var AgentErrorBoundary = class extends Component {
1168
1339
  static getDerivedStateFromError() {
1169
1340
  return { failed: true };
1170
1341
  }
1171
- componentDidCatch(_error, _info) {
1342
+ componentDidCatch(error) {
1343
+ console.warn("[search] The agent could not be shown:", error);
1172
1344
  this.props.onError();
1173
1345
  }
1174
1346
  render() {
@@ -1181,6 +1353,10 @@ function useHandoff({
1181
1353
  agentContext,
1182
1354
  onUnavailable
1183
1355
  }) {
1356
+ const onUnavailableRef = useRef(onUnavailable);
1357
+ useEffect(() => {
1358
+ onUnavailableRef.current = onUnavailable;
1359
+ });
1184
1360
  useEffect(() => {
1185
1361
  let cancelled = false;
1186
1362
  let timer;
@@ -1196,7 +1372,10 @@ function useHandoff({
1196
1372
  return;
1197
1373
  }
1198
1374
  if (Date.now() - startedAt >= HANDLE_TIMEOUT_MS) {
1199
- onUnavailable();
1375
+ console.warn(
1376
+ `[search] The agent did not start within ${HANDLE_TIMEOUT_MS / 1e3} seconds.`
1377
+ );
1378
+ onUnavailableRef.current();
1200
1379
  return;
1201
1380
  }
1202
1381
  timer = setTimeout(tryOpen, HANDLE_POLL_MS);
@@ -1238,16 +1417,6 @@ function NuiAgentPanel({
1238
1417
  }
1239
1418
  ) });
1240
1419
  }
1241
- var SUBMIT_WAIT_TIMEOUT_MS = 2e3;
1242
- function uniqueRecentSearches(queries) {
1243
- const seen = /* @__PURE__ */ new Set();
1244
- return queries.filter((query) => {
1245
- const key = query.trim().toLowerCase();
1246
- if (!key || seen.has(key)) return false;
1247
- seen.add(key);
1248
- return true;
1249
- });
1250
- }
1251
1420
  function CommerceSearchView({
1252
1421
  copy,
1253
1422
  isOpen,
@@ -1262,11 +1431,9 @@ function CommerceSearchView({
1262
1431
  minimumSearchLength = MINIMUM_SEARCH_CHAR_LENGTH,
1263
1432
  gtmContext
1264
1433
  }) {
1265
- const [flag, setFlag] = useState(void 0);
1266
1434
  const [handoff, setHandoff] = useState(null);
1267
1435
  const agentQuery = handoff?.query ?? null;
1268
1436
  const [pendingTerm, setPendingTerm] = useState(null);
1269
- const [recentSearches, setRecentSearches] = useState([]);
1270
1437
  const search = useSearchSuggestions({
1271
1438
  service,
1272
1439
  enabled: isOpen && agentQuery === null
@@ -1278,31 +1445,25 @@ function CommerceSearchView({
1278
1445
  hasMoreResults,
1279
1446
  loading,
1280
1447
  query,
1281
- settledQuery,
1282
- suggestionFetchFailed,
1283
1448
  suggestions,
1284
1449
  updateQuery
1285
1450
  } = search;
1286
- const enabled = agent !== void 0 && isNuiSearchEnabled({
1451
+ const agentEnabled = useNuiAgentGate({
1452
+ hasAgent: agent !== void 0,
1453
+ isOpen,
1287
1454
  isAuthenticated,
1288
- flag,
1289
- allowGuests: allowGuestAgent
1455
+ allowGuestAgent
1456
+ });
1457
+ const { recentSearches, clearRecentSearches } = useRecentSearches({
1458
+ service,
1459
+ isOpen,
1460
+ refreshKey: agentQuery
1290
1461
  });
1291
1462
  const items = getVisibleSuggestions({
1292
1463
  didYouMean,
1293
1464
  suggestions
1294
1465
  });
1295
1466
  const showDidYouMean = didYouMean.length > 0;
1296
- useEffect(() => {
1297
- if (isOpen) setFlag(resolveNuiSearchFlag());
1298
- }, [isOpen]);
1299
- useEffect(() => {
1300
- if (isOpen) {
1301
- setRecentSearches(
1302
- uniqueRecentSearches(service.getRecentSearches())
1303
- );
1304
- }
1305
- }, [isOpen, service, agentQuery]);
1306
1467
  const close = useCallback(() => {
1307
1468
  setHandoff(null);
1308
1469
  setPendingTerm(null);
@@ -1341,72 +1502,16 @@ function CommerceSearchView({
1341
1502
  },
1342
1503
  [items, navigateTo, service, trackSemantic]
1343
1504
  );
1344
- const decide = useCallback(
1345
- (term) => decideSearchRoute({
1346
- enabled,
1347
- snapshot: {
1348
- query: term,
1349
- loading,
1350
- settledQuery,
1351
- suggestions,
1352
- suggestionFetchFailed
1353
- }
1354
- }),
1355
- [enabled, loading, settledQuery, suggestions, suggestionFetchFailed]
1356
- );
1357
- const act = useCallback(
1358
- (term, route) => {
1359
- setPendingTerm(null);
1360
- if (route === "handoff") {
1361
- trackChatEntryPoint(gtmContext ?? {});
1362
- setHandoff({
1363
- query: term,
1364
- didYouMean: didYouMean.map((item) => item.value)
1365
- });
1366
- return;
1367
- }
1368
- runSearch(term);
1369
- },
1370
- [didYouMean, gtmContext, runSearch]
1371
- );
1372
- const handleSubmit = useCallback(
1373
- (value) => {
1374
- const term = value.trim();
1375
- if (!term) return;
1376
- if (term.length < minimumSearchLength) {
1377
- runSearch(term);
1378
- return;
1379
- }
1380
- const route = decide(term);
1381
- if (route === "wait") {
1382
- setPendingTerm(term);
1383
- return;
1384
- }
1385
- act(term, route);
1386
- },
1387
- [act, decide, minimumSearchLength, runSearch]
1388
- );
1389
- const latest = useRef({ act, decide });
1390
- latest.current = { act, decide };
1391
- useEffect(() => {
1392
- if (pendingTerm === null) return void 0;
1393
- const route = latest.current.decide(pendingTerm);
1394
- if (route !== "wait") {
1395
- latest.current.act(pendingTerm, route);
1396
- return void 0;
1397
- }
1398
- const timer = setTimeout(
1399
- () => latest.current.act(pendingTerm, "search"),
1400
- SUBMIT_WAIT_TIMEOUT_MS
1401
- );
1402
- return () => clearTimeout(timer);
1403
- }, [
1505
+ const handleSubmit = useSubmitRouting({
1506
+ enabled: agentEnabled,
1507
+ minimumSearchLength,
1508
+ search,
1509
+ ...gtmContext ? { gtmContext } : {},
1404
1510
  pendingTerm,
1405
- loading,
1406
- settledQuery,
1407
- suggestions,
1408
- suggestionFetchFailed
1409
- ]);
1511
+ setPendingTerm,
1512
+ onSearch: runSearch,
1513
+ onHandoff: setHandoff
1514
+ });
1410
1515
  const handleSelect = useCallback(
1411
1516
  (option) => {
1412
1517
  if (isSuggestionDisabled(
@@ -1431,10 +1536,11 @@ function CommerceSearchView({
1431
1536
  )) {
1432
1537
  return;
1433
1538
  }
1434
- const result = service.activateSuggestionAction(
1435
- option
1539
+ navigateTo(
1540
+ service.activateSuggestionAction(
1541
+ option
1542
+ )
1436
1543
  );
1437
- void Promise.resolve(result).then(navigateTo).catch(() => void 0);
1438
1544
  },
1439
1545
  [navigateTo, service]
1440
1546
  );
@@ -1449,10 +1555,6 @@ function CommerceSearchView({
1449
1555
  },
1450
1556
  [navigateTo, service, trackSemantic]
1451
1557
  );
1452
- const handleClearRecent = useCallback(() => {
1453
- service.clearRecentSearches();
1454
- setRecentSearches([]);
1455
- }, [service]);
1456
1558
  const handleAgentUnavailable = useCallback(() => {
1457
1559
  if (agentQuery !== null)
1458
1560
  navigateTo(service.submitProductSearch(agentQuery));
@@ -1501,7 +1603,7 @@ function CommerceSearchView({
1501
1603
  recentSearches: {
1502
1604
  items: recentSearches,
1503
1605
  onSelect: handleRecentSelect,
1504
- onClear: handleClearRecent
1606
+ onClear: clearRecentSearches
1505
1607
  },
1506
1608
  agentPanel
1507
1609
  }
@@ -1527,8 +1629,11 @@ function CommerceSearch({
1527
1629
  language,
1528
1630
  enabled: hasOpened
1529
1631
  });
1530
- const customerTypeRef = useRef(GUEST);
1531
- customerTypeRef.current = customer.isAuthenticated ? customer.accountType ?? GUEST : GUEST;
1632
+ const customerType = customer.isAuthenticated ? customer.accountType ?? GUEST : GUEST;
1633
+ const customerTypeRef = useRef(customerType);
1634
+ useEffect(() => {
1635
+ customerTypeRef.current = customerType;
1636
+ }, [customerType]);
1532
1637
  const service = useMemo(
1533
1638
  () => createSearchService({
1534
1639
  country,