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

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
@@ -1,11 +1,11 @@
1
1
  import { useState, useEffect, useRef, useMemo, useCallback, Component } from 'react';
2
2
  import configurationSdk from '@nuskin/configuration-sdk';
3
3
  import { SearchPanel } from '@nuskin/react-search-components';
4
+ import { jsx } from 'react/jsx-runtime';
4
5
  import { SalesAIChatWidget } from '@nuskin/react-nui-chat-sdk';
5
6
  import '@nuskin/react-nui-chat-sdk/styles.css';
6
7
  import { createProductClient, ProductProvider, useProductById, ProductCard } from '@nuskin/react-product-sdk';
7
8
  import styled from '@emotion/styled';
8
- import { jsx } from 'react/jsx-runtime';
9
9
 
10
10
  // src/components/CommerceSearch/CommerceSearch.tsx
11
11
  var { getConfiguration, getEnvironmentCode } = configurationSdk;
@@ -173,25 +173,38 @@ function useSearchPanelCopy({
173
173
  }
174
174
 
175
175
  // src/services/localeParts.ts
176
- function normalizeLocaleParts({
176
+ var TWO_LETTERS = /^[A-Za-z]{2}$/;
177
+ function marketProblem(country, language) {
178
+ const countryText = String(country ?? "").trim();
179
+ const languageText = String(language ?? "").trim();
180
+ if (TWO_LETTERS.test(countryText) && TWO_LETTERS.test(languageText)) {
181
+ return void 0;
182
+ }
183
+ return `the market is not valid (country ${JSON.stringify(countryText)}, language ${JSON.stringify(languageText)}): each must be two letters, for example US and en`;
184
+ }
185
+ function isValidMarket(country, language) {
186
+ return marketProblem(country, language) === void 0;
187
+ }
188
+ function createLocaleParts({
177
189
  country,
178
- language,
179
- locale
190
+ language
180
191
  }) {
181
- const normalizedCountry = String(country ?? "").trim().toUpperCase();
182
- const normalizedLanguage = String(language ?? "").trim().toLowerCase();
183
- const normalizedLocale = String(locale ?? "").trim() || "UNKNOWN";
192
+ const problem = marketProblem(country, language);
193
+ if (problem) throw new Error(problem);
194
+ const normalizedCountry = country.trim().toUpperCase();
195
+ const normalizedLanguage = language.trim().toLowerCase();
184
196
  return {
185
- country: normalizedCountry || "UNKNOWN",
186
- language: normalizedLanguage || "unknown",
187
- locale: normalizedLocale
197
+ country: normalizedCountry,
198
+ language: normalizedLanguage,
199
+ locale: `${normalizedCountry}-${normalizedLanguage}`
188
200
  };
189
201
  }
190
- function toCatalogLocale(localeParts) {
191
- const language = String(localeParts.language ?? "en").toLowerCase();
192
- const country = String(localeParts.country ?? "US").toUpperCase();
202
+ function toCatalogLocale({ country, language }) {
193
203
  return `${language}_${country}`;
194
204
  }
205
+ function toPathPrefix({ country, language }) {
206
+ return `/${country.toLowerCase()}/${language}`;
207
+ }
195
208
 
196
209
  // src/services/catalogRequest.ts
197
210
  function resolveApiGatewayPath(apiGatewayPath, apiName = STOREFRONT_SHOPPING_CONTEXT) {
@@ -234,12 +247,21 @@ async function resolveCatalogRequestConfig({
234
247
  getMarketConfig
235
248
  }) {
236
249
  const marketConfig = getMarketConfig ? await getMarketConfig({ locationLike, country }) ?? null : null;
237
- return {
238
- suggestionsUrl: buildCatalogSuggestionsUrl(
239
- resolveCatalogApiBaseUrl(marketConfig)
240
- ),
241
- storeId: resolveCatalogStoreId(marketConfig)
242
- };
250
+ const suggestionsUrl = buildCatalogSuggestionsUrl(
251
+ resolveCatalogApiBaseUrl(marketConfig)
252
+ );
253
+ if (!suggestionsUrl) {
254
+ throw new Error(
255
+ `no suggestions API for market ${country}: the market configuration has no gateway`
256
+ );
257
+ }
258
+ const storeId = resolveCatalogStoreId(marketConfig);
259
+ if (!storeId) {
260
+ throw new Error(
261
+ `no suggestions API for market ${country}: the market configuration has no store`
262
+ );
263
+ }
264
+ return { suggestionsUrl, storeId };
243
265
  }
244
266
  function resolveCustomerTypeHeaders(getCustomerType) {
245
267
  const customerType = getCustomerType?.()?.trim();
@@ -284,7 +306,7 @@ function getSessionStorage(providedStorage) {
284
306
  return null;
285
307
  }
286
308
  function getRecentHeaderSearchesStorageKey(localeParts) {
287
- const { country, language, locale } = normalizeLocaleParts(localeParts);
309
+ const { country, language, locale } = localeParts;
288
310
  return `${RECENT_SEARCHES_PREFIX}:${locale}:${country}:${language}`;
289
311
  }
290
312
  function getRecentHeaderSearches(localeParts, storage) {
@@ -347,13 +369,14 @@ function buildSearchResultsPath(query, localeParts) {
347
369
  /%2D/gi,
348
370
  "-"
349
371
  );
350
- const country = localeParts.country.toLowerCase();
351
- const language = localeParts.language.toLowerCase();
352
- return `/${country}/${language}${DEFAULT_SEARCH_PATH}/${encodedQuery}`;
372
+ return `${toPathPrefix(localeParts)}${DEFAULT_SEARCH_PATH}/${encodedQuery}`;
353
373
  }
374
+ var SCHEME = /^[a-z][a-z0-9+.-]*:/i;
354
375
  function normalizeOptionUrl(rawValue) {
355
376
  if (typeof rawValue !== "string") return "";
356
- return rawValue.trim();
377
+ const value = rawValue.trim();
378
+ if (!value || value.startsWith("/") || SCHEME.test(value)) return value;
379
+ return `/${value}`;
357
380
  }
358
381
  function safeDecodeURIComponent(value) {
359
382
  try {
@@ -383,16 +406,14 @@ function slugifyPathSegment(value) {
383
406
  function buildProductSlugPath(slugValue, localeParts) {
384
407
  const slug = slugifyPathSegment(slugValue);
385
408
  if (!slug) return "";
386
- const country = String(localeParts.country ?? "US").toLowerCase();
387
- const language = String(localeParts.language ?? "en").toLowerCase();
388
- return `/${country}/${language}/product/${slug}`;
409
+ return `${toPathPrefix(localeParts)}/product/${slug}`;
389
410
  }
390
411
  function isNavigableUrl(value) {
391
412
  if (!value) return false;
392
413
  return value.startsWith("/") || /^https?:\/\//i.test(value);
393
414
  }
394
415
  function stripLocalePrefix(path, localeParts) {
395
- const prefix = `/${localeParts.country.toLowerCase()}/${localeParts.language.toLowerCase()}`;
416
+ const prefix = toPathPrefix(localeParts);
396
417
  return path.startsWith(prefix) ? path.slice(prefix.length) || "/" : path;
397
418
  }
398
419
  function resolveOptionHref(option, localeParts) {
@@ -446,26 +467,20 @@ function getBrowserLocation(providedLocation) {
446
467
  return window.location;
447
468
  return null;
448
469
  }
449
- function resolveFetch(fetchImplOption) {
450
- if (fetchImplOption !== void 0) return fetchImplOption;
451
- return typeof fetch === "function" ? fetch.bind(globalThis) : null;
452
- }
453
- function createSearchService(options = {}) {
470
+ function createSearchService(options) {
454
471
  const {
455
472
  fetchImpl: fetchImplOption,
456
473
  location: providedLocation,
457
474
  storage: providedStorage,
458
475
  country,
459
476
  language,
460
- locale,
461
477
  getMarketConfig,
462
478
  getCustomerType,
463
479
  minimumSearchLength = MINIMUM_SEARCH_CHAR_LENGTH
464
480
  } = options;
465
- const fetchImpl = resolveFetch(fetchImplOption);
466
481
  const locationLike = getBrowserLocation(providedLocation);
467
482
  const storage = getSessionStorage(providedStorage);
468
- const localeParts = normalizeLocaleParts({ country, language, locale });
483
+ const localeParts = createLocaleParts({ country, language });
469
484
  let catalogRequestConfigPromise = null;
470
485
  function loadCatalogRequestConfig() {
471
486
  if (!catalogRequestConfigPromise) {
@@ -487,13 +502,12 @@ function createSearchService(options = {}) {
487
502
  const service = {
488
503
  async fetchSuggestions(searchTerm) {
489
504
  const trimmedSearchTerm = String(searchTerm ?? "").trim();
490
- if (!trimmedSearchTerm || trimmedSearchTerm.length < minimumSearchLength || !fetchImpl) {
505
+ if (!trimmedSearchTerm || trimmedSearchTerm.length < minimumSearchLength) {
491
506
  return emptySuggestions();
492
507
  }
493
508
  const { suggestionsUrl, storeId } = await loadCatalogRequestConfig();
494
- if (!suggestionsUrl || !storeId) return emptySuggestions();
495
509
  const responsePayload = await fetchJson(
496
- fetchImpl,
510
+ fetchImplOption ?? fetch.bind(globalThis),
497
511
  buildSuggestionsRequestUrl({
498
512
  suggestionsUrl,
499
513
  searchTerm: trimmedSearchTerm,
@@ -547,7 +561,7 @@ function createSearchService(options = {}) {
547
561
 
548
562
  // src/services/marketConfig.ts
549
563
  var fetchMarketConfig = async ({ country }) => {
550
- const countryCode = String(country ?? "").trim().toUpperCase() || "US";
564
+ const countryCode = country.trim().toUpperCase();
551
565
  const configuration = await getConfiguration([EQUINOX_MARKETS_MAP], {
552
566
  country: countryCode,
553
567
  ...getEnvironmentCode() ? { environment: getEnvironmentCode() } : {},
@@ -570,14 +584,25 @@ var SEARCH_GTM_USER_STATUS = {
570
584
  var GTM_FALLBACK_NA = "NA";
571
585
 
572
586
  // src/utils/navigationTarget.ts
587
+ var EXTERNAL_URL = /^https?:\/\//i;
588
+ var INTERNAL_PATH = /^\/(?![/\\])[^\t\n\r]*$/;
573
589
  function toNavigationTarget(path) {
574
- return /^https?:\/\//i.test(path) ? { type: "external", url: path } : { type: "internal", path };
590
+ if (EXTERNAL_URL.test(path)) return { type: "external", url: path };
591
+ if (INTERNAL_PATH.test(path)) return { type: "internal", path };
592
+ return void 0;
575
593
  }
576
594
 
577
595
  // src/utils/productEnvironment.ts
578
- var PRODUCT_ENVIRONMENTS = ["dev", "test", "prod"];
596
+ var PRODUCT_ENVIRONMENTS = {
597
+ dev: true,
598
+ test: true,
599
+ prod: true
600
+ };
601
+ function isProductEnvironment(code) {
602
+ return Object.keys(PRODUCT_ENVIRONMENTS).includes(code);
603
+ }
579
604
  function toProductEnvironment(environmentCode) {
580
- return environmentCode && PRODUCT_ENVIRONMENTS.includes(environmentCode) ? environmentCode : void 0;
605
+ return environmentCode !== void 0 && isProductEnvironment(environmentCode) ? environmentCode : void 0;
581
606
  }
582
607
  function getProductEnvironment() {
583
608
  try {
@@ -586,6 +611,47 @@ function getProductEnvironment() {
586
611
  return void 0;
587
612
  }
588
613
  }
614
+ var NOTHING = () => void 0;
615
+ function CommerceSearchUnavailable({
616
+ isOpen,
617
+ onClose,
618
+ country,
619
+ language,
620
+ logo
621
+ }) {
622
+ useEffect(() => {
623
+ console.error(
624
+ `[search] Search is unavailable: ${marketProblem(country, language)}`
625
+ );
626
+ }, [country, language]);
627
+ return /* @__PURE__ */ jsx(
628
+ SearchPanel,
629
+ {
630
+ isOpen,
631
+ onClose,
632
+ copy: SEARCH_PANEL_COPY_EN,
633
+ logo,
634
+ input: { value: "", onChange: NOTHING, onSubmit: NOTHING },
635
+ suggestions: {
636
+ items: [],
637
+ loading: false,
638
+ showNoResults: false,
639
+ showDidYouMean: false,
640
+ hasMore: false,
641
+ query: "",
642
+ onSelect: NOTHING,
643
+ onRunAction: NOTHING,
644
+ onSeeMore: NOTHING
645
+ },
646
+ recentSearches: {
647
+ items: [],
648
+ onSelect: NOTHING,
649
+ onClear: NOTHING
650
+ },
651
+ agentPanel: /* @__PURE__ */ jsx("p", { role: "alert", children: "Search is unavailable right now." })
652
+ }
653
+ );
654
+ }
589
655
 
590
656
  // src/utils/nuiSearchGate.ts
591
657
  var NUI_SEARCH_FLAG = "enableNuiSearch";
@@ -1069,6 +1135,7 @@ function useSearchSuggestions({
1069
1135
  [enabled, service, resetSuggestionsState, settle]
1070
1136
  );
1071
1137
  useEffect(() => {
1138
+ latestRequestId.current += 1;
1072
1139
  if (!enabled) {
1073
1140
  resetSuggestionsState();
1074
1141
  return void 0;
@@ -1076,7 +1143,6 @@ function useSearchSuggestions({
1076
1143
  if (fetchDebounced.current) {
1077
1144
  clearTimeout(fetchDebounced.current);
1078
1145
  }
1079
- latestRequestId.current += 1;
1080
1146
  fetchDebounced.current = setTimeout(() => {
1081
1147
  void runFetch(query);
1082
1148
  }, DEBOUNCE_MS);
@@ -1358,11 +1424,11 @@ function useHandoff({
1358
1424
  onUnavailableRef.current = onUnavailable;
1359
1425
  });
1360
1426
  useEffect(() => {
1361
- let cancelled = false;
1427
+ let canceled = false;
1362
1428
  let timer;
1363
1429
  const startedAt = Date.now();
1364
1430
  const tryOpen = () => {
1365
- if (cancelled) return;
1431
+ if (canceled) return;
1366
1432
  const handle = getSalesAIHandle();
1367
1433
  if (handle) {
1368
1434
  handle.open({
@@ -1382,7 +1448,7 @@ function useHandoff({
1382
1448
  };
1383
1449
  tryOpen();
1384
1450
  return () => {
1385
- cancelled = true;
1451
+ canceled = true;
1386
1452
  if (timer !== void 0) clearTimeout(timer);
1387
1453
  };
1388
1454
  }, [query]);
@@ -1610,7 +1676,22 @@ function CommerceSearchView({
1610
1676
  );
1611
1677
  }
1612
1678
  var GUEST = "guest";
1613
- function CommerceSearch({
1679
+ function CommerceSearch(props) {
1680
+ if (!isValidMarket(props.country, props.language)) {
1681
+ return /* @__PURE__ */ jsx(
1682
+ CommerceSearchUnavailable,
1683
+ {
1684
+ isOpen: props.isOpen,
1685
+ onClose: props.onClose,
1686
+ country: props.country,
1687
+ language: props.language,
1688
+ logo: props.logo
1689
+ }
1690
+ );
1691
+ }
1692
+ return /* @__PURE__ */ jsx(ValidMarketSearch, { ...props });
1693
+ }
1694
+ function ValidMarketSearch({
1614
1695
  isOpen,
1615
1696
  onClose,
1616
1697
  navigate,
@@ -1638,7 +1719,6 @@ function CommerceSearch({
1638
1719
  () => createSearchService({
1639
1720
  country,
1640
1721
  language,
1641
- locale: `${country.toUpperCase()}-${language.toLowerCase()}`,
1642
1722
  getMarketConfig: fetchMarketConfig,
1643
1723
  minimumSearchLength,
1644
1724
  getCustomerType: () => customerTypeRef.current
@@ -1646,7 +1726,17 @@ function CommerceSearch({
1646
1726
  [country, language, minimumSearchLength]
1647
1727
  );
1648
1728
  const navigateToPath = useCallback(
1649
- (path) => navigate(toNavigationTarget(path)),
1729
+ (path) => {
1730
+ const target = toNavigationTarget(path);
1731
+ if (target) {
1732
+ navigate(target);
1733
+ } else {
1734
+ console.warn(
1735
+ "[search] Not navigating to a path the SDK does not recognize:",
1736
+ path
1737
+ );
1738
+ }
1739
+ },
1650
1740
  [navigate]
1651
1741
  );
1652
1742
  const { isAuthenticated, accountType, userId } = customer;