@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.js CHANGED
@@ -127,21 +127,41 @@ async function fetchSearchPanelCopy(_options = {}) {
127
127
 
128
128
  // src/copy/loadSearchPanelCopy.ts
129
129
  var copyByLanguage = /* @__PURE__ */ new Map();
130
+ var loadedByLanguage = /* @__PURE__ */ new Map();
131
+ function cacheKey(language) {
132
+ return (language ?? "").toLowerCase();
133
+ }
130
134
  function loadSearchPanelCopy(language) {
131
- const key = (language ?? "").toLowerCase();
135
+ const key = cacheKey(language);
132
136
  const cached = copyByLanguage.get(key);
133
137
  if (cached) return cached;
134
138
  const pending = fetchSearchPanelCopy(language ? { } : {});
135
139
  copyByLanguage.set(key, pending);
136
- pending.catch(() => copyByLanguage.delete(key));
140
+ pending.then(
141
+ (copy) => {
142
+ if (copyByLanguage.get(key) === pending) {
143
+ loadedByLanguage.set(key, copy);
144
+ }
145
+ },
146
+ () => {
147
+ if (copyByLanguage.get(key) === pending) {
148
+ copyByLanguage.delete(key);
149
+ }
150
+ }
151
+ );
137
152
  return pending;
138
153
  }
154
+ function peekSearchPanelCopy(language) {
155
+ return loadedByLanguage.get(cacheKey(language));
156
+ }
139
157
 
140
158
  // src/hooks/useSearchPanelCopy.ts
141
159
  function useSearchPanelCopy({
142
160
  language
143
161
  }) {
144
- const [copy, setCopy] = react.useState(void 0);
162
+ const [copy, setCopy] = react.useState(
163
+ () => peekSearchPanelCopy(language)
164
+ );
145
165
  react.useEffect(() => {
146
166
  let isStale = false;
147
167
  loadSearchPanelCopy(language).then((next) => {
@@ -159,21 +179,7 @@ function useSearchPanelCopy({
159
179
  return copy;
160
180
  }
161
181
 
162
- // src/services/SearchApiService.ts
163
- function canUseStorage(storage) {
164
- return Boolean(storage) && typeof storage.getItem === "function";
165
- }
166
- function getSessionStorage(providedStorage) {
167
- if (providedStorage) return providedStorage;
168
- if (typeof window !== "undefined") {
169
- try {
170
- return window.sessionStorage;
171
- } catch {
172
- return null;
173
- }
174
- }
175
- return null;
176
- }
182
+ // src/services/localeParts.ts
177
183
  function normalizeLocaleParts({
178
184
  country,
179
185
  language,
@@ -188,6 +194,102 @@ function normalizeLocaleParts({
188
194
  locale: normalizedLocale
189
195
  };
190
196
  }
197
+ function toCatalogLocale(localeParts) {
198
+ const language = String(localeParts.language ?? "en").toLowerCase();
199
+ const country = String(localeParts.country ?? "US").toUpperCase();
200
+ return `${language}_${country}`;
201
+ }
202
+
203
+ // src/services/catalogRequest.ts
204
+ function resolveApiGatewayPath(apiGatewayPath, apiName = STOREFRONT_SHOPPING_CONTEXT) {
205
+ const apiGateway = String(apiGatewayPath ?? "").trim();
206
+ if (!apiGateway) return "";
207
+ return apiGateway.replace(/\{api_name\}/gi, apiName).replace(/\/$/, "");
208
+ }
209
+ function resolveCatalogApiBaseUrl(marketConfig) {
210
+ return resolveApiGatewayPath(marketConfig?.api_gateway_path);
211
+ }
212
+ function resolveCatalogStoreId(marketConfig) {
213
+ return String(marketConfig?.store_id ?? "").trim();
214
+ }
215
+ function buildCatalogSuggestionsUrl(apiBaseUrl) {
216
+ const normalizedBase = String(apiBaseUrl ?? "").trim().replace(/\/$/, "");
217
+ if (!normalizedBase) return "";
218
+ const suggestionsPath = normalizedBase.endsWith(
219
+ `/${STOREFRONT_SHOPPING_CONTEXT}`
220
+ ) ? "/catalogs/search/suggestions" : CATALOG_SUGGESTIONS_PATH;
221
+ return `${normalizedBase}${suggestionsPath}`;
222
+ }
223
+ function buildSuggestionsRequestUrl({
224
+ suggestionsUrl,
225
+ searchTerm,
226
+ storeId,
227
+ localeParts
228
+ }) {
229
+ const queryParams = new URLSearchParams({
230
+ searchTerm,
231
+ page: String(DEFAULT_SUGGESTION_PAGE),
232
+ size: String(DEFAULT_SUGGESTION_SIZE),
233
+ locale: toCatalogLocale(localeParts),
234
+ storeId
235
+ });
236
+ return `${suggestionsUrl}?${queryParams.toString()}`;
237
+ }
238
+ async function resolveCatalogRequestConfig({
239
+ locationLike,
240
+ country,
241
+ getMarketConfig
242
+ }) {
243
+ const marketConfig = getMarketConfig ? await getMarketConfig({ locationLike, country }) ?? null : null;
244
+ return {
245
+ suggestionsUrl: buildCatalogSuggestionsUrl(
246
+ resolveCatalogApiBaseUrl(marketConfig)
247
+ ),
248
+ storeId: resolveCatalogStoreId(marketConfig)
249
+ };
250
+ }
251
+ function resolveCustomerTypeHeaders(getCustomerType) {
252
+ const customerType = getCustomerType?.()?.trim();
253
+ return customerType ? { "X-Customer-Type": customerType } : void 0;
254
+ }
255
+ var NO_RESULTS_ERROR_CODE = "SFOSER002";
256
+ async function isNoResultsResponse(response) {
257
+ if (response.status !== 404) return false;
258
+ try {
259
+ const body = await response.json();
260
+ return body?.code === NO_RESULTS_ERROR_CODE;
261
+ } catch {
262
+ return false;
263
+ }
264
+ }
265
+ async function fetchJson(fetchImpl, requestUrl, requestHeaders = {}) {
266
+ const response = await fetchImpl(requestUrl, {
267
+ method: "GET",
268
+ credentials: "include",
269
+ headers: { Accept: "application/json", ...requestHeaders }
270
+ });
271
+ if (!response.ok) {
272
+ if (await isNoResultsResponse(response)) return null;
273
+ throw new Error(`Request failed with status ${response.status}`);
274
+ }
275
+ return response.json();
276
+ }
277
+
278
+ // src/services/recentSearches.ts
279
+ function canUseStorage(storage) {
280
+ return Boolean(storage) && typeof storage.getItem === "function";
281
+ }
282
+ function getSessionStorage(providedStorage) {
283
+ if (providedStorage) return providedStorage;
284
+ if (typeof window !== "undefined") {
285
+ try {
286
+ return window.sessionStorage;
287
+ } catch {
288
+ return null;
289
+ }
290
+ }
291
+ return null;
292
+ }
191
293
  function getRecentHeaderSearchesStorageKey(localeParts) {
192
294
  const { country, language, locale } = normalizeLocaleParts(localeParts);
193
295
  return `${RECENT_SEARCHES_PREFIX}:${locale}:${country}:${language}`;
@@ -243,63 +345,8 @@ function clearRecentHeaderSearches(localeParts, storage) {
243
345
  } catch {
244
346
  }
245
347
  }
246
- function getBrowserLocation(providedLocation) {
247
- if (providedLocation) return providedLocation;
248
- if (typeof window !== "undefined" && window.location)
249
- return window.location;
250
- return null;
251
- }
252
- function resolveApiGatewayPath(apiGatewayPath, apiName = STOREFRONT_SHOPPING_CONTEXT) {
253
- const apiGateway = String(apiGatewayPath ?? "").trim();
254
- if (!apiGateway) return "";
255
- return apiGateway.replace(/\{api_name\}/gi, apiName).replace(/\/$/, "");
256
- }
257
- function resolveCatalogApiBaseUrl(marketConfig) {
258
- return resolveApiGatewayPath(marketConfig?.api_gateway_path);
259
- }
260
- function resolveCatalogStoreId(marketConfig) {
261
- return String(marketConfig?.store_id ?? "").trim();
262
- }
263
- function buildCatalogSuggestionsUrl(apiBaseUrl) {
264
- const normalizedBase = String(apiBaseUrl ?? "").trim().replace(/\/$/, "");
265
- if (!normalizedBase) return "";
266
- const suggestionsPath = normalizedBase.endsWith(
267
- `/${STOREFRONT_SHOPPING_CONTEXT}`
268
- ) ? "/catalogs/search/suggestions" : CATALOG_SUGGESTIONS_PATH;
269
- return `${normalizedBase}${suggestionsPath}`;
270
- }
271
- function resolveSuggestionsUrl(suggestionsApi, locationLike) {
272
- const pathOrUrl = String(suggestionsApi ?? "").trim();
273
- if (!pathOrUrl) return "";
274
- if (pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")) {
275
- return pathOrUrl;
276
- }
277
- const normalizedPath = pathOrUrl.startsWith("/") ? pathOrUrl : `/${pathOrUrl}`;
278
- const origin = String(locationLike?.origin ?? "").trim();
279
- return origin ? `${origin}${normalizedPath}` : normalizedPath;
280
- }
281
- async function resolveCatalogRequestConfig({
282
- suggestionsApi,
283
- storeIdOption,
284
- locationLike,
285
- country,
286
- getMarketConfig
287
- }) {
288
- const explicitStoreId = storeIdOption !== void 0 && storeIdOption !== null && String(storeIdOption).trim() !== "" ? String(storeIdOption).trim() : "";
289
- const marketConfig = getMarketConfig ? await getMarketConfig({ locationLike, country }) ?? null : null;
290
- const configuredApiBase = resolveCatalogApiBaseUrl(marketConfig);
291
- const configuredStoreId = resolveCatalogStoreId(marketConfig);
292
- const explicitSuggestionsApi = String(suggestionsApi ?? "").trim();
293
- return {
294
- suggestionsUrl: explicitSuggestionsApi ? resolveSuggestionsUrl(explicitSuggestionsApi, locationLike) : buildCatalogSuggestionsUrl(configuredApiBase),
295
- storeId: explicitStoreId || configuredStoreId
296
- };
297
- }
298
- function toCatalogLocale(localeParts) {
299
- const language = String(localeParts.language ?? "en").toLowerCase();
300
- const country = String(localeParts.country ?? "US").toUpperCase();
301
- return `${language}_${country}`;
302
- }
348
+
349
+ // src/services/searchPaths.ts
303
350
  function buildSearchResultsPath(query, localeParts) {
304
351
  const trimmedQuery = String(query ?? "").trim();
305
352
  const hyphenatedQuery = trimmedQuery.replace(/\s+/g, "-");
@@ -369,50 +416,52 @@ function resolveOptionHref(option, localeParts) {
369
416
  }
370
417
  return "";
371
418
  }
372
- function resolveCustomerTypeHeaders(getCustomerType) {
373
- const customerType = getCustomerType?.()?.trim();
374
- return customerType ? { "X-Customer-Type": customerType } : void 0;
419
+
420
+ // src/services/suggestionsPayload.ts
421
+ function emptySuggestions() {
422
+ return { suggestions: [], didYouMean: [], totalResultsCount: 0 };
375
423
  }
376
- var NO_RESULTS_ERROR_CODE = "SFOSER002";
377
- async function isNoResultsResponse(response) {
378
- if (response.status !== 404) return false;
379
- try {
380
- const body = await response.json();
381
- return body?.code === NO_RESULTS_ERROR_CODE;
382
- } catch {
383
- return false;
424
+ function resolveTotalResultsCount(payload, fallback) {
425
+ if (typeof payload?.totalResultsCount === "number") {
426
+ return payload.totalResultsCount;
384
427
  }
428
+ const pageableTotal = Number(
429
+ payload?.pageableInfo?.totalCount ?? Number.NaN
430
+ );
431
+ return Number.isNaN(pageableTotal) ? fallback : pageableTotal;
385
432
  }
386
- async function fetchJsonFromCandidates(fetchImpl, requestUrls, requestHeaders = {}) {
387
- let lastError;
388
- for (const requestUrl of requestUrls) {
389
- try {
390
- const response = await fetchImpl(requestUrl, {
391
- method: "GET",
392
- credentials: "include",
393
- headers: { Accept: "application/json", ...requestHeaders }
394
- });
395
- if (!response.ok) {
396
- if (await isNoResultsResponse(response)) return null;
397
- lastError = new Error(
398
- `Request failed with status ${response.status}`
399
- );
400
- continue;
401
- }
402
- return await response.json();
403
- } catch (error) {
404
- lastError = error;
405
- }
406
- }
407
- throw lastError ?? new Error("Unable to fetch header search data");
433
+ function normalizeSuggestionsPayload(payload) {
434
+ const suggestions = Array.isArray(payload?.suggestions) ? payload.suggestions : [];
435
+ const products = Array.isArray(payload?.product) ? payload.product : null;
436
+ const rawDidYouMean = payload?.didYouMean ?? payload?.did_you_mean;
437
+ const didYouMean = Array.isArray(rawDidYouMean) ? rawDidYouMean : [];
438
+ return {
439
+ ...payload ?? {},
440
+ suggestions,
441
+ didYouMean,
442
+ totalResultsCount: resolveTotalResultsCount(
443
+ payload,
444
+ products?.length ?? suggestions.length
445
+ )
446
+ };
447
+ }
448
+
449
+ // src/services/SearchApiService.ts
450
+ function getBrowserLocation(providedLocation) {
451
+ if (providedLocation) return providedLocation;
452
+ if (typeof window !== "undefined" && window.location)
453
+ return window.location;
454
+ return null;
455
+ }
456
+ function resolveFetch(fetchImplOption) {
457
+ if (fetchImplOption !== void 0) return fetchImplOption;
458
+ return typeof fetch === "function" ? fetch.bind(globalThis) : null;
408
459
  }
409
460
  function createSearchService(options = {}) {
410
461
  const {
411
462
  fetchImpl: fetchImplOption,
412
463
  location: providedLocation,
413
464
  storage: providedStorage,
414
- suggestionsApi,
415
- storeId: storeIdOption,
416
465
  country,
417
466
  language,
418
467
  locale,
@@ -420,7 +469,7 @@ function createSearchService(options = {}) {
420
469
  getCustomerType,
421
470
  minimumSearchLength = MINIMUM_SEARCH_CHAR_LENGTH
422
471
  } = options;
423
- const fetchImpl = fetchImplOption !== void 0 ? fetchImplOption : typeof fetch === "function" ? fetch.bind(globalThis) : null;
472
+ const fetchImpl = resolveFetch(fetchImplOption);
424
473
  const locationLike = getBrowserLocation(providedLocation);
425
474
  const storage = getSessionStorage(providedStorage);
426
475
  const localeParts = normalizeLocaleParts({ country, language, locale });
@@ -428,8 +477,6 @@ function createSearchService(options = {}) {
428
477
  function loadCatalogRequestConfig() {
429
478
  if (!catalogRequestConfigPromise) {
430
479
  catalogRequestConfigPromise = resolveCatalogRequestConfig({
431
- suggestionsApi,
432
- storeIdOption,
433
480
  locationLike,
434
481
  country: localeParts.country,
435
482
  getMarketConfig
@@ -448,47 +495,21 @@ function createSearchService(options = {}) {
448
495
  async fetchSuggestions(searchTerm) {
449
496
  const trimmedSearchTerm = String(searchTerm ?? "").trim();
450
497
  if (!trimmedSearchTerm || trimmedSearchTerm.length < minimumSearchLength || !fetchImpl) {
451
- return {
452
- suggestions: [],
453
- didYouMean: [],
454
- totalResultsCount: 0
455
- };
498
+ return emptySuggestions();
456
499
  }
457
500
  const { suggestionsUrl, storeId } = await loadCatalogRequestConfig();
458
- if (!suggestionsUrl || !storeId) {
459
- return {
460
- suggestions: [],
461
- didYouMean: [],
462
- totalResultsCount: 0
463
- };
464
- }
465
- const queryParams = new URLSearchParams({
466
- searchTerm: trimmedSearchTerm,
467
- page: String(DEFAULT_SUGGESTION_PAGE),
468
- size: String(DEFAULT_SUGGESTION_SIZE),
469
- locale: toCatalogLocale(localeParts),
470
- storeId: String(storeId)
471
- });
472
- const requestUrls = [`${suggestionsUrl}?${queryParams.toString()}`];
473
- const responsePayload = await fetchJsonFromCandidates(
501
+ if (!suggestionsUrl || !storeId) return emptySuggestions();
502
+ const responsePayload = await fetchJson(
474
503
  fetchImpl,
475
- requestUrls,
504
+ buildSuggestionsRequestUrl({
505
+ suggestionsUrl,
506
+ searchTerm: trimmedSearchTerm,
507
+ storeId,
508
+ localeParts
509
+ }),
476
510
  resolveCustomerTypeHeaders(getCustomerType)
477
511
  );
478
- const suggestionsField = Array.isArray(responsePayload?.suggestions) ? responsePayload?.suggestions : [];
479
- const productField = Array.isArray(responsePayload?.product) ? responsePayload?.product : null;
480
- const rawDidYouMean = responsePayload?.didYouMean ?? responsePayload?.did_you_mean;
481
- const didYouMeanField = Array.isArray(rawDidYouMean) ? rawDidYouMean : [];
482
- const pageableTotal = Number(
483
- responsePayload?.pageableInfo?.totalCount ?? Number.NaN
484
- );
485
- const totalResultsCount = typeof responsePayload?.totalResultsCount === "number" ? responsePayload.totalResultsCount : !Number.isNaN(pageableTotal) ? pageableTotal : productField?.length ?? suggestionsField.length;
486
- return {
487
- ...responsePayload ?? {},
488
- suggestions: suggestionsField,
489
- didYouMean: didYouMeanField,
490
- totalResultsCount
491
- };
512
+ return normalizeSuggestionsPayload(responsePayload);
492
513
  },
493
514
  submitProductSearch(query, option) {
494
515
  const trimmedQuery = String(query ?? "").trim();
@@ -573,7 +594,90 @@ function getProductEnvironment() {
573
594
  }
574
595
  }
575
596
 
576
- // src/utils/catalogHelpers.ts
597
+ // src/utils/nuiSearchGate.ts
598
+ var NUI_SEARCH_FLAG = "enableNuiSearch";
599
+ function parseFlagValue(raw) {
600
+ const value = raw?.trim().toLowerCase();
601
+ if (value === "true" || value === "1") return true;
602
+ if (value === "false" || value === "0") return false;
603
+ return void 0;
604
+ }
605
+ function readCookieFlag(cookie) {
606
+ for (const part of cookie.split(";")) {
607
+ const separator = part.indexOf("=");
608
+ if (separator < 0) continue;
609
+ if (part.slice(0, separator).trim() !== NUI_SEARCH_FLAG) continue;
610
+ try {
611
+ return parseFlagValue(
612
+ decodeURIComponent(part.slice(separator + 1))
613
+ );
614
+ } catch {
615
+ return void 0;
616
+ }
617
+ }
618
+ return void 0;
619
+ }
620
+ function resolveNuiSearchFlag(env = {}) {
621
+ const inBrowser = typeof window !== "undefined" && typeof document !== "undefined";
622
+ const search = env.search ?? (inBrowser ? window.location.search : "");
623
+ const cookie = env.cookie ?? (inBrowser ? document.cookie : "");
624
+ return parseFlagValue(new URLSearchParams(search).get(NUI_SEARCH_FLAG)) ?? readCookieFlag(cookie);
625
+ }
626
+ function isNuiSearchEnabled({
627
+ isAuthenticated,
628
+ flag,
629
+ allowGuests = false
630
+ }) {
631
+ return flag === true && (isAuthenticated === true || allowGuests);
632
+ }
633
+
634
+ // src/hooks/useNuiAgentGate.ts
635
+ function useNuiAgentGate({
636
+ hasAgent,
637
+ isOpen,
638
+ isAuthenticated,
639
+ allowGuestAgent
640
+ }) {
641
+ const [flag, setFlag] = react.useState(void 0);
642
+ react.useEffect(() => {
643
+ if (isOpen) setFlag(resolveNuiSearchFlag());
644
+ }, [isOpen]);
645
+ return hasAgent && isNuiSearchEnabled({
646
+ isAuthenticated,
647
+ flag,
648
+ allowGuests: allowGuestAgent
649
+ });
650
+ }
651
+ function uniqueRecentSearches(queries) {
652
+ const seen = /* @__PURE__ */ new Set();
653
+ return queries.filter((query) => {
654
+ const key = query.trim().toLowerCase();
655
+ if (!key || seen.has(key)) return false;
656
+ seen.add(key);
657
+ return true;
658
+ });
659
+ }
660
+ function useRecentSearches({
661
+ service,
662
+ isOpen,
663
+ refreshKey
664
+ }) {
665
+ const [recentSearches, setRecentSearches] = react.useState([]);
666
+ react.useEffect(() => {
667
+ if (isOpen) {
668
+ setRecentSearches(
669
+ uniqueRecentSearches(service.getRecentSearches())
670
+ );
671
+ }
672
+ }, [isOpen, service, refreshKey]);
673
+ const clearRecentSearches = react.useCallback(() => {
674
+ service.clearRecentSearches();
675
+ setRecentSearches([]);
676
+ }, [service]);
677
+ return { recentSearches, clearRecentSearches };
678
+ }
679
+
680
+ // src/utils/catalogStatus.ts
577
681
  var CART_DISABLED_PRODUCT_STATUSES = /* @__PURE__ */ new Set([
578
682
  "discontinued",
579
683
  "preview",
@@ -621,6 +725,8 @@ function isSuggestionDisabled(suggestionOption) {
621
725
  !suggestionOption || suggestionOption["out_of_stock"] || suggestionOption["outOfStock"]
622
726
  );
623
727
  }
728
+
729
+ // src/utils/catalogHelpers.ts
624
730
  function getImageFromProperty(properties = {}) {
625
731
  let image = String(properties["primaryimage"] ?? "").trim() || String(properties["imageURL"] ?? "").trim();
626
732
  if (!image) {
@@ -858,6 +964,46 @@ function normalizeSuggestionsResponse(response) {
858
964
  didYouMean: normalizeDidYouMean(r.didYouMean)
859
965
  };
860
966
  }
967
+ function resolveTotalResultsCount2(response, fallback) {
968
+ if (typeof response.totalResultsCount === "number") {
969
+ return response.totalResultsCount;
970
+ }
971
+ if (typeof response.pageableInfo?.totalCount === "number") {
972
+ return response.pageableInfo.totalCount;
973
+ }
974
+ return fallback;
975
+ }
976
+ var FETCH_FAILED = {
977
+ suggestions: [],
978
+ didYouMean: [],
979
+ hasMoreResults: false,
980
+ emptyFetched: true,
981
+ failed: true
982
+ };
983
+ function toSettledResults(response) {
984
+ const { suggestions: allSuggestions, didYouMean } = normalizeSuggestionsResponse(response);
985
+ if (didYouMean.length > 0) {
986
+ return {
987
+ suggestions: [],
988
+ didYouMean,
989
+ hasMoreResults: false,
990
+ emptyFetched: false,
991
+ failed: false
992
+ };
993
+ }
994
+ const total = Math.max(
995
+ resolveTotalResultsCount2(response, allSuggestions.length),
996
+ allSuggestions.length
997
+ );
998
+ const suggestions = allSuggestions.slice(0, MAX_VISIBLE_SUGGESTIONS);
999
+ return {
1000
+ suggestions,
1001
+ didYouMean,
1002
+ hasMoreResults: total > MAX_VISIBLE_SUGGESTIONS,
1003
+ emptyFetched: suggestions.length === 0,
1004
+ failed: false
1005
+ };
1006
+ }
861
1007
  function useSearchSuggestions({
862
1008
  service,
863
1009
  enabled = true
@@ -869,7 +1015,6 @@ function useSearchSuggestions({
869
1015
  const [emptyFetched, setEmptyFetched] = react.useState(false);
870
1016
  const [suggestionFetchFailed, setSuggestionFetchFailed] = react.useState(false);
871
1017
  const [hasMoreResults, setHasMoreResults] = react.useState(false);
872
- const [totalResultsCount, setTotalResultsCount] = react.useState(0);
873
1018
  const [settledQuery, setSettledQuery] = react.useState("");
874
1019
  const fetchDebounced = react.useRef(null);
875
1020
  const latestRequestId = react.useRef(0);
@@ -879,7 +1024,6 @@ function useSearchSuggestions({
879
1024
  setEmptyFetched(false);
880
1025
  setSuggestionFetchFailed(false);
881
1026
  setHasMoreResults(false);
882
- setTotalResultsCount(0);
883
1027
  setSettledQuery("");
884
1028
  setLoading(false);
885
1029
  }, []);
@@ -897,6 +1041,14 @@ function useSearchSuggestions({
897
1041
  resetSuggestionsState();
898
1042
  setQuery("");
899
1043
  }, [resetSuggestionsState]);
1044
+ const settle = react.useCallback((results, term) => {
1045
+ setDidYouMean(results.didYouMean);
1046
+ setSuggestions(results.suggestions);
1047
+ setHasMoreResults(results.hasMoreResults);
1048
+ setSuggestionFetchFailed(results.failed);
1049
+ setSettledQuery(term);
1050
+ setEmptyFetched(results.emptyFetched);
1051
+ }, []);
900
1052
  const runFetch = react.useCallback(
901
1053
  async (nextQuery) => {
902
1054
  const trimmedQuery = nextQuery.trim();
@@ -908,51 +1060,20 @@ function useSearchSuggestions({
908
1060
  setLoading(true);
909
1061
  setEmptyFetched(false);
910
1062
  setSuggestionFetchFailed(false);
1063
+ let results;
911
1064
  try {
912
- const fetchedSuggestions = await service.fetchSuggestions(trimmedQuery);
913
- if (requestId !== latestRequestId.current) {
914
- return;
915
- }
916
- const {
917
- suggestions: allSuggestions,
918
- didYouMean: suggestedTerms
919
- } = normalizeSuggestionsResponse(fetchedSuggestions);
920
- const visibleSuggestions = allSuggestions.slice(
921
- 0,
922
- MAX_VISIBLE_SUGGESTIONS
1065
+ results = toSettledResults(
1066
+ await service.fetchSuggestions(trimmedQuery)
923
1067
  );
924
- const hasDidYouMean = suggestedTerms.length > 0;
925
- const normalizedTotal = typeof fetchedSuggestions.totalResultsCount === "number" ? fetchedSuggestions.totalResultsCount : typeof fetchedSuggestions.pageableInfo?.totalCount === "number" ? fetchedSuggestions.pageableInfo.totalCount : allSuggestions.length;
926
- const effectiveTotal = hasDidYouMean ? suggestedTerms.length : Math.max(normalizedTotal, allSuggestions.length);
927
- setDidYouMean(suggestedTerms);
928
- setSuggestions(hasDidYouMean ? [] : visibleSuggestions);
929
- setHasMoreResults(
930
- !hasDidYouMean && effectiveTotal > MAX_VISIBLE_SUGGESTIONS
931
- );
932
- setTotalResultsCount(effectiveTotal);
933
- setSuggestionFetchFailed(false);
934
- setSettledQuery(trimmedQuery);
935
- setEmptyFetched(
936
- hasDidYouMean ? false : visibleSuggestions.length === 0 && suggestedTerms.length === 0
937
- );
938
- } catch {
939
- if (requestId !== latestRequestId.current) {
940
- return;
941
- }
942
- setSuggestions([]);
943
- setDidYouMean([]);
944
- setEmptyFetched(true);
945
- setSuggestionFetchFailed(true);
946
- setHasMoreResults(false);
947
- setTotalResultsCount(0);
948
- setSettledQuery(trimmedQuery);
949
- } finally {
950
- if (requestId === latestRequestId.current) {
951
- setLoading(false);
952
- }
1068
+ } catch (error) {
1069
+ console.warn("[search] Could not load suggestions:", error);
1070
+ results = FETCH_FAILED;
953
1071
  }
1072
+ if (requestId !== latestRequestId.current) return;
1073
+ settle(results, trimmedQuery);
1074
+ setLoading(false);
954
1075
  },
955
- [enabled, service, resetSuggestionsState]
1076
+ [enabled, service, resetSuggestionsState, settle]
956
1077
  );
957
1078
  react.useEffect(() => {
958
1079
  if (!enabled) {
@@ -976,15 +1097,12 @@ function useSearchSuggestions({
976
1097
  clearQuery,
977
1098
  emptyFetched,
978
1099
  hasMoreResults,
979
- hasQuery: Boolean(query.trim()),
980
1100
  didYouMean,
981
1101
  loading,
982
1102
  query,
983
1103
  settledQuery,
984
- setQuery,
985
1104
  suggestionFetchFailed,
986
1105
  suggestions,
987
- totalResultsCount,
988
1106
  updateQuery
989
1107
  };
990
1108
  }
@@ -1059,43 +1177,6 @@ function trackProductSearch(input) {
1059
1177
  });
1060
1178
  }
1061
1179
 
1062
- // src/utils/nuiSearchGate.ts
1063
- var NUI_SEARCH_FLAG = "enableNuiSearch";
1064
- function parseFlagValue(raw) {
1065
- const value = raw?.trim().toLowerCase();
1066
- if (value === "true" || value === "1") return true;
1067
- if (value === "false" || value === "0") return false;
1068
- return void 0;
1069
- }
1070
- function readCookieFlag(cookie) {
1071
- for (const part of cookie.split(";")) {
1072
- const separator = part.indexOf("=");
1073
- if (separator < 0) continue;
1074
- if (part.slice(0, separator).trim() !== NUI_SEARCH_FLAG) continue;
1075
- try {
1076
- return parseFlagValue(
1077
- decodeURIComponent(part.slice(separator + 1))
1078
- );
1079
- } catch {
1080
- return void 0;
1081
- }
1082
- }
1083
- return void 0;
1084
- }
1085
- function resolveNuiSearchFlag(env = {}) {
1086
- const inBrowser = typeof window !== "undefined" && typeof document !== "undefined";
1087
- const search = env.search ?? (inBrowser ? window.location.search : "");
1088
- const cookie = env.cookie ?? (inBrowser ? document.cookie : "");
1089
- return parseFlagValue(new URLSearchParams(search).get(NUI_SEARCH_FLAG)) ?? readCookieFlag(cookie);
1090
- }
1091
- function isNuiSearchEnabled({
1092
- isAuthenticated,
1093
- flag,
1094
- allowGuests = false
1095
- }) {
1096
- return flag === true && (isAuthenticated === true || allowGuests);
1097
- }
1098
-
1099
1180
  // src/utils/searchRouting.ts
1100
1181
  function decideSearchRoute({
1101
1182
  enabled,
@@ -1108,6 +1189,96 @@ function decideSearchRoute({
1108
1189
  return snapshot.suggestions.length > 0 ? "search" : "handoff";
1109
1190
  }
1110
1191
 
1192
+ // src/hooks/useSubmitRouting.ts
1193
+ var SUBMIT_WAIT_TIMEOUT_MS = 2e3;
1194
+ function useSubmitRouting({
1195
+ enabled,
1196
+ minimumSearchLength,
1197
+ search,
1198
+ gtmContext,
1199
+ pendingTerm,
1200
+ setPendingTerm,
1201
+ onSearch,
1202
+ onHandoff
1203
+ }) {
1204
+ const {
1205
+ didYouMean,
1206
+ loading,
1207
+ settledQuery,
1208
+ suggestionFetchFailed,
1209
+ suggestions
1210
+ } = search;
1211
+ const decide = react.useCallback(
1212
+ (term) => decideSearchRoute({
1213
+ enabled,
1214
+ snapshot: {
1215
+ query: term,
1216
+ loading,
1217
+ settledQuery,
1218
+ suggestions,
1219
+ suggestionFetchFailed
1220
+ }
1221
+ }),
1222
+ [enabled, loading, settledQuery, suggestions, suggestionFetchFailed]
1223
+ );
1224
+ const act = react.useCallback(
1225
+ (term, route) => {
1226
+ setPendingTerm(null);
1227
+ if (route === "handoff") {
1228
+ trackChatEntryPoint(gtmContext ?? {});
1229
+ onHandoff({
1230
+ query: term,
1231
+ didYouMean: didYouMean.map((item) => item.value)
1232
+ });
1233
+ return;
1234
+ }
1235
+ onSearch(term);
1236
+ },
1237
+ [didYouMean, gtmContext, onHandoff, onSearch, setPendingTerm]
1238
+ );
1239
+ const submit = react.useCallback(
1240
+ (value) => {
1241
+ const term = value.trim();
1242
+ if (!term) return;
1243
+ if (term.length < minimumSearchLength) {
1244
+ onSearch(term);
1245
+ return;
1246
+ }
1247
+ const route = decide(term);
1248
+ if (route === "wait") {
1249
+ setPendingTerm(term);
1250
+ return;
1251
+ }
1252
+ act(term, route);
1253
+ },
1254
+ [act, decide, minimumSearchLength, onSearch, setPendingTerm]
1255
+ );
1256
+ const latest = react.useRef({ act, decide });
1257
+ react.useEffect(() => {
1258
+ latest.current = { act, decide };
1259
+ });
1260
+ react.useEffect(() => {
1261
+ if (pendingTerm === null) return void 0;
1262
+ const route = latest.current.decide(pendingTerm);
1263
+ if (route !== "wait") {
1264
+ latest.current.act(pendingTerm, route);
1265
+ return void 0;
1266
+ }
1267
+ const timer = setTimeout(
1268
+ () => latest.current.act(pendingTerm, "search"),
1269
+ SUBMIT_WAIT_TIMEOUT_MS
1270
+ );
1271
+ return () => clearTimeout(timer);
1272
+ }, [
1273
+ pendingTerm,
1274
+ loading,
1275
+ settledQuery,
1276
+ suggestions,
1277
+ suggestionFetchFailed
1278
+ ]);
1279
+ return submit;
1280
+ }
1281
+
1111
1282
  // src/utils/cachedProductClient.ts
1112
1283
  function withProductCache(client) {
1113
1284
  const lookups = /* @__PURE__ */ new Map();
@@ -1175,7 +1346,8 @@ var AgentErrorBoundary = class extends react.Component {
1175
1346
  static getDerivedStateFromError() {
1176
1347
  return { failed: true };
1177
1348
  }
1178
- componentDidCatch(_error, _info) {
1349
+ componentDidCatch(error) {
1350
+ console.warn("[search] The agent could not be shown:", error);
1179
1351
  this.props.onError();
1180
1352
  }
1181
1353
  render() {
@@ -1188,6 +1360,10 @@ function useHandoff({
1188
1360
  agentContext,
1189
1361
  onUnavailable
1190
1362
  }) {
1363
+ const onUnavailableRef = react.useRef(onUnavailable);
1364
+ react.useEffect(() => {
1365
+ onUnavailableRef.current = onUnavailable;
1366
+ });
1191
1367
  react.useEffect(() => {
1192
1368
  let cancelled = false;
1193
1369
  let timer;
@@ -1203,7 +1379,10 @@ function useHandoff({
1203
1379
  return;
1204
1380
  }
1205
1381
  if (Date.now() - startedAt >= HANDLE_TIMEOUT_MS) {
1206
- onUnavailable();
1382
+ console.warn(
1383
+ `[search] The agent did not start within ${HANDLE_TIMEOUT_MS / 1e3} seconds.`
1384
+ );
1385
+ onUnavailableRef.current();
1207
1386
  return;
1208
1387
  }
1209
1388
  timer = setTimeout(tryOpen, HANDLE_POLL_MS);
@@ -1245,16 +1424,6 @@ function NuiAgentPanel({
1245
1424
  }
1246
1425
  ) });
1247
1426
  }
1248
- var SUBMIT_WAIT_TIMEOUT_MS = 2e3;
1249
- function uniqueRecentSearches(queries) {
1250
- const seen = /* @__PURE__ */ new Set();
1251
- return queries.filter((query) => {
1252
- const key = query.trim().toLowerCase();
1253
- if (!key || seen.has(key)) return false;
1254
- seen.add(key);
1255
- return true;
1256
- });
1257
- }
1258
1427
  function CommerceSearchView({
1259
1428
  copy,
1260
1429
  isOpen,
@@ -1269,11 +1438,9 @@ function CommerceSearchView({
1269
1438
  minimumSearchLength = MINIMUM_SEARCH_CHAR_LENGTH,
1270
1439
  gtmContext
1271
1440
  }) {
1272
- const [flag, setFlag] = react.useState(void 0);
1273
1441
  const [handoff, setHandoff] = react.useState(null);
1274
1442
  const agentQuery = handoff?.query ?? null;
1275
1443
  const [pendingTerm, setPendingTerm] = react.useState(null);
1276
- const [recentSearches, setRecentSearches] = react.useState([]);
1277
1444
  const search = useSearchSuggestions({
1278
1445
  service,
1279
1446
  enabled: isOpen && agentQuery === null
@@ -1285,31 +1452,25 @@ function CommerceSearchView({
1285
1452
  hasMoreResults,
1286
1453
  loading,
1287
1454
  query,
1288
- settledQuery,
1289
- suggestionFetchFailed,
1290
1455
  suggestions,
1291
1456
  updateQuery
1292
1457
  } = search;
1293
- const enabled = agent !== void 0 && isNuiSearchEnabled({
1458
+ const agentEnabled = useNuiAgentGate({
1459
+ hasAgent: agent !== void 0,
1460
+ isOpen,
1294
1461
  isAuthenticated,
1295
- flag,
1296
- allowGuests: allowGuestAgent
1462
+ allowGuestAgent
1463
+ });
1464
+ const { recentSearches, clearRecentSearches } = useRecentSearches({
1465
+ service,
1466
+ isOpen,
1467
+ refreshKey: agentQuery
1297
1468
  });
1298
1469
  const items = getVisibleSuggestions({
1299
1470
  didYouMean,
1300
1471
  suggestions
1301
1472
  });
1302
1473
  const showDidYouMean = didYouMean.length > 0;
1303
- react.useEffect(() => {
1304
- if (isOpen) setFlag(resolveNuiSearchFlag());
1305
- }, [isOpen]);
1306
- react.useEffect(() => {
1307
- if (isOpen) {
1308
- setRecentSearches(
1309
- uniqueRecentSearches(service.getRecentSearches())
1310
- );
1311
- }
1312
- }, [isOpen, service, agentQuery]);
1313
1474
  const close = react.useCallback(() => {
1314
1475
  setHandoff(null);
1315
1476
  setPendingTerm(null);
@@ -1348,72 +1509,16 @@ function CommerceSearchView({
1348
1509
  },
1349
1510
  [items, navigateTo, service, trackSemantic]
1350
1511
  );
1351
- const decide = react.useCallback(
1352
- (term) => decideSearchRoute({
1353
- enabled,
1354
- snapshot: {
1355
- query: term,
1356
- loading,
1357
- settledQuery,
1358
- suggestions,
1359
- suggestionFetchFailed
1360
- }
1361
- }),
1362
- [enabled, loading, settledQuery, suggestions, suggestionFetchFailed]
1363
- );
1364
- const act = react.useCallback(
1365
- (term, route) => {
1366
- setPendingTerm(null);
1367
- if (route === "handoff") {
1368
- trackChatEntryPoint(gtmContext ?? {});
1369
- setHandoff({
1370
- query: term,
1371
- didYouMean: didYouMean.map((item) => item.value)
1372
- });
1373
- return;
1374
- }
1375
- runSearch(term);
1376
- },
1377
- [didYouMean, gtmContext, runSearch]
1378
- );
1379
- const handleSubmit = react.useCallback(
1380
- (value) => {
1381
- const term = value.trim();
1382
- if (!term) return;
1383
- if (term.length < minimumSearchLength) {
1384
- runSearch(term);
1385
- return;
1386
- }
1387
- const route = decide(term);
1388
- if (route === "wait") {
1389
- setPendingTerm(term);
1390
- return;
1391
- }
1392
- act(term, route);
1393
- },
1394
- [act, decide, minimumSearchLength, runSearch]
1395
- );
1396
- const latest = react.useRef({ act, decide });
1397
- latest.current = { act, decide };
1398
- react.useEffect(() => {
1399
- if (pendingTerm === null) return void 0;
1400
- const route = latest.current.decide(pendingTerm);
1401
- if (route !== "wait") {
1402
- latest.current.act(pendingTerm, route);
1403
- return void 0;
1404
- }
1405
- const timer = setTimeout(
1406
- () => latest.current.act(pendingTerm, "search"),
1407
- SUBMIT_WAIT_TIMEOUT_MS
1408
- );
1409
- return () => clearTimeout(timer);
1410
- }, [
1512
+ const handleSubmit = useSubmitRouting({
1513
+ enabled: agentEnabled,
1514
+ minimumSearchLength,
1515
+ search,
1516
+ ...gtmContext ? { gtmContext } : {},
1411
1517
  pendingTerm,
1412
- loading,
1413
- settledQuery,
1414
- suggestions,
1415
- suggestionFetchFailed
1416
- ]);
1518
+ setPendingTerm,
1519
+ onSearch: runSearch,
1520
+ onHandoff: setHandoff
1521
+ });
1417
1522
  const handleSelect = react.useCallback(
1418
1523
  (option) => {
1419
1524
  if (isSuggestionDisabled(
@@ -1438,10 +1543,11 @@ function CommerceSearchView({
1438
1543
  )) {
1439
1544
  return;
1440
1545
  }
1441
- const result = service.activateSuggestionAction(
1442
- option
1546
+ navigateTo(
1547
+ service.activateSuggestionAction(
1548
+ option
1549
+ )
1443
1550
  );
1444
- void Promise.resolve(result).then(navigateTo).catch(() => void 0);
1445
1551
  },
1446
1552
  [navigateTo, service]
1447
1553
  );
@@ -1456,10 +1562,6 @@ function CommerceSearchView({
1456
1562
  },
1457
1563
  [navigateTo, service, trackSemantic]
1458
1564
  );
1459
- const handleClearRecent = react.useCallback(() => {
1460
- service.clearRecentSearches();
1461
- setRecentSearches([]);
1462
- }, [service]);
1463
1565
  const handleAgentUnavailable = react.useCallback(() => {
1464
1566
  if (agentQuery !== null)
1465
1567
  navigateTo(service.submitProductSearch(agentQuery));
@@ -1508,7 +1610,7 @@ function CommerceSearchView({
1508
1610
  recentSearches: {
1509
1611
  items: recentSearches,
1510
1612
  onSelect: handleRecentSelect,
1511
- onClear: handleClearRecent
1613
+ onClear: clearRecentSearches
1512
1614
  },
1513
1615
  agentPanel
1514
1616
  }
@@ -1534,8 +1636,11 @@ function CommerceSearch({
1534
1636
  language,
1535
1637
  enabled: hasOpened
1536
1638
  });
1537
- const customerTypeRef = react.useRef(GUEST);
1538
- customerTypeRef.current = customer.isAuthenticated ? customer.accountType ?? GUEST : GUEST;
1639
+ const customerType = customer.isAuthenticated ? customer.accountType ?? GUEST : GUEST;
1640
+ const customerTypeRef = react.useRef(customerType);
1641
+ react.useEffect(() => {
1642
+ customerTypeRef.current = customerType;
1643
+ }, [customerType]);
1539
1644
  const service = react.useMemo(
1540
1645
  () => createSearchService({
1541
1646
  country,